1 package com.android.server.slice;
2 
3 import static android.testing.TestableContentResolver.UNSTABLE;
4 
5 import static org.junit.Assert.assertArrayEquals;
6 import static org.junit.Assert.assertEquals;
7 import static org.junit.Assert.assertFalse;
8 import static org.junit.Assert.assertNull;
9 import static org.junit.Assert.assertTrue;
10 import static org.mockito.ArgumentMatchers.any;
11 import static org.mockito.ArgumentMatchers.anyInt;
12 import static org.mockito.ArgumentMatchers.anyString;
13 import static org.mockito.ArgumentMatchers.argThat;
14 import static org.mockito.ArgumentMatchers.eq;
15 import static org.mockito.ArgumentMatchers.nullable;
16 import static org.mockito.Mockito.doAnswer;
17 import static org.mockito.Mockito.mock;
18 import static org.mockito.Mockito.verify;
19 import static org.mockito.Mockito.when;
20 
21 import android.app.slice.ISliceListener;
22 import android.app.slice.SliceProvider;
23 import android.app.slice.SliceSpec;
24 import android.content.ContentProvider;
25 import android.content.IContentProvider;
26 import android.net.Uri;
27 import android.os.Binder;
28 import android.os.Handler;
29 import android.os.IBinder;
30 import android.os.IBinder.DeathRecipient;
31 import android.os.RemoteException;
32 import android.testing.AndroidTestingRunner;
33 import android.testing.TestableLooper;
34 import android.testing.TestableLooper.RunWithLooper;
35 
36 import androidx.test.filters.SmallTest;
37 
38 import com.android.server.UiServiceTestCase;
39 
40 import org.junit.Before;
41 import org.junit.Test;
42 import org.junit.runner.RunWith;
43 import org.mockito.ArgumentCaptor;
44 
45 @SmallTest
46 @RunWith(AndroidTestingRunner.class)
47 @RunWithLooper
48 public class PinnedSliceStateTest extends UiServiceTestCase {
49 
50     private static final String AUTH = "my.authority";
51     private static final Uri TEST_URI = Uri.parse("content://" + AUTH + "/path");
52 
53     private static final SliceSpec[] FIRST_SPECS = new SliceSpec[]{
54             new SliceSpec("spec1", 3),
55             new SliceSpec("spec2", 3),
56             new SliceSpec("spec3", 2),
57             new SliceSpec("spec4", 1),
58     };
59 
60     private static final SliceSpec[] SECOND_SPECS = new SliceSpec[]{
61             new SliceSpec("spec2", 1),
62             new SliceSpec("spec3", 2),
63             new SliceSpec("spec4", 3),
64             new SliceSpec("spec5", 4),
65     };
66 
67     private SliceManagerService mSliceService;
68     private PinnedSliceState mPinnedSliceManager;
69     private IContentProvider mIContentProvider;
70     private ContentProvider mContentProvider;
71     private IBinder mToken = new Binder();
72 
73     @Before
setUp()74     public void setUp() {
75         mSliceService = mock(SliceManagerService.class);
76         when(mSliceService.getContext()).thenReturn(mContext);
77         when(mSliceService.getLock()).thenReturn(new Object());
78         when(mSliceService.getHandler()).thenReturn(
79                 new Handler(TestableLooper.get(this).getLooper()));
80         mContentProvider = mock(ContentProvider.class);
81         mIContentProvider = mock(IContentProvider.class);
82         when(mContentProvider.getIContentProvider()).thenReturn(mIContentProvider);
83         mContext.getContentResolver().addProvider(AUTH, mContentProvider, UNSTABLE);
84         mPinnedSliceManager = new PinnedSliceState(mSliceService, TEST_URI, "pkg");
85     }
86 
87     @Test
testMergeSpecs()88     public void testMergeSpecs() {
89         // No annotations to start.
90         assertNull(mPinnedSliceManager.getSpecs());
91 
92         mPinnedSliceManager.mergeSpecs(FIRST_SPECS);
93         assertArrayEquals(FIRST_SPECS, mPinnedSliceManager.getSpecs());
94 
95         mPinnedSliceManager.mergeSpecs(SECOND_SPECS);
96         assertArrayEquals(new SliceSpec[]{
97                 // spec1 is gone because it's not in the second set.
98                 new SliceSpec("spec2", 1), // spec2 is 1 because it's smaller in the second set.
99                 new SliceSpec("spec3", 2), // spec3 is the same in both sets
100                 new SliceSpec("spec4", 1), // spec4 is 1 because it's smaller in the first set.
101                 // spec5 is gone because it's not in the first set.
102         }, mPinnedSliceManager.getSpecs());
103     }
104 
105     @Test
testSendPinnedOnPin()106     public void testSendPinnedOnPin() throws RemoteException {
107         TestableLooper.get(this).processAllMessages();
108 
109         // When pinned for the first time, a pinned message should be sent.
110         mPinnedSliceManager.pin("pkg", FIRST_SPECS, mToken);
111         TestableLooper.get(this).processAllMessages();
112 
113         verify(mIContentProvider).call(any(), anyString(),
114                 eq(SliceProvider.METHOD_PIN), eq(null), argThat(b -> {
115                     assertEquals(TEST_URI, b.getParcelable(SliceProvider.EXTRA_BIND_URI));
116                     return true;
117                 }));
118     }
119 
120     @Test
testPkgPin()121     public void testPkgPin() {
122         assertFalse(mPinnedSliceManager.hasPinOrListener());
123 
124         mPinnedSliceManager.pin("pkg", FIRST_SPECS, mToken);
125         assertTrue(mPinnedSliceManager.hasPinOrListener());
126 
127         assertTrue(mPinnedSliceManager.unpin("pkg", mToken));
128         assertFalse(mPinnedSliceManager.hasPinOrListener());
129     }
130 
131     @Test
testMultiPkgPin()132     public void testMultiPkgPin() {
133         IBinder t2 = new Binder();
134         assertFalse(mPinnedSliceManager.hasPinOrListener());
135 
136         mPinnedSliceManager.pin("pkg", FIRST_SPECS, mToken);
137         assertTrue(mPinnedSliceManager.hasPinOrListener());
138         mPinnedSliceManager.pin("pkg2", FIRST_SPECS, t2);
139 
140         assertFalse(mPinnedSliceManager.unpin("pkg", mToken));
141         assertTrue(mPinnedSliceManager.unpin("pkg2", t2));
142         assertFalse(mPinnedSliceManager.hasPinOrListener());
143     }
144 
145     @Test
testListenerDeath()146     public void testListenerDeath() throws RemoteException {
147         ISliceListener listener = mock(ISliceListener.class);
148         IBinder binder = mock(IBinder.class);
149         when(binder.isBinderAlive()).thenReturn(true);
150         when(listener.asBinder()).thenReturn(binder);
151         assertFalse(mPinnedSliceManager.hasPinOrListener());
152 
153         mPinnedSliceManager.pin(mContext.getPackageName(), FIRST_SPECS, binder);
154         assertTrue(mPinnedSliceManager.hasPinOrListener());
155 
156         ArgumentCaptor<DeathRecipient> arg = ArgumentCaptor.forClass(DeathRecipient.class);
157         verify(binder).linkToDeath(arg.capture(), anyInt());
158 
159         when(binder.isBinderAlive()).thenReturn(false);
160         arg.getValue().binderDied();
161 
162         verify(mSliceService).removePinnedSlice(eq(TEST_URI));
163         assertFalse(mPinnedSliceManager.hasPinOrListener());
164     }
165 
166     @Test
testPinFailed()167     public void testPinFailed() throws Exception {
168         // Throw exception when trying to pin
169         doAnswer(invocation -> {
170             throw new Exception("Pin failed");
171         }).when(mIContentProvider).call(any(), anyString(), anyString(),
172                 nullable(String.class), any());
173 
174         TestableLooper.get(this).processAllMessages();
175 
176         // When pinned for the first time, a pinned message should be sent.
177         mPinnedSliceManager.pin("pkg", FIRST_SPECS, mToken);
178         TestableLooper.get(this).processAllMessages();
179 
180         verify(mIContentProvider).call(any(), anyString(),
181                 eq(SliceProvider.METHOD_PIN), eq(null), argThat(b -> {
182                     assertEquals(TEST_URI, b.getParcelable(SliceProvider.EXTRA_BIND_URI));
183                     return true;
184                 }));
185     }
186 }
187