1 /* 2 * Copyright (C) 2019 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 package android.view.textclassifier.intent; 17 18 import android.annotation.Nullable; 19 import android.app.PendingIntent; 20 import android.app.RemoteAction; 21 import android.content.ComponentName; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.pm.PackageManager; 25 import android.content.pm.ResolveInfo; 26 import android.graphics.drawable.Icon; 27 import android.os.Bundle; 28 import android.text.TextUtils; 29 import android.view.textclassifier.ExtrasUtils; 30 import android.view.textclassifier.Log; 31 import android.view.textclassifier.TextClassification; 32 import android.view.textclassifier.TextClassifier; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.internal.util.Preconditions; 36 37 /** 38 * Helper class to store the information from which RemoteActions are built. 39 * 40 * @hide 41 */ 42 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) 43 public final class LabeledIntent { 44 private static final String TAG = "LabeledIntent"; 45 public static final int DEFAULT_REQUEST_CODE = 0; 46 private static final TitleChooser DEFAULT_TITLE_CHOOSER = 47 (labeledIntent, resolveInfo) -> { 48 if (!TextUtils.isEmpty(labeledIntent.titleWithEntity)) { 49 return labeledIntent.titleWithEntity; 50 } 51 return labeledIntent.titleWithoutEntity; 52 }; 53 54 @Nullable 55 public final String titleWithoutEntity; 56 @Nullable 57 public final String titleWithEntity; 58 public final String description; 59 @Nullable 60 public final String descriptionWithAppName; 61 // Do not update this intent. 62 public final Intent intent; 63 public final int requestCode; 64 65 /** 66 * Initializes a LabeledIntent. 67 * 68 * <p>NOTE: {@code requestCode} is required to not be {@link #DEFAULT_REQUEST_CODE} 69 * if distinguishing info (e.g. the classified text) is represented in intent extras only. 70 * In such circumstances, the request code should represent the distinguishing info 71 * (e.g. by generating a hashcode) so that the generated PendingIntent is (somewhat) 72 * unique. To be correct, the PendingIntent should be definitely unique but we try a 73 * best effort approach that avoids spamming the system with PendingIntents. 74 */ 75 // TODO: Fix the issue mentioned above so the behaviour is correct. LabeledIntent( @ullable String titleWithoutEntity, @Nullable String titleWithEntity, String description, @Nullable String descriptionWithAppName, Intent intent, int requestCode)76 public LabeledIntent( 77 @Nullable String titleWithoutEntity, 78 @Nullable String titleWithEntity, 79 String description, 80 @Nullable String descriptionWithAppName, 81 Intent intent, 82 int requestCode) { 83 if (TextUtils.isEmpty(titleWithEntity) && TextUtils.isEmpty(titleWithoutEntity)) { 84 throw new IllegalArgumentException( 85 "titleWithEntity and titleWithoutEntity should not be both null"); 86 } 87 this.titleWithoutEntity = titleWithoutEntity; 88 this.titleWithEntity = titleWithEntity; 89 this.description = Preconditions.checkNotNull(description); 90 this.descriptionWithAppName = descriptionWithAppName; 91 this.intent = Preconditions.checkNotNull(intent); 92 this.requestCode = requestCode; 93 } 94 95 /** 96 * Return the resolved result. 97 * 98 * @param context the context to resolve the result's intent and action 99 * @param titleChooser for choosing an action title 100 * @param textLanguagesBundle containing language detection information 101 */ 102 @Nullable resolve( Context context, @Nullable TitleChooser titleChooser, @Nullable Bundle textLanguagesBundle)103 public Result resolve( 104 Context context, 105 @Nullable TitleChooser titleChooser, 106 @Nullable Bundle textLanguagesBundle) { 107 final PackageManager pm = context.getPackageManager(); 108 final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0); 109 110 if (resolveInfo == null || resolveInfo.activityInfo == null) { 111 Log.w(TAG, "resolveInfo or activityInfo is null"); 112 return null; 113 } 114 final String packageName = resolveInfo.activityInfo.packageName; 115 final String className = resolveInfo.activityInfo.name; 116 if (packageName == null || className == null) { 117 Log.w(TAG, "packageName or className is null"); 118 return null; 119 } 120 Intent resolvedIntent = new Intent(intent); 121 resolvedIntent.putExtra( 122 TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, 123 getFromTextClassifierExtra(textLanguagesBundle)); 124 boolean shouldShowIcon = false; 125 Icon icon = null; 126 if (!"android".equals(packageName)) { 127 // We only set the component name when the package name is not resolved to "android" 128 // to workaround a bug that explicit intent with component name == ResolverActivity 129 // can't be launched on keyguard. 130 resolvedIntent.setComponent(new ComponentName(packageName, className)); 131 if (resolveInfo.activityInfo.getIconResource() != 0) { 132 icon = Icon.createWithResource( 133 packageName, resolveInfo.activityInfo.getIconResource()); 134 shouldShowIcon = true; 135 } 136 } 137 if (icon == null) { 138 // RemoteAction requires that there be an icon. 139 icon = Icon.createWithResource( 140 "android", com.android.internal.R.drawable.ic_more_items); 141 } 142 final PendingIntent pendingIntent = 143 TextClassification.createPendingIntent(context, resolvedIntent, requestCode); 144 titleChooser = titleChooser == null ? DEFAULT_TITLE_CHOOSER : titleChooser; 145 CharSequence title = titleChooser.chooseTitle(this, resolveInfo); 146 if (TextUtils.isEmpty(title)) { 147 Log.w(TAG, "Custom titleChooser return null, fallback to the default titleChooser"); 148 title = DEFAULT_TITLE_CHOOSER.chooseTitle(this, resolveInfo); 149 } 150 final RemoteAction action = 151 new RemoteAction(icon, title, resolveDescription(resolveInfo, pm), pendingIntent); 152 action.setShouldShowIcon(shouldShowIcon); 153 return new Result(resolvedIntent, action); 154 } 155 resolveDescription(ResolveInfo resolveInfo, PackageManager packageManager)156 private String resolveDescription(ResolveInfo resolveInfo, PackageManager packageManager) { 157 if (!TextUtils.isEmpty(descriptionWithAppName)) { 158 // Example string format of descriptionWithAppName: "Use %1$s to open map". 159 String applicationName = getApplicationName(resolveInfo, packageManager); 160 if (!TextUtils.isEmpty(applicationName)) { 161 return String.format(descriptionWithAppName, applicationName); 162 } 163 } 164 return description; 165 } 166 167 @Nullable getApplicationName( ResolveInfo resolveInfo, PackageManager packageManager)168 private String getApplicationName( 169 ResolveInfo resolveInfo, PackageManager packageManager) { 170 if (resolveInfo.activityInfo == null) { 171 return null; 172 } 173 if ("android".equals(resolveInfo.activityInfo.packageName)) { 174 return null; 175 } 176 if (resolveInfo.activityInfo.applicationInfo == null) { 177 return null; 178 } 179 return (String) packageManager.getApplicationLabel( 180 resolveInfo.activityInfo.applicationInfo); 181 } 182 getFromTextClassifierExtra(@ullable Bundle textLanguagesBundle)183 private Bundle getFromTextClassifierExtra(@Nullable Bundle textLanguagesBundle) { 184 if (textLanguagesBundle != null) { 185 final Bundle bundle = new Bundle(); 186 ExtrasUtils.putTextLanguagesExtra(bundle, textLanguagesBundle); 187 return bundle; 188 } else { 189 return Bundle.EMPTY; 190 } 191 } 192 193 /** 194 * Data class that holds the result. 195 */ 196 public static final class Result { 197 public final Intent resolvedIntent; 198 public final RemoteAction remoteAction; 199 Result(Intent resolvedIntent, RemoteAction remoteAction)200 public Result(Intent resolvedIntent, RemoteAction remoteAction) { 201 this.resolvedIntent = Preconditions.checkNotNull(resolvedIntent); 202 this.remoteAction = Preconditions.checkNotNull(remoteAction); 203 } 204 } 205 206 /** 207 * An object to choose a title from resolved info. If {@code null} is returned, 208 * {@link #titleWithEntity} will be used if it exists, {@link #titleWithoutEntity} otherwise. 209 */ 210 public interface TitleChooser { 211 /** 212 * Picks a title from a {@link LabeledIntent} by looking into resolved info. 213 * {@code resolveInfo} is guaranteed to have a non-null {@code activityInfo}. 214 */ 215 @Nullable chooseTitle(LabeledIntent labeledIntent, ResolveInfo resolveInfo)216 CharSequence chooseTitle(LabeledIntent labeledIntent, ResolveInfo resolveInfo); 217 } 218 } 219