blob: 5268ca798367cb9c0dcbdac5029ca269ab69033f [file] [log] [blame]
/*
* Copyright (C) 2017 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.intelligence.suggestions.eligibility;
import static android.content.Intent.EXTRA_COMPONENT_NAME;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ProviderInfo;
import android.content.pm.ResolveInfo;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
import com.android.settings.intelligence.R;
import com.android.settings.intelligence.suggestions.model.CandidateSuggestion;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Filters candidate list to only valid ones.
*/
public class CandidateSuggestionFilter {
private static final String TAG = "CandidateSuggestionFilter";
private static CandidateSuggestionFilter sChecker;
private static ExecutorService sExecutorService;
public static CandidateSuggestionFilter getInstance() {
if (sChecker == null) {
sChecker = new CandidateSuggestionFilter();
sExecutorService = Executors.newCachedThreadPool();
}
return sChecker;
}
@NonNull
public synchronized List<CandidateSuggestion> filterCandidates(Context context,
List<CandidateSuggestion> candidates) {
final long startTime = System.currentTimeMillis();
final List<CandidateFilterTask> checkTasks = new ArrayList<>();
final List<CandidateSuggestion> incompleteCandidates = new ArrayList<>();
if (candidates == null) {
return incompleteCandidates;
}
// Put a check task into ExecutorService for each candidate.
for (CandidateSuggestion candidate : candidates) {
final CandidateFilterTask task = new CandidateFilterTask(context, candidate);
sExecutorService.execute(task);
checkTasks.add(task);
}
for (CandidateFilterTask task : checkTasks) {
try {
long checkTaskTimeOutValue =
context.getResources().getInteger(R.integer.check_task_timeout_ms);
final CandidateSuggestion candidate = task.get(checkTaskTimeOutValue,
TimeUnit.MILLISECONDS);
if (candidate != null) {
incompleteCandidates.add(candidate);
}
} catch (TimeoutException | InterruptedException | ExecutionException e) {
Log.w(TAG, "Error checking completion state for " + task.getId());
}
}
final long endTime = System.currentTimeMillis();
Log.d(TAG, "filterCandidates duration: " + (endTime - startTime));
return incompleteCandidates;
}
/**
* {@link FutureTask} that filters status for a suggestion candidate.
* <p/>
* If the candidate status is valid, {@link #get()} will return the candidate itself.
* Otherwise it returns null.
*/
static class CandidateFilterTask extends FutureTask<CandidateSuggestion> {
private static final String EXTRA_CANDIDATE_ID = "candidate_id";
private static final String RESULT_IS_COMPLETE = "candidate_is_complete";
private final String mId;
public CandidateFilterTask(Context context, CandidateSuggestion candidate) {
super(new GetSuggestionStatusCallable(context, candidate));
mId = candidate.getId();
}
public String getId() {
return mId;
}
@VisibleForTesting
static class GetSuggestionStatusCallable implements Callable<CandidateSuggestion> {
@VisibleForTesting
static final String CONTENT_PROVIDER_INTENT_ACTION =
"com.android.settings.action.SUGGESTION_STATE_PROVIDER";
private static final String METHOD_GET_SUGGESTION_STATE = "getSuggestionState";
private final Context mContext;
private final CandidateSuggestion mCandidate;
public GetSuggestionStatusCallable(Context context, CandidateSuggestion candidate) {
mContext = context.getApplicationContext();
mCandidate = candidate;
}
@Override
public CandidateSuggestion call() throws Exception {
// First find if candidate has any state provider.
final String packageName = mCandidate.getComponent().getPackageName();
final Intent probe = new Intent(CONTENT_PROVIDER_INTENT_ACTION)
.setPackage(packageName);
final List<ResolveInfo> providers = mContext.getPackageManager()
.queryIntentContentProviders(probe, 0 /* flags */);
if (providers == null || providers.isEmpty()) {
// No provider, let it go through
return mCandidate;
}
final ProviderInfo providerInfo = providers.get(0).providerInfo;
if (providerInfo == null || TextUtils.isEmpty(providerInfo.authority)) {
// Bad provider - don't let candidate pass through.
return null;
}
// Query candidate state (isComplete)
final Uri uri = new Uri.Builder()
.scheme(ContentResolver.SCHEME_CONTENT)
.authority(providerInfo.authority)
.build();
final Bundle result = mContext.getContentResolver().call(
uri, METHOD_GET_SUGGESTION_STATE, null /* args */,
buildGetSuggestionStateExtras(mCandidate));
final boolean isComplete = result.getBoolean(RESULT_IS_COMPLETE, false);
Log.d(TAG, "Suggestion state result " + result);
return isComplete ? null : mCandidate;
}
@VisibleForTesting
static Bundle buildGetSuggestionStateExtras(CandidateSuggestion candidate) {
final Bundle args = new Bundle();
final String id = candidate.getId();
args.putString(EXTRA_CANDIDATE_ID, id);
args.putParcelable(EXTRA_COMPONENT_NAME, candidate.getComponent());
return args;
}
}
}
}