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