blob: cbd9d1a522f640f10f32026be96144860db3479f [file] [log] [blame]
Tony Makac9b4d82019-02-15 13:57:38 +00001/*
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 */
Tony Mak8ab9b182019-03-01 16:44:17 +000016package android.view.textclassifier.intent;
Tony Makac9b4d82019-02-15 13:57:38 +000017
18import android.annotation.Nullable;
19import android.app.PendingIntent;
20import android.app.RemoteAction;
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.Intent;
24import android.content.pm.PackageManager;
25import android.content.pm.ResolveInfo;
26import android.graphics.drawable.Icon;
Tony Mak72e17972019-03-16 10:28:42 +000027import android.os.Bundle;
Tony Makac9b4d82019-02-15 13:57:38 +000028import android.text.TextUtils;
Tony Mak72e17972019-03-16 10:28:42 +000029import android.view.textclassifier.ExtrasUtils;
Tony Mak8ab9b182019-03-01 16:44:17 +000030import android.view.textclassifier.Log;
31import android.view.textclassifier.TextClassification;
Tony Mak72e17972019-03-16 10:28:42 +000032import android.view.textclassifier.TextClassifier;
Tony Makac9b4d82019-02-15 13:57:38 +000033
34import com.android.internal.annotations.VisibleForTesting;
Daulet Zhanguzine1559472019-12-18 14:17:56 +000035
36import java.util.Objects;
Tony Makac9b4d82019-02-15 13:57:38 +000037
38/**
39 * Helper class to store the information from which RemoteActions are built.
40 *
41 * @hide
42 */
43@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
44public final class LabeledIntent {
45 private static final String TAG = "LabeledIntent";
46 public static final int DEFAULT_REQUEST_CODE = 0;
47 private static final TitleChooser DEFAULT_TITLE_CHOOSER =
48 (labeledIntent, resolveInfo) -> {
49 if (!TextUtils.isEmpty(labeledIntent.titleWithEntity)) {
50 return labeledIntent.titleWithEntity;
51 }
52 return labeledIntent.titleWithoutEntity;
53 };
54
55 @Nullable
56 public final String titleWithoutEntity;
57 @Nullable
58 public final String titleWithEntity;
59 public final String description;
Tony Mak15b64be2019-04-01 20:02:29 +010060 @Nullable
61 public final String descriptionWithAppName;
Tony Makac9b4d82019-02-15 13:57:38 +000062 // Do not update this intent.
63 public final Intent intent;
64 public final int requestCode;
65
66 /**
67 * Initializes a LabeledIntent.
68 *
69 * <p>NOTE: {@code requestCode} is required to not be {@link #DEFAULT_REQUEST_CODE}
70 * if distinguishing info (e.g. the classified text) is represented in intent extras only.
71 * In such circumstances, the request code should represent the distinguishing info
72 * (e.g. by generating a hashcode) so that the generated PendingIntent is (somewhat)
73 * unique. To be correct, the PendingIntent should be definitely unique but we try a
74 * best effort approach that avoids spamming the system with PendingIntents.
75 */
76 // TODO: Fix the issue mentioned above so the behaviour is correct.
77 public LabeledIntent(
78 @Nullable String titleWithoutEntity,
79 @Nullable String titleWithEntity,
80 String description,
Tony Mak15b64be2019-04-01 20:02:29 +010081 @Nullable String descriptionWithAppName,
Tony Makac9b4d82019-02-15 13:57:38 +000082 Intent intent,
83 int requestCode) {
84 if (TextUtils.isEmpty(titleWithEntity) && TextUtils.isEmpty(titleWithoutEntity)) {
85 throw new IllegalArgumentException(
86 "titleWithEntity and titleWithoutEntity should not be both null");
87 }
88 this.titleWithoutEntity = titleWithoutEntity;
89 this.titleWithEntity = titleWithEntity;
Daulet Zhanguzine1559472019-12-18 14:17:56 +000090 this.description = Objects.requireNonNull(description);
Tony Mak15b64be2019-04-01 20:02:29 +010091 this.descriptionWithAppName = descriptionWithAppName;
Daulet Zhanguzine1559472019-12-18 14:17:56 +000092 this.intent = Objects.requireNonNull(intent);
Tony Makac9b4d82019-02-15 13:57:38 +000093 this.requestCode = requestCode;
94 }
95
96 /**
97 * Return the resolved result.
Tony Mak72e17972019-03-16 10:28:42 +000098 *
99 * @param context the context to resolve the result's intent and action
100 * @param titleChooser for choosing an action title
101 * @param textLanguagesBundle containing language detection information
Tony Makac9b4d82019-02-15 13:57:38 +0000102 */
103 @Nullable
104 public Result resolve(
Tony Mak72e17972019-03-16 10:28:42 +0000105 Context context,
106 @Nullable TitleChooser titleChooser,
107 @Nullable Bundle textLanguagesBundle) {
Tony Makac9b4d82019-02-15 13:57:38 +0000108 final PackageManager pm = context.getPackageManager();
109 final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
Tony Makc12035e2019-02-26 17:45:34 +0000110
111 if (resolveInfo == null || resolveInfo.activityInfo == null) {
112 Log.w(TAG, "resolveInfo or activityInfo is null");
113 return null;
114 }
115 final String packageName = resolveInfo.activityInfo.packageName;
116 final String className = resolveInfo.activityInfo.name;
117 if (packageName == null || className == null) {
118 Log.w(TAG, "packageName or className is null");
119 return null;
120 }
Tony Makac9b4d82019-02-15 13:57:38 +0000121 Intent resolvedIntent = new Intent(intent);
Tony Mak72e17972019-03-16 10:28:42 +0000122 resolvedIntent.putExtra(
123 TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER,
124 getFromTextClassifierExtra(textLanguagesBundle));
Tony Makac9b4d82019-02-15 13:57:38 +0000125 boolean shouldShowIcon = false;
Tony Makc12035e2019-02-26 17:45:34 +0000126 Icon icon = null;
127 if (!"android".equals(packageName)) {
Tony Mak82e60022019-06-05 11:53:28 +0100128 // We only set the component name when the package name is not resolved to "android"
129 // to workaround a bug that explicit intent with component name == ResolverActivity
130 // can't be launched on keyguard.
131 resolvedIntent.setComponent(new ComponentName(packageName, className));
Tony Makac9b4d82019-02-15 13:57:38 +0000132 if (resolveInfo.activityInfo.getIconResource() != 0) {
133 icon = Icon.createWithResource(
134 packageName, resolveInfo.activityInfo.getIconResource());
135 shouldShowIcon = true;
136 }
137 }
138 if (icon == null) {
139 // RemoteAction requires that there be an icon.
Tony Mak72e17972019-03-16 10:28:42 +0000140 icon = Icon.createWithResource(
141 "android", com.android.internal.R.drawable.ic_more_items);
Tony Makac9b4d82019-02-15 13:57:38 +0000142 }
143 final PendingIntent pendingIntent =
144 TextClassification.createPendingIntent(context, resolvedIntent, requestCode);
Tony Mak72e17972019-03-16 10:28:42 +0000145 titleChooser = titleChooser == null ? DEFAULT_TITLE_CHOOSER : titleChooser;
Tony Makac9b4d82019-02-15 13:57:38 +0000146 CharSequence title = titleChooser.chooseTitle(this, resolveInfo);
147 if (TextUtils.isEmpty(title)) {
148 Log.w(TAG, "Custom titleChooser return null, fallback to the default titleChooser");
149 title = DEFAULT_TITLE_CHOOSER.chooseTitle(this, resolveInfo);
150 }
Tony Mak15b64be2019-04-01 20:02:29 +0100151 final RemoteAction action =
152 new RemoteAction(icon, title, resolveDescription(resolveInfo, pm), pendingIntent);
Tony Makac9b4d82019-02-15 13:57:38 +0000153 action.setShouldShowIcon(shouldShowIcon);
154 return new Result(resolvedIntent, action);
155 }
156
Tony Mak15b64be2019-04-01 20:02:29 +0100157 private String resolveDescription(ResolveInfo resolveInfo, PackageManager packageManager) {
158 if (!TextUtils.isEmpty(descriptionWithAppName)) {
159 // Example string format of descriptionWithAppName: "Use %1$s to open map".
160 String applicationName = getApplicationName(resolveInfo, packageManager);
161 if (!TextUtils.isEmpty(applicationName)) {
162 return String.format(descriptionWithAppName, applicationName);
163 }
164 }
165 return description;
166 }
167
168 @Nullable
169 private String getApplicationName(
170 ResolveInfo resolveInfo, PackageManager packageManager) {
171 if (resolveInfo.activityInfo == null) {
172 return null;
173 }
174 if ("android".equals(resolveInfo.activityInfo.packageName)) {
175 return null;
176 }
177 if (resolveInfo.activityInfo.applicationInfo == null) {
178 return null;
179 }
180 return (String) packageManager.getApplicationLabel(
181 resolveInfo.activityInfo.applicationInfo);
182 }
183
Tony Mak72e17972019-03-16 10:28:42 +0000184 private Bundle getFromTextClassifierExtra(@Nullable Bundle textLanguagesBundle) {
185 if (textLanguagesBundle != null) {
186 final Bundle bundle = new Bundle();
187 ExtrasUtils.putTextLanguagesExtra(bundle, textLanguagesBundle);
188 return bundle;
189 } else {
190 return Bundle.EMPTY;
191 }
192 }
193
Tony Makac9b4d82019-02-15 13:57:38 +0000194 /**
195 * Data class that holds the result.
196 */
197 public static final class Result {
198 public final Intent resolvedIntent;
199 public final RemoteAction remoteAction;
200
201 public Result(Intent resolvedIntent, RemoteAction remoteAction) {
Daulet Zhanguzine1559472019-12-18 14:17:56 +0000202 this.resolvedIntent = Objects.requireNonNull(resolvedIntent);
203 this.remoteAction = Objects.requireNonNull(remoteAction);
Tony Makac9b4d82019-02-15 13:57:38 +0000204 }
205 }
206
207 /**
208 * An object to choose a title from resolved info. If {@code null} is returned,
209 * {@link #titleWithEntity} will be used if it exists, {@link #titleWithoutEntity} otherwise.
210 */
211 public interface TitleChooser {
212 /**
213 * Picks a title from a {@link LabeledIntent} by looking into resolved info.
Tony Makc12035e2019-02-26 17:45:34 +0000214 * {@code resolveInfo} is guaranteed to have a non-null {@code activityInfo}.
Tony Makac9b4d82019-02-15 13:57:38 +0000215 */
216 @Nullable
217 CharSequence chooseTitle(LabeledIntent labeledIntent, ResolveInfo resolveInfo);
218 }
219}