1 /*
2  * Copyright (C) 2014 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 com.android.cts.splitapp;
18 
19 import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
20 import static org.xmlpull.v1.XmlPullParser.START_TAG;
21 
22 import android.content.BroadcastReceiver;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.IntentFilter;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.ProviderInfo;
30 import android.content.pm.ResolveInfo;
31 import android.content.res.Configuration;
32 import android.content.res.Resources;
33 import android.database.Cursor;
34 import android.database.sqlite.SQLiteDatabase;
35 import android.graphics.Bitmap;
36 import android.graphics.Canvas;
37 import android.graphics.drawable.Drawable;
38 import android.os.ConditionVariable;
39 import android.os.Environment;
40 import android.os.ParcelFileDescriptor;
41 import android.os.StatFs;
42 import android.system.Os;
43 import android.system.OsConstants;
44 import android.system.StructStat;
45 import android.test.AndroidTestCase;
46 import android.test.MoreAsserts;
47 import android.util.DisplayMetrics;
48 import android.util.Log;
49 
50 import org.xmlpull.v1.XmlPullParser;
51 import org.xmlpull.v1.XmlPullParserException;
52 
53 import java.io.BufferedReader;
54 import java.io.DataInputStream;
55 import java.io.DataOutputStream;
56 import java.io.File;
57 import java.io.FileDescriptor;
58 import java.io.FileInputStream;
59 import java.io.FileOutputStream;
60 import java.io.IOException;
61 import java.io.InputStreamReader;
62 import java.lang.reflect.Field;
63 import java.lang.reflect.Method;
64 import java.util.List;
65 import java.util.Locale;
66 
67 public class SplitAppTest extends AndroidTestCase {
68     private static final String TAG = "SplitAppTest";
69     private static final String PKG = "com.android.cts.splitapp";
70 
71     private static final long MB_IN_BYTES = 1 * 1024 * 1024;
72 
73     public static boolean sFeatureTouched = false;
74     public static String sFeatureValue = null;
75 
testNothing()76     public void testNothing() throws Exception {
77     }
78 
testSingleBase()79     public void testSingleBase() throws Exception {
80         final Resources r = getContext().getResources();
81         final PackageManager pm = getContext().getPackageManager();
82 
83         // Should have untouched resources from base
84         assertEquals(false, r.getBoolean(R.bool.my_receiver_enabled));
85 
86         assertEquals("blue", r.getString(R.string.my_string1));
87         assertEquals("purple", r.getString(R.string.my_string2));
88 
89         assertEquals(0xff00ff00, r.getColor(R.color.my_color));
90         assertEquals(123, r.getInteger(R.integer.my_integer));
91 
92         assertEquals("base", getXmlTestValue(r.getXml(R.xml.my_activity_meta)));
93 
94         // We know about drawable IDs, but they're stripped from base
95         try {
96             r.getDrawable(R.drawable.image);
97             fail("Unexpected drawable in base");
98         } catch (Resources.NotFoundException expected) {
99         }
100 
101         // Should have base assets
102         assertAssetContents(r, "file1.txt", "FILE1");
103         assertAssetContents(r, "dir/dirfile1.txt", "DIRFILE1");
104 
105         try {
106             assertAssetContents(r, "file2.txt", null);
107             fail("Unexpected asset file2");
108         } catch (IOException expected) {
109         }
110 
111         // Should only have base manifest items
112         Intent intent = new Intent(Intent.ACTION_MAIN);
113         intent.addCategory(Intent.CATEGORY_LAUNCHER);
114         intent.setPackage(PKG);
115 
116         List<ResolveInfo> result = pm.queryIntentActivities(intent, 0);
117         assertEquals(1, result.size());
118         assertEquals("com.android.cts.splitapp.MyActivity", result.get(0).activityInfo.name);
119 
120         // Receiver disabled by default in base
121         intent = new Intent(Intent.ACTION_DATE_CHANGED);
122         intent.setPackage(PKG);
123 
124         result = pm.queryBroadcastReceivers(intent, 0);
125         assertEquals(0, result.size());
126 
127         // We shouldn't have any native code in base
128         try {
129             Native.add(2, 4);
130             fail("Unexpected native code in base");
131         } catch (UnsatisfiedLinkError expected) {
132         }
133     }
134 
testDensitySingle()135     public void testDensitySingle() throws Exception {
136         final Resources r = getContext().getResources();
137 
138         // We should still have base resources
139         assertEquals("blue", r.getString(R.string.my_string1));
140         assertEquals("purple", r.getString(R.string.my_string2));
141 
142         // Now we know about drawables, but only mdpi
143         final Drawable d = r.getDrawable(R.drawable.image);
144         assertEquals(0xff7e00ff, getDrawableColor(d));
145     }
146 
testDensityAll()147     public void testDensityAll() throws Exception {
148         final Resources r = getContext().getResources();
149 
150         // We should still have base resources
151         assertEquals("blue", r.getString(R.string.my_string1));
152         assertEquals("purple", r.getString(R.string.my_string2));
153 
154         // Pretend that we're at each density
155         updateDpi(r, DisplayMetrics.DENSITY_MEDIUM);
156         assertEquals(0xff7e00ff, getDrawableColor(r.getDrawable(R.drawable.image)));
157 
158         updateDpi(r, DisplayMetrics.DENSITY_HIGH);
159         assertEquals(0xff00fcff, getDrawableColor(r.getDrawable(R.drawable.image)));
160 
161         updateDpi(r, DisplayMetrics.DENSITY_XHIGH);
162         assertEquals(0xff80ff00, getDrawableColor(r.getDrawable(R.drawable.image)));
163 
164         updateDpi(r, DisplayMetrics.DENSITY_XXHIGH);
165         assertEquals(0xffff0000, getDrawableColor(r.getDrawable(R.drawable.image)));
166     }
167 
testDensityBest1()168     public void testDensityBest1() throws Exception {
169         final Resources r = getContext().getResources();
170 
171         // Pretend that we're really high density, but we only have mdpi installed
172         updateDpi(r, DisplayMetrics.DENSITY_XXHIGH);
173         assertEquals(0xff7e00ff, getDrawableColor(r.getDrawable(R.drawable.image)));
174     }
175 
testDensityBest2()176     public void testDensityBest2() throws Exception {
177         final Resources r = getContext().getResources();
178 
179         // Pretend that we're really high density, and now we have better match
180         updateDpi(r, DisplayMetrics.DENSITY_XXHIGH);
181         assertEquals(0xffff0000, getDrawableColor(r.getDrawable(R.drawable.image)));
182     }
183 
testApi()184     public void testApi() throws Exception {
185         final Resources r = getContext().getResources();
186         final PackageManager pm = getContext().getPackageManager();
187 
188         // We should have updated boolean, different from base
189         assertEquals(true, r.getBoolean(R.bool.my_receiver_enabled));
190 
191         // Receiver should be enabled now
192         Intent intent = new Intent(Intent.ACTION_DATE_CHANGED);
193         intent.setPackage(PKG);
194 
195         List<ResolveInfo> result = pm.queryBroadcastReceivers(intent, 0);
196         assertEquals(1, result.size());
197         assertEquals("com.android.cts.splitapp.MyReceiver", result.get(0).activityInfo.name);
198     }
199 
testLocale()200     public void testLocale() throws Exception {
201         final Resources r = getContext().getResources();
202 
203         updateLocale(r, Locale.ENGLISH);
204         assertEquals("blue", r.getString(R.string.my_string1));
205         assertEquals("purple", r.getString(R.string.my_string2));
206 
207         updateLocale(r, Locale.GERMAN);
208         assertEquals("blau", r.getString(R.string.my_string1));
209         assertEquals("purple", r.getString(R.string.my_string2));
210 
211         updateLocale(r, Locale.FRENCH);
212         assertEquals("blue", r.getString(R.string.my_string1));
213         assertEquals("pourpre", r.getString(R.string.my_string2));
214     }
215 
testNative()216     public void testNative() throws Exception {
217         Log.d(TAG, "testNative() thinks it's using ABI " + Native.arch());
218 
219         // Make sure we can do the maths
220         assertEquals(11642, Native.add(4933, 6709));
221     }
222 
testFeatureBase()223     public void testFeatureBase() throws Exception {
224         final Resources r = getContext().getResources();
225         final PackageManager pm = getContext().getPackageManager();
226 
227         // Should have untouched resources from base
228         assertEquals(false, r.getBoolean(R.bool.my_receiver_enabled));
229 
230         assertEquals("blue", r.getString(R.string.my_string1));
231         assertEquals("purple", r.getString(R.string.my_string2));
232 
233         assertEquals(0xff00ff00, r.getColor(R.color.my_color));
234         assertEquals(123, r.getInteger(R.integer.my_integer));
235 
236         assertEquals("base", getXmlTestValue(r.getXml(R.xml.my_activity_meta)));
237 
238         // And that we can access resources from feature
239         assertEquals("red", r.getString(r.getIdentifier("feature_string", "string", PKG)));
240         assertEquals(123, r.getInteger(r.getIdentifier("feature_integer", "integer", PKG)));
241 
242         final Class<?> featR = Class.forName("com.android.cts.splitapp.FeatureR");
243         final int boolId = (int) featR.getDeclaredField("feature_receiver_enabled").get(null);
244         final int intId = (int) featR.getDeclaredField("feature_integer").get(null);
245         final int stringId = (int) featR.getDeclaredField("feature_string").get(null);
246         assertEquals(true, r.getBoolean(boolId));
247         assertEquals(123, r.getInteger(intId));
248         assertEquals("red", r.getString(stringId));
249 
250         // Should have both base and feature assets
251         assertAssetContents(r, "file1.txt", "FILE1");
252         assertAssetContents(r, "file2.txt", "FILE2");
253         assertAssetContents(r, "dir/dirfile1.txt", "DIRFILE1");
254         assertAssetContents(r, "dir/dirfile2.txt", "DIRFILE2");
255 
256         // Should have both base and feature components
257         Intent intent = new Intent(Intent.ACTION_MAIN);
258         intent.addCategory(Intent.CATEGORY_LAUNCHER);
259         intent.setPackage(PKG);
260         List<ResolveInfo> result = pm.queryIntentActivities(intent, 0);
261         assertEquals(2, result.size());
262         assertEquals("com.android.cts.splitapp.MyActivity", result.get(0).activityInfo.name);
263         assertEquals("com.android.cts.splitapp.FeatureActivity", result.get(1).activityInfo.name);
264 
265         // Receiver only enabled in feature
266         intent = new Intent(Intent.ACTION_DATE_CHANGED);
267         intent.setPackage(PKG);
268         result = pm.queryBroadcastReceivers(intent, 0);
269         assertEquals(1, result.size());
270         assertEquals("com.android.cts.splitapp.FeatureReceiver", result.get(0).activityInfo.name);
271 
272         // And we should have a service
273         intent = new Intent("com.android.cts.splitapp.service");
274         intent.setPackage(PKG);
275         result = pm.queryIntentServices(intent, 0);
276         assertEquals(1, result.size());
277         assertEquals("com.android.cts.splitapp.FeatureService", result.get(0).serviceInfo.name);
278 
279         // And a provider too
280         ProviderInfo info = pm.resolveContentProvider("com.android.cts.splitapp.provider", 0);
281         assertEquals("com.android.cts.splitapp.FeatureProvider", info.name);
282 
283         // And assert that we spun up the provider in this process
284         final Class<?> provider = Class.forName("com.android.cts.splitapp.FeatureProvider");
285         final Field field = provider.getDeclaredField("sCreated");
286         assertTrue("Expected provider to have been created", (boolean) field.get(null));
287         assertTrue("Expected provider to have touched us", sFeatureTouched);
288         assertEquals(r.getString(R.string.my_string1), sFeatureValue);
289 
290         // Finally ensure that we can execute some code from split
291         final Class<?> logic = Class.forName("com.android.cts.splitapp.FeatureLogic");
292         final Method method = logic.getDeclaredMethod("mult", new Class[] {
293                 Integer.TYPE, Integer.TYPE });
294         assertEquals(72, (int) method.invoke(null, 12, 6));
295 
296         // Make sure we didn't get an extra flag from feature split
297         assertTrue("Someone parsed application flag!",
298                 (getContext().getApplicationInfo().flags & ApplicationInfo.FLAG_LARGE_HEAP) == 0);
299 
300         // Make sure we have permission from base APK
301         getContext().enforceCallingOrSelfPermission(android.Manifest.permission.CAMERA, null);
302 
303         try {
304             // But no new permissions from the feature APK
305             getContext().enforceCallingOrSelfPermission(android.Manifest.permission.INTERNET, null);
306             fail("Whaaa, we somehow gained permission from feature?");
307         } catch (SecurityException expected) {
308         }
309     }
310 
testBaseInstalled()311     public void testBaseInstalled() throws Exception {
312         final ConditionVariable cv = new ConditionVariable();
313         final BroadcastReceiver r = new BroadcastReceiver() {
314             @Override
315             public void onReceive(Context context, Intent intent) {
316                 assertEquals(1, intent.getIntExtra("CREATE_COUNT", -1));
317                 assertEquals(0, intent.getIntExtra("NEW_INTENT_COUNT", -1));
318                 cv.open();
319             }
320         };
321         final IntentFilter filter = new IntentFilter("com.android.cts.norestart.BROADCAST");
322         getContext().registerReceiver(r, filter);
323         final Intent i = new Intent("com.android.cts.norestart.START");
324         i.addCategory(Intent.CATEGORY_DEFAULT);
325         getContext().startActivity(i);
326         assertTrue(cv.block(2000L));
327         getContext().unregisterReceiver(r);
328     }
329 
330     /**
331      * Tests a running activity remains active while a new feature split is installed.
332      * <p>
333      * Prior to running this test, the activity must be started. That is currently
334      * done in {@link #testBaseInstalled()}.
335      */
testFeatureInstalled()336     public void testFeatureInstalled() throws Exception {
337         final ConditionVariable cv = new ConditionVariable();
338         final BroadcastReceiver r = new BroadcastReceiver() {
339             @Override
340             public void onReceive(Context context, Intent intent) {
341                 assertEquals(1, intent.getIntExtra("CREATE_COUNT", -1));
342                 assertEquals(1, intent.getIntExtra("NEW_INTENT_COUNT", -1));
343                 cv.open();
344             }
345         };
346         final IntentFilter filter = new IntentFilter("com.android.cts.norestart.BROADCAST");
347         getContext().registerReceiver(r, filter);
348         final Intent i = new Intent("com.android.cts.norestart.START");
349         i.addCategory(Intent.CATEGORY_DEFAULT);
350         getContext().startActivity(i);
351         assertTrue(cv.block(2000L));
352         getContext().unregisterReceiver(r);
353     }
354 
testFeatureApi()355     public void testFeatureApi() throws Exception {
356         final Resources r = getContext().getResources();
357         final PackageManager pm = getContext().getPackageManager();
358 
359         // Should have untouched resources from base
360         assertEquals(false, r.getBoolean(R.bool.my_receiver_enabled));
361 
362         // And that we can access resources from feature
363         assertEquals(321, r.getInteger(r.getIdentifier("feature_integer", "integer", PKG)));
364 
365         final Class<?> featR = Class.forName("com.android.cts.splitapp.FeatureR");
366         final int boolId = (int) featR.getDeclaredField("feature_receiver_enabled").get(null);
367         final int intId = (int) featR.getDeclaredField("feature_integer").get(null);
368         final int stringId = (int) featR.getDeclaredField("feature_string").get(null);
369         assertEquals(false, r.getBoolean(boolId));
370         assertEquals(321, r.getInteger(intId));
371         assertEquals("red", r.getString(stringId));
372 
373         // And now both receivers should be disabled
374         Intent intent = new Intent(Intent.ACTION_DATE_CHANGED);
375         intent.setPackage(PKG);
376         List<ResolveInfo> result = pm.queryBroadcastReceivers(intent, 0);
377         assertEquals(0, result.size());
378     }
379 
380     /**
381      * Write app data in a number of locations that expect to remain intact over
382      * long periods of time, such as across app moves.
383      */
testDataWrite()384     public void testDataWrite() throws Exception {
385         final String token = String.valueOf(android.os.Process.myUid());
386         writeString(getContext().getFileStreamPath("my_int"), token);
387 
388         final SQLiteDatabase db = getContext().openOrCreateDatabase("my_db",
389                 Context.MODE_PRIVATE, null);
390         try {
391             db.execSQL("DROP TABLE IF EXISTS my_table");
392             db.execSQL("CREATE TABLE my_table(value INTEGER)");
393             db.execSQL("INSERT INTO my_table VALUES (101), (102), (103)");
394         } finally {
395             db.close();
396         }
397     }
398 
399     /**
400      * Verify that data written by {@link #testDataWrite()} is still intact.
401      */
testDataRead()402     public void testDataRead() throws Exception {
403         final String token = String.valueOf(android.os.Process.myUid());
404         assertEquals(token, readString(getContext().getFileStreamPath("my_int")));
405 
406         final SQLiteDatabase db = getContext().openOrCreateDatabase("my_db",
407                 Context.MODE_PRIVATE, null);
408         try {
409             final Cursor cursor = db.query("my_table", null, null, null, null, null, "value ASC");
410             try {
411                 assertEquals(3, cursor.getCount());
412                 assertTrue(cursor.moveToPosition(0));
413                 assertEquals(101, cursor.getInt(0));
414                 assertTrue(cursor.moveToPosition(1));
415                 assertEquals(102, cursor.getInt(0));
416                 assertTrue(cursor.moveToPosition(2));
417                 assertEquals(103, cursor.getInt(0));
418             } finally {
419                 cursor.close();
420             }
421         } finally {
422             db.close();
423         }
424     }
425 
426     /**
427      * Verify that app is installed on internal storage.
428      */
testDataInternal()429     public void testDataInternal() throws Exception {
430         final StructStat internal = Os.stat(Environment.getDataDirectory().getAbsolutePath());
431         final StructStat actual = Os.stat(getContext().getFilesDir().getAbsolutePath());
432         assertEquals(internal.st_dev, actual.st_dev);
433     }
434 
435     /**
436      * Verify that app is not installed on internal storage.
437      */
testDataNotInternal()438     public void testDataNotInternal() throws Exception {
439         final StructStat internal = Os.stat(Environment.getDataDirectory().getAbsolutePath());
440         final StructStat actual = Os.stat(getContext().getFilesDir().getAbsolutePath());
441         MoreAsserts.assertNotEqual(internal.st_dev, actual.st_dev);
442     }
443 
testPrimaryDataWrite()444     public void testPrimaryDataWrite() throws Exception {
445         final String token = String.valueOf(android.os.Process.myUid());
446         writeString(new File(getContext().getExternalFilesDir(null), "my_ext"), token);
447     }
448 
testPrimaryDataRead()449     public void testPrimaryDataRead() throws Exception {
450         final String token = String.valueOf(android.os.Process.myUid());
451         assertEquals(token, readString(new File(getContext().getExternalFilesDir(null), "my_ext")));
452     }
453 
454     /**
455      * Verify shared storage behavior when on internal storage.
456      */
testPrimaryInternal()457     public void testPrimaryInternal() throws Exception {
458         assertTrue("emulated", Environment.isExternalStorageEmulated());
459         assertFalse("removable", Environment.isExternalStorageRemovable());
460         assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
461     }
462 
463     /**
464      * Verify shared storage behavior when on physical storage.
465      */
testPrimaryPhysical()466     public void testPrimaryPhysical() throws Exception {
467         assertFalse("emulated", Environment.isExternalStorageEmulated());
468         assertTrue("removable", Environment.isExternalStorageRemovable());
469         assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
470     }
471 
472     /**
473      * Verify shared storage behavior when on adopted storage.
474      */
testPrimaryAdopted()475     public void testPrimaryAdopted() throws Exception {
476         assertTrue("emulated", Environment.isExternalStorageEmulated());
477         assertTrue("removable", Environment.isExternalStorageRemovable());
478         assertEquals(Environment.MEDIA_MOUNTED, Environment.getExternalStorageState());
479     }
480 
481     /**
482      * Verify that shared storage is unmounted.
483      */
testPrimaryUnmounted()484     public void testPrimaryUnmounted() throws Exception {
485         MoreAsserts.assertNotEqual(Environment.MEDIA_MOUNTED,
486                 Environment.getExternalStorageState());
487     }
488 
489     /**
490      * Verify that shared storage lives on same volume as app.
491      */
testPrimaryOnSameVolume()492     public void testPrimaryOnSameVolume() throws Exception {
493         final File current = getContext().getFilesDir();
494         final File primary = Environment.getExternalStorageDirectory();
495 
496         // Shared storage may jump through another filesystem for permission
497         // enforcement, so we verify that total/free space are identical.
498         final long totalDelta = Math.abs(current.getTotalSpace() - primary.getTotalSpace());
499         final long freeDelta = Math.abs(current.getFreeSpace() - primary.getFreeSpace());
500         if (totalDelta > MB_IN_BYTES || freeDelta > MB_IN_BYTES) {
501             fail("Expected primary storage to be on same volume as app");
502         }
503     }
504 
testCodeCacheWrite()505     public void testCodeCacheWrite() throws Exception {
506         assertTrue(new File(getContext().getFilesDir(), "normal.raw").createNewFile());
507         assertTrue(new File(getContext().getCodeCacheDir(), "cache.raw").createNewFile());
508     }
509 
testCodeCacheRead()510     public void testCodeCacheRead() throws Exception {
511         assertTrue(new File(getContext().getFilesDir(), "normal.raw").exists());
512         assertFalse(new File(getContext().getCodeCacheDir(), "cache.raw").exists());
513     }
514 
testRevision0_0()515     public void testRevision0_0() throws Exception {
516         final PackageInfo info = getContext().getPackageManager()
517                 .getPackageInfo(getContext().getPackageName(), 0);
518         assertEquals(0, info.baseRevisionCode);
519         assertEquals(1, info.splitRevisionCodes.length);
520         assertEquals(0, info.splitRevisionCodes[0]);
521     }
522 
testRevision12_0()523     public void testRevision12_0() throws Exception {
524         final PackageInfo info = getContext().getPackageManager()
525                 .getPackageInfo(getContext().getPackageName(), 0);
526         assertEquals(12, info.baseRevisionCode);
527         assertEquals(1, info.splitRevisionCodes.length);
528         assertEquals(0, info.splitRevisionCodes[0]);
529     }
530 
testRevision0_12()531     public void testRevision0_12() throws Exception {
532         final PackageInfo info = getContext().getPackageManager()
533                 .getPackageInfo(getContext().getPackageName(), 0);
534         assertEquals(0, info.baseRevisionCode);
535         assertEquals(1, info.splitRevisionCodes.length);
536         assertEquals(12, info.splitRevisionCodes[0]);
537     }
538 
updateDpi(Resources r, int densityDpi)539     private static void updateDpi(Resources r, int densityDpi) {
540         final Configuration c = new Configuration(r.getConfiguration());
541         c.densityDpi = densityDpi;
542         r.updateConfiguration(c, r.getDisplayMetrics());
543     }
544 
updateLocale(Resources r, Locale locale)545     private static void updateLocale(Resources r, Locale locale) {
546         final Configuration c = new Configuration(r.getConfiguration());
547         c.locale = locale;
548         r.updateConfiguration(c, r.getDisplayMetrics());
549     }
550 
getDrawableColor(Drawable d)551     private static int getDrawableColor(Drawable d) {
552         final Bitmap bitmap = Bitmap.createBitmap(d.getIntrinsicWidth(), d.getIntrinsicHeight(),
553                 Bitmap.Config.ARGB_8888);
554         final Canvas canvas = new Canvas(bitmap);
555         d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
556         d.draw(canvas);
557         return bitmap.getPixel(0, 0);
558     }
559 
getXmlTestValue(XmlPullParser in)560     private static String getXmlTestValue(XmlPullParser in) throws XmlPullParserException,
561             IOException {
562         int type;
563         while ((type = in.next()) != END_DOCUMENT) {
564             if (type == START_TAG) {
565                 final String tag = in.getName();
566                 if ("tag".equals(tag)) {
567                     return in.getAttributeValue(null, "value");
568                 }
569             }
570         }
571         return null;
572     }
573 
assertAssetContents(Resources r, String path, String expected)574     private static void assertAssetContents(Resources r, String path, String expected)
575             throws IOException {
576         BufferedReader in = null;
577         try {
578             in = new BufferedReader(new InputStreamReader(r.getAssets().open(path)));
579             assertEquals(expected, in.readLine());
580         } finally {
581             if (in != null) in.close();
582         }
583     }
584 
writeString(File file, String value)585     private static void writeString(File file, String value) throws IOException {
586         final DataOutputStream os = new DataOutputStream(new FileOutputStream(file));
587         try {
588             os.writeUTF(value);
589         } finally {
590             os.close();
591         }
592     }
593 
readString(File file)594     private static String readString(File file) throws IOException {
595         final DataInputStream is = new DataInputStream(new FileInputStream(file));
596         try {
597             return is.readUTF();
598         } finally {
599             is.close();
600         }
601     }
602 }
603