summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile.am25
-rw-r--r--NEWS8
-rw-r--r--android/AndroidManifest.xml29
-rw-r--r--android/res/layout/custom_notification_gb.xml22
-rw-r--r--android/res/layout/log_item.xml5
-rw-r--r--android/res/layout/settings.xml37
-rw-r--r--android/res/values/strings.xml6
-rw-r--r--android/src/Bridge.java8
-rw-r--r--android/src/IMain.aidl12
-rw-r--r--android/src/IMainCallback.aidl9
-rw-r--r--android/src/Main.java417
-rw-r--r--android/src/Receiver.java41
-rw-r--r--android/src/Settings.java254
-rw-r--r--src/LogBackend.cxx5
-rw-r--r--src/Main.cxx7
-rw-r--r--src/Main.hxx3
-rw-r--r--src/android/LogListener.cxx46
-rw-r--r--src/android/LogListener.hxx32
-rw-r--r--src/storage/plugins/CurlStorage.cxx14
19 files changed, 927 insertions, 53 deletions
diff --git a/Makefile.am b/Makefile.am
index 69b8056f0..aa37de4bb 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -329,7 +329,8 @@ libjava_a_SOURCES = \
noinst_LIBRARIES += libandroid.a
libandroid_a_SOURCES = \
src/android/Context.cxx src/android/Context.hxx \
- src/android/Environment.cxx src/android/Environment.hxx
+ src/android/Environment.cxx src/android/Environment.hxx \
+ src/android/LogListener.cxx src/android/LogListener.hxx
libandroid_a_CPPFLAGS = $(AM_CPPFLAGS) -Iandroid/build/include
noinst_LIBRARIES += libmain.a
@@ -353,6 +354,7 @@ ANDROID_BUILD_TOOLS_DIR = $(ANDROID_SDK)/build-tools/$(ANDROID_SDK_BUILD_TOOLS_V
ANDROID_SDK_PLATFORM_DIR = $(ANDROID_SDK)/platforms/$(ANDROID_SDK_PLATFORM)
JAVAC = javac
+AIDL = $(ANDROID_BUILD_TOOLS_DIR)/aidl
AAPT = $(ANDROID_BUILD_TOOLS_DIR)/aapt
DX = $(ANDROID_BUILD_TOOLS_DIR)/dx
ZIPALIGN = $(ANDROID_BUILD_TOOLS_DIR)/zipalign
@@ -360,16 +362,26 @@ ZIPALIGN = $(ANDROID_BUILD_TOOLS_DIR)/zipalign
ANDROID_XML_RES := $(wildcard $(srcdir)/android/res/*/*.xml)
ANDROID_XML_RES_COPIES := $(patsubst $(srcdir)/android/%,android/build/%,$(ANDROID_XML_RES))
-JAVA_SOURCE_NAMES = Bridge.java Loader.java Main.java
+JAVA_SOURCE_NAMES = Bridge.java Loader.java Main.java Settings.java
JAVA_SOURCE_PATHS = $(addprefix $(srcdir)/android/src/,$(JAVA_SOURCE_NAMES))
JAVA_CLASSFILES_DIR = android/build/classes
+AIDL_FILES = $(wildcard $(srcdir)/android/src/*.aidl)
+AIDL_JAVA_FILES = $(patsubst $(srcdir)/android/src/%.aidl,android/build/src/org/musicpd/%.java,$(AIDL_FILES))
+
+android/build/src/org/musicpd/IMain.java: android/build/src/org/musicpd/IMainCallback.java
+
+$(AIDL_JAVA_FILES): android/build/src/org/musicpd/%.java: $(srcdir)/android/src/%.aidl
+ @$(MKDIR_P) $(@D)
+ @cp $< $(@D)/
+ $(AIDL) -Iandroid/build/src -oandroid/build/src $(patsubst %.java,%.aidl,$@)
+
$(ANDROID_XML_RES_COPIES): $(ANDROID_XML_RES)
@$(MKDIR_P) $(dir $@)
cp $(patsubst android/build/%,$(srcdir)/android/%,$@) $@
-android/build/resources.apk: $(ANDROID_XML_RES_COPIES) android/build/res/drawable/icon.png
+android/build/resources.apk: $(ANDROID_XML_RES_COPIES) android/build/res/drawable/icon.png android/build/res/drawable/notification_icon.png
@$(MKDIR_P) android/build/gen
$(AAPT) package -f -m --auto-add-overlay \
--custom-package org.musicpd \
@@ -382,15 +394,15 @@ android/build/resources.apk: $(ANDROID_XML_RES_COPIES) android/build/res/drawabl
# R.java is generated by aapt, when resources.apk is generated
android/build/gen/org/musicpd/R.java: android/build/resources.apk
-android/build/classes.dex: $(JAVA_SOURCE_PATHS) android/build/gen/org/musicpd/R.java
+android/build/classes.dex: $(JAVA_SOURCE_PATHS) $(AIDL_JAVA_FILES) android/build/gen/org/musicpd/R.java
@$(MKDIR_P) $(JAVA_CLASSFILES_DIR)
$(JAVAC) -source 1.6 -target 1.6 -Xlint:-options \
-cp $(ANDROID_SDK_PLATFORM_DIR)/android.jar:$(JAVA_CLASSFILES_DIR) \
+ -h android/build/include \
-d $(JAVA_CLASSFILES_DIR) $^
$(DX) --dex --output $@ $(JAVA_CLASSFILES_DIR)
android/build/include/org_musicpd_Bridge.h: android/build/classes.dex
- javah -classpath $(ANDROID_SDK_PLATFORM_DIR)/android.jar:$(JAVA_CLASSFILES_DIR) -d $(@D) org.musicpd.Bridge
BUILT_SOURCES = android/build/include/org_musicpd_Bridge.h
@@ -403,6 +415,9 @@ android/build/res/drawable/icon.png: mpd.svg
mkdir -p $(@D)
rsvg-convert --width=48 --height=48 $< -o $@
+android/build/res/drawable/notification_icon.png: android/build/res/drawable/icon.png
+ convert $< -colorspace Gray -gamma 2.2 $@
+
.DELETE_ON_ERROR: android/build/unsigned.apk
android/build/unsigned.apk: android/build/classes.dex android/build/resources.apk android/build/lib/$(ANDROID_ABI)/libmpd.so
cp android/build/resources.apk $@
diff --git a/NEWS b/NEWS
index 2ee55c7df..dd3e02033 100644
--- a/NEWS
+++ b/NEWS
@@ -44,6 +44,14 @@ ver 0.21 (not yet released)
* systemd watchdog support
* require GCC 6
+ver 0.20.22 (not yet released)
+* storage
+ - curl: URL-encode paths
+* Android
+ - now runs as a service
+ - add button to start/stop MPD
+ - add option to auto-start on boot
+
ver 0.20.21 (2018/08/17)
* database
- proxy: add "password" setting
diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml
index b6abd1124..5dddf034e 100644
--- a/android/AndroidManifest.xml
+++ b/android/AndroidManifest.xml
@@ -2,23 +2,32 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.musicpd"
android:installLocation="auto"
- android:versionCode="20"
- android:versionName="0.20.21">
+ android:versionCode="21"
+ android:versionName="0.20.22">
- <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="21"/>
+ <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="26"/>
- <application android:icon="@drawable/icon" android:label="@string/app_name">
- <activity android:name=".Main"
- android:label="@string/app_name"
- android:launchMode="singleInstance">
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
+ <uses-permission android:name="android.permission.WAKE_LOCK"/>
+ <uses-permission android:name="android.permission.INTERNET"/>
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+
+ <application android:allowBackup="true"
+ android:icon="@drawable/icon"
+ android:label="@string/app_name">
+ <activity android:name=".Settings"
+ android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
+ <receiver android:name=".Receiver">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
+ </receiver>
+ <service android:name=".Main" android:process=":main"/>
</application>
- <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
- <uses-permission android:name="android.permission.WAKE_LOCK"/>
- <uses-permission android:name="android.permission.INTERNET"/>
</manifest>
diff --git a/android/res/layout/custom_notification_gb.xml b/android/res/layout/custom_notification_gb.xml
new file mode 100644
index 000000000..92a6036e2
--- /dev/null
+++ b/android/res/layout/custom_notification_gb.xml
@@ -0,0 +1,22 @@
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/layout"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:padding="10dp" >
+ <ImageView android:id="@+id/image"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:layout_alignParentLeft="true"
+ android:layout_marginRight="10dp" />
+ <TextView android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@id/image"
+ style="Custom Notification Title" />
+ <TextView android:id="@+id/text"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toRightOf="@id/image"
+ android:layout_below="@id/title"
+ style="Custom Notification Text" />
+</RelativeLayout>
diff --git a/android/res/layout/log_item.xml b/android/res/layout/log_item.xml
new file mode 100644
index 000000000..e6e74c913
--- /dev/null
+++ b/android/res/layout/log_item.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:typeface="monospace" />
diff --git a/android/res/layout/settings.xml b/android/res/layout/settings.xml
new file mode 100644
index 000000000..46e471b05
--- /dev/null
+++ b/android/res/layout/settings.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+ <ToggleButton
+ android:id="@+id/run"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textOn="@string/toggle_button_run_on"
+ android:textOff="@string/toggle_button_run_off" />
+
+ <CheckBox
+ android:id="@+id/run_on_boot"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/checkbox_run_on_boot" />
+
+ <CheckBox
+ android:id="@+id/wakelock"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/checkbox_wakelock" />
+
+ <TextView
+ android:id="@+id/status"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <ListView
+ android:id="@+id/log_list"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="10dip" />
+
+</LinearLayout>
diff --git a/android/res/values/strings.xml b/android/res/values/strings.xml
index 416c8de9f..fc5a15bda 100644
--- a/android/res/values/strings.xml
+++ b/android/res/values/strings.xml
@@ -2,4 +2,10 @@
<resources>
<string name="app_name">MPD</string>
+ <string name="notification_title_mpd_running">Music Player Daemon is running</string>
+ <string name="notification_text_mpd_running">Touch for MPD options.</string>
+ <string name="toggle_button_run_on">MPD is running</string>
+ <string name="toggle_button_run_off">MPD is not running</string>
+ <string name="checkbox_run_on_boot">Run MPD automatically on boot</string>
+ <string name="checkbox_wakelock">Prevent suspend when MPD is running (Wakelock)</string>
</resources>
diff --git a/android/src/Bridge.java b/android/src/Bridge.java
index be8eabb6b..fad919204 100644
--- a/android/src/Bridge.java
+++ b/android/src/Bridge.java
@@ -25,6 +25,12 @@ import android.content.Context;
* Bridge to native code.
*/
public class Bridge {
- public static native void run(Context context);
+
+ /* used by jni */
+ public interface LogListener {
+ public void onLog(int priority, String msg);
+ }
+
+ public static native void run(Context context, LogListener logListener);
public static native void shutdown();
}
diff --git a/android/src/IMain.aidl b/android/src/IMain.aidl
new file mode 100644
index 000000000..ba7050d79
--- /dev/null
+++ b/android/src/IMain.aidl
@@ -0,0 +1,12 @@
+package org.musicpd;
+import org.musicpd.IMainCallback;
+
+interface IMain
+{
+ void start();
+ void stop();
+ void setWakelockEnabled(boolean enabled);
+ boolean isRunning();
+ void registerCallback(IMainCallback cb);
+ void unregisterCallback(IMainCallback cb);
+}
diff --git a/android/src/IMainCallback.aidl b/android/src/IMainCallback.aidl
new file mode 100644
index 000000000..c8cdaa4a0
--- /dev/null
+++ b/android/src/IMainCallback.aidl
@@ -0,0 +1,9 @@
+package org.musicpd;
+
+interface IMainCallback
+{
+ void onStarted();
+ void onStopped();
+ void onError(String error);
+ void onLog(int priority, String msg);
+}
diff --git a/android/src/Main.java b/android/src/Main.java
index da64a1976..5a5d9d048 100644
--- a/android/src/Main.java
+++ b/android/src/Main.java
@@ -19,57 +19,406 @@
package org.musicpd;
-import android.app.Activity;
-import android.os.Bundle;
+import android.annotation.TargetApi;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
import android.os.Build;
-import android.os.Handler;
-import android.os.Message;
-import android.widget.TextView;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
import android.util.Log;
+import android.widget.RemoteViews;
-public class Main extends Activity implements Runnable {
- private static final String TAG = "MPD";
+public class Main extends Service implements Runnable {
+ private static final String TAG = "Main";
+ private static final String REMOTE_ERROR = "MPD process was killed";
+ private static final int MAIN_STATUS_ERROR = -1;
+ private static final int MAIN_STATUS_STOPPED = 0;
+ private static final int MAIN_STATUS_STARTED = 1;
- Thread thread;
+ private static final int MSG_SEND_STATUS = 0;
+ private static final int MSG_SEND_LOG = 1;
- TextView textView;
+ private Thread mThread = null;
+ private int mStatus = MAIN_STATUS_STOPPED;
+ private boolean mAbort = false;
+ private String mError = null;
+ private final RemoteCallbackList<IMainCallback> mCallbacks = new RemoteCallbackList<IMainCallback>();
+ private final IBinder mBinder = new MainStub(this);
+ private PowerManager.WakeLock mWakelock = null;
- final Handler quitHandler = new Handler() {
- public void handleMessage(Message msg) {
- textView.setText("Music Player Daemon has quit");
+ static class MainStub extends IMain.Stub {
+ private Main mService;
+ MainStub(Main service) {
+ mService = service;
+ }
+ public void start() {
+ mService.start();
+ }
+ public void stop() {
+ mService.stop();
+ }
+ public void setWakelockEnabled(boolean enabled) {
+ mService.setWakelockEnabled(enabled);
+ }
+ public boolean isRunning() {
+ return mService.isRunning();
+ }
+ public void registerCallback(IMainCallback cb) {
+ mService.registerCallback(cb);
+ }
+ public void unregisterCallback(IMainCallback cb) {
+ mService.unregisterCallback(cb);
+ }
+ }
- // TODO: what now? restart?
+ private synchronized void sendMessage(int what, int arg1, int arg2, Object obj) {
+ int i = mCallbacks.beginBroadcast();
+ while (i > 0) {
+ i--;
+ final IMainCallback cb = mCallbacks.getBroadcastItem(i);
+ try {
+ switch (what) {
+ case MSG_SEND_STATUS:
+ switch (arg1) {
+ case MAIN_STATUS_ERROR:
+ cb.onError((String)obj);
+ break;
+ case MAIN_STATUS_STOPPED:
+ cb.onStopped();
+ break;
+ case MAIN_STATUS_STARTED:
+ cb.onStarted();
+ break;
+ }
+ break;
+ case MSG_SEND_LOG:
+ cb.onLog(arg1, (String) obj);
+ break;
+ }
+ } catch (RemoteException e) {
}
- };
+ }
+ mCallbacks.finishBroadcast();
+ }
- @Override protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
+ private Bridge.LogListener mLogListener = new Bridge.LogListener() {
+ @Override
+ public void onLog(int priority, String msg) {
+ sendMessage(MSG_SEND_LOG, priority, 0, msg);
+ }
+ };
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ start();
+ if (intent != null && intent.getBooleanExtra("wakelock", false))
+ setWakelockEnabled(true);
+ return START_STICKY;
+ }
+ @Override
+ public void run() {
if (!Loader.loaded) {
- TextView tv = new TextView(this);
- tv.setText("Failed to load the native MPD libary.\n" +
- "Report this problem to us, and include the following information:\n" +
- "SUPPORTED_ABIS=" + String.join(", ", Build.SUPPORTED_ABIS) + "\n" +
- "PRODUCT=" + Build.PRODUCT + "\n" +
- "FINGERPRINT=" + Build.FINGERPRINT + "\n" +
- "error=" + Loader.error);
- setContentView(tv);
+ final String error = "Failed to load the native MPD libary.\n" +
+ "Report this problem to us, and include the following information:\n" +
+ "SUPPORTED_ABIS=" + String.join(", ", Build.SUPPORTED_ABIS) + "\n" +
+ "PRODUCT=" + Build.PRODUCT + "\n" +
+ "FINGERPRINT=" + Build.FINGERPRINT + "\n" +
+ "error=" + Loader.error;
+ setStatus(MAIN_STATUS_ERROR, error);
+ stopSelf();
return;
}
+ synchronized (this) {
+ if (mAbort)
+ return;
+ setStatus(MAIN_STATUS_STARTED, null);
+ }
+ Bridge.run(this, mLogListener);
+ setStatus(MAIN_STATUS_STOPPED, null);
+ }
+
+ private synchronized void setStatus(int status, String error) {
+ mStatus = status;
+ mError = error;
+ sendMessage(MSG_SEND_STATUS, mStatus, 0, mError);
+ }
+
+ @TargetApi(Build.VERSION_CODES.GINGERBREAD)
+ private Notification buildNotificationGB(int title, int text, int icon, PendingIntent contentIntent) {
+ final Notification notification = new Notification();
+ notification.icon = R.drawable.icon;
+ notification.contentView = new RemoteViews(getPackageName(), R.layout.custom_notification_gb);
+ notification.contentView.setImageViewResource(R.id.image, icon);
+ notification.contentView.setTextViewText(R.id.title, getText(title));
+ notification.contentView.setTextViewText(R.id.text, getText(text));
+ notification.contentIntent = contentIntent;
+ return notification;
+ }
+
+ @TargetApi(Build.VERSION_CODES.HONEYCOMB)
+ private Notification buildNotificationHC(int title, int text, int icon, PendingIntent contentIntent) {
+ return new Notification.Builder(this)
+ .setContentTitle(getText(title))
+ .setContentText(getText(text))
+ .setSmallIcon(icon)
+ .setContentIntent(contentIntent)
+ .getNotification();
+ }
+
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+ private Notification buildNotificationJB(int title, int text, int icon, PendingIntent contentIntent) {
+ return new Notification.Builder(this)
+ .setContentTitle(getText(title))
+ .setContentText(getText(text))
+ .setSmallIcon(icon)
+ .setContentIntent(contentIntent)
+ .build();
+ }
+
+ private void start() {
+ if (mThread != null)
+ return;
+ mThread = new Thread(this);
+ mThread.start();
+
+ final Intent mainIntent = new Intent(this, Settings.class);
+ mainIntent.setAction("android.intent.action.MAIN");
+ mainIntent.addCategory("android.intent.category.LAUNCHER");
+ final PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
+ mainIntent, PendingIntent.FLAG_CANCEL_CURRENT);
+
+ Notification notification;
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
+ notification = buildNotificationJB(
+ R.string.notification_title_mpd_running,
+ R.string.notification_text_mpd_running,
+ R.drawable.notification_icon,
+ contentIntent);
+ else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
+ notification = buildNotificationHC(
+ R.string.notification_title_mpd_running,
+ R.string.notification_text_mpd_running,
+ R.drawable.notification_icon,
+ contentIntent);
+ else
+ notification = buildNotificationGB(
+ R.string.notification_title_mpd_running,
+ R.string.notification_text_mpd_running,
+ R.drawable.notification_icon,
+ contentIntent);
+
+ startForeground(R.string.notification_title_mpd_running, notification);
+ startService(new Intent(this, Main.class));
+ }
+
+ private void stop() {
+ if (mThread != null) {
+ if (mThread.isAlive()) {
+ synchronized (this) {
+ if (mStatus == MAIN_STATUS_STARTED)
+ Bridge.shutdown();
+ else
+ mAbort = true;
+ }
+ }
+ try {
+ mThread.join();
+ mThread = null;
+ mAbort = false;
+ } catch (InterruptedException ie) {}
+ }
+ setWakelockEnabled(false);
+ stopForeground(true);
+ stopSelf();
+ }
+
+ private void setWakelockEnabled(boolean enabled) {
+ if (enabled && mWakelock == null) {
+ PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
+ mWakelock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+ mWakelock.acquire();
+ Log.d(TAG, "Wakelock acquired");
+ } else if (!enabled && mWakelock != null) {
+ mWakelock.release();
+ mWakelock = null;
+ Log.d(TAG, "Wakelock released");
+ }
+ }
+
+ private boolean isRunning() {
+ return mThread != null && mThread.isAlive();
+ }
+
+ private void registerCallback(IMainCallback cb) {
+ if (cb != null) {
+ mCallbacks.register(cb);
+ sendMessage(MSG_SEND_STATUS, mStatus, 0, mError);
+ }
+ }
- if (thread == null || !thread.isAlive()) {
- thread = new Thread(this, "NativeMain");
- thread.start();
+ private void unregisterCallback(IMainCallback cb) {
+ if (cb != null) {
+ mCallbacks.unregister(cb);
}
+ }
+
+ /*
+ * Client that bind the Main Service in order to send commands and receive callback
+ */
+ public static class Client {
- textView = new TextView(this);
- textView.setText("Music Player Daemon is running"
- + "\nCAUTION: this version is EXPERIMENTAL!");
- setContentView(textView);
+ public interface Callback {
+ public void onStarted();
+ public void onStopped();
+ public void onError(String error);
+ public void onLog(int priority, String msg);
+ }
+
+ private boolean mBound = false;
+ private final Context mContext;
+ private Callback mCallback;
+ private IMain mIMain = null;
+
+ private final IMainCallback.Stub mICallback = new IMainCallback.Stub() {
+
+ @Override
+ public void onStopped() throws RemoteException {
+ mCallback.onStopped();
+ }
+
+ @Override
+ public void onStarted() throws RemoteException {
+ mCallback.onStarted();
+ }
+
+ @Override
+ public void onError(String error) throws RemoteException {
+ mCallback.onError(error);
+ }
+
+ @Override
+ public void onLog(int priority, String msg) throws RemoteException {
+ mCallback.onLog(priority, msg);
+ }
+ };
+
+ private final ServiceConnection mServiceConnection = new ServiceConnection() {
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ synchronized (this) {
+ mIMain = IMain.Stub.asInterface(service);
+ try {
+ if (mCallback != null)
+ mIMain.registerCallback(mICallback);
+ } catch (RemoteException e) {
+ if (mCallback != null)
+ mCallback.onError(REMOTE_ERROR);
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (mCallback != null)
+ mCallback.onError(REMOTE_ERROR);
+ }
+ };
+
+ public Client(Context context, Callback cb) throws IllegalArgumentException {
+ if (context == null)
+ throw new IllegalArgumentException("Context can't be null");
+ mContext = context;
+ mCallback = cb;
+ mBound = mContext.bindService(new Intent(mContext, Main.class), mServiceConnection, Context.BIND_AUTO_CREATE);
+ }
+
+ public boolean start() {
+ synchronized (this) {
+ if (mIMain != null) {
+ try {
+ mIMain.start();
+ return true;
+ } catch (RemoteException e) {
+ }
+ }
+ return false;
+ }
+ }
+
+ public boolean stop() {
+ synchronized (this) {
+ if (mIMain != null) {
+ try {
+ mIMain.stop();
+ return true;
+ } catch (RemoteException e) {
+ }
+ }
+ return false;
+ }
+ }
+
+ public boolean setWakelockEnabled(boolean enabled) {
+ synchronized (this) {
+ if (mIMain != null) {
+ try {
+ mIMain.setWakelockEnabled(enabled);
+ return true;
+ } catch (RemoteException e) {
+ }
+ }
+ return false;
+ }
+ }
+
+ public boolean isRunning() {
+ synchronized (this) {
+ if (mIMain != null) {
+ try {
+ return mIMain.isRunning();
+ } catch (RemoteException e) {
+ }
+ }
+ return false;
+ }
+ }
+
+ public void release() {
+ if (mBound) {
+ synchronized (this) {
+ if (mIMain != null && mICallback != null) {
+ try {
+ if (mCallback != null)
+ mIMain.unregisterCallback(mICallback);
+ } catch (RemoteException e) {
+ }
+ }
+ }
+ mBound = false;
+ mContext.unbindService(mServiceConnection);
+ }
+ }
}
- @Override public void run() {
- Bridge.run(this);
- quitHandler.sendMessage(quitHandler.obtainMessage());
+ /*
+ * start Main service without any callback
+ */
+ public static void start(Context context, boolean wakelock) {
+ context.startService(new Intent(context, Main.class).putExtra("wakelock", wakelock));
}
}
diff --git a/android/src/Receiver.java b/android/src/Receiver.java
new file mode 100644
index 000000000..e24a29fbc
--- /dev/null
+++ b/android/src/Receiver.java
@@ -0,0 +1,41 @@
+
+/*
+ * Copyright (C) 2003-2014 The Music Player Daemon Project
+ * http://www.musicpd.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.
+ *
+ * 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package org.musicpd;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class Receiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d("Receiver", "onReceive: " + intent);
+ if (intent.getAction() == "android.intent.action.BOOT_COMPLETED") {
+ if (Settings.Preferences.getBoolean(context,
+ Settings.Preferences.KEY_RUN_ON_BOOT, false)) {
+ final boolean wakelock = Settings.Preferences.getBoolean(context,
+ Settings.Preferences.KEY_WAKELOCK, false);
+ Main.start(context, wakelock);
+ }
+ }
+ }
+}
diff --git a/android/src/Settings.java b/android/src/Settings.java
new file mode 100644
index 000000000..69b5305e2
--- /dev/null
+++ b/android/src/Settings.java
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.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.
+ *
+ * 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+package org.musicpd;
+
+import java.util.LinkedList;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.widget.ArrayAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.CompoundButton.OnCheckedChangeListener;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+
+public class Settings extends Activity {
+ private static final String TAG = "Settings";
+ private Main.Client mClient;
+ private TextView mTextStatus;
+ private ToggleButton mRunButton;
+ private boolean mFirstRun;
+ private LinkedList<String> mLogListArray = new LinkedList<String>();
+ private ListView mLogListView;
+ private ArrayAdapter<String> mLogListAdapter;
+
+ private static final int MAX_LOGS = 500;
+
+ private static final int MSG_ERROR = 0;
+ private static final int MSG_STOPPED = 1;
+ private static final int MSG_STARTED = 2;
+ private static final int MSG_LOG = 3;
+
+ public static class Preferences {
+ public static final String KEY_RUN_ON_BOOT ="run_on_boot";
+ public static final String KEY_WAKELOCK ="wakelock";
+
+ public static SharedPreferences get(Context context) {
+ return context.getSharedPreferences(TAG, MODE_PRIVATE);
+ }
+
+ public static void putBoolean(Context context, String key, boolean value) {
+ final SharedPreferences prefs = get(context);
+
+ if (prefs == null)
+ return;
+ final Editor editor = prefs.edit();
+ editor.putBoolean(key, value);
+ editor.apply();
+ }
+
+ public static boolean getBoolean(Context context, String key, boolean defValue) {
+ final SharedPreferences prefs = get(context);
+
+ return prefs != null ? prefs.getBoolean(key, defValue) : defValue;
+ }
+ }
+
+ private Handler mHandler = new Handler(new Handler.Callback() {
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_ERROR:
+ Log.d(TAG, "onError");
+
+ mClient.release();
+ connectClient();
+
+ mRunButton.setEnabled(false);
+ mRunButton.setChecked(false);
+
+ mTextStatus.setText((String)msg.obj);
+ mFirstRun = true;
+ break;
+ case MSG_STOPPED:
+ Log.d(TAG, "onStopped");
+ mRunButton.setEnabled(true);
+ if (!mFirstRun && Preferences.getBoolean(Settings.this, Preferences.KEY_RUN_ON_BOOT, false))
+ mRunButton.setChecked(true);
+ else
+ mRunButton.setChecked(false);
+ mFirstRun = true;
+ break;
+ case MSG_STARTED:
+ Log.d(TAG, "onStarted");
+ mRunButton.setChecked(true);
+ mFirstRun = true;
+ mTextStatus.setText("CAUTION: this version is EXPERIMENTAL!"); // XXX
+ break;
+ case MSG_LOG:
+ if (mLogListArray.size() > MAX_LOGS)
+ mLogListArray.remove(0);
+ String priority;
+ switch (msg.arg1) {
+ case Log.DEBUG:
+ priority = "D";
+ break;
+ case Log.ERROR:
+ priority = "E";
+ break;
+ case Log.INFO:
+ priority = "I";
+ break;
+ case Log.VERBOSE:
+ priority = "V";
+ break;
+ case Log.WARN:
+ priority = "W";
+ break;
+ default:
+ priority = "";
+ }
+ mLogListArray.add(priority + "/ " + (String)msg.obj);
+ mLogListAdapter.notifyDataSetChanged();
+
+ break;
+ }
+ return true;
+ }
+ });
+
+ private final OnCheckedChangeListener mOnRunChangeListener = new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ if (mClient != null) {
+ if (isChecked) {
+ mClient.start();
+ if (Preferences.getBoolean(Settings.this,
+ Preferences.KEY_WAKELOCK, false))
+ mClient.setWakelockEnabled(true);
+ } else {
+ mClient.stop();
+ }
+ }
+ }
+ };
+
+ private final OnCheckedChangeListener mOnRunOnBootChangeListener = new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ Preferences.putBoolean(Settings.this, Preferences.KEY_RUN_ON_BOOT, isChecked);
+ if (isChecked && mClient != null && !mRunButton.isChecked())
+ mRunButton.setChecked(true);
+ }
+ };
+
+ private final OnCheckedChangeListener mOnWakelockChangeListener = new OnCheckedChangeListener() {
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ Preferences.putBoolean(Settings.this, Preferences.KEY_WAKELOCK, isChecked);
+ if (mClient != null && mClient.isRunning())
+ mClient.setWakelockEnabled(isChecked);
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ setContentView(R.layout.settings);
+ mRunButton = (ToggleButton) findViewById(R.id.run);
+ mRunButton.setOnCheckedChangeListener(mOnRunChangeListener);
+
+ mTextStatus = (TextView) findViewById(R.id.status);
+
+ mLogListAdapter = new ArrayAdapter<String>(this, R.layout.log_item, mLogListArray);
+
+ mLogListView = (ListView) findViewById(R.id.log_list);
+ mLogListView.setAdapter(mLogListAdapter);
+ mLogListView.setTranscriptMode(ListView.TRANSCRIPT_MODE_NORMAL);
+
+ CheckBox checkbox = (CheckBox) findViewById(R.id.run_on_boot);
+ checkbox.setOnCheckedChangeListener(mOnRunOnBootChangeListener);
+ if (Preferences.getBoolean(this, Preferences.KEY_RUN_ON_BOOT, false))
+ checkbox.setChecked(true);
+
+ checkbox = (CheckBox) findViewById(R.id.wakelock);
+ checkbox.setOnCheckedChangeListener(mOnWakelockChangeListener);
+ if (Preferences.getBoolean(this, Preferences.KEY_WAKELOCK, false))
+ checkbox.setChecked(true);
+
+ super.onCreate(savedInstanceState);
+ }
+
+ private void connectClient() {
+ mClient = new Main.Client(this, new Main.Client.Callback() {
+
+ private void removeMessages() {
+ /* don't remove log messages */
+ mHandler.removeMessages(MSG_STOPPED);
+ mHandler.removeMessages(MSG_STARTED);
+ mHandler.removeMessages(MSG_ERROR);
+ }
+
+ @Override
+ public void onStopped() {
+ removeMessages();
+ mHandler.sendEmptyMessage(MSG_STOPPED);
+ }
+
+ @Override
+ public void onStarted() {
+ removeMessages();
+ mHandler.sendEmptyMessage(MSG_STARTED);
+ }
+
+ @Override
+ public void onError(String error) {
+ removeMessages();
+ mHandler.sendMessage(Message.obtain(mHandler, MSG_ERROR, error));
+ }
+
+ @Override
+ public void onLog(int priority, String msg) {
+ mHandler.sendMessage(Message.obtain(mHandler, MSG_LOG, priority, 0, msg));
+ }
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ mFirstRun = false;
+ connectClient();
+ super.onStart();
+ }
+
+ @Override
+ protected void onStop() {
+ mClient.release();
+ mClient = null;
+ super.onStop();
+ }
+}
diff --git a/src/LogBackend.cxx b/src/LogBackend.cxx
index 767170d2a..16e784efb 100644
--- a/src/LogBackend.cxx
+++ b/src/LogBackend.cxx
@@ -34,6 +34,8 @@
#ifdef ANDROID
#include <android/log.h>
+#include "android/LogListener.hxx"
+#include "Main.hxx"
static int
ToAndroidLogLevel(LogLevel log_level) noexcept
@@ -179,6 +181,9 @@ Log(const Domain &domain, LogLevel level, const char *msg) noexcept
#ifdef ANDROID
__android_log_print(ToAndroidLogLevel(level), "MPD",
"%s: %s", domain.GetName(), msg);
+ if (logListener != nullptr)
+ logListener->OnLog(Java::GetEnv(), ToAndroidLogLevel(level),
+ "%s: %s", domain.GetName(), msg);
#else
if (level < log_threshold)
diff --git a/src/Main.cxx b/src/Main.cxx
index 9a56ad4e9..4e1a65b85 100644
--- a/src/Main.cxx
+++ b/src/Main.cxx
@@ -95,6 +95,7 @@
#include "java/File.hxx"
#include "android/Environment.hxx"
#include "android/Context.hxx"
+#include "android/LogListener.hxx"
#include "fs/FileSystem.hxx"
#include "org_musicpd_Bridge.h"
#endif
@@ -128,6 +129,7 @@ static constexpr unsigned DEFAULT_BUFFER_BEFORE_PLAY = 10;
#ifdef ANDROID
Context *context;
+LogListener *logListener;
#endif
Instance *instance;
@@ -723,16 +725,19 @@ mpd_main_after_fork(const ConfigData &raw_config, const Config &config)
gcc_visibility_default
JNIEXPORT void JNICALL
-Java_org_musicpd_Bridge_run(JNIEnv *env, jclass, jobject _context)
+Java_org_musicpd_Bridge_run(JNIEnv *env, jclass, jobject _context, jobject _logListener)
{
Java::Init(env);
Java::File::Initialise(env);
Environment::Initialise(env);
context = new Context(env, _context);
+ if (_logListener != nullptr)
+ logListener = new LogListener(env, _logListener);
mpd_main(0, nullptr);
+ delete logListener;
delete context;
Environment::Deinitialise(env);
}
diff --git a/src/Main.hxx b/src/Main.hxx
index 8fb267919..47c330ac0 100644
--- a/src/Main.hxx
+++ b/src/Main.hxx
@@ -25,7 +25,10 @@ class Context;
struct Instance;
#ifdef ANDROID
+#include "android/LogListener.hxx"
+
extern Context *context;
+extern LogListener *logListener;
#endif
extern Instance *instance;
diff --git a/src/android/LogListener.cxx b/src/android/LogListener.cxx
new file mode 100644
index 000000000..c7fed8811
--- /dev/null
+++ b/src/android/LogListener.cxx
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.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.
+ *
+ * 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+#include "LogListener.hxx"
+#include "java/Class.hxx"
+#include "java/String.hxx"
+#include "util/AllocatedString.hxx"
+#include "util/FormatString.hxx"
+
+void
+LogListener::OnLog(JNIEnv *env, int priority, const char *fmt, ...) const
+{
+ assert(env != nullptr);
+
+ Java::Class cls(env, env->GetObjectClass(Get()));
+
+ jmethodID method = env->GetMethodID(cls, "onLog",
+ "(ILjava/lang/String;)V");
+
+ assert(method);
+
+ va_list args;
+ va_start(args, fmt);
+ const auto log = FormatStringV(fmt, args);
+ va_end(args);
+
+ env->CallVoidMethod(Get(), method, priority,
+ Java::String(env, log.c_str()).Get());
+}
diff --git a/src/android/LogListener.hxx b/src/android/LogListener.hxx
new file mode 100644
index 000000000..2c81078fe
--- /dev/null
+++ b/src/android/LogListener.hxx
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2003-2018 The Music Player Daemon Project
+ * http://www.musicpd.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.
+ *
+ * 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPD_ANDROID_LOG_LISTENER_HXX
+#define MPD_ANDROID_LOG_LISTENER_HXX
+
+#include "java/Object.hxx"
+
+class LogListener : public Java::Object {
+public:
+ LogListener(JNIEnv *env, jobject obj):Java::Object(env, obj) {}
+
+ void OnLog(JNIEnv *env, int priority, const char *fmt, ...) const;
+};
+
+#endif
diff --git a/src/storage/plugins/CurlStorage.cxx b/src/storage/plugins/CurlStorage.cxx
index b924c7cd8..37d00b6c7 100644
--- a/src/storage/plugins/CurlStorage.cxx
+++ b/src/storage/plugins/CurlStorage.cxx
@@ -36,6 +36,7 @@
#include "thread/Cond.hxx"
#include "util/ASCII.hxx"
#include "util/ChronoUtil.hxx"
+#include "util/IterableSplitString.hxx"
#include "util/RuntimeError.hxx"
#include "util/StringCompare.hxx"
#include "util/StringFormat.hxx"
@@ -77,9 +78,18 @@ CurlStorage::MapUTF8(const char *uri_utf8) const noexcept
if (StringIsEmpty(uri_utf8))
return base;
- // TODO: escape the given URI
+ CurlEasy easy;
+ std::string path_esc;
+
+ for (auto elt: IterableSplitString(uri_utf8, '/')) {
+ char *elt_esc = easy.Escape(elt.data, elt.size);
+ if (!path_esc.empty())
+ path_esc.push_back('/');
+ path_esc += elt_esc;
+ curl_free(elt_esc);
+ }
- return PathTraitsUTF8::Build(base.c_str(), uri_utf8);
+ return PathTraitsUTF8::Build(base.c_str(), path_esc.c_str());
}
const char *