[rhythmbox] Android plugin



commit 1c8e189e3cb464459ae16fdcfc1974a23c0ef357
Author: Jonathan Matthew <jonathan d14n org>
Date:   Tue Aug 18 22:43:38 2015 +1000

    Android plugin
    
    This provides access to Android devices mounted by gvfs using MTP.  Unlike the
    existing MTP plugin, it doesn't talk directly to the device using libmtp.
    
    This is specifically aimed at Android devices because they don't work well with
    the MTP plugin's attempts to unmount the gvfs mount and establish a libmtp
    session, they don't provide useful metadata via MTP, and their media format is
    effectively uniform (though this may change if opus becomes more important).
    Android devices accessed via MTP can be assumed to be running at least Android
    4.0, which means MP3, FLAC, Ogg Vorbis and MP4 AAC are all supported.
    
    The plugin requires GUdev in order to access device identifiers.  The gvfs MTP
    backend also requires GUdev, so this shouldn't be a problem.
    
    Metadata caching was added to make this less painful to use.  It reduces the
    scan time for my phone with 15GB of music from 2:45 to 9 seconds, using a 600kb
    tdb file.

 configure.ac                         |    1 +
 data/org.gnome.rhythmbox.gschema.xml |    5 +
 plugins/Makefile.am                  |    4 +
 plugins/android/Makefile.am          |   52 ++
 plugins/android/android-info.ui      |  232 ++++++++
 plugins/android/android-toolbar.ui   |   29 +
 plugins/android/android.mpi          |    8 +
 plugins/android/android.plugin.in    |   12 +
 plugins/android/rb-android-plugin.c  |  296 ++++++++++
 plugins/android/rb-android-source.c  |  980 ++++++++++++++++++++++++++++++++++
 plugins/android/rb-android-source.h  |   61 +++
 po/POTFILES.in                       |    5 +
 12 files changed, 1685 insertions(+), 0 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 94f8f63..52b4d84 100644
--- a/configure.ac
+++ b/configure.ac
@@ -767,6 +767,7 @@ data/icons/src/Makefile
 sources/Makefile
 sources/sync/Makefile
 plugins/Makefile
+plugins/android/Makefile
 plugins/audiocd/Makefile
 plugins/audioscrobbler/Makefile
 plugins/brasero-disc-recorder/Makefile
diff --git a/data/org.gnome.rhythmbox.gschema.xml b/data/org.gnome.rhythmbox.gschema.xml
index 3dc678c..2d4df10 100644
--- a/data/org.gnome.rhythmbox.gschema.xml
+++ b/data/org.gnome.rhythmbox.gschema.xml
@@ -306,6 +306,11 @@
     <child name='source' schema='org.gnome.rhythmbox.source'/>
   </schema>
 
+  <schema id="org.gnome.rhythmbox.plugins.android" path="/org/gnome/rhythmbox/plugins/android/">
+    <child name='source' schema='org.gnome.rhythmbox.source'/>
+    <child name="encoding" schema="org.gnome.rhythmbox.encoding-settings"/>
+  </schema>
+
   <schema id="org.gnome.rhythmbox.plugins.generic-player" 
path="/org/gnome/rhythmbox/plugins/generic-player/">
     <child name='source' schema='org.gnome.rhythmbox.source'/>
     <child name="encoding" schema="org.gnome.rhythmbox.encoding-settings"/>
diff --git a/plugins/Makefile.am b/plugins/Makefile.am
index 94de29c..bea62b2 100644
--- a/plugins/Makefile.am
+++ b/plugins/Makefile.am
@@ -65,3 +65,7 @@ endif
 if ENABLE_GRILO
 SUBDIRS += grilo
 endif
