/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.intelligence.suggestions.model; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.res.Resources; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.service.settings.suggestions.Suggestion; import androidx.annotation.VisibleForTesting; import android.text.TextUtils; import android.util.Log; import com.android.settings.intelligence.suggestions.eligibility.AccountEligibilityChecker; import com.android.settings.intelligence.suggestions.eligibility.AutomotiveEligibilityChecker; import com.android.settings.intelligence.suggestions.eligibility.ConnectivityEligibilityChecker; import com.android.settings.intelligence.suggestions.eligibility.DismissedChecker; import com.android.settings.intelligence.suggestions.eligibility.FeatureEligibilityChecker; import com.android.settings.intelligence.suggestions.eligibility.ProviderEligibilityChecker; import java.util.List; /** * A wrapper to {@link android.content.pm.ResolveInfo} that matches Suggestion signature. *

* This class contains necessary metadata to eventually be * processed into a {@link android.service.settings.suggestions.Suggestion}. */ public class CandidateSuggestion { private static final String TAG = "CandidateSuggestion"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the title text that should be displayed for the preference. */ @VisibleForTesting public static final String META_DATA_PREFERENCE_TITLE = "com.android.settings.title"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the summary text that should be displayed for the preference. */ @VisibleForTesting public static final String META_DATA_PREFERENCE_SUMMARY = "com.android.settings.summary"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the content provider providing the summary text that should be displayed for the * preference. * * Summary provided by the content provider overrides any static summary. */ @VisibleForTesting public static final String META_DATA_PREFERENCE_SUMMARY_URI = "com.android.settings.summary_uri"; /** * Name of the meta-data item that should be set in the AndroidManifest.xml * to specify the icon that should be displayed for the preference. */ @VisibleForTesting public static final String META_DATA_PREFERENCE_ICON = "com.android.settings.icon"; /** * Hint for type of suggestion UI to be displayed. */ @VisibleForTesting public static final String META_DATA_PREFERENCE_CUSTOM_VIEW = "com.android.settings.custom_view"; private final String mId; private final Context mContext; private final ResolveInfo mResolveInfo; private final ComponentName mComponent; private final Intent mIntent; private final boolean mIsEligible; private final boolean mIgnoreAppearRule; public CandidateSuggestion(Context context, ResolveInfo resolveInfo, boolean ignoreAppearRule) { mContext = context; mIgnoreAppearRule = ignoreAppearRule; mResolveInfo = resolveInfo; mIntent = new Intent().setClassName( resolveInfo.activityInfo.packageName, resolveInfo.activityInfo.name); mComponent = mIntent.getComponent(); mId = generateId(); mIsEligible = initIsEligible(); } public String getId() { return mId; } public ComponentName getComponent() { return mComponent; } /** * Whether or not this candidate is eligible for display. *

* Note: eligible doesn't mean it will be displayed. */ public boolean isEligible() { return mIsEligible; } public Suggestion toSuggestion() { if (!mIsEligible) { return null; } final Suggestion.Builder builder = new Suggestion.Builder(mId); updateBuilder(builder); return builder.build(); } /** * Checks device condition against suggestion requirement. Returns true if the suggestion is * eligible. *

* Note: eligible doesn't mean it will be displayed. */ private boolean initIsEligible() { if (!ProviderEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) { return false; } if (!ConnectivityEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) { return false; } if (!FeatureEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) { return false; } if (!AccountEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) { return false; } if (!DismissedChecker.isEligible(mContext, mId, mResolveInfo, mIgnoreAppearRule)) { return false; } if (!AutomotiveEligibilityChecker.isEligible(mContext, mId, mResolveInfo)) { return false; } return true; } private void updateBuilder(Suggestion.Builder builder) { final PackageManager pm = mContext.getPackageManager(); final String packageName = mComponent.getPackageName(); int iconRes = 0; int flags = 0; CharSequence title = null; CharSequence summary = null; Icon icon = null; // Get the activity's meta-data try { final Resources res = pm.getResourcesForApplication(packageName); final Bundle metaData = mResolveInfo.activityInfo.metaData; if (res != null && metaData != null) { // First get override data final Bundle overrideData = getOverrideData(metaData); // Get icon icon = getIconFromBundle(overrideData, META_DATA_PREFERENCE_ICON); if (icon == null) { if (metaData.containsKey(META_DATA_PREFERENCE_ICON)) { iconRes = metaData.getInt(META_DATA_PREFERENCE_ICON); } else { iconRes = mResolveInfo.activityInfo.icon; } if (iconRes != 0) { icon = Icon.createWithResource( mResolveInfo.activityInfo.packageName, iconRes); } } // Get title title = getStringFromBundle(overrideData, META_DATA_PREFERENCE_TITLE); if (TextUtils.isEmpty(title) && metaData.containsKey(META_DATA_PREFERENCE_TITLE)) { if (metaData.get(META_DATA_PREFERENCE_TITLE) instanceof Integer) { title = res.getString(metaData.getInt(META_DATA_PREFERENCE_TITLE)); } else { title = metaData.getString(META_DATA_PREFERENCE_TITLE); } } // Get summary summary = getStringFromBundle(overrideData, META_DATA_PREFERENCE_SUMMARY); if (TextUtils.isEmpty(summary) && metaData.containsKey(META_DATA_PREFERENCE_SUMMARY)) { if (metaData.get(META_DATA_PREFERENCE_SUMMARY) instanceof Integer) { summary = res.getString(metaData.getInt(META_DATA_PREFERENCE_SUMMARY)); } else { summary = metaData.getString(META_DATA_PREFERENCE_SUMMARY); } } // Detect remote view flags = metaData.containsKey(META_DATA_PREFERENCE_CUSTOM_VIEW) ? Suggestion.FLAG_HAS_BUTTON : 0; } } catch (PackageManager.NameNotFoundException | Resources.NotFoundException e) { Log.w(TAG, "Couldn't find info", e); } // Set the preference title to the activity's label if no // meta-data is found if (TextUtils.isEmpty(title)) { title = mResolveInfo.activityInfo.loadLabel(pm); } builder.setTitle(title) .setSummary(summary) .setFlags(flags) .setIcon(icon) .setPendingIntent(PendingIntent .getActivity(mContext, 0 /* requestCode */, mIntent, 0 /* flags */)); } /** * Extracts a string from bundle. */ private CharSequence getStringFromBundle(Bundle bundle, String key) { if (bundle == null || TextUtils.isEmpty(key)) { return null; } return bundle.getString(key); } /** Extracts an Icon object from bundle. */ private Icon getIconFromBundle(Bundle bundle, String key) { if (bundle == null || TextUtils.isEmpty(key)) { return null; } return bundle.getParcelable(key); } private Bundle getOverrideData(Bundle metadata) { if (metadata == null || !metadata.containsKey(META_DATA_PREFERENCE_SUMMARY_URI)) { Log.d(TAG, "Metadata null or has no info about summary_uri"); return null; } final String uriString = metadata.getString(META_DATA_PREFERENCE_SUMMARY_URI); final Bundle bundle = getBundleFromUri(uriString); return bundle; } /** * Calls method through ContentProvider and expects a bundle in return. */ private Bundle getBundleFromUri(String uriString) { final Uri uri = Uri.parse(uriString); final String method = getMethodFromUri(uri); if (TextUtils.isEmpty(method)) { return null; } try { return mContext.getContentResolver().call(uri, method, null /* args */, null /* bundle */); } catch (IllegalArgumentException e){ Log.d(TAG, "Unknown summary_uri", e); return null; } } /** * Returns the first path segment of the uri if it exists as the method, otherwise null. */ private String getMethodFromUri(Uri uri) { if (uri == null) { return null; } final List pathSegments = uri.getPathSegments(); if ((pathSegments == null) || pathSegments.isEmpty()) { return null; } return pathSegments.get(0); } private String generateId() { return mComponent.flattenToString(); } }