Home | History | Annotate | Download | only in model
      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