// Copyright 2016 Google, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package com.firebase.jobdispatcher;
import static android.content.Context.BIND_AUTO_CREATE;
import android.content.Context;
import android.content.Intent;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.v4.util.SimpleArrayMap;
import android.util.Log;
import com.firebase.jobdispatcher.JobService.JobResult;
import java.lang.ref.WeakReference;
/**
* ExecutionDelegator tracks local Binder connections to client JobServices and handles
* communication with those services.
*/
/* package */ class ExecutionDelegator {
@VisibleForTesting
static final int JOB_FINISHED = 1;
static final String TAG = "FJD.ExternalReceiver";
interface JobFinishedCallback {
void onJobFinished(@NonNull JobInvocation jobInvocation, @JobResult int result);
}
/**
* A mapping of {@link JobInvocation} to (local) binder connections.
* Synchronized by itself.
*/
private final SimpleArrayMap<JobInvocation, JobServiceConnection> serviceConnections =
new SimpleArrayMap<>();
private final ResponseHandler responseHandler =
new ResponseHandler(Looper.getMainLooper(), new WeakReference<>(this));
private final Context context;
private final JobFinishedCallback jobFinishedCallback;
ExecutionDelegator(Context context, JobFinishedCallback jobFinishedCallback) {
this.context = context;
this.jobFinishedCallback = jobFinishedCallback;
}
/**
* Executes the provided {@code jobInvocation} by kicking off the creation of a new Binder
* connection to the Service.
*
* @return true if the service was bound successfully.
*/
boolean executeJob(JobInvocation jobInvocation) {
if (jobInvocation == null) {
return false;
}
JobServiceConnection conn = new JobServiceConnection(jobInvocation,
responseHandler.obtainMessage(JOB_FINISHED));
synchronized (serviceConnections) {
JobServiceConnection oldConnection = serviceConnections.put(jobInvocation, conn);
if (oldConnection != null) {
Log.e(TAG, "Received execution request for already running job");
}
return context.bindService(createBindIntent(jobInvocation), conn, BIND_AUTO_CREATE);
}
}
@NonNull
private Intent createBindIntent(JobParameters jobParameters) {
Intent execReq = new Intent(JobService.ACTION_EXECUTE);
execReq.setClassName(context, jobParameters.getService());
return execReq;
}
void stopJob(JobInvocation job) {
synchronized (serviceConnections) {
JobServiceConnection jobServiceConnection = serviceConnections.remove(job);
if (jobServiceConnection != null) {
jobServiceConnection.onStop();
safeUnbindService(jobServiceConnection);
}
}
}
private void safeUnbindService(JobServiceConnection connection) {
if (connection != null && connection.isBound()) {
try {
context.unbindService(connection);
} catch (IllegalArgumentException e) {
Log.w(TAG, "Error unbinding service: " + e.getMessage());
}
}
}
private void onJobFinishedMessage(JobInvocation jobInvocation, int result) {
synchronized (serviceConnections) {
JobServiceConnection connection = serviceConnections.remove(jobInvocation);
safeUnbindService(connection);
}
jobFinishedCallback.onJobFinished(jobInvocation, result);
}
private static class ResponseHandler extends Handler {
/**
* We hold a WeakReference to the ExecutionDelegator because it holds a reference to a
* Service Context and Handlers are often kept in memory longer than you'd expect because
* any pending Messages can maintain references to them.
*/
private final WeakReference<ExecutionDelegator> executionDelegatorReference;
ResponseHandler(Looper looper, WeakReference<ExecutionDelegator> executionDelegator) {
super(looper);
this.executionDelegatorReference = executionDelegator;
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case JOB_FINISHED:
if (msg.obj instanceof JobInvocation) {
ExecutionDelegator delegator = this.executionDelegatorReference.get();
if (delegator == null) {
Log.wtf(TAG, "handleMessage: service was unexpectedly GC'd"
+ ", can't send job result");
return;
}
delegator.onJobFinishedMessage((JobInvocation) msg.obj, msg.arg1);
return;
}
Log.wtf(TAG, "handleMessage: unknown obj returned");
return;
default:
Log.wtf(TAG, "handleMessage: unknown message type received: " + msg.what);
}
}
}
}