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 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 112 public String getId() { 113 return mId; 114 } 115 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 */ 125 public boolean isEligible() { 126 return mIsEligible; 127 } 128 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 */ 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 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 */ 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 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 */ 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 */ 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 288 private String generateId() { 289 return mComponent.flattenToString(); 290 } 291 } 292