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 androidx.annotation.NonNull;
29 import androidx.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 
getInstance()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
filterCandidates(Context context, List<CandidateSuggestion> candidates)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 
CandidateFilterTask(Context context, CandidateSuggestion candidate)110         public CandidateFilterTask(Context context, CandidateSuggestion candidate) {
111             super(new GetSuggestionStatusCallable(context, candidate));
112             mId = candidate.getId();
113         }
114 
getId()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 
GetSuggestionStatusCallable(Context context, CandidateSuggestion candidate)129             public GetSuggestionStatusCallable(Context context, CandidateSuggestion candidate) {
130                 mContext = context.getApplicationContext();
131                 mCandidate = candidate;
132             }
133 
134             @Override
call()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
buildGetSuggestionStateExtras(CandidateSuggestion candidate)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