// 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 android.app.Service; import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.Looper; import android.os.Messenger; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.support.v4.util.SimpleArrayMap; import android.util.Log; import com.firebase.jobdispatcher.JobService.JobResult; /** * Handles incoming execute requests from the GooglePlay driver and forwards them to your Service. */ public class GooglePlayReceiver extends Service implements ExecutionDelegator.JobFinishedCallback { /** * Logging tag. */ /* package */ static final String TAG = "FJD.GooglePlayReceiver"; /** * The action sent by Google Play services that triggers job execution. */ @VisibleForTesting static final String ACTION_EXECUTE = "com.google.android.gms.gcm.ACTION_TASK_READY"; /** Action sent by Google Play services when your app has been updated. */ @VisibleForTesting static final String ACTION_INITIALIZE = "com.google.android.gms.gcm.SERVICE_ACTION_INITIALIZE"; private static final String ERROR_NULL_INTENT = "Null Intent passed, terminating"; private static final String ERROR_UNKNOWN_ACTION = "Unknown action received, terminating"; private static final String ERROR_NO_DATA = "No data provided, terminating"; private static final JobCoder prefixedCoder = new JobCoder(BundleProtocol.PACKED_PARAM_BUNDLE_PREFIX, true); private final Object lock = new Object(); private final GooglePlayCallbackExtractor callbackExtractor = new GooglePlayCallbackExtractor(); /** * The single Messenger that's returned from valid onBind requests. Guarded by {@link #lock}. */ @VisibleForTesting Messenger serviceMessenger; /** * The ExecutionDelegator used to communicate with client JobServices. Guarded by {@link #lock}. */ private ExecutionDelegator executionDelegator; /** * Endpoint (String) -> Tag (String) -> JobCallback */ private SimpleArrayMap<String, SimpleArrayMap<String, JobCallback>> callbacks = new SimpleArrayMap<>(1); private static void sendResultSafely(JobCallback callback, int result) { try { callback.jobFinished(result); } catch (Throwable e) { Log.e(TAG, "Encountered error running callback", e.getCause()); } } @Override public final int onStartCommand(Intent intent, int flags, int startId) { try { super.onStartCommand(intent, flags, startId); if (intent == null) { Log.w(TAG, ERROR_NULL_INTENT); return START_NOT_STICKY; } String action = intent.getAction(); if (ACTION_EXECUTE.equals(action)) { getExecutionDelegator().executeJob(prepareJob(intent)); return START_NOT_STICKY; } else if (ACTION_INITIALIZE.equals(action)) { return START_NOT_STICKY; } Log.e(TAG, ERROR_UNKNOWN_ACTION); return START_NOT_STICKY; } finally { synchronized (this) { if (callbacks.isEmpty()) { stopSelf(startId); } } } } @Nullable @Override public IBinder onBind(Intent intent) { // Only Lollipop+ supports UID checking messages, so we can't trust this system on older // platforms. if (intent == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP || !ACTION_EXECUTE.equals(intent.getAction())) { return null; } return getServiceMessenger().getBinder(); } private Messenger getServiceMessenger() { synchronized (lock) { if (serviceMessenger == null) { serviceMessenger = new Messenger(new GooglePlayMessageHandler(Looper.getMainLooper(), this)); } return serviceMessenger; } } /* package */ ExecutionDelegator getExecutionDelegator() { synchronized (lock) { if (executionDelegator == null) { executionDelegator = new ExecutionDelegator(this, this); } return executionDelegator; } } @Nullable @VisibleForTesting JobInvocation prepareJob(Intent intent) { Bundle intentExtras = intent.getExtras(); if (intentExtras == null) { Log.e(TAG, ERROR_NO_DATA); return null; } // get the callback first. If we don't have this we can't talk back to the backend. JobCallback callback = callbackExtractor.extractCallback(intentExtras); if (callback == null) { Log.i(TAG, "no callback found"); return null; } return prepareJob(intentExtras, callback); } @Nullable JobInvocation prepareJob(Bundle bundle, JobCallback callback) { JobInvocation job = prefixedCoder.decodeIntentBundle(bundle); if (job == null) { Log.e(TAG, "unable to decode job"); sendResultSafely(callback, JobService.RESULT_FAIL_NORETRY); return null; } synchronized (this) { SimpleArrayMap<String, JobCallback> map = callbacks.get(job.getService()); if (map == null) { map = new SimpleArrayMap<>(1); callbacks.put(job.getService(), map); } map.put(job.getTag(), callback); } return job; } @Override public synchronized void onJobFinished(@NonNull JobInvocation js, @JobResult int result) { SimpleArrayMap<String, JobCallback> map = callbacks.get(js.getService()); if (map == null) { return; } JobCallback callback = map.remove(js.getTag()); if (callback != null) { if (Log.isLoggable(TAG, Log.VERBOSE)) { Log.v(TAG, "sending jobFinished for " + js.getTag() + " = " + result); } sendResultSafely(callback, result); } if (map.isEmpty()) { callbacks.remove(js.getService()); } } static JobCoder getJobCoder() { return prefixedCoder; } }