blob: 489088890ca3ec2219cd1bc1ceee40839d7bb394 [file] [log] [blame]
/*
* Copyright 2019 The Android Open Source Project
*
* 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 androidx.work.impl.foreground;
import static android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE;
import static androidx.work.impl.model.WorkSpecKt.generationalId;
import android.app.Notification;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.annotation.VisibleForTesting;
import androidx.work.ForegroundInfo;
import androidx.work.Logger;
import androidx.work.impl.ExecutionListener;
import androidx.work.impl.WorkManagerImpl;
import androidx.work.impl.constraints.WorkConstraintsCallback;
import androidx.work.impl.constraints.WorkConstraintsTracker;
import androidx.work.impl.constraints.WorkConstraintsTrackerImpl;
import androidx.work.impl.model.WorkGenerationalId;
import androidx.work.impl.model.WorkSpec;
import androidx.work.impl.utils.taskexecutor.TaskExecutor;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
/**
* Handles requests for executing {@link androidx.work.WorkRequest}s on behalf of
* {@link SystemForegroundService}.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public class SystemForegroundDispatcher implements WorkConstraintsCallback, ExecutionListener {
// Synthetic access
@SuppressWarnings("WeakerAccess")
static final String TAG = Logger.tagWithPrefix("SystemFgDispatcher");
// keys
private static final String KEY_NOTIFICATION = "KEY_NOTIFICATION";
private static final String KEY_NOTIFICATION_ID = "KEY_NOTIFICATION_ID";
private static final String KEY_FOREGROUND_SERVICE_TYPE = "KEY_FOREGROUND_SERVICE_TYPE";
private static final String KEY_WORKSPEC_ID = "KEY_WORKSPEC_ID";
private static final String KEY_GENERATION = "KEY_GENERATION";
// actions
private static final String ACTION_START_FOREGROUND = "ACTION_START_FOREGROUND";
private static final String ACTION_NOTIFY = "ACTION_NOTIFY";
private static final String ACTION_CANCEL_WORK = "ACTION_CANCEL_WORK";
private static final String ACTION_STOP_FOREGROUND = "ACTION_STOP_FOREGROUND";
private Context mContext;
private WorkManagerImpl mWorkManagerImpl;
private final TaskExecutor mTaskExecutor;
@SuppressWarnings("WeakerAccess") // Synthetic access
final Object mLock;
@SuppressWarnings("WeakerAccess") // Synthetic access
WorkGenerationalId mCurrentForegroundId;
@SuppressWarnings("WeakerAccess") // Synthetic access
final Map<WorkGenerationalId, ForegroundInfo> mForegroundInfoById;
@SuppressWarnings("WeakerAccess") // Synthetic access
final Map<WorkGenerationalId, WorkSpec> mWorkSpecById;
@SuppressWarnings("WeakerAccess") // Synthetic access
final Set<WorkSpec> mTrackedWorkSpecs;
@SuppressWarnings("WeakerAccess") // Synthetic access
final WorkConstraintsTracker mConstraintsTracker;
@Nullable
private Callback mCallback;
SystemForegroundDispatcher(@NonNull Context context) {
mContext = context;
mLock = new Object();
mWorkManagerImpl = WorkManagerImpl.getInstance(mContext);
mTaskExecutor = mWorkManagerImpl.getWorkTaskExecutor();
mCurrentForegroundId = null;
mForegroundInfoById = new LinkedHashMap<>();
mTrackedWorkSpecs = new HashSet<>();
mWorkSpecById = new HashMap<>();
mConstraintsTracker = new WorkConstraintsTrackerImpl(mWorkManagerImpl.getTrackers(), this);
mWorkManagerImpl.getProcessor().addExecutionListener(this);
}
@VisibleForTesting
SystemForegroundDispatcher(
@NonNull Context context,
@NonNull WorkManagerImpl workManagerImpl,
@NonNull WorkConstraintsTracker tracker) {
mContext = context;
mLock = new Object();
mWorkManagerImpl = workManagerImpl;
mTaskExecutor = mWorkManagerImpl.getWorkTaskExecutor();
mCurrentForegroundId = null;
mForegroundInfoById = new LinkedHashMap<>();
mTrackedWorkSpecs = new HashSet<>();
mWorkSpecById = new HashMap<>();
mConstraintsTracker = tracker;
mWorkManagerImpl.getProcessor().addExecutionListener(this);
}
@MainThread
@Override
public void onExecuted(@NonNull WorkGenerationalId id, boolean needsReschedule) {
boolean removed = false;
synchronized (mLock) {
WorkSpec workSpec = mWorkSpecById.remove(id);
if (workSpec != null) {
removed = mTrackedWorkSpecs.remove(workSpec);
}
if (removed) {
// Stop tracking constraints.
mConstraintsTracker.replace(mTrackedWorkSpecs);
}
}
ForegroundInfo removedInfo = mForegroundInfoById.remove(id);
// Promote new notifications to the foreground if necessary.
if (id.equals(mCurrentForegroundId)) {
if (mForegroundInfoById.size() > 0) {
// Find the next eligible ForegroundInfo
// LinkedHashMap uses insertion order, so find the last one because that was
// the most recent ForegroundInfo used. That way when different WorkSpecs share
// notification ids, we still end up in a reasonably good place.
Iterator<Map.Entry<WorkGenerationalId, ForegroundInfo>> iterator =
mForegroundInfoById.entrySet().iterator();
Map.Entry<WorkGenerationalId, ForegroundInfo> entry = iterator.next();
while (iterator.hasNext()) {
entry = iterator.next();
}
mCurrentForegroundId = entry.getKey();
if (mCallback != null) {
ForegroundInfo info = entry.getValue();
mCallback.startForeground(
info.getNotificationId(),
info.getForegroundServiceType(),
info.getNotification());
// We used NotificationManager before to update notifications, so ensure
// that we reference count the Notification instance down by
// cancelling the notification.
mCallback.cancelNotification(info.getNotificationId());
}
}
}
// Keep track of the reference and use that when cancelling Notification. This is because
// the work-testing library uses a direct executor and does *not* call this method
// on the main thread.
Callback callback = mCallback;
if (removedInfo != null && callback != null) {
// Explicitly decrement the reference count for the notification
// We are doing this without having to wait for the handleStop() to clean up
// Notifications. This is because the Processor stops foreground workers on the
// dedicated task executor thread. Meanwhile Notifications are managed on the main
// thread, so there is a chance that handleStop() fires before onExecuted() is called
// on the main thread.
Logger.get().debug(TAG,
"Removing Notification (id: " + removedInfo.getNotificationId()
+ ", workSpecId: " + id
+ ", notificationType: " + removedInfo.getForegroundServiceType());
callback.cancelNotification(removedInfo.getNotificationId());
}
}
@MainThread
void setCallback(@NonNull Callback callback) {
if (mCallback != null) {
Logger.get().error(TAG, "A callback already exists.");
return;
}
mCallback = callback;
}
void onStartCommand(@NonNull Intent intent) {
String action = intent.getAction();
if (ACTION_START_FOREGROUND.equals(action)) {
handleStartForeground(intent);
// Call handleNotify() which in turn calls startForeground() as part of handing this
// command. This is important for some OEMs.
handleNotify(intent);
} else if (ACTION_NOTIFY.equals(action)) {
handleNotify(intent);
} else if (ACTION_CANCEL_WORK.equals(action)) {
handleCancelWork(intent);
} else if (ACTION_STOP_FOREGROUND.equals(action)) {
handleStop(intent);
}
}
@MainThread
void onDestroy() {
mCallback = null;
synchronized (mLock) {
mConstraintsTracker.reset();
}
mWorkManagerImpl.getProcessor().removeExecutionListener(this);
}
@MainThread
private void handleStartForeground(@NonNull Intent intent) {
Logger.get().info(TAG, "Started foreground service " + intent);
final String workSpecId = intent.getStringExtra(KEY_WORKSPEC_ID);
mTaskExecutor.executeOnTaskThread(new Runnable() {
@Override
public void run() {
WorkSpec workSpec = mWorkManagerImpl.getProcessor().getRunningWorkSpec(workSpecId);
// Only track constraints if there are constraints that need to be tracked
// (constraints are immutable)
if (workSpec != null && workSpec.hasConstraints()) {
synchronized (mLock) {
mWorkSpecById.put(generationalId(workSpec), workSpec);
mTrackedWorkSpecs.add(workSpec);
mConstraintsTracker.replace(mTrackedWorkSpecs);
}
}
}
});
}
@MainThread
private void handleNotify(@NonNull Intent intent) {
int notificationId = intent.getIntExtra(KEY_NOTIFICATION_ID, 0);
int notificationType = intent.getIntExtra(KEY_FOREGROUND_SERVICE_TYPE, 0);
String workSpecId = intent.getStringExtra(KEY_WORKSPEC_ID);
int generation = intent.getIntExtra(KEY_GENERATION, 0);
WorkGenerationalId workId = new WorkGenerationalId(workSpecId, generation);
Notification notification = intent.getParcelableExtra(KEY_NOTIFICATION);
Logger.get().debug(TAG,
"Notifying with (id:" + notificationId
+ ", workSpecId: " + workSpecId
+ ", notificationType :" + notificationType + ")");
if (notification != null && mCallback != null) {
// Keep track of this ForegroundInfo
ForegroundInfo info = new ForegroundInfo(
notificationId, notification, notificationType);
mForegroundInfoById.put(workId, info);
if (mCurrentForegroundId == null) {
// This is the current workSpecId which owns the Foreground lifecycle.
mCurrentForegroundId = workId;
mCallback.startForeground(notificationId, notificationType, notification);
} else {
// Update notification
mCallback.notify(notificationId, notification);
// Update the notification in the foreground such that it's the union of
// all current foreground service types if necessary.
if (notificationType != FOREGROUND_SERVICE_TYPE_NONE
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
int foregroundServiceType = FOREGROUND_SERVICE_TYPE_NONE;
for (Map.Entry<WorkGenerationalId, ForegroundInfo> entry
: mForegroundInfoById.entrySet()) {
ForegroundInfo foregroundInfo = entry.getValue();
foregroundServiceType |= foregroundInfo.getForegroundServiceType();
}
ForegroundInfo currentInfo =
mForegroundInfoById.get(mCurrentForegroundId);
if (currentInfo != null) {
mCallback.startForeground(
currentInfo.getNotificationId(),
foregroundServiceType,
currentInfo.getNotification()
);
}
}
}
}
}
@MainThread
void handleStop(@NonNull Intent intent) {
Logger.get().info(TAG, "Stopping foreground service");
if (mCallback != null) {
mCallback.stop();
}
}
@MainThread
private void handleCancelWork(@NonNull Intent intent) {
Logger.get().info(TAG, "Stopping foreground work for " + intent);
String workSpecId = intent.getStringExtra(KEY_WORKSPEC_ID);
if (workSpecId != null && !TextUtils.isEmpty(workSpecId)) {
mWorkManagerImpl.cancelWorkById(UUID.fromString(workSpecId));
}
}
@Override
public void onAllConstraintsMet(@NonNull List<WorkSpec> workSpecs) {
// Do nothing
}
@Override
public void onAllConstraintsNotMet(@NonNull List<WorkSpec> workSpecs) {
if (!workSpecs.isEmpty()) {
for (WorkSpec workSpec : workSpecs) {
String workSpecId = workSpec.id;
Logger.get().debug(TAG,
"Constraints unmet for WorkSpec " + workSpecId);
mWorkManagerImpl.stopForegroundWork(generationalId(workSpec));
}
}
}
/**
* The {@link Intent} is used to start a foreground {@link android.app.Service}.
*
* @param context The application {@link Context}
* @param workSpecId The WorkSpec id of the Worker being executed in the context of the
* foreground service
* @return The {@link Intent}
*/
@NonNull
public static Intent createStartForegroundIntent(
@NonNull Context context,
@NonNull WorkGenerationalId id,
@NonNull ForegroundInfo info) {
Intent intent = new Intent(context, SystemForegroundService.class);
intent.setAction(ACTION_START_FOREGROUND);
intent.putExtra(KEY_WORKSPEC_ID, id.getWorkSpecId());
intent.putExtra(KEY_GENERATION, id.getGeneration());
intent.putExtra(KEY_NOTIFICATION_ID, info.getNotificationId());
intent.putExtra(KEY_FOREGROUND_SERVICE_TYPE, info.getForegroundServiceType());
intent.putExtra(KEY_NOTIFICATION, info.getNotification());
return intent;
}
/**
* The {@link Intent} is used to cancel foreground work for a given {@link String} workSpecId.
*
* @param context The application {@link Context}
* @param workSpecId The WorkSpec id of the Worker being executed in the context of the
* foreground service
* @return The {@link Intent}
*/
@NonNull
public static Intent createCancelWorkIntent(
@NonNull Context context,
@NonNull String workSpecId) {
Intent intent = new Intent(context, SystemForegroundService.class);
intent.setAction(ACTION_CANCEL_WORK);
// Set data to make it unique for filterEquals()
intent.setData(Uri.parse("workspec://" + workSpecId));
intent.putExtra(KEY_WORKSPEC_ID, workSpecId);
return intent;
}
/**
* The {@link Intent} which is used to display a {@link Notification} via
* {@link SystemForegroundService}.
*
* @param context The application {@link Context}
* @param id The {@link WorkSpec} id
* @param info The {@link ForegroundInfo}
* @return The {@link Intent}
*/
@NonNull
public static Intent createNotifyIntent(
@NonNull Context context,
@NonNull WorkGenerationalId id,
@NonNull ForegroundInfo info) {
Intent intent = new Intent(context, SystemForegroundService.class);
intent.setAction(ACTION_NOTIFY);
intent.putExtra(KEY_NOTIFICATION_ID, info.getNotificationId());
intent.putExtra(KEY_FOREGROUND_SERVICE_TYPE, info.getForegroundServiceType());
intent.putExtra(KEY_NOTIFICATION, info.getNotification());
intent.putExtra(KEY_WORKSPEC_ID, id.getWorkSpecId());
intent.putExtra(KEY_GENERATION, id.getGeneration());
return intent;
}
/**
* The {@link Intent} which can be used to stop {@link SystemForegroundService}.
*
* @param context The application {@link Context}
* @return The {@link Intent}
*/
@NonNull
public static Intent createStopForegroundIntent(@NonNull Context context) {
Intent intent = new Intent(context, SystemForegroundService.class);
intent.setAction(ACTION_STOP_FOREGROUND);
return intent;
}
/**
* Used to notify that all pending commands are now completed.
*/
interface Callback {
/**
* An implementation of this callback should call
* {@link android.app.Service#startForeground(int, Notification, int)}.
*/
void startForeground(
int notificationId,
int notificationType,
@NonNull Notification notification);
/**
* Used to update the {@link Notification}.
*/
void notify(int notificationId, @NonNull Notification notification);
/**
* Used to cancel a {@link Notification}.
*/
void cancelNotification(int notificationId);
/**
* Used to stop the {@link SystemForegroundService}.
*/
void stop();
}
}