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 androidx.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                 icon = getIconFromBundle(overrideData, META_DATA_PREFERENCE_ICON);
186                 if (icon == null) {
187                     if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) {
188                         iconRes = metaData.getInt(META_DATA_PREFERENCE_ICON);
189                     } else {
190                         iconRes = mResolveInfo.activityInfo.icon;
191                     }
192                     if (iconRes != 0) {
193                         icon = Icon.createWithResource(
194                                 mResolveInfo.activityInfo.packageName, iconRes);
195                     }
196                 }
197                 // Get title
198                 title = getStringFromBundle(overrideData, META_DATA_PREFERENCE_TITLE);
199                 if (TextUtils.isEmpty(title) && metaData.containsKey(META_DATA_PREFERENCE_TITLE)) {
200                     if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) {
201                         title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE));
202                     } else {
203                         title = metaData.getString(META_DATA_PREFERENCE_TITLE);
204                     }
205                 }
206                 // Get summary
207                 summary = getStringFromBundle(overrideData, META_DATA_PREFERENCE_SUMMARY);
208                 if (TextUtils.isEmpty(summary)
209                         && metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) {
210                     if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) {
211                         summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY));
212                     } else {
213                         summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY);
214                     }
215                 }
216                 // Detect remote view
217                 flags = metaData.containsKey(META_DATA_PREFERENCE_CUSTOM_VIEW)
218                         ? Suggestion.FLAG_HAS_BUTTON : 0;
219             }
220         } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) {
221             Log.w(TAG, "Couldn't find info", e);
222         }
223 
224         // Set the preference title to the activity's label if no
225         // meta-data is found
226         if (TextUtils.isEmpty(title)) {
227             title = mResolveInfo.activityInfo.loadLabel(pm);
228         }
229         builder.setTitle(title)
230                 .setSummary(summary)
231                 .setFlags(flags)
232                 .setIcon(icon)
233                 .setPendingIntent(PendingIntent
234                         .getActivity(mContext, 0 /* requestCode */, mIntent, 0 /* flags */));
235     }
236 
237     /**
238      * Extracts a string from bundle.
239      */
getStringFromBundle(Bundle bundle, String key)240     private CharSequence getStringFromBundle(Bundle bundle, String key) {
241         if (bundle == null || TextUtils.isEmpty(key)) {
242             return null;
243         }
244         return bundle.getString(key);
245     }
246 
247     /** Extracts an Icon object from bundle. */
getIconFromBundle(Bundle bundle, String key)248     private Icon getIconFromBundle(Bundle bundle, String key) {
249         if (bundle == null || TextUtils.isEmpty(key)) {
250             return null;
251         }
252         return bundle.getParcelable(key);
253     }
254 
getOverrideData(Bundle metadata)255     private Bundle getOverrideData(Bundle metadata) {
256         if (metadata == null || !metadata.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) {
257             Log.d(TAG, "Metadata null or has no info about summary_uri");
258             return null;
259         }
260 
261         final String uriString = metadata.getString(META_DATA_PREFERENCE_SUMMARY_URI);
262         final Bundle bundle = getBundleFromUri(uriString);
263         return bundle;
264     }
265 
266     /**
267      * Calls method through ContentProvider and expects a bundle in return.
268      */
getBundleFromUri(String uriString)269     private Bundle getBundleFromUri(String uriString) {
270         final Uri uri = Uri.parse(uriString);
271 
272         final String method = getMethodFromUri(uri);
273         if (TextUtils.isEmpty(method)) {
274             return null;
275         }
276         try {
277             return mContext.getContentResolver().call(uri, method, null /* args */,
278                     null /* bundle */);
279         } catch (IllegalArgumentException e){
280             Log.d(TAG, "Unknown summary_uri", e);
281             return null;
282         }
283     }
284 
285     /**
286      * Returns the first path segment of the uri if it exists as the method, otherwise null.
287      */
getMethodFromUri(Uri uri)288     private String getMethodFromUri(Uri uri) {
289         if (uri == null) {
290             return null;
291         }
292         final List<String> pathSegments = uri.getPathSegments();
293         if ((pathSegments == null) || pathSegments.isEmpty()) {
294             return null;
295         }
296         return pathSegments.get(0);
297     }
298 
generateId()299     private String generateId() {
300         return mComponent.flattenToString();
301     }
302 }
303