diff options
6 files changed, 223 insertions, 80 deletions
diff --git a/services/people/java/com/android/server/people/data/AppUsageStatsData.java b/services/people/java/com/android/server/people/data/AppUsageStatsData.java new file mode 100644 index 000000000000..6baef38fffc1 --- /dev/null +++ b/services/people/java/com/android/server/people/data/AppUsageStatsData.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 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 com.android.server.people.data; + +import com.android.internal.annotations.VisibleForTesting; + +/** The data containing package usage info. */ +public class AppUsageStatsData { + + private int mLaunchCount; + + private int mChosenCount; + + @VisibleForTesting + public AppUsageStatsData(int chosenCount, int launchCount) { + this.mChosenCount = chosenCount; + this.mLaunchCount = launchCount; + } + + public AppUsageStatsData() { + } + + public int getLaunchCount() { + return mLaunchCount; + } + + void incrementLaunchCountBy(int launchCount) { + this.mLaunchCount += launchCount; + } + + public int getChosenCount() { + return mChosenCount; + } + + void incrementChosenCountBy(int chosenCount) { + this.mChosenCount += chosenCount; + } +} diff --git a/services/people/java/com/android/server/people/data/DataManager.java b/services/people/java/com/android/server/people/data/DataManager.java index 107c41a47507..bbb0215788fb 100644 --- a/services/people/java/com/android/server/people/data/DataManager.java +++ b/services/people/java/com/android/server/people/data/DataManager.java @@ -257,13 +257,16 @@ public class DataManager { } /** - * Queries launch counts of apps within {@code packageNameFilter} between {@code startTime} - * and {@code endTime}. + * Queries usage stats of apps within {@code packageNameFilter} between {@code startTime} and + * {@code endTime}. + * + * @return a map which keys are package names and values are {@link AppUsageStatsData}. */ @NonNull - public Map<String, Integer> queryAppLaunchCount(@UserIdInt int callingUserId, long startTime, + public Map<String, AppUsageStatsData> queryAppUsageStats( + @UserIdInt int callingUserId, long startTime, long endTime, Set<String> packageNameFilter) { - return UsageStatsQueryHelper.queryAppLaunchCount(callingUserId, startTime, endTime, + return UsageStatsQueryHelper.queryAppUsageStats(callingUserId, startTime, endTime, packageNameFilter); } diff --git a/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java b/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java index 6e6fea93c803..d89bbe9dd14e 100644 --- a/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java +++ b/services/people/java/com/android/server/people/data/UsageStatsQueryHelper.java @@ -137,27 +137,48 @@ class UsageStatsQueryHelper { } /** - * Queries {@link UsageStatsManagerInternal} for launch count of apps within {@code - * packageNameFilter} between {@code startTime} and {@code endTime}.obfuscateInstantApps + * Queries {@link UsageStatsManagerInternal} for usage stats of apps within {@code + * packageNameFilter} between {@code startTime} and {@code endTime}. * - * @return a map which keys are package names and values are app launch counts. + * @return a map which keys are package names and values are {@link AppUsageStatsData}. */ - static Map<String, Integer> queryAppLaunchCount(@UserIdInt int userId, long startTime, + static Map<String, AppUsageStatsData> queryAppUsageStats(@UserIdInt int userId, long startTime, long endTime, Set<String> packageNameFilter) { List<UsageStats> stats = getUsageStatsManagerInternal().queryUsageStatsForUser(userId, UsageStatsManager.INTERVAL_BEST, startTime, endTime, /* obfuscateInstantApps= */ false); - Map<String, Integer> aggregatedStats = new ArrayMap<>(); + Map<String, AppUsageStatsData> aggregatedStats = new ArrayMap<>(); for (UsageStats stat : stats) { String packageName = stat.getPackageName(); if (packageNameFilter.contains(packageName)) { - aggregatedStats.put(packageName, - aggregatedStats.getOrDefault(packageName, 0) + stat.getAppLaunchCount()); + AppUsageStatsData packageStats = aggregatedStats.computeIfAbsent(packageName, + (key) -> new AppUsageStatsData()); + packageStats.incrementChosenCountBy(sumChooserCounts(stat.mChooserCounts)); + packageStats.incrementLaunchCountBy(stat.getAppLaunchCount()); } } return aggregatedStats; } + private static int sumChooserCounts(ArrayMap<String, ArrayMap<String, Integer>> chooserCounts) { + int sum = 0; + if (chooserCounts == null) { + return sum; + } + int chooserCountsSize = chooserCounts.size(); + for (int i = 0; i < chooserCountsSize; i++) { + ArrayMap<String, Integer> counts = chooserCounts.valueAt(i); + if (counts == null) { + continue; + } + final int annotationSize = counts.size(); + for (int j = 0; j < annotationSize; j++) { + sum += counts.valueAt(j); + } + } + return sum; + } + private void onInAppConversationEnded(@NonNull PackageData packageData, @NonNull UsageEvents.Event endEvent) { ComponentName activityName = diff --git a/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java b/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java index 76f252efb412..c77843cfb044 100644 --- a/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java +++ b/services/people/java/com/android/server/people/prediction/SharesheetModelScorer.java @@ -27,17 +27,18 @@ import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.ChooserActivity; +import com.android.server.people.data.AppUsageStatsData; import com.android.server.people.data.DataManager; import com.android.server.people.data.Event; import java.time.Duration; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.concurrent.TimeUnit; +import java.util.function.Function; /** Ranking scorer for Sharesheet targets. */ class SharesheetModelScorer { @@ -50,8 +51,8 @@ class SharesheetModelScorer { private static final float RECENCY_SCORE_SUBSEQUENT_DECAY = 0.02F; private static final long ONE_MONTH_WINDOW = TimeUnit.DAYS.toMillis(30); private static final long FOREGROUND_APP_PROMO_TIME_WINDOW = TimeUnit.MINUTES.toMillis(10); + private static final float USAGE_STATS_CHOOSER_SCORE_INITIAL_DECAY = 0.9F; private static final float FREQUENTLY_USED_APP_SCORE_INITIAL_DECAY = 0.3F; - private static final float FREQUENTLY_USED_APP_SCORE_DECAY = 0.9F; @VisibleForTesting static final float FOREGROUND_APP_WEIGHT = 0F; @VisibleForTesting @@ -192,14 +193,16 @@ class SharesheetModelScorer { targetsList.add(index, shareTarget); } promoteForegroundApp(shareTargetMap, dataManager, callingUserId); - promoteFrequentlyUsedApps(shareTargetMap, targetsLimit, dataManager, callingUserId); + promoteMostChosenAndFrequentlyUsedApps(shareTargetMap, targetsLimit, dataManager, + callingUserId); } /** - * Promotes frequently used sharing apps, if recommended apps based on sharing history have not - * reached the limit (e.g. user did not share any content in last couple weeks) + * Promotes frequently chosen sharing apps and frequently used sharing apps as per + * UsageStatsManager, if recommended apps based on sharing history have not reached the limit + * (e.g. user did not share any content in last couple weeks) */ - private static void promoteFrequentlyUsedApps( + private static void promoteMostChosenAndFrequentlyUsedApps( Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, int targetsLimit, @NonNull DataManager dataManager, @UserIdInt int callingUserId) { int validPredictionNum = 0; @@ -217,39 +220,50 @@ class SharesheetModelScorer { return; } long now = System.currentTimeMillis(); - Map<String, Integer> appLaunchCountsMap = dataManager.queryAppLaunchCount( - callingUserId, now - ONE_MONTH_WINDOW, now, shareTargetMap.keySet()); - List<Pair<String, Integer>> appLaunchCounts = new ArrayList<>(); - minValidScore *= FREQUENTLY_USED_APP_SCORE_INITIAL_DECAY; - for (Map.Entry<String, Integer> entry : appLaunchCountsMap.entrySet()) { - if (entry.getValue() > 0) { - appLaunchCounts.add(new Pair(entry.getKey(), entry.getValue())); - } + Map<String, AppUsageStatsData> appStatsMap = + dataManager.queryAppUsageStats( + callingUserId, now - ONE_MONTH_WINDOW, now, shareTargetMap.keySet()); + // Promotes frequently chosen sharing apps as per UsageStatsManager. + minValidScore = promoteApp(shareTargetMap, appStatsMap, AppUsageStatsData::getChosenCount, + USAGE_STATS_CHOOSER_SCORE_INITIAL_DECAY * minValidScore, minValidScore); + // Promotes frequently used sharing apps as per UsageStatsManager. + promoteApp(shareTargetMap, appStatsMap, AppUsageStatsData::getLaunchCount, + FREQUENTLY_USED_APP_SCORE_INITIAL_DECAY * minValidScore, minValidScore); + } + + private static float promoteApp( + Map<String, List<ShareTargetPredictor.ShareTarget>> shareTargetMap, + Map<String, AppUsageStatsData> appStatsMap, + Function<AppUsageStatsData, Integer> countFunc, float baseScore, float minValidScore) { + int maxCount = 0; + for (AppUsageStatsData data : appStatsMap.values()) { + maxCount = Math.max(maxCount, countFunc.apply(data)); } - Collections.sort(appLaunchCounts, (p1, p2) -> -Integer.compare(p1.second, p2.second)); - for (Pair<String, Integer> entry : appLaunchCounts) { - if (!shareTargetMap.containsKey(entry.first)) { - continue; - } - ShareTargetPredictor.ShareTarget target = shareTargetMap.get(entry.first).get(0); - if (target.getScore() > 0f) { - continue; - } - target.setScore(minValidScore); - minValidScore *= FREQUENTLY_USED_APP_SCORE_DECAY; - if (DEBUG) { - Slog.d(TAG, String.format( - "SharesheetModel: promoteFrequentUsedApps packageName: %s, className: %s," - + " total:%.2f", - target.getAppTarget().getPackageName(), - target.getAppTarget().getClassName(), - target.getScore())); - } - validPredictionNum++; - if (validPredictionNum == targetsLimit) { - return; + if (maxCount > 0) { + for (Map.Entry<String, AppUsageStatsData> entry : appStatsMap.entrySet()) { + if (!shareTargetMap.containsKey(entry.getKey())) { + continue; + } + ShareTargetPredictor.ShareTarget target = shareTargetMap.get(entry.getKey()).get(0); + if (target.getScore() > 0f) { + continue; + } + float curScore = baseScore * countFunc.apply(entry.getValue()) / maxCount; + target.setScore(curScore); + if (curScore > 0) { + minValidScore = Math.min(minValidScore, curScore); + } + if (DEBUG) { + Slog.d(TAG, String.format( + "SharesheetModel: promote as per AppUsageStats packageName: %s, " + + "className: %s, total:%.2f", + target.getAppTarget().getPackageName(), + target.getAppTarget().getClassName(), + target.getScore())); + } } } + return minValidScore; } /** diff --git a/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java index 03d9ad51e6c5..30ff1196cec0 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/UsageStatsQueryHelperTest.java @@ -34,6 +34,7 @@ import android.app.usage.UsageStats; import android.app.usage.UsageStatsManagerInternal; import android.content.Context; import android.content.LocusId; +import android.util.ArrayMap; import androidx.test.InstrumentationRegistry; @@ -196,39 +197,42 @@ public final class UsageStatsQueryHelperTest { } @Test - public void testQueryAppLaunchCount() { - - UsageStats packageStats1 = createUsageStats(PKG_NAME_1, 2); - UsageStats packageStats2 = createUsageStats(PKG_NAME_1, 3); - UsageStats packageStats3 = createUsageStats(PKG_NAME_2, 1); + public void testQueryAppUsageStats() { + UsageStats packageStats1 = createUsageStats(PKG_NAME_1, 2, createDummyChooserCounts()); + UsageStats packageStats2 = createUsageStats(PKG_NAME_1, 3, null); + UsageStats packageStats3 = createUsageStats(PKG_NAME_2, 1, createDummyChooserCounts()); when(mUsageStatsManagerInternal.queryUsageStatsForUser(anyInt(), anyInt(), anyLong(), anyLong(), anyBoolean())).thenReturn( List.of(packageStats1, packageStats2, packageStats3)); - Map<String, Integer> appLaunchCounts = mHelper.queryAppLaunchCount(USER_ID_PRIMARY, 90_000L, - 200_000L, Set.of(PKG_NAME_1, PKG_NAME_2)); + Map<String, AppUsageStatsData> appLaunchChooserCountCounts = + mHelper.queryAppUsageStats(USER_ID_PRIMARY, 90_000L, + 200_000L, Set.of(PKG_NAME_1, PKG_NAME_2)); - assertEquals(2, appLaunchCounts.size()); - assertEquals(5, (long) appLaunchCounts.get(PKG_NAME_1)); - assertEquals(1, (long) appLaunchCounts.get(PKG_NAME_2)); + assertEquals(2, appLaunchChooserCountCounts.size()); + assertEquals(4, (long) appLaunchChooserCountCounts.get(PKG_NAME_1).getChosenCount()); + assertEquals(5, (long) appLaunchChooserCountCounts.get(PKG_NAME_1).getLaunchCount()); + assertEquals(4, (long) appLaunchChooserCountCounts.get(PKG_NAME_2).getChosenCount()); + assertEquals(1, (long) appLaunchChooserCountCounts.get(PKG_NAME_2).getLaunchCount()); } @Test - public void testQueryAppLaunchCount_packageNameFiltered() { - - UsageStats packageStats1 = createUsageStats(PKG_NAME_1, 2); - UsageStats packageStats2 = createUsageStats(PKG_NAME_1, 3); - UsageStats packageStats3 = createUsageStats(PKG_NAME_2, 1); + public void testQueryAppUsageStats_packageNameFiltered() { + UsageStats packageStats1 = createUsageStats(PKG_NAME_1, 2, createDummyChooserCounts()); + UsageStats packageStats2 = createUsageStats(PKG_NAME_1, 3, createDummyChooserCounts()); + UsageStats packageStats3 = createUsageStats(PKG_NAME_2, 1, null); when(mUsageStatsManagerInternal.queryUsageStatsForUser(anyInt(), anyInt(), anyLong(), anyLong(), anyBoolean())).thenReturn( List.of(packageStats1, packageStats2, packageStats3)); - Map<String, Integer> appLaunchCounts = mHelper.queryAppLaunchCount(USER_ID_PRIMARY, 90_000L, - 200_000L, - Set.of(PKG_NAME_1)); + Map<String, AppUsageStatsData> appLaunchChooserCountCounts = + mHelper.queryAppUsageStats(USER_ID_PRIMARY, 90_000L, + 200_000L, + Set.of(PKG_NAME_1)); - assertEquals(1, appLaunchCounts.size()); - assertEquals(5, (long) appLaunchCounts.get(PKG_NAME_1)); + assertEquals(1, appLaunchChooserCountCounts.size()); + assertEquals(8, (long) appLaunchChooserCountCounts.get(PKG_NAME_1).getChosenCount()); + assertEquals(5, (long) appLaunchChooserCountCounts.get(PKG_NAME_1).getLaunchCount()); } private void addUsageEvents(UsageEvents.Event... events) { @@ -237,13 +241,27 @@ public final class UsageStatsQueryHelperTest { anyInt())).thenReturn(usageEvents); } - private static UsageStats createUsageStats(String packageName, int launchCount) { + private static UsageStats createUsageStats(String packageName, int launchCount, + ArrayMap<String, ArrayMap<String, Integer>> chooserCounts) { UsageStats packageStats = new UsageStats(); packageStats.mPackageName = packageName; packageStats.mAppLaunchCount = launchCount; + packageStats.mChooserCounts = chooserCounts; return packageStats; } + private static ArrayMap<String, ArrayMap<String, Integer>> createDummyChooserCounts() { + ArrayMap<String, ArrayMap<String, Integer>> chooserCounts = new ArrayMap<>(); + ArrayMap<String, Integer> counts1 = new ArrayMap<>(); + counts1.put("text", 2); + counts1.put("image", 1); + chooserCounts.put("intent1", counts1); + ArrayMap<String, Integer> counts2 = new ArrayMap<>(); + counts2.put("video", 1); + chooserCounts.put("intent2", counts2); + return chooserCounts; + } + private static <T> void addLocalServiceMock(Class<T> clazz, T mock) { LocalServices.removeServiceForTest(clazz); LocalServices.addService(clazz, mock); diff --git a/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java b/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java index 9fc17763b8e0..45fff48ade55 100644 --- a/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java +++ b/services/tests/servicestests/src/com/android/server/people/prediction/SharesheetModelScorerTest.java @@ -31,6 +31,7 @@ import android.app.usage.UsageEvents; import android.os.UserHandle; import android.util.Range; +import com.android.server.people.data.AppUsageStatsData; import com.android.server.people.data.DataManager; import com.android.server.people.data.Event; import com.android.server.people.data.EventHistory; @@ -257,6 +258,39 @@ public final class SharesheetModelScorerTest { } @Test + public void testComputeScoreForAppShare_promoteFrequentlyChosenApps() { + when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); + when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); + when(mEventHistory3.getEventIndex(anySet())).thenReturn(mEventIndex3); + when(mEventHistory4.getEventIndex(anySet())).thenReturn(mEventIndex4); + when(mEventHistory5.getEventIndex(anySet())).thenReturn(mEventIndex5); + when(mEventHistory1.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex6); + when(mEventHistory2.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex7); + when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); + when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); + when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); + when(mDataManager.queryAppUsageStats(anyInt(), anyLong(), anyLong(), anySet())) + .thenReturn( + Map.of(PACKAGE_1, new AppUsageStatsData(1, 0), + PACKAGE_2, new AppUsageStatsData(2, 0), + PACKAGE_3, new AppUsageStatsData(3, 0))); + + SharesheetModelScorer.computeScoreForAppShare( + List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, + mShareTarget6), + Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); + + verify(mDataManager, times(1)).queryAppUsageStats(anyInt(), anyLong(), anyLong(), + anySet()); + assertEquals(0.9f, mShareTarget5.getScore(), DELTA); + assertEquals(0.6f, mShareTarget3.getScore(), DELTA); + assertEquals(0.3f, mShareTarget1.getScore(), DELTA); + assertEquals(0f, mShareTarget2.getScore(), DELTA); + assertEquals(0f, mShareTarget4.getScore(), DELTA); + assertEquals(0f, mShareTarget6.getScore(), DELTA); + } + + @Test public void testComputeScoreForAppShare_promoteFrequentlyUsedApps() { when(mEventHistory1.getEventIndex(anySet())).thenReturn(mEventIndex1); when(mEventHistory2.getEventIndex(anySet())).thenReturn(mEventIndex2); @@ -268,22 +302,22 @@ public final class SharesheetModelScorerTest { when(mEventHistory3.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex8); when(mEventHistory4.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex9); when(mEventHistory5.getEventIndex(Event.TYPE_SHARE_TEXT)).thenReturn(mEventIndex10); - when(mDataManager.queryAppLaunchCount(anyInt(), anyLong(), anyLong(), anySet())) + when(mDataManager.queryAppUsageStats(anyInt(), anyLong(), anyLong(), anySet())) .thenReturn( - Map.of(PACKAGE_1, 1, - PACKAGE_2, 2, - PACKAGE_3, 3)); + Map.of(PACKAGE_1, new AppUsageStatsData(0, 1), + PACKAGE_2, new AppUsageStatsData(0, 2), + PACKAGE_3, new AppUsageStatsData(1, 0))); SharesheetModelScorer.computeScoreForAppShare( List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, mShareTarget6), Event.TYPE_SHARE_TEXT, 20, NOW, mDataManager, USER_ID); - verify(mDataManager, times(1)).queryAppLaunchCount(anyInt(), anyLong(), anyLong(), + verify(mDataManager, times(1)).queryAppUsageStats(anyInt(), anyLong(), anyLong(), anySet()); - assertEquals(0.3f, mShareTarget5.getScore(), DELTA); + assertEquals(0.9f, mShareTarget5.getScore(), DELTA); assertEquals(0.27f, mShareTarget3.getScore(), DELTA); - assertEquals(0.243f, mShareTarget1.getScore(), DELTA); + assertEquals(0.135f, mShareTarget1.getScore(), DELTA); assertEquals(0f, mShareTarget2.getScore(), DELTA); assertEquals(0f, mShareTarget4.getScore(), DELTA); assertEquals(0f, mShareTarget6.getScore(), DELTA); @@ -306,18 +340,19 @@ public final class SharesheetModelScorerTest { when(mEventIndex3.getMostRecentActiveTimeSlot()).thenReturn(FIVE_DAYS_AGO); when(mEventIndex4.getMostRecentActiveTimeSlot()).thenReturn(EIGHT_DAYS_AGO); when(mEventIndex5.getMostRecentActiveTimeSlot()).thenReturn(null); - when(mDataManager.queryAppLaunchCount(anyInt(), anyLong(), anyLong(), anySet())) + when(mDataManager.queryAppUsageStats(anyInt(), anyLong(), anyLong(), anySet())) .thenReturn( - Map.of(PACKAGE_1, 1, - PACKAGE_2, 2, - PACKAGE_3, 3)); + Map.of(PACKAGE_1, new AppUsageStatsData(0, 1), + PACKAGE_2, new AppUsageStatsData(0, 2), + PACKAGE_3, new AppUsageStatsData(1, 0))); SharesheetModelScorer.computeScoreForAppShare( List.of(mShareTarget1, mShareTarget2, mShareTarget3, mShareTarget4, mShareTarget5, mShareTarget6), Event.TYPE_SHARE_TEXT, 4, NOW, mDataManager, USER_ID); - verify(mDataManager, never()).queryAppLaunchCount(anyInt(), anyLong(), anyLong(), anySet()); + verify(mDataManager, never()).queryAppUsageStats(anyInt(), anyLong(), anyLong(), + anySet()); assertEquals(0.4f, mShareTarget1.getScore(), DELTA); assertEquals(0.35f, mShareTarget2.getScore(), DELTA); assertEquals(0.33f, mShareTarget3.getScore(), DELTA); |