1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.intelligence.suggestions.eligibility; 18 19 import static android.content.Intent.EXTRA_COMPONENT_NAME; 20 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.ProviderInfo; 25 import android.content.pm.ResolveInfo; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.support.annotation.NonNull; 29 import android.support.annotation.VisibleForTesting; 30 import android.text.TextUtils; 31 import android.util.Log; 32 33 import com.android.settings.intelligence.R; 34 import com.android.settings.intelligence.suggestions.model.CandidateSuggestion; 35 36 import java.util.ArrayList; 37 import java.util.List; 38 import java.util.concurrent.Callable; 39 import java.util.concurrent.ExecutionException; 40 import java.util.concurrent.ExecutorService; 41 import java.util.concurrent.Executors; 42 import java.util.concurrent.FutureTask; 43 import java.util.concurrent.TimeUnit; 44 import java.util.concurrent.TimeoutException; 45 46 /** 47 * Filters candidate list to only valid ones. 48 */ 49 public class CandidateSuggestionFilter { 50 51 private static final String TAG = "CandidateSuggestionFilter"; 52 53 private static CandidateSuggestionFilter sChecker; 54 private static ExecutorService sExecutorService; 55 56 public static CandidateSuggestionFilter getInstance() { 57 if (sChecker == null) { 58 sChecker = new CandidateSuggestionFilter(); 59 sExecutorService = Executors.newCachedThreadPool(); 60 } 61 return sChecker; 62 } 63 64 @NonNull 65 public synchronized List<CandidateSuggestion> filterCandidates(Context context, 66 List<CandidateSuggestion> candidates) { 67 final long startTime = System.currentTimeMillis(); 68 final List<CandidateFilterTask> checkTasks = new ArrayList<>(); 69 final List<CandidateSuggestion> incompleteCandidates = new ArrayList<>(); 70 if (candidates == null) { 71 return incompleteCandidates; 72 } 73 // Put a check task into ExecutorService for each candidate. 74 for (CandidateSuggestion candidate : candidates) { 75 final CandidateFilterTask task = new CandidateFilterTask(context, candidate); 76 sExecutorService.execute(task); 77 checkTasks.add(task); 78 } 79 for (CandidateFilterTask task : checkTasks) { 80 try { 81 long checkTaskTimeOutValue = 82 context.getResources().getInteger(R.integer.check_task_timeout_ms); 83 final CandidateSuggestion candidate = task.get(checkTaskTimeOutValue, 84 TimeUnit.MILLISECONDS); 85 if (candidate != null) { 86 incompleteCandidates.add(candidate); 87 } 88 } catch (TimeoutException | InterruptedException | ExecutionException e) { 89 Log.w(TAG, "Error checking completion state for " + task.getId()); 90 } 91 } 92 final long endTime = System.currentTimeMillis(); 93 Log.d(TAG, "filterCandidates duration: " + (endTime - startTime)); 94 return incompleteCandidates; 95 } 96 97 /** 98 * {@link FutureTask} that filters status for a suggestion candidate. 99 * <p/> 100 * If the candidate status is valid, {@link #get()} will return the candidate itself. 101 * Otherwise it returns null. 102 */ 103 static class CandidateFilterTask extends FutureTask<CandidateSuggestion> { 104 105 private static final String EXTRA_CANDIDATE_ID = "candidate_id"; 106 private static final String RESULT_IS_COMPLETE = "candidate_is_complete"; 107 108 private final String mId; 109 110 public CandidateFilterTask(Context context, CandidateSuggestion candidate) { 111 super(new GetSuggestionStatusCallable(context, candidate)); 112 mId = candidate.getId(); 113 } 114 115 public String getId() { 116 return mId; 117 } 118 119 @VisibleForTesting 120 static class GetSuggestionStatusCallable implements Callable<CandidateSuggestion> { 121 @VisibleForTesting 122 static final String CONTENT_PROVIDER_INTENT_ACTION = 123 "com.android.settings.action.SUGGESTION_STATE_PROVIDER"; 124 private static final String METHOD_GET_SUGGESTION_STATE = "getSuggestionState"; 125 126 private final Context mContext; 127 private final CandidateSuggestion mCandidate; 128 129 public GetSuggestionStatusCallable(Context context, CandidateSuggestion candidate) { 130 mContext = context.getApplicationContext(); 131 mCandidate = candidate; 132 } 133 134 @Override 135 public CandidateSuggestion call() throws Exception { 136 // First find if candidate has any state provider. 137 final String packageName = mCandidate.getComponent().getPackageName(); 138 final Intent probe = new Intent(CONTENT_PROVIDER_INTENT_ACTION) 139 .setPackage(packageName); 140 final List<ResolveInfo> providers = mContext.getPackageManager() 141 .queryIntentContentProviders(probe, 0 /* flags */); 142 if (providers == null || providers.isEmpty()) { 143 // No provider, let it go through 144 return mCandidate; 145 } 146 final ProviderInfo providerInfo = providers.get(0).providerInfo; 147 if (providerInfo == null || TextUtils.isEmpty(providerInfo.authority)) { 148 // Bad provider - don't let candidate pass through. 149 return null; 150 } 151 // Query candidate state (isComplete) 152 final Uri uri = new Uri.Builder() 153 .scheme(ContentResolver.SCHEME_CONTENT) 154 .authority(providerInfo.authority) 155 .build(); 156 final Bundle result = mContext.getContentResolver().call( 157 uri, METHOD_GET_SUGGESTION_STATE, null /* args */, 158 buildGetSuggestionStateExtras(mCandidate)); 159 final boolean isComplete = result.getBoolean(RESULT_IS_COMPLETE, false); 160 Log.d(TAG, "Suggestion state result " + result); 161 return isComplete ? null : mCandidate; 162 } 163 164 @VisibleForTesting 165 static Bundle buildGetSuggestionStateExtras(CandidateSuggestion candidate) { 166 final Bundle args = new Bundle(); 167 final String id = candidate.getId(); 168 args.putString(EXTRA_CANDIDATE_ID, id); 169 args.putParcelable(EXTRA_COMPONENT_NAME, candidate.getComponent()); 170 return args; 171 } 172 } 173 } 174 } 175