2018年3月30日金曜日

Wallpaper の取得に permission が必要になっていたのでコードの変遷を調べてみた

昔は WallpaperManager の getDrawable() では READ_EXTERNAL_STORAGE permission が必要なかったのですが、targetSdkVersion をあげたところ必要だと怒られてしまったので、コードの変遷を追ってみました。

以下のコードを試します。
  1. class MainActivity : AppCompatActivity() {  
  2.   
  3.     override fun onCreate(savedInstanceState: Bundle?) {  
  4.         super.onCreate(savedInstanceState)  
  5.         setContentView(R.layout.activity_main)  
  6.   
  7.         val d = WallpaperManager.getInstance(this).drawable  
  8.     }  
  9. }  

targetSdkVersion = 27

targetSdkVersion = 27 で実行すると、READ_EXTERNAL_STORAGE permission が無いと怒られました。

Caused by: java.lang.SecurityException: read wallpaper: Neither user 10278 nor current process has android.permission.READ_EXTERNAL_STORAGE.

targetSdkVersion = 26

targetSdkVersion = 26 (compileSdkVersion は 27)で実行すると怒られませんでした。代わりに Warning がログに出ます。

W/WallpaperManager: No permission to access wallpaper, suppressing exception to avoid crashing legacy app.

android-27 での変更

26と27の間でコードの変更がありました。

android-26
  1. public Drawable getDrawable() {  
  2.     Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM);  
  3.     if (bm != null) {  
  4.         Drawable dr = new BitmapDrawable(mContext.getResources(), bm);  
  5.         dr.setDither(false);  
  6.         return dr;  
  7.     }  
  8.     return null;  
  9. }  
  10.   
  11. static class Globals extends IWallpaperManagerCallback.Stub {  
  12.     ...  
  13.   
  14.     public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault,  
  15.             @SetWallpaperFlags int which, int userId) {  
  16.         ...  
  17.         synchronized (this) {  
  18.             if (mCachedWallpaper != null && mCachedWallpaperUserId == userId) {  
  19.                 return mCachedWallpaper;  
  20.             }  
  21.             mCachedWallpaper = null;  
  22.             mCachedWallpaperUserId = 0;  
  23.             try {  
  24.                 mCachedWallpaper = getCurrentWallpaperLocked(userId);  
  25.                 mCachedWallpaperUserId = userId;  
  26.             } catch (OutOfMemoryError e) {  
  27.                 Log.w(TAG, "No memory load current wallpaper", e);  
  28.             }  
  29.             if (mCachedWallpaper != null) {  
  30.                 return mCachedWallpaper;  
  31.             }  
  32.         }  
  33.         ...  
  34.     }  
android-27
  1. public Drawable getDrawable() {  
  2.     Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM);  
  3.     if (bm != null) {  
  4.         Drawable dr = new BitmapDrawable(mContext.getResources(), bm);  
  5.         dr.setDither(false);  
  6.         return dr;  
  7.     }  
  8.     return null;  
  9. }  
  10.   
  11. private static class Globals extends IWallpaperManagerCallback.Stub {  
  12.     ...  
  13.   
  14.     public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault,  
  15.             @SetWallpaperFlags int which, int userId) {  
  16.         ...  
  17.         synchronized (this) {  
  18.             ...  
  19.             try {  
  20.                 mCachedWallpaper = getCurrentWallpaperLocked(context, userId);  
  21.                 mCachedWallpaperUserId = userId;  
  22.             } catch (OutOfMemoryError e) {  
  23.                 Log.w(TAG, "Out of memory loading the current wallpaper: " + e);  
  24.             } catch (SecurityException e) {  
  25.                 if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) {  
  26.                     Log.w(TAG, "No permission to access wallpaper, suppressing"  
  27.                             + " exception to avoid crashing legacy app.");  
  28.                 } else {  
  29.                     // Post-O apps really most sincerely need the permission.  
  30.                     throw e;  
  31.                 }  
  32.             }  
  33.             if (mCachedWallpaper != null) {  
  34.                 return mCachedWallpaper;  
  35.             }  
  36.         }  
  37.         ...  
  38.     }  
