Main.java 11 KB
Newer Older
Max Kellermann's avatar
Max Kellermann committed
1
/*
2
 * Copyright 2003-2018 The Music Player Daemon Project
Max Kellermann's avatar
Max Kellermann committed
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
 * 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;

22 23
import android.annotation.TargetApi;
import android.app.Notification;
24
import android.app.NotificationManager;
25 26 27 28 29 30
import android.app.PendingIntent;
import android.app.Service;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
Max Kellermann's avatar
Max Kellermann committed
31
import android.os.Build;
32 33 34 35
import android.os.IBinder;
import android.os.PowerManager;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
Max Kellermann's avatar
Max Kellermann committed
36
import android.util.Log;
37
import android.widget.RemoteViews;
Max Kellermann's avatar
Max Kellermann committed
38

39 40 41
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

42 43 44 45 46 47
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;
Max Kellermann's avatar
Max Kellermann committed
48

49 50
	private static final int MSG_SEND_STATUS = 0;
	private static final int MSG_SEND_LOG = 1;
Max Kellermann's avatar
Max Kellermann committed
51

52 53 54 55 56 57 58
	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;
59

60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
	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);
		}
	}
84

85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
	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) {
110
			}
111 112 113
		}
		mCallbacks.finishBroadcast();
	}
114

115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
	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;
	}
Max Kellermann's avatar
Max Kellermann committed
134

135 136
	@Override
	public void run() {
Max Kellermann's avatar
Max Kellermann committed
137
		if (!Loader.loaded) {
138 139 140 141 142 143 144 145
			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();
Max Kellermann's avatar
Max Kellermann committed
146 147
			return;
		}
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
		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);
	}

163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
	private Notification.Builder createNotificationBuilderWithChannel() {
		final NotificationManager notificationManager = (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE);
		if (notificationManager == null)
			return null;

		final String id = "org.musicpd";
		final String name = "MPD service";
		final int importance = 3; /* NotificationManager.IMPORTANCE_DEFAULT */

		try {
			Class<?> ncClass = Class.forName("android.app.NotificationChannel");
			Constructor<?> ncCtor = ncClass.getConstructor(String.class, CharSequence.class, int.class);
			Object nc = ncCtor.newInstance(id, name, importance);

			Method nmCreateNotificationChannelMethod =
				NotificationManager.class.getMethod("createNotificationChannel", ncClass);
			nmCreateNotificationChannelMethod.invoke(notificationManager, nc);

			Constructor nbCtor = Notification.Builder.class.getConstructor(Context.class, String.class);
			return (Notification.Builder) nbCtor.newInstance(this, id);
		} catch (Exception e)
		{
			Log.e(TAG, "error creating the NotificationChannel", e);
			return null;
		}
	}

190 191 192 193 194 195 196 197 198 199
	private void start() {
		if (mThread != null)
			return;

		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);

200 201 202 203 204 205 206 207 208 209 210
		Notification.Builder nBuilder;
		if (Build.VERSION.SDK_INT >= 26 /* Build.VERSION_CODES.O */)
		{
			nBuilder = createNotificationBuilderWithChannel();
			if (nBuilder == null)
				return;
		}
		else
			nBuilder = new Notification.Builder(this);

		Notification notification = nBuilder.setContentTitle(getText(R.string.notification_title_mpd_running))
211 212 213 214
			.setContentText(getText(R.string.notification_text_mpd_running))
			.setSmallIcon(R.drawable.notification_icon)
			.setContentIntent(contentIntent)
			.build();
215

216 217 218
		mThread = new Thread(this);
		mThread.start();

219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266
		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);
		}
	}
Max Kellermann's avatar
Max Kellermann committed
267

268 269 270
	private void unregisterCallback(IMainCallback cb) {
		if (cb != null) {
			mCallbacks.unregister(cb);
Max Kellermann's avatar
Max Kellermann committed
271
		}
272 273 274 275 276 277
	}

	/*
	 * Client that bind the Main Service in order to send commands and receive callback
	 */
	public static class Client {
Max Kellermann's avatar
Max Kellermann committed
278

279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410
		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);
			}
		}
Max Kellermann's avatar
Max Kellermann committed
411 412
	}

413 414 415 416 417
	/*
	 * start Main service without any callback
	 */
	public static void start(Context context, boolean wakelock) {
		context.startService(new Intent(context, Main.class).putExtra("wakelock", wakelock));
Max Kellermann's avatar
Max Kellermann committed
418 419
	}
}