Merge "Prototype for Sharesheet direct share row append mechanism" into rvc-dev
This commit is contained in:
@@ -221,6 +221,12 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
|
||||
*/
|
||||
abstract float getScore(ComponentName name);
|
||||
|
||||
/**
|
||||
* Returns the list of top K component names which have highest
|
||||
* {@link #getScore(ComponentName)}
|
||||
*/
|
||||
abstract List<ComponentName> getTopComponentNames(int topK);
|
||||
|
||||
/** Handles result message sent to mHandler. */
|
||||
abstract void handleResultMessage(Message message);
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Uses an {@link AppPredictor} to sort Resolver targets. If the AppPredictionService appears to be
|
||||
@@ -159,6 +161,15 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
|
||||
return 1.0f - (((float) rank) / consecutiveSumOfRanks);
|
||||
}
|
||||
|
||||
@Override
|
||||
List<ComponentName> getTopComponentNames(int topK) {
|
||||
return mTargetRanks.entrySet().stream()
|
||||
.sorted(Entry.comparingByValue())
|
||||
.limit(topK)
|
||||
.map(Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
void updateModel(ComponentName componentName) {
|
||||
if (mResolverRankerService != null) {
|
||||
|
||||
@@ -223,6 +223,11 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS,
|
||||
DEFAULT_SALT_EXPIRATION_DAYS);
|
||||
|
||||
private boolean mAppendDirectShareEnabled = DeviceConfig.getBoolean(
|
||||
DeviceConfig.NAMESPACE_SYSTEMUI,
|
||||
SystemUiDeviceConfigFlags.APPEND_DIRECT_SHARE_ENABLED,
|
||||
false);
|
||||
|
||||
private Bundle mReplacementExtras;
|
||||
private IntentSender mChosenComponentSender;
|
||||
private IntentSender mRefinementIntentSender;
|
||||
@@ -409,6 +414,11 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
private static final int WATCHDOG_TIMEOUT_MAX_MILLIS = 10000;
|
||||
private static final int WATCHDOG_TIMEOUT_MIN_MILLIS = 3000;
|
||||
|
||||
private static final int DEFAULT_DIRECT_SHARE_TIMEOUT_MILLIS = 1500;
|
||||
private int mDirectShareTimeout = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
|
||||
SystemUiDeviceConfigFlags.SHARE_SHEET_DIRECT_SHARE_TIMEOUT,
|
||||
DEFAULT_DIRECT_SHARE_TIMEOUT_MILLIS);
|
||||
|
||||
private boolean mMinTimeoutPassed = false;
|
||||
|
||||
private void removeAllMessages() {
|
||||
@@ -427,15 +437,14 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "queryTargets setting watchdog timer for "
|
||||
+ WATCHDOG_TIMEOUT_MIN_MILLIS + "-"
|
||||
+ mDirectShareTimeout + "-"
|
||||
+ WATCHDOG_TIMEOUT_MAX_MILLIS + "ms");
|
||||
}
|
||||
|
||||
sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_MIN_TIMEOUT,
|
||||
WATCHDOG_TIMEOUT_MIN_MILLIS);
|
||||
sendEmptyMessageDelayed(CHOOSER_TARGET_SERVICE_WATCHDOG_MAX_TIMEOUT,
|
||||
WATCHDOG_TIMEOUT_MAX_MILLIS);
|
||||
|
||||
mAppendDirectShareEnabled ? mDirectShareTimeout : WATCHDOG_TIMEOUT_MAX_MILLIS);
|
||||
}
|
||||
|
||||
private void maybeStopServiceRequestTimer() {
|
||||
@@ -463,6 +472,7 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
final ServiceResultInfo sri = (ServiceResultInfo) msg.obj;
|
||||
if (!mServiceConnections.contains(sri.connection)) {
|
||||
Log.w(TAG, "ChooserTargetServiceConnection " + sri.connection
|
||||
+ sri.originalTarget.getResolveInfo().activityInfo.packageName
|
||||
+ " returned after being removed from active connections."
|
||||
+ " Have you considered returning results faster?");
|
||||
break;
|
||||
@@ -474,7 +484,7 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
if (adapterForUserHandle != null) {
|
||||
adapterForUserHandle.addServiceResults(sri.originalTarget,
|
||||
sri.resultTargets, TARGET_TYPE_CHOOSER_TARGET,
|
||||
/* directShareShortcutInfoCache */ null);
|
||||
/* directShareShortcutInfoCache */ null, mServiceConnections);
|
||||
}
|
||||
}
|
||||
unbindService(sri.connection);
|
||||
@@ -489,6 +499,7 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
break;
|
||||
|
||||
case CHOOSER_TARGET_SERVICE_WATCHDOG_MAX_TIMEOUT:
|
||||
mMinTimeoutPassed = true;
|
||||
unbindRemainingServices();
|
||||
maybeStopServiceRequestTimer();
|
||||
break;
|
||||
@@ -513,7 +524,7 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
if (adapterForUserHandle != null) {
|
||||
adapterForUserHandle.addServiceResults(
|
||||
resultInfo.originalTarget, resultInfo.resultTargets, msg.arg1,
|
||||
mDirectShareShortcutInfoCache);
|
||||
mDirectShareShortcutInfoCache, mServiceConnections);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -1481,7 +1492,7 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
/* origTarget */ null,
|
||||
Lists.newArrayList(mCallerChooserTargets),
|
||||
TARGET_TYPE_DEFAULT,
|
||||
/* directShareShortcutInfoCache */ null);
|
||||
/* directShareShortcutInfoCache */ null, mServiceConnections);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3584,6 +3595,10 @@ public class ChooserActivity extends ResolverActivity implements
|
||||
? mOriginalTarget.getResolveInfo().activityInfo.toString()
|
||||
: "<connection destroyed>") + "}";
|
||||
}
|
||||
|
||||
public ComponentName getComponentName() {
|
||||
return mOriginalTarget.getResolveInfo().activityInfo.getComponentName();
|
||||
}
|
||||
}
|
||||
|
||||
static class ServiceResultInfo {
|
||||
|
||||
@@ -32,8 +32,10 @@ import android.content.pm.ShortcutInfo;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.provider.DeviceConfig;
|
||||
import android.service.chooser.ChooserTarget;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
@@ -44,17 +46,26 @@ import com.android.internal.app.chooser.DisplayResolveInfo;
|
||||
import com.android.internal.app.chooser.MultiDisplayResolveInfo;
|
||||
import com.android.internal.app.chooser.SelectableTargetInfo;
|
||||
import com.android.internal.app.chooser.TargetInfo;
|
||||
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class ChooserListAdapter extends ResolverListAdapter {
|
||||
private static final String TAG = "ChooserListAdapter";
|
||||
private static final boolean DEBUG = false;
|
||||
|
||||
private boolean mAppendDirectShareEnabled = DeviceConfig.getBoolean(
|
||||
DeviceConfig.NAMESPACE_SYSTEMUI,
|
||||
SystemUiDeviceConfigFlags.APPEND_DIRECT_SHARE_ENABLED,
|
||||
false);
|
||||
|
||||
private boolean mEnableStackedApps = true;
|
||||
|
||||
public static final int NO_POSITION = -1;
|
||||
@@ -84,6 +95,11 @@ public class ChooserListAdapter extends ResolverListAdapter {
|
||||
// Reserve spots for incoming direct share targets by adding placeholders
|
||||
private ChooserTargetInfo
|
||||
mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo();
|
||||
private int mValidServiceTargetsNum = 0;
|
||||
private final Map<ComponentName, Pair<List<ChooserTargetInfo>, Integer>>
|
||||
mParkingDirectShareTargets = new HashMap<>();
|
||||
private Set<ComponentName> mPendingChooserTargetService = new HashSet<>();
|
||||
private Set<ComponentName> mShortcutComponents = new HashSet<>();
|
||||
private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
|
||||
private final List<TargetInfo> mCallerTargets = new ArrayList<>();
|
||||
|
||||
@@ -189,6 +205,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
|
||||
|
||||
void refreshListView() {
|
||||
if (mListViewDataChanged) {
|
||||
if (mAppendDirectShareEnabled) {
|
||||
appendServiceTargetsWithQuota();
|
||||
}
|
||||
super.notifyDataSetChanged();
|
||||
}
|
||||
mListViewDataChanged = false;
|
||||
@@ -198,6 +217,10 @@ public class ChooserListAdapter extends ResolverListAdapter {
|
||||
private void createPlaceHolders() {
|
||||
mNumShortcutResults = 0;
|
||||
mServiceTargets.clear();
|
||||
mValidServiceTargetsNum = 0;
|
||||
mParkingDirectShareTargets.clear();
|
||||
mPendingChooserTargetService.clear();
|
||||
mShortcutComponents.clear();
|
||||
for (int i = 0; i < MAX_SERVICE_TARGETS; i++) {
|
||||
mServiceTargets.add(mPlaceHolderTargetInfo);
|
||||
}
|
||||
@@ -393,12 +416,19 @@ public class ChooserListAdapter extends ResolverListAdapter {
|
||||
*/
|
||||
public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
|
||||
@ChooserActivity.ShareTargetType int targetType,
|
||||
Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos) {
|
||||
Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
|
||||
List<ChooserActivity.ChooserTargetServiceConnection>
|
||||
pendingChooserTargetServiceConnections) {
|
||||
if (DEBUG) {
|
||||
Log.d(TAG, "addServiceResults " + origTarget + ", " + targets.size()
|
||||
Log.d(TAG, "addServiceResults " + origTarget.getResolvedComponentName() + ", "
|
||||
+ targets.size()
|
||||
+ " targets");
|
||||
}
|
||||
|
||||
if (mAppendDirectShareEnabled) {
|
||||
parkTargetIntoMemory(origTarget, targets, targetType, directShareToShortcutInfos,
|
||||
pendingChooserTargetServiceConnections);
|
||||
return;
|
||||
}
|
||||
if (targets.size() == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -449,6 +479,126 @@ public class ChooserListAdapter extends ResolverListAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Park {@code targets} into memory for the moment to surface them later when view is refreshed.
|
||||
* Components pending on ChooserTargetService query are also recorded.
|
||||
*/
|
||||
private void parkTargetIntoMemory(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
|
||||
@ChooserActivity.ShareTargetType int targetType,
|
||||
Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
|
||||
List<ChooserActivity.ChooserTargetServiceConnection>
|
||||
pendingChooserTargetServiceConnections) {
|
||||
ComponentName origComponentName = origTarget.getResolvedComponentName();
|
||||
mPendingChooserTargetService = pendingChooserTargetServiceConnections.stream()
|
||||
.map(ChooserActivity.ChooserTargetServiceConnection::getComponentName)
|
||||
.filter(componentName -> !componentName.equals(origComponentName))
|
||||
.collect(Collectors.toSet());
|
||||
// Park targets in memory
|
||||
if (!targets.isEmpty() && !mParkingDirectShareTargets.containsKey(origComponentName)) {
|
||||
final boolean isShortcutResult =
|
||||
(targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
|
||||
|| targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
|
||||
Context contextAsUser = mContext.createContextAsUser(getUserHandle(),
|
||||
0 /* flags */);
|
||||
List<ChooserTargetInfo> parkingTargetInfos = targets.stream()
|
||||
.map(target ->
|
||||
new SelectableTargetInfo(
|
||||
contextAsUser, origTarget, target, target.getScore(),
|
||||
mSelectableTargetInfoComunicator,
|
||||
(isShortcutResult ? directShareToShortcutInfos.get(target)
|
||||
: null))
|
||||
)
|
||||
.collect(Collectors.toList());
|
||||
mParkingDirectShareTargets.put(origComponentName,
|
||||
new Pair<>(parkingTargetInfos, 0));
|
||||
if (isShortcutResult) {
|
||||
mShortcutComponents.add(origComponentName);
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Append targets of top ranked share app into direct share row with quota limit. Remove
|
||||
* appended ones from memory.
|
||||
*/
|
||||
private void appendServiceTargetsWithQuota() {
|
||||
int maxRankedTargets = mChooserListCommunicator.getMaxRankedTargets();
|
||||
List<ComponentName> topComponentNames = getTopComponentNames(maxRankedTargets);
|
||||
int appRank = 0;
|
||||
for (ComponentName component : topComponentNames) {
|
||||
if (!mPendingChooserTargetService.contains(component)
|
||||
&& !mParkingDirectShareTargets.containsKey(component)) {
|
||||
continue;
|
||||
}
|
||||
appRank++;
|
||||
Pair<List<ChooserTargetInfo>, Integer> parkingTargetsItem =
|
||||
mParkingDirectShareTargets.get(component);
|
||||
if (parkingTargetsItem != null && parkingTargetsItem.second == 0) {
|
||||
List<ChooserTargetInfo> parkingTargets = parkingTargetsItem.first;
|
||||
int initTargetsQuota = appRank <= maxRankedTargets / 2 ? 2 : 1;
|
||||
int insertedNum = 0;
|
||||
while (insertedNum < initTargetsQuota && !parkingTargets.isEmpty()) {
|
||||
if (!checkDuplicateTarget(parkingTargets.get(0))) {
|
||||
mServiceTargets.add(mValidServiceTargetsNum, parkingTargets.get(0));
|
||||
mValidServiceTargetsNum++;
|
||||
insertedNum++;
|
||||
}
|
||||
parkingTargets.remove(0);
|
||||
}
|
||||
mParkingDirectShareTargets.put(component, new Pair<>(parkingTargets, insertedNum));
|
||||
if (mShortcutComponents.contains(component)) {
|
||||
mNumShortcutResults += insertedNum;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append all remaining targets (parking in memory) into direct share row as per their ranking.
|
||||
*/
|
||||
private void fillAllServiceTargets() {
|
||||
int maxRankedTargets = mChooserListCommunicator.getMaxRankedTargets();
|
||||
List<ComponentName> topComponentNames = getTopComponentNames(maxRankedTargets);
|
||||
// Append all remaining targets of top recommended components into direct share row.
|
||||
for (ComponentName component : topComponentNames) {
|
||||
if (!mParkingDirectShareTargets.containsKey(component)) {
|
||||
continue;
|
||||
}
|
||||
mParkingDirectShareTargets.get(component).first.stream()
|
||||
.filter(target -> !checkDuplicateTarget(target))
|
||||
.forEach(target -> {
|
||||
if (mShortcutComponents.contains(component)) {
|
||||
mNumShortcutResults++;
|
||||
}
|
||||
mServiceTargets.add(target);
|
||||
});
|
||||
mParkingDirectShareTargets.remove(component);
|
||||
}
|
||||
// Append all remaining shortcuts targets into direct share row.
|
||||
List<ChooserTargetInfo> remainingTargets = new ArrayList<>();
|
||||
mParkingDirectShareTargets.entrySet().stream()
|
||||
.filter(entry -> mShortcutComponents.contains(entry.getKey()))
|
||||
.map(entry -> entry.getValue())
|
||||
.map(pair -> pair.first)
|
||||
.forEach(remainingTargets::addAll);
|
||||
remainingTargets.sort(
|
||||
(t1, t2) -> -Float.compare(t1.getModifiedScore(), t2.getModifiedScore()));
|
||||
mServiceTargets.addAll(remainingTargets);
|
||||
mNumShortcutResults += remainingTargets.size();
|
||||
mParkingDirectShareTargets.clear();
|
||||
}
|
||||
|
||||
private boolean checkDuplicateTarget(ChooserTargetInfo chooserTargetInfo) {
|
||||
// Check for duplicates and abort if found
|
||||
for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
|
||||
if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int getNumShortcutResults() {
|
||||
return mNumShortcutResults;
|
||||
}
|
||||
@@ -487,7 +637,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
|
||||
*/
|
||||
public void completeServiceTargetLoading() {
|
||||
mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo);
|
||||
|
||||
if (mAppendDirectShareEnabled) {
|
||||
fillAllServiceTargets();
|
||||
}
|
||||
if (mServiceTargets.isEmpty()) {
|
||||
mServiceTargets.add(new ChooserActivity.EmptyTargetInfo());
|
||||
}
|
||||
|
||||
@@ -153,6 +153,14 @@ public class ResolverListAdapter extends BaseAdapter {
|
||||
return mResolverListController.getScore(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of top K component names which have highest
|
||||
* {@link #getScore(DisplayResolveInfo)}
|
||||
*/
|
||||
public List<ComponentName> getTopComponentNames(int topK) {
|
||||
return mResolverListController.getTopComponentNames(topK);
|
||||
}
|
||||
|
||||
public void updateModel(ComponentName componentName) {
|
||||
mResolverListController.updateModel(componentName);
|
||||
}
|
||||
|
||||
@@ -367,6 +367,14 @@ public class ResolverListController {
|
||||
return mResolverComparator.getScore(target.getResolvedComponentName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of top K component names which have highest
|
||||
* {@link #getScore(DisplayResolveInfo)}
|
||||
*/
|
||||
public List<ComponentName> getTopComponentNames(int topK) {
|
||||
return mResolverComparator.getTopComponentNames(topK);
|
||||
}
|
||||
|
||||
public void updateModel(ComponentName componentName) {
|
||||
mResolverComparator.updateModel(componentName);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Ranks and compares packages based on usage stats and uses the {@link ResolverRankerService}.
|
||||
@@ -252,6 +253,15 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
List<ComponentName> getTopComponentNames(int topK) {
|
||||
return mTargetsDict.entrySet().stream()
|
||||
.sorted((o1, o2) -> -Float.compare(getScore(o1.getKey()), getScore(o2.getKey())))
|
||||
.limit(topK)
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
// update ranking model when the connection to it is valid.
|
||||
@Override
|
||||
public void updateModel(ComponentName componentName) {
|
||||
|
||||
@@ -377,6 +377,17 @@ public final class SystemUiDeviceConfigFlags {
|
||||
public static final String NAV_BAR_HANDLE_SHOW_OVER_LOCKSCREEN =
|
||||
"nav_bar_handle_show_over_lockscreen";
|
||||
|
||||
/**
|
||||
* (int) Timeout threshold, in millisecond, that Sharesheet waits for direct share targets.
|
||||
*/
|
||||
public static final String SHARE_SHEET_DIRECT_SHARE_TIMEOUT =
|
||||
"share_sheet_direct_share_timeout";
|
||||
|
||||
/**
|
||||
* (boolean) Whether append direct share on Sharesheet is enabled.
|
||||
*/
|
||||
public static final String APPEND_DIRECT_SHARE_ENABLED = "append_direct_share_enabled";
|
||||
|
||||
/**
|
||||
* (boolean) Whether to enable user-drag resizing for PIP.
|
||||
*/
|
||||
|
||||
@@ -98,6 +98,11 @@ public class AbstractResolverComparatorTest {
|
||||
|
||||
@Override
|
||||
void handleResultMessage(Message message) {}
|
||||
|
||||
@Override
|
||||
List<ComponentName> getTopComponentNames(int topK) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
return testComparator;
|
||||
}
|
||||
|
||||
@@ -987,7 +987,8 @@ public class ChooserActivityTest {
|
||||
/* resolveInfoPresentationGetter */ null),
|
||||
serviceTargets,
|
||||
TARGET_TYPE_CHOOSER_TARGET,
|
||||
directShareToShortcutInfos)
|
||||
directShareToShortcutInfos,
|
||||
null)
|
||||
);
|
||||
|
||||
// Thread.sleep shouldn't be a thing in an integration test but it's
|
||||
@@ -1058,7 +1059,8 @@ public class ChooserActivityTest {
|
||||
/* resolveInfoPresentationGetter */ null),
|
||||
serviceTargets,
|
||||
TARGET_TYPE_CHOOSER_TARGET,
|
||||
directShareToShortcutInfos)
|
||||
directShareToShortcutInfos,
|
||||
null)
|
||||
);
|
||||
// Thread.sleep shouldn't be a thing in an integration test but it's
|
||||
// necessary here because of the way the code is structured
|
||||
@@ -1145,7 +1147,8 @@ public class ChooserActivityTest {
|
||||
/* resolveInfoPresentationGetter */ null),
|
||||
serviceTargets,
|
||||
TARGET_TYPE_CHOOSER_TARGET,
|
||||
directShareToShortcutInfos)
|
||||
directShareToShortcutInfos,
|
||||
null)
|
||||
);
|
||||
// Thread.sleep shouldn't be a thing in an integration test but it's
|
||||
// necessary here because of the way the code is structured
|
||||
|
||||
Reference in New Issue
Block a user