Support copy action in ExtServices

1. Implemented CopyCodeActivity to copy the text from the incoming intent
2. Support ConversationAction of type == "copy"

Test: 1. atest SmartActionsHelperTest
      2. Send myself a message "Authentication code: 12345", observe
         the copy action. Tap on it, observe a toast and verify that
         the code is copied

BUG: 126193140
Change-Id: I73ac3b36413fd5f632951b48910c557a22b20c52
This commit is contained in:
Tony Mak
2019-03-04 15:08:45 +00:00
parent a49171ea5c
commit 9e0dfdce60
7 changed files with 168 additions and 13 deletions

View File

@@ -3681,6 +3681,7 @@
<java-symbol type="array" name="config_displayWhiteBalanceAmbientColorTemperatures" />
<java-symbol type="array" name="config_displayWhiteBalanceDisplayColorTemperatures" />
<java-symbol type="drawable" name="ic_action_open" />
<java-symbol type="drawable" name="ic_menu_copy_material" />
<!-- MIME types -->
<java-symbol type="string" name="mime_type_folder" />

View File

@@ -25,10 +25,6 @@ LOCAL_PRIVATE_PLATFORM_APIS := true
LOCAL_CERTIFICATE := platform
LOCAL_AAPT_FLAGS := --shared-lib
LOCAL_EXPORT_PACKAGE_RESOURCES := true
LOCAL_PROGUARD_FLAG_FILES := proguard.proguard
LOCAL_PRIVILEGED_MODULE := true

View File

@@ -78,6 +78,10 @@
</intent-filter>
</service>
<activity android:name=".notification.CopyCodeActivity"
android:exported="false"
android:theme="@android:style/Theme.NoDisplay"/>
<library android:name="android.ext.services"/>
</application>

View File

@@ -24,4 +24,10 @@
<item>EDIT_DISTANCE</item>
<item>EXACT_MATCH</item>
</string-array>
<!-- Action chip to copy a one time code to the user's clipboard [CHAR LIMIT=NONE]-->
<string name="copy_code_desc">Copy \u201c<xliff:g id="code" example="12345">%1$s</xliff:g>\u201c</string>
<!-- Toast to display when text is copied to the device clipboard [CHAR LIMIT=64]-->
<string name="code_copied_to_clipboard">Code copied</string>
</resources>

View File

@@ -0,0 +1,54 @@
/*
* 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.ext.services.notification;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Intent;
import android.ext.services.R;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;
/**
* An activity that copies text in the Bundle.
*/
public class CopyCodeActivity extends Activity {
private static final String TAG = "CopyCodeActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
handleIntent();
finish();
}
private void handleIntent() {
String code = getIntent().getStringExtra(Intent.EXTRA_TEXT);
if (TextUtils.isEmpty(code)) {
Log.w(TAG, "handleIntent: empty code");
return;
}
ClipboardManager clipboardManager = getSystemService(ClipboardManager.class);
ClipData clipData = ClipData.newPlainText(null, code);
clipboardManager.setPrimaryClip(clipData);
Toast.makeText(getApplicationContext(), R.string.code_copied_to_clipboard,
Toast.LENGTH_SHORT).show();
}
}

View File

