1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.server.pm.shortcutmanagertest;
17 
18 import static junit.framework.Assert.assertEquals;
19 import static junit.framework.Assert.assertFalse;
20 import static junit.framework.Assert.assertNotNull;
21 import static junit.framework.Assert.assertNull;
22 import static junit.framework.Assert.assertTrue;
23 import static junit.framework.Assert.fail;
24 
25 import static org.junit.Assert.assertNotEquals;
26 import static org.mockito.Matchers.any;
27 import static org.mockito.Matchers.anyList;
28 import static org.mockito.Matchers.anyString;
29 import static org.mockito.Matchers.eq;
30 import static org.mockito.Mockito.atLeastOnce;
31 import static org.mockito.Mockito.mock;
32 import static org.mockito.Mockito.reset;
33 import static org.mockito.Mockito.times;
34 import static org.mockito.Mockito.verify;
35 
36 import android.app.Instrumentation;
37 import android.content.ComponentName;
38 import android.content.Context;
39 import android.content.LocusId;
40 import android.content.pm.LauncherApps;
41 import android.content.pm.LauncherApps.Callback;
42 import android.content.pm.ShortcutInfo;
43 import android.graphics.Bitmap;
44 import android.graphics.BitmapFactory;
45 import android.os.BaseBundle;
46 import android.os.Bundle;
47 import android.os.Handler;
48 import android.os.Looper;
49 import android.os.Parcel;
50 import android.os.ParcelFileDescriptor;
51 import android.os.PersistableBundle;
52 import android.os.UserHandle;
53 import android.test.MoreAsserts;
54 import android.util.Log;
55 
56 import junit.framework.Assert;
57 
58 import org.hamcrest.BaseMatcher;
59 import org.hamcrest.Description;
60 import org.hamcrest.Matcher;
61 import org.json.JSONException;
62 import org.json.JSONObject;
63 import org.mockito.ArgumentCaptor;
64 import org.mockito.ArgumentMatchers;
65 import org.mockito.hamcrest.MockitoHamcrest;
66 
67 import java.io.BufferedReader;
68 import java.io.File;
69 import java.io.FileNotFoundException;
70 import java.io.FileReader;
71 import java.io.IOException;
72 import java.util.ArrayList;
73 import java.util.Arrays;
74 import java.util.Collection;
75 import java.util.Collections;
76 import java.util.Comparator;
77 import java.util.LinkedHashSet;
78 import java.util.List;
79 import java.util.Set;
80 import java.util.SortedSet;
81 import java.util.TreeSet;
82 import java.util.concurrent.CountDownLatch;
83 import java.util.function.BooleanSupplier;
84 import java.util.function.Consumer;
85 import java.util.function.Function;
86 import java.util.function.Predicate;
87 
88 /**
89  * Common utility methods for ShortcutManager tests.  This is used by both CTS and the unit tests.
90  * Because it's used by CTS too, it can only access the public APIs.
91  */
92 public class ShortcutManagerTestUtils {
93     private static final String TAG = "ShortcutManagerUtils";
94 
95     private static final boolean ENABLE_DUMPSYS = true; // DO NOT SUBMIT WITH true
96 
97     private static final int STANDARD_TIMEOUT_SEC = 5;
98 
99     private static final String[] EMPTY_STRINGS = new String[0];
100 
ShortcutManagerTestUtils()101     private ShortcutManagerTestUtils() {
102     }
103 
readAll(File file)104     public static List<String> readAll(File file) throws FileNotFoundException {
105         return readAll(ParcelFileDescriptor.open(
106                 file.getAbsoluteFile(), ParcelFileDescriptor.MODE_READ_ONLY));
107     }
108 
readAll(ParcelFileDescriptor pfd)109     public static List<String> readAll(ParcelFileDescriptor pfd) {
110         try {
111             try {
112                 final ArrayList<String> ret = new ArrayList<>();
113                 try (BufferedReader r = new BufferedReader(
114                         new FileReader(pfd.getFileDescriptor()))) {
115                     String line;
116                     while ((line = r.readLine()) != null) {
117                         ret.add(line);
118                     }
119                     r.readLine();
120                 }
121                 return ret;
122             } finally {
123                 pfd.close();
124             }
125         } catch (IOException e) {
126             throw new RuntimeException(e);
127         }
128     }
129 
concatResult(List<String> result)130     public static String concatResult(List<String> result) {
131         final StringBuilder sb = new StringBuilder();
132         for (String s : result) {
133             sb.append(s);
134             sb.append("\n");
135         }
136         return sb.toString();
137     }
138 
resultContains(List<String> result, String expected)139     public static boolean resultContains(List<String> result, String expected) {
140         for (String line : result) {
141             if (line.contains(expected)) {
142                 return true;
143             }
144         }
145         return false;
146     }
147 
assertSuccess(List<String> result)148     public static List<String> assertSuccess(List<String> result) {
149         if (!resultContains(result, "Success")) {
150             fail("Command failed.  Result was:\n" + concatResult(result));
151         }
152         return result;
153     }
154 
assertContains(List<String> result, String expected)155     public static List<String> assertContains(List<String> result, String expected) {
156         if (!resultContains(result, expected)) {
157             fail("Didn't contain expected string=" + expected
158                     + "\nActual:\n" + concatResult(result));
159         }
160         return result;
161     }
162 
runCommand(Instrumentation instrumentation, String command)163     public static List<String> runCommand(Instrumentation instrumentation, String command) {
164         return runCommand(instrumentation, command, null);
165     }
runCommand(Instrumentation instrumentation, String command, Predicate<List<String>> resultAsserter)166     public static List<String> runCommand(Instrumentation instrumentation, String command,
167             Predicate<List<String>> resultAsserter) {
168         Log.d(TAG, "Running command: " + command);
169         final List<String> result;
170         try {
171             result = readAll(
172                     instrumentation.getUiAutomation().executeShellCommand(command));
173         } catch (Exception e) {
174             throw new RuntimeException(e);
175         }
176         if (resultAsserter != null && !resultAsserter.test(result)) {
177             fail("Command '" + command + "' failed, output was:\n" + concatResult(result));
178         }
179         return result;
180     }
181 
runCommandForNoOutput(Instrumentation instrumentation, String command)182     public static void runCommandForNoOutput(Instrumentation instrumentation, String command) {
183         runCommand(instrumentation, command, result -> result.size() == 0);
184     }
185 
runShortcutCommand(Instrumentation instrumentation, String command, Predicate<List<String>> resultAsserter)186     public static List<String> runShortcutCommand(Instrumentation instrumentation, String command,
187             Predicate<List<String>> resultAsserter) {
188         return runCommand(instrumentation, "cmd shortcut " + command, resultAsserter);
189     }
190 
runShortcutCommandForSuccess(Instrumentation instrumentation, String command)191     public static List<String> runShortcutCommandForSuccess(Instrumentation instrumentation,
192             String command) {
193         return runShortcutCommand(instrumentation, command, result -> result.contains("Success"));
194     }
195 
getDefaultLauncher(Instrumentation instrumentation)196     public static String getDefaultLauncher(Instrumentation instrumentation) {
197         final String PREFIX = "Launcher: ComponentInfo{";
198         final String POSTFIX = "}";
199         final List<String> result = runShortcutCommandForSuccess(
200                 instrumentation, "get-default-launcher");
201         for (String s : result) {
202             if (s.startsWith(PREFIX) && s.endsWith(POSTFIX)) {
203                 return s.substring(PREFIX.length(), s.length() - POSTFIX.length());
204             }
205         }
206         fail("Default launcher not found");
207         return null;
208     }
209 
setDefaultLauncher(Instrumentation instrumentation, String component)210     public static void setDefaultLauncher(Instrumentation instrumentation, String component) {
211         runCommand(instrumentation, "cmd package set-home-activity --user "
212                 + instrumentation.getContext().getUserId() + " " + component,
213                 result -> result.contains("Success"));
214         runCommand(instrumentation, "cmd shortcut clear-default-launcher --user "
215                         + instrumentation.getContext().getUserId(),
216                 result -> result.contains("Success"));
217     }
218 
setDefaultLauncher(Instrumentation instrumentation, Context packageContext)219     public static void setDefaultLauncher(Instrumentation instrumentation, Context packageContext) {
220         setDefaultLauncher(instrumentation, packageContext.getPackageName()
221                 + "/android.content.pm.cts.shortcutmanager.packages.Launcher");
222     }
223 
overrideConfig(Instrumentation instrumentation, String config)224     public static void overrideConfig(Instrumentation instrumentation, String config) {
225         runShortcutCommandForSuccess(instrumentation, "override-config " + config);
226     }
227 
resetConfig(Instrumentation instrumentation)228     public static void resetConfig(Instrumentation instrumentation) {
229         runShortcutCommandForSuccess(instrumentation, "reset-config");
230     }
231 
resetThrottling(Instrumentation instrumentation)232     public static void resetThrottling(Instrumentation instrumentation) {
233         runShortcutCommandForSuccess(instrumentation, "reset-throttling");
234     }
235 
resetAllThrottling(Instrumentation instrumentation)236     public static void resetAllThrottling(Instrumentation instrumentation) {
237         runShortcutCommandForSuccess(instrumentation, "reset-all-throttling");
238     }
239 
clearShortcuts(Instrumentation instrumentation, int userId, String packageName)240     public static void clearShortcuts(Instrumentation instrumentation, int userId,
241             String packageName) {
242         runShortcutCommandForSuccess(instrumentation, "clear-shortcuts "
243                 + " --user " + userId + " " + packageName);
244     }
245 
anyContains(List<String> result, String expected)246     public static void anyContains(List<String> result, String expected) {
247         for (String l : result) {
248             if (l.contains(expected)) {
249                 return;
250             }
251         }
252         fail("Result didn't contain '" + expected + "': was\n" + result);
253     }
254 
enableComponent(Instrumentation instrumentation, ComponentName cn, boolean enable)255     public static void enableComponent(Instrumentation instrumentation, ComponentName cn,
256             boolean enable) {
257 
258         final String word = (enable ? "enable" : "disable");
259         runCommand(instrumentation,
260                 "pm " + word + " " + cn.flattenToString()
261                 , result ->concatResult(result).contains(word));
262     }
263 
appOps(Instrumentation instrumentation, String packageName, String op, String mode)264     public static void appOps(Instrumentation instrumentation, String packageName,
265             String op, String mode) {
266         runCommand(instrumentation, "appops set " + packageName + " " + op + " " + mode);
267     }
268 
dumpsysShortcut(Instrumentation instrumentation)269     public static void dumpsysShortcut(Instrumentation instrumentation) {
270         if (!ENABLE_DUMPSYS) {
271             return;
272         }
273         Log.e(TAG, "Dumpsys shortcut");
274         for (String s : runCommand(instrumentation, "dumpsys shortcut")) {
275             Log.e(TAG, s);
276         }
277     }
278 
getCheckinDump(Instrumentation instrumentation)279     public static JSONObject getCheckinDump(Instrumentation instrumentation) throws JSONException {
280         return new JSONObject(concatResult(runCommand(instrumentation, "dumpsys shortcut -c")));
281     }
282 
isLowRamDevice(Instrumentation instrumentation)283     public static boolean isLowRamDevice(Instrumentation instrumentation) throws JSONException {
284         return getCheckinDump(instrumentation).getBoolean("lowRam");
285     }
286 
getIconSize(Instrumentation instrumentation)287     public static int getIconSize(Instrumentation instrumentation) throws JSONException {
288         return getCheckinDump(instrumentation).getInt("iconSize");
289     }
290 
makeBundle(Object... keysAndValues)291     public static Bundle makeBundle(Object... keysAndValues) {
292         assertTrue((keysAndValues.length % 2) == 0);
293 
294         if (keysAndValues.length == 0) {
295             return null;
296         }
297         final Bundle ret = new Bundle();
298 
299         for (int i = keysAndValues.length - 2; i >= 0; i -= 2) {
300             final String key = keysAndValues[i].toString();
301             final Object value = keysAndValues[i + 1];
302 
303             if (value == null) {
304                 ret.putString(key, null);
305             } else if (value instanceof Integer) {
306                 ret.putInt(key, (Integer) value);
307             } else if (value instanceof String) {
308                 ret.putString(key, (String) value);
309             } else if (value instanceof Bundle) {
310                 ret.putBundle(key, (Bundle) value);
311             } else {
312                 fail("Type not supported yet: " + value.getClass().getName());
313             }
314         }
315         return ret;
316     }
317 
makePersistableBundle(Object... keysAndValues)318     public static PersistableBundle makePersistableBundle(Object... keysAndValues) {
319         assertTrue((keysAndValues.length % 2) == 0);
320 
321         if (keysAndValues.length == 0) {
322             return null;
323         }
324         final PersistableBundle ret = new PersistableBundle();
325 
326         for (int i = keysAndValues.length - 2; i >= 0; i -= 2) {
327             final String key = keysAndValues[i].toString();
328             final Object value = keysAndValues[i + 1];
329 
330             if (value == null) {
331                 ret.putString(key, null);
332             } else if (value instanceof Integer) {
333                 ret.putInt(key, (Integer) value);
334             } else if (value instanceof String) {
335                 ret.putString(key, (String) value);
336             } else if (value instanceof PersistableBundle) {
337                 ret.putPersistableBundle(key, (PersistableBundle) value);
338             } else {
339                 fail("Type not supported yet: " + value.getClass().getName());
340             }
341         }
342         return ret;
343     }
344 
array(T... array)345     public static <T> T[] array(T... array) {
346         return array;
347     }
348 
list(T... array)349     public static <T> List<T> list(T... array) {
350         return Arrays.asList(array);
351     }
352 
hashSet(Set<T> in)353     public static <T> Set<T> hashSet(Set<T> in) {
354         return new LinkedHashSet<>(in);
355     }
356 
set(T... values)357     public static <T> Set<T> set(T... values) {
358         return set(v -> v, values);
359     }
360 
set(Function<V, T> converter, V... values)361     public static <T, V> Set<T> set(Function<V, T> converter, V... values) {
362         return set(converter, Arrays.asList(values));
363     }
364 
set(Function<V, T> converter, List<V> values)365     public static <T, V> Set<T> set(Function<V, T> converter, List<V> values) {
366         final LinkedHashSet<T> ret = new LinkedHashSet<>();
367         for (V v : values) {
368             ret.add(converter.apply(v));
369         }
370         return ret;
371     }
372 
locusId(String id)373     public static LocusId locusId(String id) {
374         return new LocusId(id);
375     }
376 
resetAll(Collection<?> mocks)377     public static void resetAll(Collection<?> mocks) {
378         for (Object o : mocks) {
379             reset(o);
380         }
381     }
382 
assertEmpty(T collection)383     public static <T extends Collection<?>> T assertEmpty(T collection) {
384         if (collection == null) {
385             return collection; // okay.
386         }
387         assertEquals(0, collection.size());
388         return collection;
389     }
390 
filter(List<ShortcutInfo> list, Predicate<ShortcutInfo> p)391     public static List<ShortcutInfo> filter(List<ShortcutInfo> list, Predicate<ShortcutInfo> p) {
392         final ArrayList<ShortcutInfo> ret = new ArrayList<>(list);
393         ret.removeIf(si -> !p.test(si));
394         return ret;
395     }
396 
filterByActivity(List<ShortcutInfo> list, ComponentName activity)397     public static List<ShortcutInfo> filterByActivity(List<ShortcutInfo> list,
398             ComponentName activity) {
399         return filter(list, si ->
400                 (si.getActivity().equals(activity)
401                         && (si.isDeclaredInManifest() || si.isDynamic())));
402     }
403 
changedSince(List<ShortcutInfo> list, long time)404     public static List<ShortcutInfo> changedSince(List<ShortcutInfo> list, long time) {
405         return filter(list, si -> si.getLastChangedTimestamp() >= time);
406     }
407 
408     @FunctionalInterface
409     public interface ExceptionRunnable {
run()410         void run() throws Exception;
411     }
412 
assertExpectException(Class<? extends Throwable> expectedExceptionType, String expectedExceptionMessageRegex, ExceptionRunnable r)413     public static void assertExpectException(Class<? extends Throwable> expectedExceptionType,
414             String expectedExceptionMessageRegex, ExceptionRunnable r) {
415         assertExpectException("", expectedExceptionType, expectedExceptionMessageRegex, r);
416     }
417 
assertCannotUpdateImmutable(Runnable r)418     public static void assertCannotUpdateImmutable(Runnable r) {
419         assertExpectException(
420                 IllegalArgumentException.class, "may not be manipulated via APIs", r::run);
421     }
422 
assertDynamicShortcutCountExceeded(Runnable r)423     public static void assertDynamicShortcutCountExceeded(Runnable r) {
424         assertExpectException(IllegalArgumentException.class,
425                 "Max number of dynamic shortcuts exceeded", r::run);
426     }
427 
assertExpectException(String message, Class<? extends Throwable> expectedExceptionType, String expectedExceptionMessageRegex, ExceptionRunnable r)428     public static void assertExpectException(String message,
429             Class<? extends Throwable> expectedExceptionType,
430             String expectedExceptionMessageRegex, ExceptionRunnable r) {
431         try {
432             r.run();
433         } catch (Throwable e) {
434             Assert.assertTrue(
435                     "Expected exception type was " + expectedExceptionType.getName()
436                             + " but caught " + e + " (message=" + message + ")",
437                     expectedExceptionType.isAssignableFrom(e.getClass()));
438             if (expectedExceptionMessageRegex != null) {
439                 MoreAsserts.assertContainsRegex(expectedExceptionMessageRegex, e.getMessage());
440             }
441             return; // Pass
442         }
443         Assert.fail("Expected exception type " + expectedExceptionType.getName()
444                 + " was not thrown");
445     }
446 
assertShortcutIds(List<ShortcutInfo> actualShortcuts, String... expectedIds)447     public static List<ShortcutInfo> assertShortcutIds(List<ShortcutInfo> actualShortcuts,
448             String... expectedIds) {
449         final SortedSet<String> expected = new TreeSet<>(list(expectedIds));
450         final SortedSet<String> actual = new TreeSet<>();
451         for (ShortcutInfo s : actualShortcuts) {
452             actual.add(s.getId());
453         }
454 
455         // Compare the sets.
456         assertEquals(expected, actual);
457         return actualShortcuts;
458     }
459 
assertShortcutIdsOrdered(List<ShortcutInfo> actualShortcuts, String... expectedIds)460     public static List<ShortcutInfo> assertShortcutIdsOrdered(List<ShortcutInfo> actualShortcuts,
461             String... expectedIds) {
462         final ArrayList<String> expected = new ArrayList<>(list(expectedIds));
463         final ArrayList<String> actual = new ArrayList<>();
464         for (ShortcutInfo s : actualShortcuts) {
465             actual.add(s.getId());
466         }
467         assertEquals(expected, actual);
468         return actualShortcuts;
469     }
470 
assertAllHaveIntents( List<ShortcutInfo> actualShortcuts)471     public static List<ShortcutInfo> assertAllHaveIntents(
472             List<ShortcutInfo> actualShortcuts) {
473         for (ShortcutInfo s : actualShortcuts) {
474             assertNotNull("ID " + s.getId(), s.getIntent());
475         }
476         return actualShortcuts;
477     }
478 
assertAllNotHaveIntents( List<ShortcutInfo> actualShortcuts)479     public static List<ShortcutInfo> assertAllNotHaveIntents(
480             List<ShortcutInfo> actualShortcuts) {
481         for (ShortcutInfo s : actualShortcuts) {
482             assertNull("ID " + s.getId(), s.getIntent());
483         }
484         return actualShortcuts;
485     }
486 
assertAllHaveTitle( List<ShortcutInfo> actualShortcuts)487     public static List<ShortcutInfo> assertAllHaveTitle(
488             List<ShortcutInfo> actualShortcuts) {
489         for (ShortcutInfo s : actualShortcuts) {
490             assertNotNull("ID " + s.getId(), s.getShortLabel());
491         }
492         return actualShortcuts;
493     }
494 
assertAllNotHaveTitle( List<ShortcutInfo> actualShortcuts)495     public static List<ShortcutInfo> assertAllNotHaveTitle(
496             List<ShortcutInfo> actualShortcuts) {
497         for (ShortcutInfo s : actualShortcuts) {
498             assertNull("ID " + s.getId(), s.getShortLabel());
499         }
500         return actualShortcuts;
501     }
502 
assertAllKeyFieldsOnly( List<ShortcutInfo> actualShortcuts)503     public static List<ShortcutInfo> assertAllKeyFieldsOnly(
504             List<ShortcutInfo> actualShortcuts) {
505         for (ShortcutInfo s : actualShortcuts) {
506             assertTrue("ID " + s.getId(), s.hasKeyFieldsOnly());
507         }
508         return actualShortcuts;
509     }
510 
assertAllNotKeyFieldsOnly( List<ShortcutInfo> actualShortcuts)511     public static List<ShortcutInfo> assertAllNotKeyFieldsOnly(
512             List<ShortcutInfo> actualShortcuts) {
513         for (ShortcutInfo s : actualShortcuts) {
514             assertFalse("ID " + s.getId(), s.hasKeyFieldsOnly());
515         }
516         return actualShortcuts;
517     }
518 
assertAllDynamic(List<ShortcutInfo> actualShortcuts)519     public static List<ShortcutInfo> assertAllDynamic(List<ShortcutInfo> actualShortcuts) {
520         for (ShortcutInfo s : actualShortcuts) {
521             assertTrue("ID " + s.getId(), s.isDynamic());
522         }
523         return actualShortcuts;
524     }
525 
assertAllPinned(List<ShortcutInfo> actualShortcuts)526     public static List<ShortcutInfo> assertAllPinned(List<ShortcutInfo> actualShortcuts) {
527         for (ShortcutInfo s : actualShortcuts) {
528             assertTrue("ID " + s.getId(), s.isPinned());
529         }
530         return actualShortcuts;
531     }
532 
assertAllDynamicOrPinned( List<ShortcutInfo> actualShortcuts)533     public static List<ShortcutInfo> assertAllDynamicOrPinned(
534             List<ShortcutInfo> actualShortcuts) {
535         for (ShortcutInfo s : actualShortcuts) {
536             assertTrue("ID " + s.getId(), s.isDynamic() || s.isPinned());
537         }
538         return actualShortcuts;
539     }
540 
assertAllManifest( List<ShortcutInfo> actualShortcuts)541     public static List<ShortcutInfo> assertAllManifest(
542             List<ShortcutInfo> actualShortcuts) {
543         for (ShortcutInfo s : actualShortcuts) {
544             assertTrue("ID " + s.getId(), s.isDeclaredInManifest());
545         }
546         return actualShortcuts;
547     }
548 
assertAllNotManifest( List<ShortcutInfo> actualShortcuts)549     public static List<ShortcutInfo> assertAllNotManifest(
550             List<ShortcutInfo> actualShortcuts) {
551         for (ShortcutInfo s : actualShortcuts) {
552             assertFalse("ID " + s.getId(), s.isDeclaredInManifest());
553         }
554         return actualShortcuts;
555     }
556 
assertAllDisabled( List<ShortcutInfo> actualShortcuts)557     public static List<ShortcutInfo> assertAllDisabled(
558             List<ShortcutInfo> actualShortcuts) {
559         for (ShortcutInfo s : actualShortcuts) {
560             assertTrue("ID " + s.getId(), !s.isEnabled());
561         }
562         return actualShortcuts;
563     }
564 
assertAllEnabled( List<ShortcutInfo> actualShortcuts)565     public static List<ShortcutInfo> assertAllEnabled(
566             List<ShortcutInfo> actualShortcuts) {
567         for (ShortcutInfo s : actualShortcuts) {
568             assertTrue("ID " + s.getId(), s.isEnabled());
569         }
570         return actualShortcuts;
571     }
572 
assertAllImmutable( List<ShortcutInfo> actualShortcuts)573     public static List<ShortcutInfo> assertAllImmutable(
574             List<ShortcutInfo> actualShortcuts) {
575         for (ShortcutInfo s : actualShortcuts) {
576             assertTrue("ID " + s.getId(), s.isImmutable());
577         }
578         return actualShortcuts;
579     }
580 
assertDynamicOnly(ShortcutInfo si)581     public static void assertDynamicOnly(ShortcutInfo si) {
582         assertTrue(si.isDynamic());
583         assertFalse(si.isPinned());
584     }
585 
assertPinnedOnly(ShortcutInfo si)586     public static void assertPinnedOnly(ShortcutInfo si) {
587         assertFalse(si.isDynamic());
588         assertFalse(si.isDeclaredInManifest());
589         assertTrue(si.isPinned());
590     }
591 
assertDynamicAndPinned(ShortcutInfo si)592     public static void assertDynamicAndPinned(ShortcutInfo si) {
593         assertTrue(si.isDynamic());
594         assertTrue(si.isPinned());
595     }
596 
assertBitmapSize(int expectedWidth, int expectedHeight, Bitmap bitmap)597     public static void assertBitmapSize(int expectedWidth, int expectedHeight, Bitmap bitmap) {
598         assertEquals("width", expectedWidth, bitmap.getWidth());
599         assertEquals("height", expectedHeight, bitmap.getHeight());
600     }
601 
assertAllUnique(Collection<T> list)602     public static <T> void assertAllUnique(Collection<T> list) {
603         final Set<Object> set = new LinkedHashSet<>();
604         for (T item : list) {
605             if (set.contains(item)) {
606                 fail("Duplicate item found: " + item + " (in the list: " + list + ")");
607             }
608             set.add(item);
609         }
610     }
611 
findShortcut(List<ShortcutInfo> list, String id)612     public static ShortcutInfo findShortcut(List<ShortcutInfo> list, String id) {
613         for (ShortcutInfo si : list) {
614             if (si.getId().equals(id)) {
615                 return si;
616             }
617         }
618         fail("Shortcut " + id + " not found in the list");
619         return null;
620     }
621 
pfdToBitmap(ParcelFileDescriptor pfd)622     public static Bitmap pfdToBitmap(ParcelFileDescriptor pfd) {
623         assertNotNull(pfd);
624         try {
625             try {
626                 return BitmapFactory.decodeFileDescriptor(pfd.getFileDescriptor());
627             } finally {
628                 pfd.close();
629             }
630         } catch (IOException e) {
631             throw new RuntimeException(e);
632         }
633     }
634 
assertBundleEmpty(BaseBundle b)635     public static void assertBundleEmpty(BaseBundle b) {
636         assertTrue(b == null || b.size() == 0);
637     }
638 
assertCallbackNotReceived(LauncherApps.Callback mock)639     public static void assertCallbackNotReceived(LauncherApps.Callback mock) {
640         verify(mock, times(0)).onShortcutsChanged(anyString(), anyList(),
641                 any(UserHandle.class));
642     }
643 
assertCallbackReceived(LauncherApps.Callback mock, UserHandle user, String packageName, String... ids)644     public static void assertCallbackReceived(LauncherApps.Callback mock,
645             UserHandle user, String packageName, String... ids) {
646         verify(mock).onShortcutsChanged(eq(packageName), checkShortcutIds(ids),
647                 eq(user));
648     }
649 
checkAssertSuccess(Runnable r)650     public static boolean checkAssertSuccess(Runnable r) {
651         try {
652             r.run();
653             return true;
654         } catch (AssertionError e) {
655             return false;
656         }
657     }
658 
checkArgument(Predicate<T> checker, String description, List<T> matchedCaptor)659     public static <T> T checkArgument(Predicate<T> checker, String description,
660             List<T> matchedCaptor) {
661         final Matcher<T> m = new BaseMatcher<T>() {
662             @Override
663             public boolean matches(Object item) {
664                 if (item == null) {
665                     return false;
666                 }
667                 final T value = (T) item;
668                 if (!checker.test(value)) {
669                     return false;
670                 }
671 
672                 if (matchedCaptor != null) {
673                     matchedCaptor.add(value);
674                 }
675                 return true;
676             }
677 
678             @Override
679             public void describeTo(Description d) {
680                 d.appendText(description);
681             }
682         };
683         return MockitoHamcrest.argThat(m);
684     }
685 
checkShortcutIds(String... ids)686     public static List<ShortcutInfo> checkShortcutIds(String... ids) {
687         return checkArgument((List<ShortcutInfo> list) -> {
688             final Set<String> actualSet = set(si -> si.getId(), list);
689             return actualSet.equals(set(ids));
690 
691         }, "Shortcut IDs=[" + Arrays.toString(ids) + "]", null);
692     }
693 
parceled(ShortcutInfo si)694     public static ShortcutInfo parceled(ShortcutInfo si) {
695         Parcel p = Parcel.obtain();
696         p.writeParcelable(si, 0);
697         p.setDataPosition(0);
698         ShortcutInfo si2 = p.readParcelable(ShortcutManagerTestUtils.class.getClassLoader());
699         p.recycle();
700         return si2;
701     }
702 
cloneShortcutList(List<ShortcutInfo> list)703     public static List<ShortcutInfo> cloneShortcutList(List<ShortcutInfo> list) {
704         if (list == null) {
705             return null;
706         }
707         final List<ShortcutInfo> ret = new ArrayList<>(list.size());
708         for (ShortcutInfo si : list) {
709             ret.add(parceled(si));
710         }
711 
712         return ret;
713     }
714 
715     private static final Comparator<ShortcutInfo> sRankComparator =
716             (ShortcutInfo a, ShortcutInfo b) -> Integer.compare(a.getRank(), b.getRank());
717 
sortedByRank(List<ShortcutInfo> shortcuts)718     public static List<ShortcutInfo> sortedByRank(List<ShortcutInfo> shortcuts) {
719         final ArrayList<ShortcutInfo> ret = new ArrayList<>(shortcuts);
720         Collections.sort(ret, sRankComparator);
721         return ret;
722     }
723 
waitUntil(String message, BooleanSupplier condition)724     public static void waitUntil(String message, BooleanSupplier condition) {
725         waitUntil(message, condition, STANDARD_TIMEOUT_SEC);
726     }
727 
waitUntil(String message, BooleanSupplier condition, int timeoutSeconds)728     public static void waitUntil(String message, BooleanSupplier condition, int timeoutSeconds) {
729         final long timeout = System.currentTimeMillis() + (timeoutSeconds * 1000L);
730         while (System.currentTimeMillis() < timeout) {
731             if (condition.getAsBoolean()) {
732                 return;
733             }
734             try {
735                 Thread.sleep(100);
736             } catch (InterruptedException e) {
737                 throw new RuntimeException(e);
738             }
739         }
740         fail("Timed out for: " + message);
741     }
742 
anyOrNull(Class<T> clazz)743     public static final <T> T anyOrNull(Class<T> clazz) {
744         return ArgumentMatchers.argThat(value -> true);
745     }
746 
anyStringOrNull()747     public static final String anyStringOrNull() {
748         return ArgumentMatchers.argThat(value -> true);
749     }
750 
assertWith(List<ShortcutInfo> list)751     public static ShortcutListAsserter assertWith(List<ShortcutInfo> list) {
752         return new ShortcutListAsserter(list);
753     }
754 
assertWith(ShortcutInfo... list)755     public static ShortcutListAsserter assertWith(ShortcutInfo... list) {
756         return assertWith(list(list));
757     }
758 
759     /**
760      * New style assertion that allows chained calls.
761      */
762     public static class ShortcutListAsserter {
763         private final ShortcutListAsserter mOriginal;
764         private final List<ShortcutInfo> mList;
765 
ShortcutListAsserter(List<ShortcutInfo> list)766         ShortcutListAsserter(List<ShortcutInfo> list) {
767             this(null, list);
768         }
769 
ShortcutListAsserter(ShortcutListAsserter original, List<ShortcutInfo> list)770         private ShortcutListAsserter(ShortcutListAsserter original, List<ShortcutInfo> list) {
771             mOriginal = (original == null) ? this : original;
772             mList = (list == null) ? new ArrayList<>(0) : new ArrayList<>(list);
773         }
774 
revertToOriginalList()775         public ShortcutListAsserter revertToOriginalList() {
776             return mOriginal;
777         }
778 
selectDynamic()779         public ShortcutListAsserter selectDynamic() {
780             return new ShortcutListAsserter(this,
781                     filter(mList, ShortcutInfo::isDynamic));
782         }
783 
selectManifest()784         public ShortcutListAsserter selectManifest() {
785             return new ShortcutListAsserter(this,
786                     filter(mList, ShortcutInfo::isDeclaredInManifest));
787         }
788 
selectPinned()789         public ShortcutListAsserter selectPinned() {
790             return new ShortcutListAsserter(this,
791                     filter(mList, ShortcutInfo::isPinned));
792         }
793 
selectFloating()794         public ShortcutListAsserter selectFloating() {
795             return new ShortcutListAsserter(this,
796                     filter(mList, (si -> si.isPinned()
797                             && !(si.isDynamic() || si.isDeclaredInManifest()))));
798         }
799 
selectByActivity(ComponentName activity)800         public ShortcutListAsserter selectByActivity(ComponentName activity) {
801             return new ShortcutListAsserter(this,
802                     ShortcutManagerTestUtils.filterByActivity(mList, activity));
803         }
804 
selectByChangedSince(long time)805         public ShortcutListAsserter selectByChangedSince(long time) {
806             return new ShortcutListAsserter(this,
807                     ShortcutManagerTestUtils.changedSince(mList, time));
808         }
809 
selectByIds(String... ids)810         public ShortcutListAsserter selectByIds(String... ids) {
811             final Set<String> idSet = set(ids);
812             final ArrayList<ShortcutInfo> selected = new ArrayList<>();
813             for (ShortcutInfo si : mList) {
814                 if (idSet.contains(si.getId())) {
815                     selected.add(si);
816                     idSet.remove(si.getId());
817                 }
818             }
819             if (idSet.size() > 0) {
820                 fail("Shortcuts not found for IDs=" + idSet);
821             }
822 
823             return new ShortcutListAsserter(this, selected);
824         }
825 
toSortByRank()826         public ShortcutListAsserter toSortByRank() {
827             return new ShortcutListAsserter(this,
828                     ShortcutManagerTestUtils.sortedByRank(mList));
829         }
830 
call(Consumer<List<ShortcutInfo>> c)831         public ShortcutListAsserter call(Consumer<List<ShortcutInfo>> c) {
832             c.accept(mList);
833             return this;
834         }
835 
haveIds(String... expectedIds)836         public ShortcutListAsserter haveIds(String... expectedIds) {
837             assertShortcutIds(mList, expectedIds);
838             return this;
839         }
840 
haveIdsOrdered(String... expectedIds)841         public ShortcutListAsserter haveIdsOrdered(String... expectedIds) {
842             assertShortcutIdsOrdered(mList, expectedIds);
843             return this;
844         }
845 
haveSequentialRanks()846         private ShortcutListAsserter haveSequentialRanks() {
847             for (int i = 0; i < mList.size(); i++) {
848                 final ShortcutInfo si = mList.get(i);
849                 assertEquals("Rank not sequential: id=" + si.getId(), i, si.getRank());
850             }
851             return this;
852         }
853 
haveRanksInOrder(String... expectedIds)854         public ShortcutListAsserter haveRanksInOrder(String... expectedIds) {
855             toSortByRank()
856                     .haveSequentialRanks()
857                     .haveIdsOrdered(expectedIds);
858             return this;
859         }
860 
isEmpty()861         public ShortcutListAsserter isEmpty() {
862             assertEquals(0, mList.size());
863             return this;
864         }
865 
areAllDynamic()866         public ShortcutListAsserter areAllDynamic() {
867             forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isDynamic()));
868             return this;
869         }
870 
areAllNotDynamic()871         public ShortcutListAsserter areAllNotDynamic() {
872             forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isDynamic()));
873             return this;
874         }
875 
areAllPinned()876         public ShortcutListAsserter areAllPinned() {
877             forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isPinned()));
878             return this;
879         }
880 
areAllNotPinned()881         public ShortcutListAsserter areAllNotPinned() {
882             forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isPinned()));
883             return this;
884         }
885 
areAllManifest()886         public ShortcutListAsserter areAllManifest() {
887             forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isDeclaredInManifest()));
888             return this;
889         }
890 
areAllNotManifest()891         public ShortcutListAsserter areAllNotManifest() {
892             forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isDeclaredInManifest()));
893             return this;
894         }
895 
areAllImmutable()896         public ShortcutListAsserter areAllImmutable() {
897             forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isImmutable()));
898             return this;
899         }
900 
areAllMutable()901         public ShortcutListAsserter areAllMutable() {
902             forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isImmutable()));
903             return this;
904         }
905 
areAllEnabled()906         public ShortcutListAsserter areAllEnabled() {
907             forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isEnabled()));
908             areAllWithDisabledReason(ShortcutInfo.DISABLED_REASON_NOT_DISABLED);
909             return this;
910         }
911 
areAllDisabled()912         public ShortcutListAsserter areAllDisabled() {
913             forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isEnabled()));
914             forAllShortcuts(s -> assertNotEquals("id=" + s.getId(),
915                     ShortcutInfo.DISABLED_REASON_NOT_DISABLED, s.getDisabledReason()));
916             return this;
917         }
918 
areAllFloating()919         public ShortcutListAsserter areAllFloating() {
920             forAllShortcuts(s -> assertTrue("id=" + s.getId(),
921                     s.isPinned() && !s.isDeclaredInManifest() && !s.isDynamic()));
922             return this;
923         }
924 
areAllNotFloating()925         public ShortcutListAsserter areAllNotFloating() {
926             forAllShortcuts(s -> assertTrue("id=" + s.getId(),
927                     !(s.isPinned() && !s.isDeclaredInManifest() && !s.isDynamic())));
928             return this;
929         }
930 
areAllOrphan()931         public ShortcutListAsserter areAllOrphan() {
932             forAllShortcuts(s -> assertTrue("id=" + s.getId(),
933                     !s.isPinned() && !s.isDeclaredInManifest() && !s.isDynamic()));
934             return this;
935         }
936 
areAllNotOrphan()937         public ShortcutListAsserter areAllNotOrphan() {
938             forAllShortcuts(s -> assertTrue("id=" + s.getId(),
939                     s.isPinned() || s.isDeclaredInManifest() || s.isDynamic()));
940             return this;
941         }
942 
areAllVisibleToPublisher()943         public ShortcutListAsserter areAllVisibleToPublisher() {
944             forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.isVisibleToPublisher()));
945             return this;
946         }
947 
areAllNotVisibleToPublisher()948         public ShortcutListAsserter areAllNotVisibleToPublisher() {
949             forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.isVisibleToPublisher()));
950             return this;
951         }
952 
areAllWithKeyFieldsOnly()953         public ShortcutListAsserter areAllWithKeyFieldsOnly() {
954             forAllShortcuts(s -> assertTrue("id=" + s.getId(), s.hasKeyFieldsOnly()));
955             return this;
956         }
957 
areAllNotWithKeyFieldsOnly()958         public ShortcutListAsserter areAllNotWithKeyFieldsOnly() {
959             forAllShortcuts(s -> assertFalse("id=" + s.getId(), s.hasKeyFieldsOnly()));
960             return this;
961         }
962 
areAllWithActivity(ComponentName activity)963         public ShortcutListAsserter areAllWithActivity(ComponentName activity) {
964             forAllShortcuts(s -> assertEquals("id=" + s.getId(), activity, s.getActivity()));
965             return this;
966         }
967 
areAllWithNoActivity()968         public ShortcutListAsserter areAllWithNoActivity() {
969             forAllShortcuts(s -> assertNull("id=" + s.getId(), s.getActivity()));
970             return this;
971         }
972 
areAllWithIntent()973         public ShortcutListAsserter areAllWithIntent() {
974             forAllShortcuts(s -> assertNotNull("id=" + s.getId(), s.getIntent()));
975             return this;
976         }
977 
areAllWithNoIntent()978         public ShortcutListAsserter areAllWithNoIntent() {
979             forAllShortcuts(s -> assertNull("id=" + s.getId(), s.getIntent()));
980             return this;
981         }
982 
areAllWithDisabledReason(int disabledReason)983         public ShortcutListAsserter areAllWithDisabledReason(int disabledReason) {
984             forAllShortcuts(s -> assertEquals("id=" + s.getId(),
985                     disabledReason, s.getDisabledReason()));
986             if (disabledReason >= ShortcutInfo.DISABLED_REASON_VERSION_LOWER) {
987                 areAllNotVisibleToPublisher();
988             } else {
989                 areAllVisibleToPublisher();
990             }
991             return this;
992         }
993 
forAllShortcuts(Consumer<ShortcutInfo> sa)994         public ShortcutListAsserter forAllShortcuts(Consumer<ShortcutInfo> sa) {
995             boolean found = false;
996             for (int i = 0; i < mList.size(); i++) {
997                 final ShortcutInfo si = mList.get(i);
998                 found = true;
999                 sa.accept(si);
1000             }
1001             assertTrue("No shortcuts found.", found);
1002             return this;
1003         }
1004 
forShortcut(Predicate<ShortcutInfo> p, Consumer<ShortcutInfo> sa)1005         public ShortcutListAsserter forShortcut(Predicate<ShortcutInfo> p,
1006                 Consumer<ShortcutInfo> sa) {
1007             boolean found = false;
1008             for (int i = 0; i < mList.size(); i++) {
1009                 final ShortcutInfo si = mList.get(i);
1010                 if (p.test(si)) {
1011                     found = true;
1012                     try {
1013                         sa.accept(si);
1014                     } catch (Throwable e) {
1015                         throw new AssertionError("Assertion failed for shortcut " + si.getId(), e);
1016                     }
1017                 }
1018             }
1019             assertTrue("Shortcut with the given condition not found.", found);
1020             return this;
1021         }
1022 
forShortcutWithId(String id, Consumer<ShortcutInfo> sa)1023         public ShortcutListAsserter forShortcutWithId(String id, Consumer<ShortcutInfo> sa) {
1024             forShortcut(si -> si.getId().equals(id), sa);
1025 
1026             return this;
1027         }
1028     }
1029 
assertBundlesEqual(BaseBundle b1, BaseBundle b2)1030     public static void assertBundlesEqual(BaseBundle b1, BaseBundle b2) {
1031         if (b1 == null && b2 == null) {
1032             return; // pass
1033         }
1034         assertNotNull("b1 is null but b2 is not", b1);
1035         assertNotNull("b2 is null but b1 is not", b2);
1036 
1037         // HashSet makes the error message readable.
1038         assertEquals(set(b1.keySet()), set(b2.keySet()));
1039 
1040         for (String key : b1.keySet()) {
1041             final Object v1 = b1.get(key);
1042             final Object v2 = b2.get(key);
1043             if (v1 == null) {
1044                 if (v2 == null) {
1045                     return;
1046                 }
1047             }
1048             if (v1.equals(v2)) {
1049                 return;
1050             }
1051 
1052             assertTrue("Only either value is null: key=" + key
1053                     + " b1=" + b1 + " b2=" + b2, v1 != null && v2 != null);
1054             assertEquals("Class mismatch: key=" + key, v1.getClass(), v2.getClass());
1055 
1056             if (v1 instanceof BaseBundle) {
1057                 assertBundlesEqual((BaseBundle) v1, (BaseBundle) v2);
1058 
1059             } else if (v1 instanceof boolean[]) {
1060                 assertTrue(Arrays.equals((boolean[]) v1, (boolean[]) v2));
1061 
1062             } else if (v1 instanceof int[]) {
1063                 MoreAsserts.assertEquals((int[]) v1, (int[]) v2);
1064 
1065             } else if (v1 instanceof double[]) {
1066                 MoreAsserts.assertEquals((double[]) v1, (double[]) v2);
1067 
1068             } else if (v1 instanceof String[]) {
1069                 MoreAsserts.assertEquals((String[]) v1, (String[]) v2);
1070 
1071             } else if (v1 instanceof Double) {
1072                 if (((Double) v1).isNaN()) {
1073                     assertTrue(((Double) v2).isNaN());
1074                 } else {
1075                     assertEquals(v1, v2);
1076                 }
1077 
1078             } else {
1079                 assertEquals(v1, v2);
1080             }
1081         }
1082     }
1083 
waitOnMainThread()1084     public static void waitOnMainThread() throws InterruptedException {
1085         final CountDownLatch latch = new CountDownLatch(1);
1086 
1087         new Handler(Looper.getMainLooper()).post(() -> latch.countDown());
1088 
1089         latch.await();
1090     }
1091 
1092     public static class LauncherCallbackAsserter {
1093         private final LauncherApps.Callback mCallback = mock(LauncherApps.Callback.class);
1094 
getMockCallback()1095         private Callback getMockCallback() {
1096             return mCallback;
1097         }
1098 
assertNoCallbackCalled()1099         public LauncherCallbackAsserter assertNoCallbackCalled() {
1100             verify(mCallback, times(0)).onShortcutsChanged(
1101                     anyString(),
1102                     any(List.class),
1103                     any(UserHandle.class));
1104             return this;
1105         }
1106 
assertNoCallbackCalledForPackage( String publisherPackageName)1107         public LauncherCallbackAsserter assertNoCallbackCalledForPackage(
1108                 String publisherPackageName) {
1109             verify(mCallback, times(0)).onShortcutsChanged(
1110                     eq(publisherPackageName),
1111                     any(List.class),
1112                     any(UserHandle.class));
1113             return this;
1114         }
1115 
assertNoCallbackCalledForPackageAndUser( String publisherPackageName, UserHandle publisherUserHandle)1116         public LauncherCallbackAsserter assertNoCallbackCalledForPackageAndUser(
1117                 String publisherPackageName, UserHandle publisherUserHandle) {
1118             verify(mCallback, times(0)).onShortcutsChanged(
1119                     eq(publisherPackageName),
1120                     any(List.class),
1121                     eq(publisherUserHandle));
1122             return this;
1123         }
1124 
assertCallbackCalledForPackageAndUser( String publisherPackageName, UserHandle publisherUserHandle)1125         public ShortcutListAsserter assertCallbackCalledForPackageAndUser(
1126                 String publisherPackageName, UserHandle publisherUserHandle) {
1127             final ArgumentCaptor<List> shortcuts = ArgumentCaptor.forClass(List.class);
1128             verify(mCallback, atLeastOnce()).onShortcutsChanged(
1129                     eq(publisherPackageName),
1130                     shortcuts.capture(),
1131                     eq(publisherUserHandle));
1132             return new ShortcutListAsserter(shortcuts.getValue());
1133         }
1134     }
1135 
assertForLauncherCallback( LauncherApps launcherApps, Runnable body)1136     public static LauncherCallbackAsserter assertForLauncherCallback(
1137             LauncherApps launcherApps, Runnable body) throws InterruptedException {
1138         final LauncherCallbackAsserter asserter = new LauncherCallbackAsserter();
1139         launcherApps.registerCallback(asserter.getMockCallback(),
1140                 new Handler(Looper.getMainLooper()));
1141 
1142         body.run();
1143 
1144         waitOnMainThread();
1145 
1146         // TODO unregister doesn't work well during unit tests.  Figure out and fix it.
1147         // launcherApps.unregisterCallback(asserter.getMockCallback());
1148 
1149         return asserter;
1150     }
1151 
assertForLauncherCallbackNoThrow( LauncherApps launcherApps, Runnable body)1152     public static LauncherCallbackAsserter assertForLauncherCallbackNoThrow(
1153             LauncherApps launcherApps, Runnable body) {
1154         try {
1155             return assertForLauncherCallback(launcherApps, body);
1156         } catch (InterruptedException e) {
1157             fail("Caught InterruptedException");
1158             return null; // Never happens.
1159         }
1160     }
1161 
retryUntil(BooleanSupplier checker, String message)1162     public static void retryUntil(BooleanSupplier checker, String message) {
1163         retryUntil(checker, message, 30);
1164     }
1165 
retryUntil(BooleanSupplier checker, String message, long timeoutSeconds)1166     public static void retryUntil(BooleanSupplier checker, String message, long timeoutSeconds) {
1167         final long timeOut = System.currentTimeMillis() + timeoutSeconds * 1000;
1168         while (!checker.getAsBoolean()) {
1169             if (System.currentTimeMillis() > timeOut) {
1170                 break;
1171             }
1172             try {
1173                 Thread.sleep(200);
1174             } catch (InterruptedException ignore) {
1175             }
1176         }
1177         assertTrue(message, checker.getAsBoolean());
1178     }
1179 }
1180