2018年3月30日金曜日

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

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

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

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 public Drawable getDrawable() { Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM); if (bm != null) { Drawable dr = new BitmapDrawable(mContext.getResources(), bm); dr.setDither(false); return dr; } return null; } static class Globals extends IWallpaperManagerCallback.Stub { ... public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault, @SetWallpaperFlags int which, int userId) { ... synchronized (this) { if (mCachedWallpaper != null && mCachedWallpaperUserId == userId) { return mCachedWallpaper; } mCachedWallpaper = null; mCachedWallpaperUserId = 0; try { mCachedWallpaper = getCurrentWallpaperLocked(userId); mCachedWallpaperUserId = userId; } catch (OutOfMemoryError e) { Log.w(TAG, "No memory load current wallpaper", e); } if (mCachedWallpaper != null) { return mCachedWallpaper; } } ... } android-27 public Drawable getDrawable() { Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, true, FLAG_SYSTEM); if (bm != null) { Drawable dr = new BitmapDrawable(mContext.getResources(), bm); dr.setDither(false); return dr; } return null; } private static class Globals extends IWallpaperManagerCallback.Stub { ... public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault, @SetWallpaperFlags int which, int userId) { ... synchronized (this) { ... try { mCachedWallpaper = getCurrentWallpaperLocked(context, userId); mCachedWallpaperUserId = userId; } catch (OutOfMemoryError e) { Log.w(TAG, "Out of memory loading the current wallpaper: " + e); } catch (SecurityException e) { if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) { Log.w(TAG, "No permission to access wallpaper, suppressing" + " exception to avoid crashing legacy app."); } else { // Post-O apps really most sincerely need the permission. throw e; } } if (mCachedWallpaper != null) { return mCachedWallpaper; } } ... } android-26 のコードに比べて catch (SecurityException e) { ... } の部分↓が増えていました。 targetSdkVersion が27以降なら SecurityException をそのまま流すように変わったということです。 } catch (SecurityException e) { if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.O) { Log.w(TAG, "No permission to access wallpaper, suppressing" + " exception to avoid crashing legacy app."); } else { // Post-O apps really most sincerely need the permission. throw e; } } さらに、getCurrentWallpaperLocked() の引数に context が増えています。

android-26 private Bitmap getCurrentWallpaperLocked(int userId) { ... try { Bundle params = new Bundle(); ParcelFileDescriptor fd = mService.getWallpaper(this, FLAG_SYSTEM, params, userId); ... } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return null; } android-27 private Bitmap getCurrentWallpaperLocked(Context context, int userId) { ... try { Bundle params = new Bundle(); ParcelFileDescriptor fd = mService.getWallpaper(context.getOpPackageName(), this, FLAG_SYSTEM, params, userId); ... } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } return null; } 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をテストしてみたいと思います。 class FizzBazz { fun fizzbazz(value: Int): String { return when { value % 15 == 0 -> "fizzbazz" value % 3 == 0 -> "fizz" value % 5 == 0 -> "bazz" else -> value.toString() } } }
  • 1. テストクラスに @RunWith アノテーションで Parameterized を指定する。
  • 2. Parameterized テストで使う値をコンストラクタの引数として受け取り保持する。
  • 3. @Parameters アノテーションをつけた static メソッドを用意する(つまり @JvmStatic が必要)。このメソッドではコンストラクタ引数に対応した値の配列のIterableを返す。
  • 4. 保持した値を使ってテストする。
/** * @RunWith で Parameterized を指定する * * Parameterized テストで使う値をコンストラクタの引数として受け取る * ここでは fizzbazz() に渡す値と、fizzbazz()の戻り値の期待値の2つを引数として受け取る */ @RunWith(Parameterized::class) class FizzBazzParameterizedTest( private val value: Int, private val expected: String ) { private lateinit var fizzBazz: FizzBazz @Before fun setUp() { fizzBazz = FizzBazz() } @Test fun testFizzBazz() { assertThat(fizzBazz.fizzbazz(value)).isEqualTo(expected) } companion object { /** * @Parameters と @JvmStatic をつける * @return [Iterable] コンストラクタに渡す値のIterableを返す */ @Parameters @JvmStatic fun data(): Iterable<Array<Any>> { return listOf( arrayOf(1, "1"), arrayOf(2, "2"), arrayOf(3, "fizz"), arrayOf(4, "4"), arrayOf(5, "bazz"), arrayOf(15, "fizzbazz") ) } } }

テスト結果




Robolectric

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

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


TextUtils を使った次の EmptyChecker をテストしてみます。 class EmptyChecker { fun isEmpty(value: String?): Boolean { return TextUtils.isEmpty(value) } } 使い方は Parameterized とほぼ同じです。
違いは @RunWith に ParameterizedRobolectricTestRunner を指定することと、@ParameterizedRobolectricTestRunner.Parameters を使うことです。 /** * @RunWith で ParameterizedRobolectricTestRunner を指定する * * Parameterized テストで使う値をコンストラクタの引数として受け取る * ここでは isEmpty() に渡す値と、isEmpty()の戻り値の期待値の2つを引数として受け取る */ @RunWith(ParameterizedRobolectricTestRunner::class) class EmptyCheckerParameterizedTest( private val value: String?, private val expected: Boolean ) { private lateinit var emptyChecker: EmptyChecker @Before fun setUp() { emptyChecker = EmptyChecker() } @Test fun testFizzBazz() { assertThat(emptyChecker.isEmpty(value)).isEqualTo(expected) } companion object { /** * @ParameterizedRobolectricTestRunner.Parameters と @JvmStatic をつける * @return [Iterable] コンストラクタに渡す値のIterableを返す */ @ParameterizedRobolectricTestRunner.Parameters @JvmStatic fun data(): Iterable<Array<Any?>> { return listOf( arrayOf<Any?>(null, true), arrayOf<Any?>("", true), arrayOf<Any?>("a", false) ) } } }

テスト結果