blob: cbd9d1a522f640f10f32026be96144860db3479f [file] [log] [blame]
/*
* Copyright (C) 2019 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 android.view.textclassifier.intent;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.textclassifier.ExtrasUtils;
import android.view.textclassifier.Log;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassifier;
import com.android.internal.annotations.VisibleForTesting;
import java.util.Objects;
/**
* Helper class to store the information from which RemoteActions are built.
*
* @hide
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public final class LabeledIntent {
private static final String TAG = "LabeledIntent";
public static final int DEFAULT_REQUEST_CODE = 0;
private static final TitleChooser DEFAULT_TITLE_CHOOSER =
(labeledIntent, resolveInfo) -> {
if (!TextUtils.isEmpty(labeledIntent.titleWithEntity)) {
return labeledIntent.titleWithEntity;
}
return labeledIntent.titleWithoutEntity;
};
@Nullable
public final String titleWithoutEntity;
@Nullable
public final String titleWithEntity;
public final String description;
@Nullable
public final String descriptionWithAppName;
// Do not update this intent.
public final Intent intent;
public final int requestCode;
/**
* Initializes a LabeledIntent.
*
* <p>NOTE: {@code requestCode} is required to not be {@link #DEFAULT_REQUEST_CODE}
* if distinguishing info (e.g. the classified text) is represented in intent extras only.
* In such circumstances, the request code should represent the distinguishing info
* (e.g. by generating a hashcode) so that the generated PendingIntent is (somewhat)
* unique. To be correct, the PendingIntent should be definitely unique but we try a
* best effort approach that avoids spamming the system with PendingIntents.
*/
// TODO: Fix the issue mentioned above so the behaviour is correct.
public LabeledIntent(
@Nullable String titleWithoutEntity,
@Nullable String titleWithEntity,
String description,
@Nullable String descriptionWithAppName,
Intent intent,
int requestCode) {
if (TextUtils.isEmpty(titleWithEntity) && TextUtils.isEmpty(titleWithoutEntity)) {
throw new IllegalArgumentException(
"titleWithEntity and titleWithoutEntity should not be both null");
}
this.titleWithoutEntity = titleWithoutEntity;
this.titleWithEntity = titleWithEntity;
this.description = Objects.requireNonNull(description);
this.descriptionWithAppName = descriptionWithAppName;
this.intent = Objects.requireNonNull(intent);
this.requestCode = requestCode;
}
/**
* Return the resolved result.
*
* @param context the context to resolve the result's intent and action
* @param titleChooser for choosing an action title
* @param textLanguagesBundle containing language detection information
*/
@Nullable
public Result resolve(
Context context,
@Nullable TitleChooser titleChooser,
@Nullable Bundle textLanguagesBundle) {
final PackageManager pm = context.getPackageManager();
final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
if (resolveInfo == null || resolveInfo.activityInfo == null) {
Log.w(TAG, "resolveInfo or activityInfo is null");
return null;
}
final String packageName = resolveInfo.activityInfo.packageName;
final String className = resolveInfo.activityInfo.name;
if (packageName == null || className == null) {
Log.w(TAG, "packageName or className is null");
return null;
}
Intent resolvedIntent = new Intent(intent);
resolvedIntent.putExtra(
TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER,
getFromTextClassifierExtra(textLanguagesBundle));
boolean shouldShowIcon = false;
Icon icon = null;
if (!"android".equals(packageName)) {
// We only set the component name when the package name is not resolved to "android"
// to workaround a bug that explicit intent with component name == ResolverActivity
// can't be launched on keyguard.
resolvedIntent.setComponent(new ComponentName(packageName, className));
if (resolveInfo.activityInfo.getIconResource() != 0) {
icon = Icon.createWithResource(
packageName, resolveInfo.activityInfo.getIconResource());
shouldShowIcon = true;
}
}
if (icon == null) {
// RemoteAction requires that there be an icon.
icon = Icon.createWithResource(
"android", com.android.internal.R.drawable.ic_more_items);
}
final PendingIntent pendingIntent =
TextClassification.createPendingIntent(context, resolvedIntent, requestCode);
titleChooser = titleChooser == null ? DEFAULT_TITLE_CHOOSER : titleChooser;
CharSequence title = titleChooser.chooseTitle(this, resolveInfo);
if (TextUtils.isEmpty(title)) {
Log.w(TAG, "Custom titleChooser return null, fallback to the default titleChooser");
title = DEFAULT_TITLE_CHOOSER.chooseTitle(this, resolveInfo);
}
final RemoteAction action =
new RemoteAction(icon, title, resolveDescription(resolveInfo, pm), pendingIntent);
action.setShouldShowIcon(shouldShowIcon);
return new Result(resolvedIntent, action);
}
private String resolveDescription(ResolveInfo resolveInfo, PackageManager packageManager) {
if (!TextUtils.isEmpty(descriptionWithAppName)) {
// Example string format of descriptionWithAppName: "Use %1$s to open map".
String applicationName = getApplicationName(resolveInfo, packageManager);
if (!TextUtils.isEmpty(applicationName)) {
return String.format(descriptionWithAppName, applicationName);
}
}
return description;
}
@Nullable
private String getApplicationName(
ResolveInfo resolveInfo, PackageManager packageManager) {
if (resolveInfo.activityInfo == null) {
return null;
}
if ("android".equals(resolveInfo.activityInfo.packageName)) {
return null;
}
if (resolveInfo.activityInfo.applicationInfo == null) {
return null;
}
return (String) packageManager.getApplicationLabel(
resolveInfo.activityInfo.applicationInfo);
}
private Bundle getFromTextClassifierExtra(@Nullable Bundle textLanguagesBundle) {
if (textLanguagesBundle != null) {
final Bundle bundle = new Bundle();
ExtrasUtils.putTextLanguagesExtra(bundle, textLanguagesBundle);
return bundle;
} else {
return Bundle.EMPTY;
}
}
/**
* Data class that holds the result.
*/
public static final class Result {
public final Intent resolvedIntent;
public final RemoteAction remoteAction;
public Result(Intent resolvedIntent, RemoteAction remoteAction) {
this.resolvedIntent = Objects.requireNonNull(resolvedIntent);
this.remoteAction = Objects.requireNonNull(remoteAction);
}
}
/**
* An object to choose a title from resolved info. If {@code null} is returned,
* {@link #titleWithEntity} will be used if it exists, {@link #titleWithoutEntity} otherwise.
*/
public interface TitleChooser {
/**
* Picks a title from a {@link LabeledIntent} by looking into resolved info.
* {@code resolveInfo} is guaranteed to have a non-null {@code activityInfo}.
*/
@Nullable
CharSequence chooseTitle(LabeledIntent labeledIntent, ResolveInfo resolveInfo);
}
}