android-26 のコードに比べて catch (SecurityException e) { ... } の部分↓が増えていました。 targetSdkVersion が27以降なら SecurityException をそのまま流すように変わったということです。
  1. catch (SecurityException e) {  
  2.     if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) {  
  3.         Log.w(TAG, "No permission to access wallpaper, suppressing"  
  4.                 + " exception to avoid crashing legacy app.");  
  5.     } else {  
  6.         // Post-O apps really most sincerely need the permission.  
  7.         throw e;  
  8.     }  
  9. }  
さらに、getCurrentWallpaperLocked() の引数に context が増えています。

android-26
  1. private Bitmap getCurrentWallpaperLocked(int userId) {  
  2.     ...  
  3.   
  4.     try {  
  5.         Bundle params = new Bundle();  
  6.         ParcelFileDescriptor fd = mService.getWallpaper(this, FLAG_SYSTEM,  
  7.                 params, userId);  
  8.         ...  
  9.     } catch (RemoteException e) {  
  10.         throw e.rethrowFromSystemServer();  
  11.     }  
  12.     return null;  
  13. }  
android-27
  1. private Bitmap getCurrentWallpaperLocked(Context context, int userId) {  
  2.     ...  
  3.   
  4.     try {  
  5.         Bundle params = new Bundle();  
  6.         ParcelFileDescriptor fd = mService.getWallpaper(context.getOpPackageName(),  
  7.                 this, FLAG_SYSTEM, params, userId);  
  8.         ...  
  9.     } catch (RemoteException e) {  
  10.         throw e.rethrowFromSystemServer();  
  11.     }  
  12.     return null;  
  13. }  
IWallpaperManager.getWallpaper() にも新しい引数が増えており、この context を使ってその引数に context.getOpPackageName() を渡しています。


ちなみに 27 (Android 8.1)では WallpaperColors API が追加されています。

結論

Wallpaper の Drawable を取得するには、READ_EXTERNAL_STORAGE permission を Manifest に追加して、Runtime Permission 処理を書く必要があります。


2018年3月27日火曜日

Android で Parameterized テスト(JUnit4, Robolectric)を行う

JUnit4

https://github.com/googlesamples/android-testing/tree/master/runner/AndroidJunitRunnerSample
にサンプルがあります。

具体的には CalculatorAddParameterizedTest.java がテストクラスで、
テスト対象のクラスは Calculator.java です。

