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