@@ -15,11 +15,15 @@
*/
package android.ext.services.notification;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Person;
import android.app.RemoteAction;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.ext.services.R;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.Parcelable;
@@ -48,11 +52,12 @@ import java.util.Deque;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class SmartActionsHelper {
private static final String KEY_ACTION_TYPE = "action_type";
private static final String KEY_ACTION_SCORE = "action_score";
static final String ENTITIES_EXTRAS = "entities-extras";
static final String KEY_ACTION_TYPE = "action_type";
static final String KEY_ACTION_SCORE = "action_score";
static final String KEY_TEXT = "text";
// If a notification has any of these flags set, it's inelgibile for actions being added.
private static final int FLAG_MASK_INELGIBILE_FOR_ACTIONS =
Notification.FLAG_ONGOING_EVENT
@@ -109,11 +114,25 @@ public class SmartActionsHelper {
repliesScore.put(textReply, conversationAction.getConfidenceScore());
}
ArrayList<Notification.Action> actions = conversationActions.stream()
.filter(conversationAction -> conversationAction.getAction() != null)
.map(action -> createNotificationAction(
action.getAction(), action.getType(), action.getConfidenceScore()))
.collect(Collectors.toCollection(ArrayList::new));
ArrayList<Notification.Action> actions = new ArrayList<>();
for (ConversationAction conversationAction : conversationActions) {
if (!TextUtils.isEmpty(conversationAction.getTextReply())) {
continue;
}
Notification.Action notificationAction;
if (conversationAction.getAction() == null) {
notificationAction =
createNotificationActionWithoutRemoteAction(conversationAction);
} else {
notificationAction = createNotificationActionFromRemoteAction(
conversationAction.getAction(),
conversationAction.getType(),
conversationAction.getConfidenceScore());
}
if (notificationAction != null) {
actions.add(notificationAction);
}
}
// Start a new session for logging if necessary.
if (!TextUtils.isEmpty(resultId)
@@ -126,6 +145,55 @@ public class SmartActionsHelper {
return new SmartSuggestions(replies, actions);
}
/**
* Creates notification action from ConversationAction that does not come up a RemoteAction.
* It could happen because we don't have common intents for some actions, like copying text.
*/
@Nullable
private Notification.Action createNotificationActionWithoutRemoteAction(
ConversationAction conversationAction) {
if (ConversationAction.TYPE_COPY.equals(conversationAction.getType())) {
return createCopyCodeAction(conversationAction);
}
return null;
}
@Nullable
private Notification.Action createCopyCodeAction(ConversationAction conversationAction) {
Bundle extras = conversationAction.getExtras();
if (extras == null) {
return null;
}
Bundle entitiesExtas = extras.getParcelable(ENTITIES_EXTRAS);
if (entitiesExtas == null) {
return null;
}
String code = entitiesExtas.getString(KEY_TEXT);
if (TextUtils.isEmpty(code)) {
return null;
}
String contentDescription = mContext.getString(R.string.copy_code_desc, code);
Intent intent = new Intent(mContext, CopyCodeActivity.class);
intent.putExtra(Intent.EXTRA_TEXT, code);
RemoteAction remoteAction = new RemoteAction(Icon.createWithResource(
mContext.getResources(),
com.android.internal.R.drawable.ic_menu_copy_material),
code,
contentDescription,
PendingIntent.getActivity(
mContext,
code.hashCode(),
intent,
PendingIntent.FLAG_UPDATE_CURRENT
));
return createNotificationActionFromRemoteAction(
remoteAction,
ConversationAction.TYPE_COPY,
conversationAction.getConfidenceScore());
}
/**
* Returns whether the suggestion might be used in the notifications in SysUI.
* <p>
@@ -292,7 +360,7 @@ public class SmartActionsHelper {
mTextClassifier.onTextClassifierEvent(textClassifierEvent);
}
private Notification.Action createNotificationAction(
private Notification.Action createNotificationActionFromRemoteAction(
RemoteAction remoteAction, String actionType, float score) {
Icon icon = remoteAction.shouldShowIcon()
? remoteAction.getIcon()

View File

@@ -36,6 +36,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.IPackageManager;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.Process;
import android.service.notification.NotificationAssistantService;
import android.service.notification.StatusBarNotification;
@@ -419,6 +420,31 @@ public class SmartActionsHelperTest {
assertTextClassifierEvent(events.get(1), TextClassifierEvent.TYPE_ACTIONS_SHOWN);
}
@Test
public void testCopyAction() {
Bundle extras = new Bundle();
Bundle entitiesExtras = new Bundle();
entitiesExtras.putString(SmartActionsHelper.KEY_TEXT, "12345");
extras.putParcelable(SmartActionsHelper.ENTITIES_EXTRAS, entitiesExtras);
ConversationAction conversationAction =
new ConversationAction.Builder(ConversationAction.TYPE_COPY)
.setExtras(extras)
.build();
when(mTextClassifier.suggestConversationActions(any(ConversationActions.Request.class)))
.thenReturn(
new ConversationActions(
Collections.singletonList(conversationAction), null));
Notification notification = createMessageNotification();
when(mStatusBarNotification.getNotification()).thenReturn(notification);
SmartActionsHelper.SmartSuggestions suggestions =
mSmartActionsHelper.suggest(createNotificationEntry());
assertThat(suggestions.actions).hasSize(1);
Notification.Action action = suggestions.actions.get(0);
assertThat(action.title).isEqualTo("12345");
}
private ZonedDateTime createZonedDateTimeFromMsUtc(long msUtc) {
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(msUtc), ZoneOffset.systemDefault());
}