package protect.videotranscoder.service; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Message; import android.os.Messenger; import android.os.RemoteException; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.v4.app.NotificationCompat; import android.util.Log; import java.io.File; import java.util.List; import nl.bravobit.ffmpeg.ExecuteBinaryResponseHandler; import nl.bravobit.ffmpeg.FFprobe; import protect.videotranscoder.FFmpegUtil; import protect.videotranscoder.R; import protect.videotranscoder.activity.MainActivity; import protect.videoeditor.IFFmpegProcessService; import static protect.videotranscoder.activity.MainActivity.FFMPEG_FAILURE_MSG; import static protect.videotranscoder.activity.MainActivity.FFMPEG_OUTPUT_FILE; import static protect.videotranscoder.activity.MainActivity.MESSENGER_INTENT_KEY; import static protect.videotranscoder.activity.MainActivity.OUTPUT_MIMETYPE; public class FFmpegProcessService extends Service { private static final String TAG = "VideoTranscoder"; private static final int NOTIFICATION_ID = 1; private static final String NOTIFICATION_CHANNEL_ID = TAG; private Messenger _activityMessenger; /** * When the app's MainActivity is created, it starts this service. This is so that the * activity and this service can communicate back and forth. See "setUiCallback()" */ @Override public int onStartCommand(Intent intent, int flags, int startId) { Log.i(TAG, "Received start command from activity"); _activityMessenger = intent.getParcelableExtra(MESSENGER_INTENT_KEY); return START_NOT_STICKY; } @Override public IBinder onBind(Intent intent) { Log.i(TAG, "Received binding."); return mBinder; } private final IFFmpegProcessService.Stub mBinder = new IFFmpegProcessService.Stub() { @Override public boolean startEncode(final List<String> ffmpegArgs, final String outputFile, final String mimetype, final int durationMs) throws RemoteException { boolean result = FFmpegUtil.init(getApplicationContext()); if (result) { final String[] args = ffmpegArgs.toArray(new String[ffmpegArgs.size()]); sendMessage(MessageId.JOB_START_MSG, outputFile); ExecuteBinaryResponseHandler handler = new ExecuteBinaryResponseHandler() { @Override public void onFailure(String s) { Log.d(TAG, "Failed with output : " + s); clearNotification(); // The last line of the output should be the failure message String[] lines = s.split("\n"); String failureMg = lines[lines.length - 1].trim(); Bundle bundle = new Bundle(); bundle.putString(FFMPEG_FAILURE_MSG, failureMg); sendMessage(MessageId.JOB_FAILED_MSG, bundle); } @Override public void onSuccess(String s) { Log.d(TAG, "Success with output : " + s); clearNotification(); Bundle bundle = new Bundle(); bundle.putString(FFMPEG_OUTPUT_FILE, outputFile); bundle.putString(OUTPUT_MIMETYPE, mimetype); sendMessage(MessageId.JOB_SUCCEDED_MSG, bundle); } @Override public void onProgress(String s) { // Progress updates look like the following: // frame= 15 fps=7.1 q=2.0 size= 26kB time=00:00:00.69 bitrate= 309.4kbits/s dup=6 drop=0 speed=0.329x Long currentTimeMs = null; String[] split = s.split(" "); for (String item : split) { if (item.startsWith("time=")) { item = item.replace("time=", ""); currentTimeMs = FFmpegUtil.timestampToMs(item); break; } } Integer percentComplete = null; if (currentTimeMs != null && currentTimeMs > 0) { percentComplete = (int) Math.floor((currentTimeMs * 100) / (float) durationMs); } sendMessage(MessageId.JOB_PROGRESS_MSG, percentComplete); } }; if (outputFile != null) { setNotification(new File(outputFile).getName()); } FFmpegUtil.call(args, handler); } else { sendMessage(MessageId.FFMPEG_UNSUPPORTED_MSG, null); } return result; } @Override public void cancel() throws RemoteException { Log.i(TAG, "Received cancel"); clearNotification(); FFmpegUtil.cancelCall(); Bundle bundle = new Bundle(); bundle.putString(FFMPEG_FAILURE_MSG, getResources().getString(R.string.encodeCanceled)); sendMessage(MessageId.JOB_FAILED_MSG, bundle); } @Override public boolean isEncoding() throws RemoteException { return FFmpegUtil.isFFmpegRunning(); } }; @Override public void onCreate() { super.onCreate(); Log.i(TAG, "Service created"); } @Override public void onDestroy() { super.onDestroy(); _activityMessenger = null; FFmpegUtil.cancelCall(); Log.i(TAG, "Service destroyed"); } private void setNotification(String filename) { String message = String.format(getString(R.string.encodingNotification), filename); String channelId = ""; if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { channelId = createNotificationChannel(); } NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId) .setOngoing(true) .setSmallIcon(R.drawable.encoding_notification) .setContentTitle(getString(R.string.app_name)) .setContentText(message); // Creates an explicit intent for the Activity Intent resultIntent = new Intent(this, MainActivity.class); PendingIntent resultPendingIntent = PendingIntent.getActivity(this, 0, resultIntent, 0); builder.setContentIntent(resultPendingIntent); startForeground(NOTIFICATION_ID, builder.build()); } @RequiresApi(Build.VERSION_CODES.O) private String createNotificationChannel() { String channelId = NOTIFICATION_CHANNEL_ID; String channelName = getString(R.string.notificationChannelName); NotificationChannel chan = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW); chan.setLockscreenVisibility(Notification.VISIBILITY_PRIVATE); NotificationManager service = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); if(service != null) { service.createNotificationChannel(chan); } else { Log.w(TAG, "Could not get NotificationManager"); } return channelId; } private void clearNotification() { stopForeground(true); } private void sendMessage(MessageId messageId, @Nullable Object params) { // If this service is launched by the JobScheduler, there's no callback Messenger. It // only exists when the MainActivity calls startService() with the callback in the Intent. if (_activityMessenger == null) { Log.d(TAG, "Service is bound, not started. There's no callback to send a message to."); return; } Message m = Message.obtain(); m.what = messageId.ordinal(); m.obj = params; try { _activityMessenger.send(m); } catch (RemoteException e) { Log.e(TAG, "Error passing service object back to activity.", e); } } }