1 /*
2  * Copyright (C) 2022 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 
17 package android.os;
18 
19 import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer;
20 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession;
21 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
22 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
23 
24 import static org.junit.Assert.assertFalse;
25 import static org.junit.Assert.assertNotNull;
26 import static org.junit.Assert.assertNull;
27 import static org.junit.Assert.assertTrue;
28 import static org.mockito.Mockito.never;
29 import static org.mockito.Mockito.times;
30 
31 import android.platform.test.annotations.Presubmit;
32 
33 import androidx.test.ext.junit.runners.AndroidJUnit4;
34 import androidx.test.filters.SmallTest;
35 
36 import com.android.dx.mockito.inline.extended.StaticMockitoSession;
37 
38 import org.junit.Test;
39 import org.junit.runner.RunWith;
40 import org.mockito.quality.Strictness;
41 import org.mockito.stubbing.Answer;
42 
43 import java.util.Objects;
44 import java.util.concurrent.atomic.AtomicReference;
45 
46 /**
47  * Test for verifying {@link android.os.Bundle} recycles the underlying parcel where needed.
48  *
49  * <p>Build/Install/Run:
50  *  atest FrameworksMockingCoreTests:android.os.BundleRecyclingTest
51  */
52 @RunWith(AndroidJUnit4.class)
53 @SmallTest
54 @Presubmit
55 public class BundleRecyclingTest {
56     private Parcel mParcelSpy;
57     private Bundle mBundle;
58 
59     @Test
bundleClear_whenUnparcelledWithoutLazy_recyclesParcelOnce()60     public void bundleClear_whenUnparcelledWithoutLazy_recyclesParcelOnce() {
61         setUpBundle(/* lazy */ 0, /* nonLazy */ 1);
62         // Will unparcel and immediately recycle parcel
63         assertNotNull(mBundle.getString("nonLazy0"));
64         verify(mParcelSpy, times(1)).recycle();
65         assertFalse(mBundle.isDefinitelyEmpty());
66 
67         // Should not recycle again
68         mBundle.clear();
69         verify(mParcelSpy, times(1)).recycle();
70         assertTrue(mBundle.isDefinitelyEmpty());
71     }
72 
73     @Test
bundleClear_whenParcelled_recyclesParcel()74     public void bundleClear_whenParcelled_recyclesParcel() {
75         setUpBundle(/* lazy */ 1);
76         assertTrue(mBundle.isParcelled());
77         verify(mParcelSpy, times(0)).recycle();
78 
79         mBundle.clear();
80         verify(mParcelSpy, times(1)).recycle();
81         assertTrue(mBundle.isDefinitelyEmpty());
82 
83         // Should not recycle again
84         mBundle.clear();
85         verify(mParcelSpy, times(1)).recycle();
86     }
87 
88     @Test
bundleClear_whenUnparcelledWithLazy_recyclesParcel()89     public void bundleClear_whenUnparcelledWithLazy_recyclesParcel() {
90         setUpBundle(/* lazy */ 1);
91 
92         // Will unparcel but keep the CustomParcelable lazy
93         assertFalse(mBundle.isEmpty());
94         verify(mParcelSpy, times(0)).recycle();
95 
96         mBundle.clear();
97         verify(mParcelSpy, times(1)).recycle();
98         assertTrue(mBundle.isDefinitelyEmpty());
99 
100         // Should not recycle again
101         mBundle.clear();
102         verify(mParcelSpy, times(1)).recycle();
103     }
104 
105     @Test
bundleClear_whenClearedWithSharedParcel_doesNotRecycleParcel()106     public void bundleClear_whenClearedWithSharedParcel_doesNotRecycleParcel() {
107         setUpBundle(/* lazy */ 1);
108 
109         Bundle copy = new Bundle();
110         copy.putAll(mBundle);
111 
112         mBundle.clear();
113         assertTrue(mBundle.isDefinitelyEmpty());
114 
115         copy.clear();
116         assertTrue(copy.isDefinitelyEmpty());
117 
118         verify(mParcelSpy, never()).recycle();
119     }
120 
121     @Test
bundleClear_whenClearedWithCopiedParcel_doesNotRecycleParcel()122     public void bundleClear_whenClearedWithCopiedParcel_doesNotRecycleParcel() {
123         setUpBundle(/* lazy */ 1);
124 
125         // Will unparcel but keep the CustomParcelable lazy
126         assertFalse(mBundle.isEmpty());
127 
128         Bundle copy = mBundle.deepCopy();
129         copy.putAll(mBundle);
130 
131         mBundle.clear();
132         assertTrue(mBundle.isDefinitelyEmpty());
133 
134         copy.clear();
135         assertTrue(copy.isDefinitelyEmpty());
136 
137         verify(mParcelSpy, never()).recycle();
138     }
139 
140     @Test
bundleGet_whenUnparcelledWithLazyValueUnwrapped_recyclesParcel()141     public void bundleGet_whenUnparcelledWithLazyValueUnwrapped_recyclesParcel() {
142         setUpBundle(/* lazy */ 1);
143 
144         // Will unparcel with a lazy value, and immediately unwrap the lazy value,
145         // with no lazy values left at the end of getParcelable
146         // Ref counting should immediately recycle
147         assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
148         verify(mParcelSpy, times(1)).recycle();
149 
150         // Should not recycle again
151         assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
152         mBundle.clear();
153         verify(mParcelSpy, times(1)).recycle();
154     }
155 
156     @Test
bundleGet_whenMultipleLazy_recyclesParcelWhenAllUnwrapped()157     public void bundleGet_whenMultipleLazy_recyclesParcelWhenAllUnwrapped() {
158         setUpBundle(/* lazy */ 2);
159 
160         assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
161         verify(mParcelSpy, times(0)).recycle();
162 
163         assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
164         verify(mParcelSpy, times(0)).recycle();
165 
166         assertNotNull(mBundle.getParcelable("lazy1", CustomParcelable.class));
167         verify(mParcelSpy, times(1)).recycle();
168 
169         // Should not recycle again
170         assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
171         mBundle.clear();
172         verify(mParcelSpy, times(1)).recycle();
173         assertTrue(mBundle.isDefinitelyEmpty());
174     }
175 
176     @Test
bundleGet_whenLazyAndNonLazy_recyclesParcelWhenAllUnwrapped()177     public void bundleGet_whenLazyAndNonLazy_recyclesParcelWhenAllUnwrapped() {
178         setUpBundle(/* lazy */ 1, /* nonLazy */ 1);
179 
180         assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
181         verify(mParcelSpy, times(1)).recycle();
182 
183         // Should not recycle again
184         assertNotNull(mBundle.getString("nonLazy0"));
185         assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
186         mBundle.clear();
187         verify(mParcelSpy, times(1)).recycle();
188     }
189 
190     @Test
bundleGet_whenLazyAndNonLazy_doesNotRecycleWhenOnlyNonLazyRetrieved()191     public void bundleGet_whenLazyAndNonLazy_doesNotRecycleWhenOnlyNonLazyRetrieved() {
192         setUpBundle(/* lazy */ 1, /* nonLazy */ 1);
193 
194         assertNotNull(mBundle.getString("nonLazy0"));
195         verify(mParcelSpy, times(0)).recycle();
196 
197         assertNotNull(mBundle.getString("nonLazy0"));
198         verify(mParcelSpy, times(0)).recycle();
199 
200         assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
201         verify(mParcelSpy, times(1)).recycle();
202     }
203 
204     @Test
bundleGet_withWithSharedParcel_doesNotRecycleParcel()205     public void bundleGet_withWithSharedParcel_doesNotRecycleParcel() {
206         setUpBundle(/* lazy */ 1);
207 
208         Bundle copy = new Bundle();
209         copy.putAll(mBundle);
210 
211         assertNotNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
212         mBundle.clear();
213 
214         assertNotNull(copy.getParcelable("lazy0", CustomParcelable.class));
215         copy.clear();
216 
217         verify(mParcelSpy, never()).recycle();
218     }
219 
220     @Test
bundleGet_getAfterLazyCleared_doesNotRecycleAgain()221     public void bundleGet_getAfterLazyCleared_doesNotRecycleAgain() {
222         setUpBundle(/* lazy */ 1);
223         mBundle.clear();
224         verify(mParcelSpy, times(1)).recycle();
225 
226         assertNull(mBundle.getParcelable("lazy0", CustomParcelable.class));
227         verify(mParcelSpy, times(1)).recycle();
228     }
229 
setUpBundle(int lazy)230     private void setUpBundle(int lazy) {
231         setUpBundle(lazy, /* nonLazy */ 0);
232     }
233 
setUpBundle(int lazy, int nonLazy)234     private void setUpBundle(int lazy, int nonLazy) {
235         AtomicReference<Parcel> parcel = new AtomicReference<>();
236         StaticMockitoSession session = mockitoSession()
237                 .strictness(Strictness.STRICT_STUBS)
238                 .spyStatic(Parcel.class)
239                 .startMocking();
240         doAnswer((Answer<Parcel>) invocationOnSpy -> {
241             Parcel spy = (Parcel) invocationOnSpy.callRealMethod();
242             spyOn(spy);
243             parcel.set(spy);
244             return spy;
245         }).when(() -> Parcel.obtain());
246 
247         Bundle bundle = new Bundle();
248         bundle.setClassLoader(getClass().getClassLoader());
249         Parcel p = createBundle(lazy, nonLazy);
250         bundle.readFromParcel(p);
251         p.recycle();
252 
253         session.finishMocking();
254 
255         mParcelSpy = parcel.get();
256         mBundle = bundle;
257     }
258 
259     /**
260      * Create a test bundle, parcel it and return the parcel.
261      */
createBundle(int lazy, int nonLazy)262     private Parcel createBundle(int lazy, int nonLazy) {
263         final Bundle source = new Bundle();
264 
265         for (int i = 0; i < nonLazy; i++) {
266             source.putString("nonLazy" + i, "Tiramisu");
267         }
268 
269         for (int i = 0; i < lazy; i++) {
270             source.putParcelable("lazy" + i, new CustomParcelable(13, "Tiramisu"));
271         }
272 
273         return getParcelledBundle(source);
274     }
275 
276     /**
277      * Take a bundle, write it to a parcel and return the parcel.
278      */
getParcelledBundle(Bundle bundle)279     private Parcel getParcelledBundle(Bundle bundle) {
280         final Parcel p = Parcel.obtain();
281         // Don't use p.writeParcelabe(), which would write the creator, which we don't need.
282         bundle.writeToParcel(p, 0);
283         p.setDataPosition(0);
284         return p;
285     }
286 
287     private static class CustomParcelable implements Parcelable {
288         public final int integer;
289         public final String string;
290 
CustomParcelable(int integer, String string)291         CustomParcelable(int integer, String string) {
292             this.integer = integer;
293             this.string = string;
294         }
295 
CustomParcelable(Parcel in)296         protected CustomParcelable(Parcel in) {
297             integer = in.readInt();
298             string = in.readString();
299         }
300 
301         @Override
describeContents()302         public int describeContents() {
303             return 0;
304         }
305 
306         @Override
writeToParcel(Parcel out, int flags)307         public void writeToParcel(Parcel out, int flags) {
308             out.writeInt(integer);
309             out.writeString(string);
310         }
311 
312         @Override
equals(Object other)313         public boolean equals(Object other) {
314             if (this == other) {
315                 return true;
316             }
317             if (!(other instanceof CustomParcelable)) {
318                 return false;
319             }
320             CustomParcelable that = (CustomParcelable) other;
321             return integer == that.integer && string.equals(that.string);
322         }
323 
324         @Override
hashCode()325         public int hashCode() {
326             return Objects.hash(integer, string);
327         }
328 
329         public static final Creator<CustomParcelable> CREATOR = new Creator<CustomParcelable>() {
330             @Override
331             public CustomParcelable createFromParcel(Parcel in) {
332                 return new CustomParcelable(in);
333             }
334             @Override
335             public CustomParcelable[] newArray(int size) {
336                 return new CustomParcelable[size];
337             }
338         };
339     }
340 }
341