+
+if USE_GUDEV
+SUBDIRS += android
+endif
diff --git a/plugins/android/Makefile.am b/plugins/android/Makefile.am
new file mode 100644
index 0000000..1c8c0bc
--- /dev/null
+++ b/plugins/android/Makefile.am
@@ -0,0 +1,52 @@
+plugindir = $(PLUGINDIR)/android
+plugindatadir = $(PLUGINDATADIR)/android
+plugin_LTLIBRARIES = libandroid.la
+
+libandroid_la_SOURCES =                                        \
+       rb-android-plugin.c                             \
+       rb-android-source.c                             \
+       rb-android-source.h
+
+libandroid_la_LIBTOOLFLAGS = --tag=disable-static
+libandroid_la_LDFLAGS =                                \
+       $(top_builddir)/shell/librhythmbox-core.la      \
+       $(GUDEV_LIBS)                                   \
+       $(PLUGIN_LIBTOOL_FLAGS)
+
+AM_CPPFLAGS =                                          \
+        -DGNOMELOCALEDIR=\""$(datadir)/locale"\"        \
+       -DG_LOG_DOMAIN=\"Rhythmbox\"                    \
+       -I$(top_srcdir)                                 \
+       -I$(top_srcdir)/lib                             \
+       -I$(top_srcdir)/lib/libmediaplayerid            \
+       -I$(top_srcdir)/metadata                        \
+       -I$(top_srcdir)/rhythmdb                        \
+       -I$(top_srcdir)/widgets                         \
+       -I$(top_srcdir)/sources                         \
+       -I$(top_srcdir)/sources/sync                    \
+       -I$(top_srcdir)/podcast                         \
+       -I$(top_srcdir)/plugins                         \
+       -I$(top_srcdir)/shell                           \
+       -DPIXMAP_DIR=\""$(datadir)/pixmaps"\"           \
+       -DSHARE_DIR=\"$(pkgdatadir)\"                   \
+       -DDATADIR=\""$(datadir)"\"                      \
+       $(GUDEV_CFLAGS)                                 \
+       $(RHYTHMBOX_CFLAGS)
+
+gtkbuilderdir = $(plugindatadir)
+gtkbuilder_DATA = android-info.ui android-toolbar.ui
+
+mpidir = $(plugindatadir)
+mpi_DATA = android.mpi
+
+plugin_in_files = android.plugin.in
+
+%.plugin: %.plugin.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po) ; $(INTLTOOL_MERGE) 
$(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache
+
+plugin_DATA = $(plugin_in_files:.plugin.in=.plugin)
+
+EXTRA_DIST = $(gtkbuilder_DATA) $(mpi_DATA) $(plugin_in_files)
+
+CLEANFILES = $(plugin_DATA)
+DISTCLEANFILES = $(plugin_DATA)
+
diff --git a/plugins/android/android-info.ui b/plugins/android/android-info.ui
new file mode 100644
index 0000000..fc68208
--- /dev/null
+++ b/plugins/android/android-info.ui
@@ -0,0 +1,232 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.18.3 -->
+<interface>
+  <requires lib="gtk+" version="3.6"/>
+  <object class="GtkFrame" id="generic-player-advanced-tab">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="border_width">12</property>
+    <property name="label_xalign">0</property>
+    <property name="shadow_type">none</property>
+    <child>
+      <object class="GtkTable" id="table2">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="border_width">12</property>
+        <property name="n_rows">4</property>
+        <property name="n_columns">2</property>
+        <property name="column_spacing">12</property>
+        <property name="row_spacing">6</property>
+        <child>
+          <object class="GtkLabel" id="label-model-value">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="right_attach">2</property>
+            <property name="top_attach">1</property>
+            <property name="bottom_attach">2</property>
+            <property name="x_options">GTK_FILL</property>
+            <property name="y_options"/>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label-model">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">Model:</property>
+          </object>
+          <packing>
+            <property name="top_attach">1</property>
+            <property name="bottom_attach">2</property>
+            <property name="x_options">GTK_FILL</property>
+            <property name="y_options"/>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label-serial-number-value">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="right_attach">2</property>
+            <property name="top_attach">2</property>
+            <property name="bottom_attach">3</property>
+            <property name="y_options"/>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label-serial-number">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">Serial number:</property>
+          </object>
+          <packing>
+            <property name="top_attach">2</property>
+            <property name="bottom_attach">3</property>
+            <property name="x_options">GTK_FILL</property>
+            <property name="y_options"/>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label-manufacturer">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+            <property name="label" translatable="yes">Manufacturer:</property>
+          </object>
+          <packing>
+            <property name="x_options">GTK_FILL</property>
+            <property name="y_options"/>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label-manufacturer-value">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="right_attach">2</property>
+            <property name="y_options"/>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="label-audio-formats">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+            <property name="yalign">0</property>
+            <property name="label" translatable="yes">Audio formats:</property>
+          </object>
+          <packing>
+            <property name="top_attach">3</property>
+            <property name="bottom_attach">4</property>
+            <property name="x_options">GTK_FILL</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="audio-format-list">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="xalign">0</property>
+            <property name="yalign">0</property>
+            <property name="wrap">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="right_attach">2</property>
+            <property name="top_attach">3</property>
+            <property name="bottom_attach">4</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <child type="label">
+      <object class="GtkLabel" id="label-frame-system">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">System</property>
+        <attributes>
+          <attribute name="weight" value="bold"/>
+        </attributes>
+      </object>
+    </child>
+  </object>
+  <object class="GtkTable" id="generic-player-basic-info">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="border_width">12</property>
+    <property name="n_rows">3</property>
+    <property name="n_columns">2</property>
+    <property name="column_spacing">12</property>
+    <property name="row_spacing">6</property>
+    <child>
+      <object class="GtkEntry" id="entry-device-name">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="invisible_char">●</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="right_attach">2</property>
+        <property name="y_options"/>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label-device-name">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+        <property name="label" translatable="yes">Device _name:</property>
+        <property name="use_underline">True</property>
+        <property name="mnemonic_widget">entry-device-name</property>
+      </object>
+      <packing>
+        <property name="x_options">GTK_FILL</property>
+        <property name="y_options"/>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label-num-tracks">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+        <property name="label" translatable="yes">Tracks:</property>
+      </object>
+      <packing>
+        <property name="top_attach">1</property>
+        <property name="bottom_attach">2</property>
+        <property name="y_options">GTK_FILL</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="label-num-playlists">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+        <property name="label" translatable="yes">Playlists:</property>
+      </object>
+      <packing>
+        <property name="top_attach">2</property>
+        <property name="bottom_attach">3</property>
+        <property name="y_options">GTK_FILL</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="num-tracks">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="right_attach">2</property>
+        <property name="top_attach">1</property>
+        <property name="bottom_attach">2</property>
+        <property name="y_options">GTK_FILL</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="num-playlists">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="right_attach">2</property>
+        <property name="top_attach">2</property>
+        <property name="bottom_attach">3</property>
+        <property name="y_options">GTK_FILL</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/plugins/android/android-toolbar.ui b/plugins/android/android-toolbar.ui
new file mode 100644
index 0000000..5f7574d
--- /dev/null
+++ b/plugins/android/android-toolbar.ui
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+<interface>
+  <menu id="android-toolbar">
+    <section>
+      <submenu>
+       <attribute name="label" translatable="yes">Edit</attribute>
+       <attribute name="rb-menu-link">edit-menu</attribute>
+       <attribute name="accel">&lt;Alt&gt;e</attribute>
+      </submenu>
+      <item>
+       <attribute name="label" translatable="yes">Browse</attribute>
+       <attribute name="rb-property-bind">show-browser</attribute>
+       <attribute name="accel">&lt;Primary&gt;b</attribute>
+      </item>
+      <item>
+       <attribute name="label" translatable="yes">View All</attribute>
+       <attribute name="rb-signal-bind">reset-filters</attribute>
+      </item>
+      <item>
+       <attribute name="label" translatable="yes">Properties</attribute>
+       <attribute name="action">app.media-player-properties</attribute>
+      </item>
+      <item>
+       <attribute name="label" translatable="yes">Sync</attribute>
+        <attribute name="action">app.media-player-sync</attribute>
+      </item>
+    </section>
+  </menu>
+</interface>
diff --git a/plugins/android/android.mpi b/plugins/android/android.mpi
new file mode 100644
index 0000000..07a2309
--- /dev/null
+++ b/plugins/android/android.mpi
@@ -0,0 +1,8 @@
+[Device]
+Product=Android
+Vendor=Android
+AccessProtocol=mtp
+
+[Media]
+OutputFormats=audio/mp4;audio/aac;application/ogg;audio/mpeg;audio/flac
+# opus?
diff --git a/plugins/android/android.plugin.in b/plugins/android/android.plugin.in
new file mode 100644
index 0000000..5f7ed62
--- /dev/null
+++ b/plugins/android/android.plugin.in
@@ -0,0 +1,12 @@
+[Plugin]
+Module=android
+IAge=2
+Builtin=true
+_Name=Android devices
+_Description=Support for Android 4.0+ devices (via MTP)
+Authors=Jonathan Matthew
+Copyright=Copyright © 2015 Jonathan Matthew
+Website=http://www.rhythmbox.org/
+
+[RB]
+InitiallyEnabled=true
diff --git a/plugins/android/rb-android-plugin.c b/plugins/android/rb-android-plugin.c
new file mode 100644
index 0000000..92b0646
--- /dev/null
+++ b/plugins/android/rb-android-plugin.c
@@ -0,0 +1,296 @@
+/*
+ * rb-android-plugin.c
+ *
+ * Copyright (C) 2006  Jonathan Matthew
+ *
+ * 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 2, or (at your option)
+ * any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ */
+
+#define __EXTENSIONS__
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include <string.h> /* For strlen */
+#include <glib/gi18n-lib.h>
+#include <gmodule.h>
+#include <gtk/gtk.h>
+#include <glib.h>
+#include <glib-object.h>
+
+#include <gudev/gudev.h>
+
+#include "rb-plugin-macros.h"
+#include "rb-debug.h"
+#include "rb-shell.h"
+#include "rb-dialog.h"
+#include "rb-removable-media-manager.h"
+#include "rb-file-helpers.h"
+#include "rb-display-page-tree.h"
+#include "rb-builder-helpers.h"
+#include "rb-application.h"
+#include "rb-android-source.h"
+
+
+#define RB_TYPE_ANDROID_PLUGIN         (rb_android_plugin_get_type ())
+#define RB_ANDROID_PLUGIN(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_ANDROID_PLUGIN, 
RBAndroidPlugin))
+#define RB_ANDROID_PLUGIN_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_ANDROID_PLUGIN, 
RBAndroidPluginClass))
+#define RB_IS_ANDROID_PLUGIN(o)                (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_ANDROID_PLUGIN))
+#define RB_IS_ANDROID_PLUGIN_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_ANDROID_PLUGIN))
+#define RB_ANDROID_PLUGIN_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_ANDROID_PLUGIN, 
RBAndroidPluginClass))
+
+typedef struct
+{
+       PeasExtensionBase parent;
+
+       GList *sources;
+} RBAndroidPlugin;
+
+typedef struct
+{
+       PeasExtensionBaseClass parent_class;
+} RBAndroidPluginClass;
+
+
+G_MODULE_EXPORT void peas_register_types (PeasObjectModule  *module);
+
+static void rb_android_plugin_init (RBAndroidPlugin *plugin);
+
+RB_DEFINE_PLUGIN(RB_TYPE_ANDROID_PLUGIN, RBAndroidPlugin, rb_android_plugin,)
+
+static void
+rb_android_plugin_init (RBAndroidPlugin *plugin)
+{
+       rb_debug ("RBAndroidPlugin initialising");
+}
+
+static void
+source_deleted_cb (RBAndroidSource *source, RBAndroidPlugin *plugin)
+{
+       plugin->sources = g_list_remove (plugin->sources, source);
+}
+
+static GUdevDevice *
+get_gudev_device (GMount *mount)
+{
+       GVolume *volume;
+       GUdevClient *client;
+       GUdevDevice *udevice = NULL;
+       char *devpath;
+       char *subsystems[] = { "usb", NULL };
+
+       volume = g_mount_get_volume (mount);
+       if (volume == NULL) {
+               return FALSE;
+       }
+       devpath = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE);
+       g_clear_object (&volume);
+
+       if (devpath == NULL) {
+               return FALSE;
+       }
+
+       client = g_udev_client_new ((const char * const *)subsystems);
+       if (client != NULL)
+               udevice = g_udev_client_query_by_device_file (client, devpath);
+
+       g_clear_object (&client);
+       return udevice;
+}
+
+static RBSource *
+create_source_cb (RBRemovableMediaManager *rmm, GMount *mount, MPIDDevice *device_info, RBAndroidPlugin 
*plugin)
+{
+       RBSource *source = NULL;
+       RBShell *shell;
+       RhythmDB *db;
+       RhythmDBEntryType *entry_type;
+       RhythmDBEntryType *error_type;
+       RhythmDBEntryType *ignore_type;
+       GUdevDevice *gudev_device;
+       GtkBuilder *builder;
+       GMenu *toolbar;
+       GVolume *volume;
+       GSettings *settings;
+       GFile *root;
+       const char *model;
+       const char *device_serial;
+       char *uri_prefix;
+       char *name;
+       char *path;
+
+       gudev_device = get_gudev_device (mount);
+       if (gudev_device == NULL)
+               return NULL;
+
+       model = g_udev_device_get_property (gudev_device, "ID_MODEL");
+       if (g_strcmp0 (model, "Android") != 0) {
+               g_object_unref (gudev_device);
+               return NULL;
+       }
+
+       device_info = mpid_device_new_from_mpi_file (rb_find_plugin_data_file (G_OBJECT (plugin), 
"android.mpi"));
+
+       volume = g_mount_get_volume (mount);
+       path = g_volume_get_identifier (volume, G_VOLUME_IDENTIFIER_KIND_UNIX_DEVICE);
+
+       g_object_get (plugin, "object", &shell, NULL);
+       g_object_get (shell, "db", &db, NULL);
+
+       device_serial = g_udev_device_get_property (gudev_device, "ID_SERIAL");
+
+       root = g_mount_get_root (mount);
+       uri_prefix = g_file_get_uri (root);
+       g_object_unref (root);
+
+       rb_debug ("metadata cache mapping: %s <=> %s", uri_prefix, device_serial);
+
+       name = g_strdup_printf ("android: %s", path);
+       entry_type = g_object_new (RB_TYPE_MEDIA_PLAYER_ENTRY_TYPE,
+                                  "db", db,
+                                  "name", name,
+                                  "save-to-disk", FALSE,
+                                  "category", RHYTHMDB_ENTRY_NORMAL,
+                                  "cache-name", "android-mtp",
+                                  "key-prefix", device_serial,
+                                  "uri-prefix", uri_prefix,
+                                  NULL);
+       rhythmdb_register_entry_type (db, entry_type);
+       g_free (name);
+
+       name = g_strdup_printf ("android (ignore): %s", path);
+       ignore_type = g_object_new (RB_TYPE_MEDIA_PLAYER_ENTRY_TYPE,
+                                   "db", db,
+                                   "name", name,
+                                   "save-to-disk", FALSE,
+                                   "category", RHYTHMDB_ENTRY_VIRTUAL,
+                                   "cache-name", "android-mtp",
+                                   "key-prefix", device_serial,
+                                   "uri-prefix", uri_prefix,
+                                   NULL);
+       rhythmdb_register_entry_type (db, ignore_type);
+       g_free (name);
+
+       name = g_strdup_printf ("android (errors): %s", path);
+       error_type = g_object_new (RHYTHMDB_TYPE_ENTRY_TYPE,
+                                  "db", db,
+                                  "name", name,
+                                  "save-to-disk", FALSE,
+                                  "category", RHYTHMDB_ENTRY_VIRTUAL,
+                                  NULL);
+       rhythmdb_register_entry_type (db, error_type);
+       g_free (name);
+
+       g_free (uri_prefix);
+       g_object_unref (db);
+
+       builder = rb_builder_load_plugin_file (G_OBJECT (plugin), "android-toolbar.ui", NULL);
+       toolbar = G_MENU (gtk_builder_get_object (builder, "android-toolbar"));
+       rb_application_link_shared_menus (RB_APPLICATION (g_application_get_default ()), toolbar);
+
+       settings = g_settings_new ("org.gnome.rhythmbox.plugins.android");
+
+       source = RB_SOURCE (g_object_new (RB_TYPE_ANDROID_SOURCE,
+                                         "plugin", plugin,
+                                         "entry-type", entry_type,
+                                         "ignore-entry-type", ignore_type,
+                                         "error-entry-type", error_type,
+                                         "mount", mount,
+                                         "shell", shell,
+                                         "device-info", device_info,
+                                         "load-status", RB_SOURCE_LOAD_STATUS_LOADING,
+                                         "settings", g_settings_get_child (settings, "source"),
+                                         "encoding-settings", g_settings_get_child (settings, "encoding"),
+                                         "toolbar-menu", toolbar,
+                                         "gudev-device", gudev_device,
+                                         NULL));
+
+       g_object_unref (settings);
+       g_object_unref (builder);
+       g_object_unref (gudev_device);
+
+       rb_shell_register_entry_type_for_source (shell, RB_SOURCE (source), entry_type);
+
+       plugin->sources = g_list_prepend (plugin->sources, source);
+       g_signal_connect_object (G_OBJECT (source),
+                                "deleted", G_CALLBACK (source_deleted_cb),
+                                plugin, 0);
+
+       g_object_unref (shell);
+       return source;
+}
+
+static void
+impl_activate (PeasActivatable *plugin)
+{
+       RBAndroidPlugin *pi = RB_ANDROID_PLUGIN (plugin);
+       RBRemovableMediaManager *rmm;
+       RBShell *shell;
+       gboolean scanned;
+
+       g_object_get (plugin, "object", &shell, NULL);
+       g_object_get (shell, "removable-media-manager", &rmm, NULL);
+
+       g_signal_connect_object (rmm, "create-source-mount", G_CALLBACK (create_source_cb), pi, 0);
+
+       g_object_get (rmm, "scanned", &scanned, NULL);
+       if (scanned)
+               rb_removable_media_manager_scan (rmm);
+
+       g_object_unref (rmm);
+       g_object_unref (shell);
+}
+
+static void
+impl_deactivate        (PeasActivatable *bplugin)
+{
+       RBAndroidPlugin *plugin = RB_ANDROID_PLUGIN (bplugin);
+       RBRemovableMediaManager *rmm;
+       RBShell *shell;
+
+       g_object_get (plugin, "object", &shell, NULL);
+       g_object_get (shell,
+                     "removable-media-manager", &rmm,
+                     NULL);
+
+       g_signal_handlers_disconnect_by_func (G_OBJECT (rmm), create_source_cb, plugin);
+
+       g_list_foreach (plugin->sources, (GFunc)rb_display_page_delete_thyself, NULL);
+       g_list_free (plugin->sources);
+       plugin->sources = NULL;
+
+       g_object_unref (rmm);
+       g_object_unref (shell);
+}
+
+G_MODULE_EXPORT void
+peas_register_types (PeasObjectModule *module)
+{
+       rb_android_plugin_register_type (G_TYPE_MODULE (module));
+       _rb_android_source_register_type (G_TYPE_MODULE (module));
+
+       peas_object_module_register_extension_type (module,
+                                                   PEAS_TYPE_ACTIVATABLE,
+                                                   RB_TYPE_ANDROID_PLUGIN);
+}
diff --git a/plugins/android/rb-android-source.c b/plugins/android/rb-android-source.c
new file mode 100644
index 0000000..b5d6b4e
--- /dev/null
+++ b/plugins/android/rb-android-source.c
@@ -0,0 +1,980 @@
+/*
+ *  Copyright (C) 2015 Jonathan Matthew <jonathan d14n org>
+ *
+ *  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 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your 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, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#define __EXTENSIONS__
+
+#include "config.h"
+
+#include <string.h>
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+
+#include <gudev/gudev.h>
+
+#include "mediaplayerid.h"
+
+#include "rb-android-source.h"
+#include "rb-debug.h"
+#include "rb-util.h"
+#include "rb-file-helpers.h"
+#include "rhythmdb.h"
+#include "rb-builder-helpers.h"
+#include "rb-application.h"
+#include "rb-transfer-target.h"
+#include "rb-device-source.h"
+#include "rb-sync-settings.h"
+#include "rb-import-errors-source.h"
+#include "rb-gst-media-types.h"
+#include "rb-task-list.h"
+
+static void rb_android_device_source_init (RBDeviceSourceInterface *interface);
+static void rb_android_transfer_target_init (RBTransferTargetInterface *interface);
+
+static void find_music_dirs (RBAndroidSource *source);
+static void update_free_space_next (RBAndroidSource *source);
+
+enum
+{
+       PROP_0,
+       PROP_MOUNT,
+       PROP_IGNORE_ENTRY_TYPE,
+       PROP_ERROR_ENTRY_TYPE,
+       PROP_DEVICE_INFO,
+       PROP_DEVICE_SERIAL,
+       PROP_GUDEV_DEVICE
+};
+
+typedef struct
+{
+       RhythmDB *db;
+
+       gboolean loaded;
+       RhythmDBImportJob *import_job;
+       RBSource *import_errors;
+       GCancellable *cancel;
+       GQueue to_scan;
+       int scanned;
+
+       RhythmDBEntryType *ignore_type;
+       RhythmDBEntryType *error_type;
+
+       MPIDDevice *device_info;
+       GUdevDevice *gudev_device;
+       GMount *mount;
+       gboolean ejecting;
+
+       GList *storage;
+       guint64 storage_capacity;
+       guint64 storage_free_space;
+       GList *query_storage;
+       guint64 storage_free_space_next;
+       guint64 storage_capacity_next;
+} RBAndroidSourcePrivate;
+
+G_DEFINE_DYNAMIC_TYPE_EXTENDED (
+       RBAndroidSource,
+       rb_android_source,
+       RB_TYPE_MEDIA_PLAYER_SOURCE,
+       0,
+       G_IMPLEMENT_INTERFACE_DYNAMIC (RB_TYPE_DEVICE_SOURCE, rb_android_device_source_init)
+       G_IMPLEMENT_INTERFACE_DYNAMIC (RB_TYPE_TRANSFER_TARGET, rb_android_transfer_target_init))
+
+#define GET_PRIVATE(o)   (G_TYPE_INSTANCE_GET_PRIVATE ((o), RB_TYPE_ANDROID_SOURCE, RBAndroidSourcePrivate))
+
+static void
+free_space_cb (GObject *obj, GAsyncResult *res, gpointer data)
+{
+       RBAndroidSource *source = RB_ANDROID_SOURCE (data);
+       RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
+       GFileInfo *info;
+       GError *error = NULL;
+
+       info = g_file_query_filesystem_info_finish (G_FILE (obj), res, &error);
+       if (info == NULL) {
+               rb_debug ("error querying filesystem free space: %s", error->message);
+               g_clear_error (&error);
+       } else {
+               priv->storage_free_space_next += g_file_info_get_attribute_uint64 (info, 
G_FILE_ATTRIBUTE_FILESYSTEM_FREE);
+               priv->storage_capacity_next += g_file_info_get_attribute_uint64 (info, 
G_FILE_ATTRIBUTE_FILESYSTEM_SIZE);
+               rb_debug ("capacity: %lu, free space: %lu", priv->storage_capacity_next, 
priv->storage_free_space_next);
+       }
+
+       priv->query_storage = priv->query_storage->next;
+       if (priv->query_storage != NULL) {
+               update_free_space_next (source);
+       } else {
+               priv->storage_free_space = priv->storage_free_space_next;
+               priv->storage_capacity = priv->storage_capacity_next;
+       }
+}
+
+static void
+update_free_space_next (RBAndroidSource *source)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
+       GFile *file;
+       const char *attrs = G_FILE_ATTRIBUTE_FILESYSTEM_FREE "," G_FILE_ATTRIBUTE_FILESYSTEM_SIZE;
+
+       file = G_FILE (priv->query_storage->data);
+       g_file_query_filesystem_info_async (file, attrs, G_PRIORITY_DEFAULT, NULL, free_space_cb, source);
+}
+
+static void
+update_free_space (RBAndroidSource *source)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
+
+       if (priv->query_storage != NULL) {
+               rb_debug ("already updating free space");
+               return;
+       }
+
+       priv->storage_free_space_next = 0;
+       priv->storage_capacity_next = 0;
+       priv->query_storage = priv->storage;
+       update_free_space_next (source);
+}
+
+
+static void
+music_dirs_done (RBAndroidSource *source)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
+       rb_debug ("finished checking for music dirs");
+       rhythmdb_import_job_start (priv->import_job);
+
+       update_free_space (source);
+}
+
+
+static void
+enum_files_cb (GObject *obj, GAsyncResult *result, gpointer data)
+{
+       RBAndroidSource *source = RB_ANDROID_SOURCE (data);
+       RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
+       GFileEnumerator *e = G_FILE_ENUMERATOR (obj);
+       GError *error = NULL;
+       GFileInfo *info;
+       GList *files;
+       GList *l;
+
+       files = g_file_enumerator_next_files_finish (e, result, &error);
+       if (error != NULL) {
+               rb_debug ("error listing files: %s", error->message);
+               music_dirs_done (source);
+               return;
+       }
+
+       if (files == NULL) {
+               priv->scanned++;
+               g_object_unref (e);
+               find_music_dirs (source);
+               return;
+       }
+
+       for (l = files; l != NULL; l = l->next) {
+               guint32 filetype;
+               info = (GFileInfo *)l->data;
+
+               filetype = g_file_info_get_attribute_uint32 (info, G_FILE_ATTRIBUTE_STANDARD_TYPE);
+               if (filetype == G_FILE_TYPE_DIRECTORY) {
+                       GFile *dir;
+                       if (priv->scanned == 0) {
+
+                               rb_debug ("got storage container %s", g_file_info_get_name (info));
+                               dir = g_file_get_child (g_file_enumerator_get_container (e), 
g_file_info_get_name (info));
+                               g_queue_push_tail (&priv->to_scan, dir);
+                       } else if (g_ascii_strcasecmp (g_file_info_get_name (info), "music") == 0) {
+                               GFile *storage;
+                               char *uri;
+
+                               storage = g_file_enumerator_get_container (e);
+                               dir = g_file_get_child (storage, g_file_info_get_name (info));
+                               uri = g_file_get_uri (dir);
+                               rb_debug ("music dir found at %s", uri);
+
+                               /* keep the container around for space/capacity calculation */
+                               priv->storage = g_list_append (priv->storage, dir);
+
+                               rhythmdb_import_job_add_uri (priv->import_job, uri);
+                               g_free (uri);
+                       }
+               }
+
+               g_object_unref (info);
+       }
+
+       g_list_free (files);
+
+       g_file_enumerator_next_files_async (G_FILE_ENUMERATOR (obj), 64, G_PRIORITY_DEFAULT, priv->cancel, 
enum_files_cb, source);
+}
+
+static void
+enum_child_cb (GObject *obj, GAsyncResult *result, gpointer data)
+{
+       RBAndroidSource *source = RB_ANDROID_SOURCE (data);
+       RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
+       GFileEnumerator *e;
+       GError *error = NULL;
+
+       e = g_file_enumerate_children_finish (G_FILE (obj), result, &error);
+       if (e == NULL) {
+               rb_debug ("enum error: %s", error->message);
+               g_clear_error (&error);
+               music_dirs_done (source);
+               return;
+       }
+
+       g_file_enumerator_next_files_async (e, 64, G_PRIORITY_DEFAULT, priv->cancel, enum_files_cb, source);
+}
+
+static void
+find_music_dirs (RBAndroidSource *source)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
+       const char *attrs =
+               G_FILE_ATTRIBUTE_STANDARD_NAME ","
+               G_FILE_ATTRIBUTE_STANDARD_TYPE;
+
+       gpointer dir;
+
+       dir = g_queue_pop_head (&priv->to_scan);
+       if (dir == NULL) {
+               music_dirs_done (source);
+               return;
+       }
+
+       rb_debug ("scanning %s", g_file_get_uri (G_FILE (dir)));
+       g_file_enumerate_children_async (G_FILE (dir),
+                                        attrs,
+                                        G_FILE_QUERY_INFO_NONE,
+                                        G_PRIORITY_DEFAULT,
+                                        priv->cancel,
+                                        enum_child_cb,
+                                        source);
+       g_object_unref (dir);
+}
+
+static void
+import_complete_cb (RhythmDBImportJob *job, int total, RBAndroidSource *source)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
+       GSettings *settings;
+       RBShell *shell;
+
+       if (priv->ejecting) {
+               rb_device_source_default_eject (RB_DEVICE_SOURCE (source));
+       } else {
+               g_object_get (source, "shell", &shell, NULL);
+               rb_shell_append_display_page (shell, RB_DISPLAY_PAGE (priv->import_errors), RB_DISPLAY_PAGE 
(source));
+               g_object_unref (shell);
+
+               g_object_set (source, "load-status", RB_SOURCE_LOAD_STATUS_LOADED, NULL);
+
+               g_object_get (source, "encoding-settings", &settings, NULL);
+               rb_transfer_target_transfer (RB_TRANSFER_TARGET (source), settings, NULL, FALSE);
+               g_object_unref (settings);
+
+               rb_media_player_source_purge_metadata_cache (RB_MEDIA_PLAYER_SOURCE (source));
+       }
+
+       g_clear_object (&priv->import_job);
+}
+
+static gboolean
+ensure_loaded (RBAndroidSource *source)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
+       RBSourceLoadStatus status;
+       RhythmDBEntryType *entry_type;
+       GMount *mount;
+       GFile *root;
+       RBTaskList *tasklist;
+       RBShell *shell;
+       char *name;
+       char *label;
+
+       if (priv->loaded) {
+               g_object_get (source, "load-status", &status, NULL);
+               return (status == RB_SOURCE_LOAD_STATUS_LOADED);
+       }
+
+       priv->loaded = TRUE;
+       rb_media_player_source_load (RB_MEDIA_PLAYER_SOURCE (source));
+
+       /* identify storage containers and find music dirs within them */
+       g_object_get (source, "mount", &mount, "entry-type", &entry_type, NULL);
+       root = g_mount_get_root (mount);
+       g_object_unref (mount);
+
+       priv->cancel = g_cancellable_new ();
+       priv->import_job = rhythmdb_import_job_new (priv->db, entry_type, priv->ignore_type, 
priv->error_type);
+       g_signal_connect_object (priv->import_job, "complete", G_CALLBACK (import_complete_cb), source, 0);
+
+       priv->scanned = 0;
+       g_queue_init (&priv->to_scan);
+       g_queue_push_tail (&priv->to_scan, root);
+       g_object_unref (entry_type);
+
+       find_music_dirs (RB_ANDROID_SOURCE (source));
+
+       g_object_get (source, "name", &name, "shell", &shell, NULL);
+       label = g_strdup_printf (_("Scanning %s"), name);
+       g_object_set (priv->import_job, "task-label", label, NULL);
+
+       g_object_get (shell, "task-list", &tasklist, NULL);
+       rb_task_list_add_task (tasklist, RB_TASK_PROGRESS (priv->import_job));
+       g_object_unref (tasklist);
+       g_object_unref (shell);
+
+       g_free (label);
+       g_free (name);
+       return FALSE;
+}
+
+
+static gboolean
+can_delete_directory (RBAndroidSource *source, GFile *dir)
+{
+       GMount *mount;
+       GFile *root;
+       char *path;
+       int i;
+       int c;
+
+       g_object_get (source, "mount", &mount, NULL);
+       root = g_mount_get_root (mount);
+       g_object_unref (mount);
+
+       /*
+        * path here will be sdcard/Music/something for anything we want to delete
+        */
+       path = g_file_get_relative_path (root, dir);
+       c = 0;
+       for (i = 0; path[i] != '\0'; i++) {
+               if (path[i] == '/')
+                       c++;
+       }
+
+       g_free (path);
+       return (c > 1);
+}
+
+static void
+delete_entries (RBAndroidSource *source, GList *entries)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
+       GList *tem;
+
+       for (tem = entries; tem != NULL; tem = tem->next) {
+               RhythmDBEntry *entry;
+               const char *uri;
+               GFile *file;
+               GFile *dir;
+
+               entry = tem->data;
+               uri = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION);
+               file = g_file_new_for_uri (uri);
+               g_file_delete (file, NULL, NULL);
+
+               /* now walk up the directory structure and delete empty dirs
+                * until we reach the root or one of the device's audio folders.
+                */
+               dir = g_file_get_parent (file);
+               while (can_delete_directory (source, dir)) {
+                       GFile *parent;
+
+                       if (g_file_delete (dir, NULL, NULL) == FALSE) {
+                               break;
+                       }
+
+                       parent = g_file_get_parent (dir);
+                       if (parent == NULL) {
+                               break;
+                       }
+                       g_object_unref (dir);
+                       dir = parent;
+               }
+
+               g_object_unref (dir);
+               g_object_unref (file);
+
+               rhythmdb_entry_delete (priv->db, entry);
+       }
+
+       rhythmdb_commit (priv->db);
+}
+
+static void
+impl_show_properties (RBMediaPlayerSource *source, GtkWidget *info_box, GtkWidget *notebook)
+{
+       RhythmDBQueryModel *model;
+       GtkBuilder *builder;
+       GtkWidget *widget;
+       GObject *plugin;
+       char *builder_file;
+       char *text;
+
+       g_object_get (source, "plugin", &plugin, NULL);
+       builder_file = rb_find_plugin_data_file (plugin, "android-info.ui");
+       g_object_unref (plugin);
+
+       if (builder_file == NULL) {
+               g_warning ("Couldn't find android-info.ui");
+               return;
+       }
+
+       builder = rb_builder_load (builder_file, NULL);
+       g_free (builder_file);
+
+       if (builder == NULL) {
+               rb_debug ("Couldn't load android-info.ui");
+               return;
+       }
+
+       /* 'basic' tab stuff */
+
+       widget = GTK_WIDGET (gtk_builder_get_object (builder, "android-basic-info"));
+       gtk_box_pack_start (GTK_BOX (info_box), widget, TRUE, TRUE, 0);
+
+       g_object_get (source, "base-query-model", &model, NULL);
+       widget = GTK_WIDGET (gtk_builder_get_object (builder, "num-tracks"));
+       text = g_strdup_printf ("%d", gtk_tree_model_iter_n_children (GTK_TREE_MODEL (model), NULL));
+       gtk_label_set_text (GTK_LABEL (widget), text);
+       g_free (text);
+       g_object_unref (model);
+
+       g_object_unref (builder);
+}
+
+static void
+impl_get_entries (RBMediaPlayerSource *source,
+                 const char *category,
+                 GHashTable *map)
+{
+       RhythmDBQueryModel *model;
+       GtkTreeIter iter;
+       gboolean podcast;
+
+       /* we don't have anything else to distinguish podcasts from regular
+        * tracks, so just use the genre.
+        */
+       podcast = (g_str_equal (category, SYNC_CATEGORY_PODCAST));
+
+       g_object_get (source, "base-query-model", &model, NULL);
+       if (gtk_tree_model_get_iter_first (GTK_TREE_MODEL (model), &iter) == FALSE) {
+               g_object_unref (model);
+               return;
+       }
+
+       do {
+               RhythmDBEntry *entry;
+               const char *genre;
+               entry = rhythmdb_query_model_iter_to_entry (model, &iter);
+               genre = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_GENRE);
+               if (g_str_equal (genre, "Podcast") == podcast) {
+                       _rb_media_player_source_add_to_map (map, entry);
+               }
+       } while (gtk_tree_model_iter_next (GTK_TREE_MODEL (model), &iter));
+
+       g_object_unref (model);
+}
+
+static void
+impl_delete_entries (RBMediaPlayerSource *source,
+                    GList *entries,
+                    RBMediaPlayerSourceDeleteCallback callback,
+                    gpointer callback_data,
+                    GDestroyNotify destroy_data)
+{
+       delete_entries (RB_ANDROID_SOURCE (source), entries);
+
+       if (callback) {
+               callback (source, callback_data);
+       }
+       if (destroy_data) {
+               destroy_data (callback_data);
+       }
+}
+
+
+static guint64
+impl_get_capacity (RBMediaPlayerSource *source)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
+       return priv->storage_capacity;
+}
+
+static guint64
+impl_get_free_space (RBMediaPlayerSource *source)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE(source);
+       return priv->storage_free_space;
+}
+
+static gboolean
+impl_can_paste (RBSource *source)
+{
+       return TRUE;
+}
+
+static RBTrackTransferBatch *
+impl_paste (RBSource *source, GList *entries)
+{
+       gboolean defer;
+       GSettings *settings;
+       RBTrackTransferBatch *batch;
+
+       defer = (ensure_loaded (RB_ANDROID_SOURCE (source)) == FALSE);
+       g_object_get (source, "encoding-settings", &settings, NULL);
+       batch = rb_transfer_target_transfer (RB_TRANSFER_TARGET (source), settings, entries, defer);
+       g_object_unref (settings);
+       return batch;
+}
+
+static gboolean
+impl_can_delete (RBSource *source)
+{
+       return TRUE;
+}
+
+static void
+impl_delete_selected (RBSource *source)
+{
+       RBEntryView *view;
+       GList *sel;
+
+       view = rb_source_get_entry_view (source);
+       sel = rb_entry_view_get_selected_entries (view);
+
+       delete_entries (RB_ANDROID_SOURCE (source), sel);
+       g_list_foreach (sel, (GFunc)rhythmdb_entry_unref, NULL);
+       g_list_free (sel);
+}
+
+
+static void
+impl_eject (RBDeviceSource *source)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE (source);
+
+       if (priv->import_job != NULL) {
+               rhythmdb_import_job_cancel (priv->import_job);
+               priv->ejecting = TRUE;
+       } else {
+               rb_device_source_default_eject (source);
+       }
+}
+
+
+static char *
+sanitize_path (const char *str)
+{
+       char *res = NULL;
+       char *s;
+
+       /* Skip leading periods, otherwise files disappear... */
+       while (*str == '.')
+               str++;
+
+       s = g_strdup (str);
+       rb_sanitize_path_for_msdos_filesystem (s);
+       res = g_uri_escape_string (s, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH_ELEMENT, TRUE);
+       g_free (s);
+       return res;
+}
+
+static char *
+impl_build_dest_uri (RBTransferTarget *target,
+                    RhythmDBEntry *entry,
+                    const char *media_type,
+                    const char *extension)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE (target);
+       const char *in_artist;
+       char *artist, *album, *title;
+       gulong track_number, disc_number;
+       char *number;
+       char *file = NULL;
+       char *storage_uri;
+       char *uri;
+       char *ext;
+       GFile *storage = NULL;
+
+       if (extension != NULL) {
+               ext = g_strconcat (".", extension, NULL);
+       } else {
+               ext = g_strdup ("");
+       }
+
+       in_artist = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM_ARTIST);
+       if (in_artist[0] == '\0') {
+               in_artist = rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ARTIST);
+       }
+       artist = sanitize_path (in_artist);
+       album = sanitize_path (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_ALBUM));
+       title = sanitize_path (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_TITLE));
+
+       /* we really do need to fix this so untagged entries actually have NULL rather than
+        * a translated string.
+        */
+       if (strcmp (artist, _("Unknown")) == 0 && strcmp (album, _("Unknown")) == 0 &&
+           g_str_has_suffix (rhythmdb_entry_get_string (entry, RHYTHMDB_PROP_LOCATION), title)) {
+               /* file isn't tagged, so just use the filename as-is, replacing the extension */
+               char *p;
+
+               p = g_utf8_strrchr (title, -1, '.');
+               if (p != NULL) {
+                       *p = '\0';
+               }
+               file = g_strdup_printf ("%s%s", title, ext);
+       }
+
+       if (file == NULL) {
+               track_number = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_TRACK_NUMBER);
+               disc_number = rhythmdb_entry_get_ulong (entry, RHYTHMDB_PROP_DISC_NUMBER);
+               if (disc_number > 0)
+                       number = g_strdup_printf ("%.02u.%.02u", (guint)disc_number, (guint)track_number);
+               else
+                       number = g_strdup_printf ("%.02u", (guint)track_number);
+
+               /* artist/album/number - title */
+               file = g_strdup_printf (G_DIR_SEPARATOR_S "%s" G_DIR_SEPARATOR_S "%s" G_DIR_SEPARATOR_S 
"%s%%20-%%20%s%s",
+                                       artist, album, number, title, ext);
+               g_free (number);
+       }
+
+       g_free (artist);
+       g_free (album);
+       g_free (title);
+       g_free (ext);
+
+       /* pick storage container to use somehow
+       for (l = priv->storage; l != NULL; l = l->next) {
+       }
+       */
+       if (priv->storage)
+               storage = priv->storage->data;
+
+       if (storage == NULL) {
+               rb_debug ("couldn't find a container to store anything in");
+               g_free (file);
+               return NULL;
+       }
+
+       storage_uri = g_file_get_uri (storage);
+       uri = g_strconcat (storage_uri, file, NULL);
+       g_free (file);
+       g_free (storage_uri);
+
+       return uri;
+}
+
+static gboolean
+impl_track_added (RBTransferTarget *target,
+                 RhythmDBEntry *entry,
+                 const char *dest,
+                 guint64 filesize,
+                 const char *media_type)
+{
+       update_free_space (RB_ANDROID_SOURCE (target));
+       return TRUE;
+}
+
+static void
+impl_selected (RBDisplayPage *page)
+{
+       ensure_loaded (RB_ANDROID_SOURCE (page));
+}
+
+static void
+impl_delete_thyself (RBDisplayPage *page)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE (page);
+
+       if (priv->import_errors != NULL) {
+               rb_display_page_delete_thyself (RB_DISPLAY_PAGE (priv->import_errors));
+               priv->import_errors = NULL;
+       }
+
+       RB_DISPLAY_PAGE_CLASS (rb_android_source_parent_class)->delete_thyself (page);
+}
+
+
+static void
+rb_android_source_init (RBAndroidSource *source)
+{
+
+}
+
+static void
+impl_constructed (GObject *object)
+{
+       RBAndroidSource *source;
+       RBAndroidSourcePrivate *priv;
+       RhythmDBEntryType *entry_type;
+       RBShell *shell;
+       char **output_formats;
+
+       RB_CHAIN_GOBJECT_METHOD (rb_android_source_parent_class, constructed, object);
+       source = RB_ANDROID_SOURCE (object);
+
+       priv = GET_PRIVATE (source);
+
+       rb_device_source_set_display_details (RB_DEVICE_SOURCE (source));
+
+       g_object_get (source,
+                     "shell", &shell,
+                     "entry-type", &entry_type,
+                     NULL);
+
+       g_object_get (shell, "db", &priv->db, NULL);
+
+       priv->import_errors = rb_import_errors_source_new (shell,
+                                                          priv->error_type,
+                                                          entry_type,
+                                                          priv->ignore_type);
+
+       g_object_get (priv->device_info, "output-formats", &output_formats, NULL);
+       if (output_formats != NULL) {
+               GstEncodingTarget *target;
+               int i;
+
+               target = gst_encoding_target_new ("android-device", "device", "", NULL);
+               for (i = 0; output_formats[i] != NULL; i++) {
+                       const char *media_type = rb_gst_mime_type_to_media_type (output_formats[i]);
+                       if (media_type != NULL) {
+                               GstEncodingProfile *profile;
+                               profile = rb_gst_get_encoding_profile (media_type);
+                               if (profile != NULL) {
+                                       gst_encoding_target_add_profile (target, profile);
+                               }
+                       }
+               }
+               g_object_set (source, "encoding-target", target, NULL);
+       }
+       g_strfreev (output_formats);
+
+       g_object_unref (shell);
+}
+
+static void
+impl_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE (object);
+
+       switch (prop_id) {
+       case PROP_IGNORE_ENTRY_TYPE:
+               priv->ignore_type = g_value_get_object (value);
+               break;
+       case PROP_ERROR_ENTRY_TYPE:
+               priv->error_type = g_value_get_object (value);
+               break;
+       case PROP_DEVICE_INFO:
+               priv->device_info = g_value_dup_object (value);
+               break;
+       case PROP_MOUNT:
+               priv->mount = g_value_dup_object (value);
+               break;
+       case PROP_GUDEV_DEVICE:
+               priv->gudev_device = g_value_dup_object (value);
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+impl_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE (object);
+
+       switch (prop_id) {
+       case PROP_IGNORE_ENTRY_TYPE:
+               g_value_set_object (value, priv->ignore_type);
+               break;
+       case PROP_ERROR_ENTRY_TYPE:
+               g_value_set_object (value, priv->error_type);
+               break;
+       case PROP_DEVICE_INFO:
+               g_value_set_object (value, priv->device_info);
+               break;
+       case PROP_MOUNT:
+               g_value_set_object (value, priv->mount);
+               break;
+       case PROP_GUDEV_DEVICE:
+               g_value_set_object (value, priv->gudev_device);
+               break;
+       case PROP_DEVICE_SERIAL:
+               g_value_set_string (value, g_udev_device_get_property (priv->gudev_device, "ID_SERIAL"));
+               break;
+       default:
+               G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+               break;
+       }
+}
+
+static void
+impl_dispose (GObject *object)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE (object);
+
+       if (priv->db != NULL) {
+               if (priv->ignore_type != NULL) {
+                       rhythmdb_entry_delete_by_type (priv->db, priv->ignore_type);
+                       g_clear_object (&priv->ignore_type);
+               }
+               if (priv->error_type != NULL) {
+                       rhythmdb_entry_delete_by_type (priv->db, priv->error_type);
+                       g_clear_object (&priv->error_type);
+               }
+
+               g_clear_object (&priv->db);
+       }
+
+       if (priv->import_job != NULL) {
+               rhythmdb_import_job_cancel (priv->import_job);
+               g_clear_object (&priv->import_job);
+       }
+
+       g_clear_object (&priv->device_info);
+       g_clear_object (&priv->mount);
+       g_clear_object (&priv->gudev_device);
+
+       G_OBJECT_CLASS (rb_android_source_parent_class)->dispose (object);
+}
+
+static void
+impl_finalize (GObject *object)
+{
+       RBAndroidSourcePrivate *priv = GET_PRIVATE (object);
+
+       g_list_free_full (priv->storage, g_object_unref);
+
+       G_OBJECT_CLASS (rb_android_source_parent_class)->finalize (object);
+}
+
+
+static void
+rb_android_device_source_init (RBDeviceSourceInterface *interface)
+{
+       interface->eject = impl_eject;
+}
+
+static void
+rb_android_transfer_target_init (RBTransferTargetInterface *interface)
+{
+       interface->build_dest_uri = impl_build_dest_uri;
+       interface->track_added = impl_track_added;
+}
+
+static void
+rb_android_source_class_init (RBAndroidSourceClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       RBDisplayPageClass *page_class = RB_DISPLAY_PAGE_CLASS (klass);
+       RBSourceClass *source_class = RB_SOURCE_CLASS (klass);
+       RBMediaPlayerSourceClass *mps_class = RB_MEDIA_PLAYER_SOURCE_CLASS (klass);
+
+       object_class->set_property = impl_set_property;
+       object_class->get_property = impl_get_property;
+       object_class->constructed = impl_constructed;
+       object_class->dispose = impl_dispose;
+       object_class->finalize = impl_finalize;
+
+       page_class->delete_thyself = impl_delete_thyself;
+       page_class->selected = impl_selected;
+
+       source_class->can_delete = impl_can_delete;
+       source_class->delete_selected = impl_delete_selected;
+       source_class->can_move_to_trash = (RBSourceFeatureFunc) rb_false_function;
+       source_class->can_paste = impl_can_paste;
+       source_class->paste = impl_paste;
+       source_class->want_uri = rb_device_source_want_uri;
+       source_class->uri_is_source = rb_device_source_uri_is_source;
+
+       mps_class->get_entries = impl_get_entries;
+       mps_class->get_capacity = impl_get_capacity;
+       mps_class->get_free_space = impl_get_free_space;
+       mps_class->delete_entries = impl_delete_entries;
+       mps_class->show_properties = impl_show_properties;
+
+       g_object_class_install_property (object_class,
+                                        PROP_ERROR_ENTRY_TYPE,
+                                        g_param_spec_object ("error-entry-type",
+                                                             "Error entry type",
+                                                             "Entry type to use for import error entries 
added by this source",
+                                                             RHYTHMDB_TYPE_ENTRY_TYPE,
+                                                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+       g_object_class_install_property (object_class,
+                                        PROP_IGNORE_ENTRY_TYPE,
+                                        g_param_spec_object ("ignore-entry-type",
+                                                             "Ignore entry type",
+                                                             "Entry type to use for ignore entries added by 
this source",
+                                                             RHYTHMDB_TYPE_ENTRY_TYPE,
+                                                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+       g_object_class_install_property (object_class,
+                                        PROP_DEVICE_INFO,
+                                        g_param_spec_object ("device-info",
+                                                             "device info",
+                                                             "device information object",
+                                                             MPID_TYPE_DEVICE,
+                                                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+       g_object_class_install_property (object_class,
+                                        PROP_MOUNT,
+                                        g_param_spec_object ("mount",
+                                                             "mount",
+                                                             "GMount object",
+                                                             G_TYPE_MOUNT,
+                                                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+       g_object_class_install_property (object_class,
+                                        PROP_GUDEV_DEVICE,
+                                        g_param_spec_object ("gudev-device",
+                                                             "gudev-device",
+                                                             "GUdev device object",
+                                                             G_UDEV_TYPE_DEVICE,
+                                                             G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
+
+       g_object_class_override_property (object_class, PROP_DEVICE_SERIAL, "serial");
+
+       g_type_class_add_private (klass, sizeof (RBAndroidSourcePrivate));
+}
+
+static void
+rb_android_source_class_finalize (RBAndroidSourceClass *klass)
+{
+}
+
+void
+_rb_android_source_register_type (GTypeModule *module)
+{
+       rb_android_source_register_type (module);
+}
+
diff --git a/plugins/android/rb-android-source.h b/plugins/android/rb-android-source.h
new file mode 100644
index 0000000..44ae358
--- /dev/null
+++ b/plugins/android/rb-android-source.h
@@ -0,0 +1,61 @@
+/*
+ *  Copyright (C) 2015 Jonathan Matthew <jonathan d14n org>
+ *
+ *  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 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  The Rhythmbox authors hereby grant permission for non-GPL compatible
+ *  GStreamer plugins to be used and distributed together with GStreamer
+ *  and Rhythmbox. This permission is above and beyond the permissions granted
+ *  by the GPL license by which Rhythmbox is covered. If you modify this code
+ *  you may extend this exception to your version of the code, but you are not
+ *  obligated to do so. If you do not wish to do so, delete this exception
+ *  statement from your 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, write to the Free Software
+ *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
+ *
+ */
+
+#ifndef __RB_ANDROID_SOURCE_H
+#define __RB_ANDROID_SOURCE_H
+
+#include "rb-shell.h"
+#include "rb-media-player-source.h"
+#include "rhythmdb.h"
+#include "rhythmdb-import-job.h"
+
+G_BEGIN_DECLS
+
+#define RB_TYPE_ANDROID_SOURCE         (rb_android_source_get_type ())
+#define RB_ANDROID_SOURCE(o)           (G_TYPE_CHECK_INSTANCE_CAST ((o), RB_TYPE_ANDROID_SOURCE, 
RBAndroidSource))
+#define RB_ANDROID_SOURCE_CLASS(k)     (G_TYPE_CHECK_CLASS_CAST((k), RB_TYPE_ANDROID_SOURCE, 
RBAndroidSourceClass))
+#define RB_IS_ANDROID_SOURCE(o)        (G_TYPE_CHECK_INSTANCE_TYPE ((o), RB_TYPE_ANDROID_SOURCE))
+#define RB_IS_ANDROID_SOURCE_CLASS(k)  (G_TYPE_CHECK_CLASS_TYPE ((k), RB_TYPE_ANDROID_SOURCE))
+#define RB_ANDROID_SOURCE_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS ((o), RB_TYPE_ANDROID_SOURCE, 
RBAndroidSourceClass))
+
+typedef struct
+{
+       RBMediaPlayerSource parent;
+} RBAndroidSource;
+
+typedef struct
+{
+       RBMediaPlayerSourceClass parent;
+} RBAndroidSourceClass;
+
+GType                          rb_android_source_get_type      (void);
+
+void                           _rb_android_source_register_type (GTypeModule *module);
+
+G_END_DECLS
+
+#endif /* __RB_ANDROID_SOURCE_H */
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 97e8da3..bb3921a 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -44,6 +44,11 @@ lib/rb-file-helpers.c
 lib/rb-util.c
 metadata/rb-metadata-dbus-client.c
 metadata/rb-metadata-gst.c
+[type: gettext/ini]plugins/android/android.plugin.in
+[type: gettext/glade]plugins/android/android-info.ui
+[type: gettext/glade]plugins/android/android-toolbar.ui
+plugins/android/rb-android-plugin.c
+plugins/android/rb-android-source.c
 [type: gettext/ini]plugins/artsearch/artsearch.plugin.in
 plugins/artsearch/artsearch.py
 plugins/artsearch/lastfm.py


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]