blob: db4ed9978a6d3f2dfdab36b1c3c4e74a9881ab81 [file]
/*
* Copyright (C) 2025 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 com.android.settings.accessibility;
import static com.android.internal.accessibility.common.NotificationConstants.EXTRA_PAGE_ID;
import android.app.job.JobInfo;
import android.app.job.JobParameters;
import android.app.job.JobScheduler;
import android.app.job.JobService;
import android.app.settings.SettingsEnums;
import android.content.ComponentName;
import android.content.Context;
import android.os.PersistableBundle;
import android.util.Slog;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.settings.accessibility.notification.NotificationHelper;
/**
* JobService implementation for scheduling a notification to inform users about an accessibility
* survey and guide them to review it.
*
* <p>This service ensures that the notification is displayed after a specified delay.
*/
public class AccessibilitySurveyNotificationJobService extends JobService {
public static final String TAG = "AccessibilitySurveyNotificationJobService";
// The base ID for job is derived from the bug component ID.
// Page-specific notification IDs are generated by adding the respective pageId to this base.
private static final int JOB_ID_BASE = 751131;
@Nullable
private NotificationHelper mNotificationHelper;
/**
* Default constructor for {@link AccessibilitySurveyNotificationJobService}.
*/
public AccessibilitySurveyNotificationJobService() {}
/**
* Constructor for {@link AccessibilitySurveyNotificationJobService} for testing purposes.
*
* @param notificationHelper The {@link NotificationHelper} instance to use, or {@code null} if
* it should be initialized lazily.
*/
@VisibleForTesting
public AccessibilitySurveyNotificationJobService(
@Nullable NotificationHelper notificationHelper) {
mNotificationHelper = notificationHelper;
}
/**
* Schedule a new job that will show a notification after the specified amount of time.
* If a job with the same ID is already pending, a new one will not be scheduled.
*
* @param context The application context.
* @param pageId The identifier for the survey page to be shown.
* @param delayMillis The minimum time in milliseconds to wait before showing the notification.
*/
public void scheduleJob(@NonNull Context context, int pageId, long delayMillis) {
final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
if (jobScheduler == null) {
return;
}
int jobId = getJobId(pageId);
if (jobScheduler.getPendingJob(jobId) == null) {
final ComponentName component = new ComponentName(
context, AccessibilitySurveyNotificationJobService.class);
final PersistableBundle bundle = new PersistableBundle();
bundle.putInt(EXTRA_PAGE_ID, pageId);
final JobInfo newJob = new JobInfo.Builder(jobId, component)
.setMinimumLatency(delayMillis)
.setExtras(bundle)
.build();
int scheduleResult = jobScheduler.schedule(newJob);
if (scheduleResult == JobScheduler.RESULT_SUCCESS) {
Slog.i(TAG, "Job successfully scheduled for pageId: " + pageId + " to run "
+ "after " + delayMillis + "ms.");
} else {
Slog.e(TAG, "Job scheduling failed for pageId: " + pageId + " with result code: "
+ scheduleResult);
}
}
}
/**
* Cancels a previously scheduled job.
*
* @param context The application context.
* @param pageId The identifier for the survey page whose job is to be cancelled.
*/
public void cancelJob(@NonNull Context context, int pageId) {
final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
if (jobScheduler == null) {
return;
}
int jobId = getJobId(pageId);
if (jobScheduler.getPendingJob(jobId) != null) {
jobScheduler.cancel(jobId);
}
}
@Override
public boolean onStartJob(@NonNull JobParameters params) {
final PersistableBundle extras = params.getExtras();
if (extras == null) {
return false;
}
int pageId = extras.getInt(EXTRA_PAGE_ID, SettingsEnums.PAGE_UNKNOWN);
if (pageId == SettingsEnums.PAGE_UNKNOWN) {
return false;
}
Slog.i(TAG, "Job starting for pageId: " + pageId);
if (mNotificationHelper == null) {
mNotificationHelper = new NotificationHelper(this);
}
mNotificationHelper.showSurveyNotification(pageId);
// Returning false here indicates that the job is finished immediately. The system will
// release the wakelock, and onStopJob will not be invoked.
return false;
}
@Override
public boolean onStopJob(@NonNull JobParameters params) {
Slog.w(TAG, "Job stopped by system. Job ID: " + params.getJobId() + ", reason: "
+ params.getStopReason());
// Called if the system stops the job, e.g., due to preemption before or during
// onStartJob's brief execution (as onStartJob returns false quickly).
// Returning true attempts to reschedule the job with its original criteria.
return true;
}
/**
* Generates a job ID based on a base ID and a page ID. This ensures that job for different
* pages will have distinct IDs.
*
* @param pageId The unique identifier of the page for which to generate a job ID.
* @return A unique integer representing the job ID for the given page.
*/
@VisibleForTesting
public int getJobId(int pageId) {
return JOB_ID_BASE + pageId;
}
}