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:
@@ -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" />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user