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