上記のサンプルを見ればだいたいわかるのですが、Kotlinで書いた以下のFizzBazzをテストしてみたいと思います。
  1. class FizzBazz {  
  2.   
  3.     fun fizzbazz(value: Int): String {  
  4.         return when {  
  5.             value % 15 == 0 -> "fizzbazz"  
  6.             value % 3 == 0 -> "fizz"  
  7.             value % 5 == 0 -> "bazz"  
  8.             else -> value.toString()  
  9.         }  
  10.     }  
  11. }  

  • 1. テストクラスに @RunWith アノテーションで Parameterized を指定する。
  • 2. Parameterized テストで使う値をコンストラクタの引数として受け取り保持する。
  • 3. @Parameters アノテーションをつけた static メソッドを用意する(つまり @JvmStatic が必要)。このメソッドではコンストラクタ引数に対応した値の配列のIterableを返す。
  • 4. 保持した値を使ってテストする。
  1. /** 
  2.  * @RunWith で Parameterized を指定する 
  3.  * 
  4.  * Parameterized テストで使う値をコンストラクタの引数として受け取る 
  5.  * ここでは fizzbazz() に渡す値と、fizzbazz()の戻り値の期待値の2つを引数として受け取る 
  6.  */  
  7. @RunWith(Parameterized::class)  
  8. class FizzBazzParameterizedTest(  
  9.         private val value: Int,  
  10.         private val expected: String  
  11. ) {  
  12.   
  13.     private lateinit var fizzBazz: FizzBazz  
  14.   
  15.     @Before  
  16.     fun setUp() {  
  17.         fizzBazz = FizzBazz()  
  18.     }  
  19.   
  20.     @Test  
  21.     fun testFizzBazz() {  
  22.         assertThat(fizzBazz.fizzbazz(value)).isEqualTo(expected)  
  23.     }  
  24.   
  25.     companion object {  
  26.   
  27.         /** 
  28.          * @Parameters と @JvmStatic をつける 
  29.          * @return [Iterable] コンストラクタに渡す値のIterableを返す 
  30.          */  
  31.         @Parameters  
  32.         @JvmStatic  
  33.         fun data(): Iterable<Array<Any>> {  
  34.             return listOf(  
  35.                     arrayOf(1"1"),  
  36.                     arrayOf(2"2"),  
  37.                     arrayOf(3"fizz"),  
  38.                     arrayOf(4"4"),  
  39.                     arrayOf(5"bazz"),  
  40.                     arrayOf(15"fizzbazz")  
  41.             )  
  42.         }  
  43.     }  
  44. }  


テスト結果




Robolectric

BlockJUnit4ClassRunnerWithParameters を継承した Runner であれば、@Parameterized.UseParametersRunnerFactory でその Runner を返す ParametersRunnerFactory を指定することで、その Runner で Parameterized テストを実行することができます。

RobolectricTestRunner は SandboxTestRunner を継承しているためこの方法は使えません。 代わりに Robolectric には ParameterizedRobolectricTestRunner が用意されています。


TextUtils を使った次の EmptyChecker をテストしてみます。
  1. class EmptyChecker {  
  2.   
  3.     fun isEmpty(value: String?): Boolean {  
  4.         return TextUtils.isEmpty(value)  
  5.     }  
  6. }  
使い方は Parameterized とほぼ同じです。
違いは @RunWith に ParameterizedRobolectricTestRunner を指定することと、@ParameterizedRobolectricTestRunner.Parameters を使うことです。
  1. /** 
  2.  * @RunWith で ParameterizedRobolectricTestRunner を指定する 
  3.  * 
  4.  * Parameterized テストで使う値をコンストラクタの引数として受け取る 
  5.  * ここでは isEmpty() に渡す値と、isEmpty()の戻り値の期待値の2つを引数として受け取る 
  6.  */  
  7. @RunWith(ParameterizedRobolectricTestRunner::class)  
  8. class EmptyCheckerParameterizedTest(  
  9.         private val value: String?,  
  10.         private val expected: Boolean  
  11. ) {  
  12.   
  13.     private lateinit var emptyChecker: EmptyChecker  
  14.   
  15.     @Before  
  16.     fun setUp() {  
  17.         emptyChecker = EmptyChecker()  
  18.     }  
  19.   
  20.     @Test  
  21.     fun testFizzBazz() {  
  22.         assertThat(emptyChecker.isEmpty(value)).isEqualTo(expected)  
  23.     }  
  24.   
  25.     companion object {  
  26.   
  27.         /** 
  28.          * @ParameterizedRobolectricTestRunner.Parameters と @JvmStatic をつける 
  29.          * @return [Iterable] コンストラクタに渡す値のIterableを返す 
  30.          */  
  31.         @ParameterizedRobolectricTestRunner.Parameters  
  32.         @JvmStatic  
  33.         fun data(): Iterable<Array<Any?>> {  
  34.             return listOf(  
  35.                     arrayOf<Any?>(nulltrue),  
  36.                     arrayOf<Any?>(""true),  
  37.                     arrayOf<Any?>("a"false)  
  38.             )  
  39.         }  
  40.     }  
  41. }  


テスト結果