• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.model;
18 
19 import android.app.PendingIntent;
20 import android.content.ComponentName;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ResolveInfo;
25 import android.content.res.Resources;
26 import android.graphics.drawable.Icon;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.service.settings.suggestions.Suggestion;
30 import android.support.annotation.VisibleForTesting;
31 import android.text.TextUtils;
32 import android.util.Log;
33 
34 import com.android.settings.intelligence.suggestions.eligibility.AccountEligibilityChecker;
35 import com.android.settings.intelligence.suggestions.eligibility.AutomotiveEligibilityChecker;
36 import com.android.settings.intelligence.suggestions.eligibility.ConnectivityEligibilityChecker;
37 import com.android.settings.intelligence.suggestions.eligibility.DismissedChecker;
38 import com.android.settings.intelligence.suggestions.eligibility.FeatureEligibilityChecker;
39 import com.android.settings.intelligence.suggestions.eligibility.ProviderEligibilityChecker;
40 
41 import java.util.List;
42 
43 /**
44  * A wrapper to {@link android.content.pm.ResolveInfo} that matches Suggestion signature.
45  * <p/>
46  * This class contains necessary metadata to eventually be
47  * processed into a {@link android.service.settings.suggestions.Suggestion}.
48  */
49 public class CandidateSuggestion {
50 
51     private static final String TAG = "CandidateSuggestion";
52 
53     /**
54      * Name of the meta-data item that should be set in the AndroidManifest.xml
55      * to specify the title text that should be displayed for the preference.
56      */
57     @VisibleForTesting
58     public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title";
59 
60     /**
61      * Name of the meta-data item that should be set in the AndroidManifest.xml
62      * to specify the summary text that should be displayed for the preference.
63      */
64     @VisibleForTesting
65     public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary";
66 
67     /**
68      * Name of the meta-data item that should be set in the AndroidManifest.xml
69      * to specify the content provider providing the summary text that should be displayed for the
70      * preference.
71      *
72      * Summary provided by the content provider overrides any static summary.
73      */
74     @VisibleForTesting
75     public static final String META_DATA_PREFERENCE_SUMMARY_URI =
76             "com.android.settings.summary_uri";
77 
78     /**
79      * Name of the meta-data item that should be set in the AndroidManifest.xml
80      * to specify the icon that should be displayed for the preference.
81      */
82     @VisibleForTesting
83     public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon";
84 
85     /**
86      * Hint for type of suggestion UI to be displayed.
87      */
88     @VisibleForTesting
89     public static final String META_DATA_PREFERENCE_CUSTOM_VIEW =
90             "com.android.settings.custom_view";
91 
92     private final String mId;
93     private final Context mContext;
94     private final ResolveInfo mResolveInfo;
95     private final ComponentName mComponent;
96     private final Intent mIntent;
97     private final boolean mIsEligible;
98     private final boolean mIgnoreAppearRule;
99 
CandidateSuggestion(Context context, ResolveInfo resolveInfo, boolean ignoreAppearRule)100     public CandidateSuggestion(Context context, ResolveInfo resolveInfo,
101             boolean ignoreAppearRule) {
102         mContext = context;
103         mIgnoreAppearRule = ignoreAppearRule;
104         mResolveInfo = resolveInfo;
105         mIntent = new Intent().setClassName(
106                 resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name);
107         mComponent = mIntent.getComponent();
108         mId = generateId();
109         mIsEligible = initIsEligible();
110     }
111 
getId()112     public String getId() {
113         return mId;
114     }
115 
getComponent()116     public ComponentName getComponent() {
117         return mComponent;
118     }
119 
120     /**
121      * Whether or not this candidate is eligible for display.
122      * <p/>
123      * Note: eligible doesn't mean it will be displayed.
124      */
isEligible()125     public boolean isEligible() {
126         return mIsEligible;
127     }
128 
toSuggestion()129     public Suggestion toSuggestion() {
130         if (!mIsEligible) {
131             return null;
132         }
133         final Suggestion.Builder builder = new Suggestion.Builder(mId);
134         updateBuilder(builder);
135         return builder.build();
136     }
137 
138     /**
139      * Checks device condition against suggestion requirement. Returns true if the suggestion is
140      * eligible.
141      * <p/>
142      * Note: eligible doesn't mean it will be displayed.
143      */
initIsEligible()144     private boolean initIsEligible() {
145         if (!ProviderEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
146             return false;
147         }
148         if (!ConnectivityEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
149             return false;
150         }
151         if (!FeatureEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
152             return false;
153         }
154         if (!AccountEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
155             return false;
156         }
157         if (!DismissedChecker.isEligible(mContext, mId, mResolveInfo, mIgnoreAppearRule)) {
158             return false;
159         }
160         if (!AutomotiveEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) {
161             return false;
162         }
163         return true;
164     }
165 
updateBuilder(Suggestion.Builder builder)166     private void updateBuilder(Suggestion.Builder builder) {
167         final PackageManager pm = mContext.getPackageManager();
168         final String packageName = mComponent.getPackageName();
169 
170         int iconRes = 0;
171         int flags = 0;
172         CharSequence title = null;
173         CharSequence summary = null;
174         Icon icon = null;
175 
176         // Get the activity's meta-data
177         try {
178             final Resources res = pm.getResourcesForApplication(packageName);
179             final Bundle metaData = mResolveInfo.activityInfo.metaData;
180 
181             if (res != null && metaData != null) {
182                 // First get override data
183                 final Bundle overrideData = getOverrideData(metaData);
184                 // Get icon
185                 if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) {
186                     iconRes = metaData.getInt(META_DATA_PREFERENCE_ICON);
187                 } else {
188                     iconRes = mResolveInfo.activityInfo.icon;
189                 }
190                 if (iconRes != 0) {
191                     icon = Icon.createWithResource(
192                             mResolveInfo.activityInfo.packageName, iconRes);
193                 }
194                 // Get title
195                 title = getStringFromBundle(overrideData, META_DATA_PREFERENCE_TITLE);
196                 if (TextUtils.isEmpty(title) && metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
197                     if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
198                         title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
199                     } else {
200                         title = metaData.getString(META_DATA_PREFERENCE_TITLE);
201                     }
202                 }
203                 // Get summary
204                 summary = getStringFromBundle(overrideData, META_DATA_PREFERENCE_SUMMARY);
205                 if (TextUtils.isEmpty(summary)
206                         && metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
207                     if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
208                         summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY));
209                     } else {
210                         summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY);
211                     }
212                 }
213                 // Detect remote view
214                 flags = metaData.containsKey(META_DATA_PREFERENCE_CUSTOM_VIEW)
215                         ? Suggestion.FLAG_HAS_BUTTON : 0;
216             }
217         } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
218             Log.w(TAG, "Couldn't find info", e);
219         }
220 
221         // Set the preference title to the activity's label if no
222         // meta-data is found
223         if (TextUtils.isEmpty(title)) {
224             title = mResolveInfo.activityInfo.loadLabel(pm);
225         }
226         builder.setTitle(title)
227                 .setSummary(summary)
228                 .setFlags(flags)
229                 .setIcon(icon)
230                 .setPendingIntent(PendingIntent
231                         .getActivity(mContext, 0 /* requestCode */, mIntent, 0 /* flags */));
232     }
233 
234     /**
235      * Extracts a string from bundle.
236      */
getStringFromBundle(Bundle bundle, String key)237     private CharSequence getStringFromBundle(Bundle bundle, String key) {
238         if (bundle == null || TextUtils.isEmpty(key)) {
239             return null;
240         }
241         return bundle.getString(key);
242     }
243 
getOverrideData(Bundle metadata)244     private Bundle getOverrideData(Bundle metadata) {
245         if (metadata == null || !metadata.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
246             Log.d(TAG, "Metadata null or has no info about summary_uri");
247             return null;
248         }
249 
250         final String uriString = metadata.getString(META_DATA_PREFERENCE_SUMMARY_URI);
251         final Bundle bundle = getBundleFromUri(uriString);
252         return bundle;
253     }
254 
255     /**
256      * Calls method through ContentProvider and expects a bundle in return.
257      */
getBundleFromUri(String uriString)258     private Bundle getBundleFromUri(String uriString) {
259         final Uri uri = Uri.parse(uriString);
260 
261         final String method = getMethodFromUri(uri);
262         if (TextUtils.isEmpty(method)) {
263             return null;
264         }
265         try {
266             return mContext.getContentResolver().call(uri, method, null /* args */,
267                     null /* bundle */);
268         } catch (IllegalArgumentException e){
269             Log.d(TAG, "Unknown summary_uri", e);
270             return null;
271         }
272     }
273 
274     /**
275      * Returns the first path segment of the uri if it exists as the method, otherwise null.
276      */
getMethodFromUri(Uri uri)277     private String getMethodFromUri(Uri uri) {
278         if (uri == null) {
279             return null;
280         }
281         final List<String> pathSegments = uri.getPathSegments();
282         if ((pathSegments == null) || pathSegments.isEmpty()) {
283             return null;
284         }
285         return pathSegments.get(0);
286     }
287 
generateId()288     private String generateId() {
289         return mComponent.flattenToString();
290     }
291 }
292