言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

開発者オプションメニューの値は3rd partyのアプリから書き換えるのが難しい

[2016.10.19 追記しました]

前回の続きです。
Quick Settings にTileを表示するところまで実装したので、次はそのTileをタップした時に何か便利な挙動をするのを目指してみます。

具体的には下記の二つどちらかできたら便利だなぁと思って調べてみました。
どちらもできませんでした:;(∩´﹏`∩);:
[↑できる方法があったので追記してます]

  1. USB debuggingのON/OFF
  2. 「Activityを保持しない」の設定のON/OFF

それではそれぞれなぜダメだったのか、調べた結果を書き残します。

USB debuggingのON/OFF

まず、開発者オプションの画面のコードを追っていきます。 それぞれの項目のlabelのstring resourceとかから適当にOpen Grokを検索すると、 DevelopmentSettings.java にたどり着きます。
で、ここのON/OFFをどうやってとっているかというと、こんな感じで Settings.Global.ADB_ENABLED の値を見ています。

   642     private void updateAllOptions() {
   643         final Context context = getActivity();
   644         final ContentResolver cr = context.getContentResolver();

(省略)


    646         updateSwitchPreference(mEnableAdb, Settings.Global.getInt(cr,
    647                 Settings.Global.ADB_ENABLED, 0) != 0);

長くなるので省略しますが、この Settings.Global.getInt()content://settings/global のcontent providerから値を取得しようとします。

   1428     public static final String AUTHORITY = "settings";


   6524     public static final class Global extends NameValueTable {
   6525         /**
   6526          * The content:// style URL for global secure settings items.  Not public.
   6527          */
   6528         public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/global");

SettingsProvider.java の中で、getの方は問題ないのですが、updateの方はpermission checkが入っています。

    754     private Setting getGlobalSetting(String name) {
    755         if (DEBUG) {
    756             Slog.v(LOG_TAG, "getGlobalSetting(" + name + ")");
    757         }
    758 
    759         // Get the value.
    760         synchronized (mLock) {
    761             return mSettingsRegistry.getSettingLocked(SETTINGS_TYPE_GLOBAL,
    762                     UserHandle.USER_SYSTEM, name);
    763         }
    764     }


    766     private boolean updateGlobalSetting(String name, String value, int requestingUserId,
    767             boolean forceNotify) {
    768         if (DEBUG) {
    769             Slog.v(LOG_TAG, "updateGlobalSetting(" + name + ", " + value + ")");
    770         }
    771         return mutateGlobalSetting(name, value, requestingUserId, MUTATION_OPERATION_UPDATE,
    772                 forceNotify);
    773     }

    792     private boolean mutateGlobalSetting(String name, String value, int requestingUserId,
    793             int operation, boolean forceNotify) {
    794         // Make sure the caller can change the settings - treated as secure.
    795         enforceWritePermission(Manifest.permission.WRITE_SECURE_SETTINGS);

書き込みを行うにはWRITE_SECURE_SETTINGS が必要なのですが、こちらは残念ながら3rd partyのアプリは設定できないようになっています。

developer.android.com

このpermissionがない場合、Security Exceptionで落ちます。。。

   1305     private void enforceWritePermission(String permission) {
   1306         if (getContext().checkCallingOrSelfPermission(permission)
   1307                 != PackageManager.PERMISSION_GRANTED) {
   1308             throw new SecurityException("Permission denial: writing to settings requires:"
   1309                     + permission);
   1310         }
   1311     }

「Activityを保持しない」の設定のON/OFF

USB debuggingと同様にDevelopmentSettings.javaを見ていきましょう。
「Activityを保持しない」のpreference keyは IMMEDIATELY_DESTROY_ACTIVITIES_KEY です。ちょっとアグレッシブな名前です。

読み込みの方はUSB debuggingと同様に Settings.Global に定義してある値を読みに行きます。

   1578     private void updateImmediatelyDestroyActivitiesOptions() {
   1579         updateSwitchPreference(mImmediatelyDestroyActivities, Settings.Global.getInt(
   1580                 getActivity().getContentResolver(), Settings.Global.ALWAYS_FINISH_ACTIVITIES, 0) != 0);
   1581     }

書き込みの方はちょっと挙動が変わります。

   1570     private void writeImmediatelyDestroyActivitiesOptions() {
   1571         try {
   1572             ActivityManagerNative.getDefault().setAlwaysFinish(
   1573                     mImmediatelyDestroyActivities.isChecked());
   1574         } catch (RemoteException ex) {
   1575         }
   1576     }

こんな感じでActivityManagerNativeを操作しています。ActivityManagerNative 自体は@hideなAPIなので残念ながら3rd partyのアプリでは使えません。
ActivityManagerService経由で操作する方法もありますが、こちらを経由するにしてもpermissionが必要です。

   11940     @Override
   11941     public void setAlwaysFinish(boolean enabled) {
   11942         enforceCallingPermission(android.Manifest.permission.SET_ALWAYS_FINISH,
   11943                 "setAlwaysFinish()");
   11944 
   11945         long ident = Binder.clearCallingIdentity();
   11946         try {
   11947             Settings.Global.putInt(
   11948                     mContext.getContentResolver(),
   11949                     Settings.Global.ALWAYS_FINISH_ACTIVITIES, enabled ? 1 : 0);
   11950 
   11951             synchronized (this) {
   11952                 mAlwaysFinishActivities = enabled;
   11953             }
   11954         } finally {
   11955             Binder.restoreCallingIdentity(ident);
   11956         }
   11957     }

SET_ALWAYS_FINISH のpermissionもWRITE_SECURE_SETTINGSと同様に3rd partyのアプリでは設定できません。

developer.android.com

というわけでどちらも3rd partyのアプリでは書き換えられないというお話でした(´・_・`)


[2016.10.19 追記]
GoogleのNick Butcherが同じことをやっていて、彼はadb commandで手動でpermissionをgrantする方法で解決していました。
この方法で解決することは手元でも確認済みです。すげえ!٩( 'ω' )و

github.com

Quick SettingsにCustom Tileを追加する

Android N からQuick Settingsに3rd Partyが好きな物を置けるようになりました。 これを使うと簡単にデバッグツール作れるのでは?と思ってやってみました٩( 'ω' )و

f:id:muumuumuumuu:20161006203754p:plain 右下のやつ

やりたかったこと

  1. Quick SettingsにTileを追加する
  2. Tileをタップした時に何か便利な挙動をする

長くなりそうなので、今回は1のみ扱います。

Quick SettingsにTileを追加する

TileServiceクラスを拡張したサービスを作ったら瞬殺です。 まずはAndroidManifestにサービスを追加してpermissionとintent filterを設定しましょう

        <service android:name=".DebugTileService"
                 android:label="@string/app_name"
                 android:icon="@drawable/ic_star_black_24dp"
                 android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" >
            <intent-filter>
                <action android:name="android.service.quicksettings.action.QS_TILE" />
            </intent-filter>
        </service>

あとはこのサービス内でclick eventのハンドリングをしましょう。

class DebugTileService : TileService() {

    override fun onClick() {
        super.onClick()
        when (qsTile.state) {
            Tile.STATE_ACTIVE -> {
                // do something
                qsTile.state = Tile.STATE_INACTIVE
            }
            Tile.STATE_INACTIVE -> {
                // do something
                qsTile.state = Tile.STATE_ACTIVE
            }
        }
        qsTile.updateTile()
    }

注意しなければならないのは、 Tile#setState() しただけでは表示は切り替わらず、 Tile#updateTile() をコールしてやる必要があります。

Kotlinで書くとproperty accessなのでちょっと意味がわかりにくいかもですが お察しください。

Quick SettingsにTileが追加できるようになる仕組み

せっかくなので上で登録したサービスがどのような仕組みでQuick Settingsに追加できるようになるのかFrameworkのコードを読んでみました。

まず、Quick Settingsのviewが QSCustomizer というLinearLayoutを拡張したクラスを持っています。
(何をQuick Settingsに置くか選べる全画面のview部分です)
このviewの表示時にQSCustomizer#show() がコールされますが、この時 TileQueryHelper クラスがnewされます。
TileQueryHelper のコンストラクタで呼ばれる addSystemTiles() の中で AsyncTask の拡張である QueryTilesTask をHandlerにpostします。そのタスクのbackground処理の中でマニフェストにintent filter(TileService.ACTION_QS_TILE) を登録してあるサービス一覧を撮ってきてtileにaddしているという仕組みでした。

Cross Reference: /frameworks/base/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java

   140     private class QueryTilesTask extends
    141             AsyncTask<Collection<QSTile<?>>, Void, Collection<TileInfo>> {
    142         @Override
    143         protected Collection<TileInfo> doInBackground(Collection<QSTile<?>>... params) {
    144             List<TileInfo> tiles = new ArrayList<>();
    145             PackageManager pm = mContext.getPackageManager();
    146             List<ResolveInfo> services = pm.queryIntentServicesAsUser(
    147                     new Intent(TileService.ACTION_QS_TILE), 0, ActivityManager.getCurrentUser());
    148             for (ResolveInfo info : services) {
    149                 String packageName = info.serviceInfo.packageName;
    150                 ComponentName componentName = new ComponentName(packageName, info.serviceInfo.name);
    151                 final CharSequence appLabel = info.serviceInfo.applicationInfo.loadLabel(pm);
    152                 String spec = CustomTile.toSpec(componentName);
    153                 State state = getState(params[0], spec);
    154                 if (state != null) {
    155                     addTile(spec, appLabel, state, false);
    156                     continue;
    157                 }

queryIntentServicesAsUser()、便利そうですが @hideなAPIですね。残念。
それにしてもAsyncTaskとかMessageとHandlerの組み合わせで非同期処理を行うパターン、AndroidのFrameworkでよく見かけます。
個人的にこのパターン結構好きです。

Android N Easter Egg (neko) に見るアンチパターン

🐱が70匹を超えたあたりから挙動がめちゃめちゃ重くなってきたAndroid N Easter Egg (neko) アプリ。 コードを読んで何が悪そうか調べてみました。

あくまで静的解析の結果なので検証してません。ごめんなさい。「ふーん」くらいに思ってください。ツッコミ大歓迎。

調査対象

下記2パターンのタイミングで重くなります。特に2は困った。

  1. 🐱一覧のダイアログでスクロールするとき
  2. Foodを選択した後、Dialogが消えず端末全体がほぼ操作不能になる

何が悪かったか

1. 🐱一覧のスクロールが重い原因

まず、アプリ全体がどうやって集まっている🐱を保存しているか。 SharedPreferenceに seedname を保存しています。

▼PrefState.java

    // Can also be used for renaming.
    public void addCat(Cat cat) {
        mPrefs.edit()
              .putString(CAT_KEY_PREFIX + String.valueOf(cat.getSeed()), cat.getName())
              .commit();
    }

seed は🐱の各パーツの色をretrieveするために使います。 (デフォルトで🐱の名前はseedになっていた気がしますが、🐱の名前はユーザが変えられるので、名前をvalueとして保持しています。)

🐱一覧のActivityの onCreate() でSharedPreferenceから全ての値をとってきて seed から各パーツの色を計算し、Drawableを描画するという処理を🐱の匹数分メインスレッドでやります。ちなみにパーツは全部で28か所くらいあります。そりゃ重くもなりますね。

2. Foodを置いた直後が重い原因

Foodが選択された後の処理ですが、🐱と同じくSharedPreferenceに書き込みます。

▼PrefState.java

    public void setFoodState(int foodState) {
        mPrefs.edit().putInt(FOOD_STATE, foodState).commit();
    }

これは別にいいんですけど、問題(の原因の一つ)は🐱と同じPreferenceに書いてしまっていること。 このSharedPreferenceを監視しているのは下記の2クラス。

A) NekoTile.java (QuickSettingsにあるFood用のタイル)
B) NekoLand.java (🐱一覧画面のActivity)

Aについて、Foodが選択された後にそのFoodの画像とテキストをタイルに表示するために監視しています。ここは別に問題なし。
問題はB。Bの方は🐱が増えたタイミングを取得するために監視しているようです。同じSharedPreferenceを監視しているせいでFoodの値が変わったタイミングでもここが動いてしまいます。

▼NekoLand.java

    @Override
    public void onPrefsChanged() {
        updateCats();
    }

    private void updateCats() {
        Cat[] cats;
        if (CAT_GEN) {
            cats = new Cat[50];
            for (int i = 0; i < cats.length; i++) {
                cats[i] = Cat.create(this);
            }
        } else {
            cats = mPrefs.getCats().toArray(new Cat[0]);
        }
        mAdapter.setCats(cats);
    }

▼PrefState.java

    public List<Cat> getCats() {
        ArrayList<Cat> cats = new ArrayList<>();
        Map<String, ?> map = mPrefs.getAll();
        for (String key : map.keySet()) {
            if (key.startsWith(CAT_KEY_PREFIX)) {
                long seed = Long.parseLong(key.substring(CAT_KEY_PREFIX.length()));
                Cat cat = new Cat(mContext, seed);
                cat.setName(String.valueOf(map.get(key)));
                cats.add(cat);
            }
        }
        return cats;
    }

ちなみにCatのコンストラクタで各パーツの色の計算とDrawableへのtintが行われています。差分チェック?知らない子ですね…
さらにAdapterのsetCats()notifyDataSetChanged() を呼び出しちゃっています。おそらく全ての🐱に対して再描画が走るんじゃないかな…

さらに悪いのが、SharedPreferenceの監視リスナーをonCreate()でつけています。(当然removeするタイミングは対となるonDestroy()です。)つまり、アプリがバックグラウンドにいる間も上記処理が走ってしまいます。画面に🐱一覧が表示されていないのに、Foodを置いただけで重くなってたのはそのためっぽい。

そもそもFoodの値が変更されたタイミングで🐱の再描画を走らせる必要がないのだから同じSharedPreferenceを利用しお互いそこを監視するのよくないですね。

どうしたらよかったか

ざっと思いついたのでこんな感じ。

  • まずはcatとfoodの保存場所を分離し、それぞれ必要な範囲のみ監視すること。Foodに関してはおそらくこれだけで問題ない。
  • cat側の監視タイミングを見直す。バックグラウンドにいる間に更新が必要なければ監視をonResume() / onPause()の間だけにする
  • 🐱の描画はできるだけ非同期で別スレッドで行う。実際にviewを触らないといけない部分だけUI Threadで。AOSPだと気軽にKotlinで書いたり外部ライブラリ導入したりできないからちょっと非同期処理面倒くさそう。

まぁEaster Egg ですもんね。作った人も100匹くらい集めるような使われ方するとも思ってなかったのかもしれない…

Android N Easter Egg (neko) のコード読んでわかったことまとめ

Android N のEaster Eggはねこあつめ的なアプリ。

コード読んでわかったことまとめます。実装観点とかFrameworkのコードの読み方とかはまたいつか別に書きたい。 以下、Easter eggのアプリ名は便宜上nekoとします。

nekoアプリの基本的な遊び方

  • いつものように設定のAndroidバージョン連打で"N" のロゴが表示される画面に行く。Nアイコン連打でToastで🐱の絵文字が表示されればnekoが使えるモードON, 🚫の絵文字が表示されればnekoが使える
  • nekoが使えるモードの時にNotificationのQuick Settings(Wi-FiとかBluetoothとかの簡易設定があるところ)の編集にneko用の"Empty dish" というアイコン(以下、🐱タイル)が追加されているのでドラッグしてQuick Settingsエリアに移動する
  • "Empty dish" をタップするとお皿におけるFoodが選べます。Foodを置いて一定時間経つとNotificationで🐱が来たことが通知される
  • 本家ねこあつめのように一度集めた🐱はいなくなったりしない
  • Notificationをタップすると集めた🐱の一覧が見られる

Foodについて

  • 選べるFoodの種類はBits(カリカリ) / Fish / Chicken / Treat(お菓子)の4種類
  • ↑の四つのうち左のものほどすぐ🐱が来てくれる
  • 来るまでのインターバルはそれぞれ 15 / 30 / 60 / 120 分
  • ただし左のものほど以前来た🐱がまた来る確率が高い
  • 新規🐱が来るのとインターバルの長さはバーター

🐱について

  • 🐱の形はみんな一緒。コードから規定の確率に従って各部分を着色している
  • 🐱の体の色は黒と白が一番確率が高い
  • 次いで茶色とか灰色とか
  • レアなところだと青、ピンク、紫、緑も居る
  • 上記の色もdarkとlightがある
  • 最強にレアなのは透明(α値が0)
  • あとは首輪とか足の色とか色々決まっている

その他

  • 🐱一覧画面で🐱を長押しするとその🐱を削除 or シェアできる
  • シェアする場合は対象の🐱のbitmapを生成しファイルに吐き出してシェアしてくれる
  • Notificationが表示されていない時に🐱一覧を見たい場合はQuick Settingsの🐱タイルを長押しするといつでも見られる*1

    *1:しばらくこれに気がつかず id:operandoさんのアプリを愛用していました。

hack-it-iron.hatenablog.com

FragmentManager#getFragments()で取得するListの要素がnullになっていたのでFrameworkのソースコードを読んでみた

FragmentManager#getFragments() の挙動がなんか思っていたのと違ったので調べてみたメモ。

具体例を挙げると、Fragment A がattachされている状態で、Fragment B をFragmentTransaction#add() -> remove() した時に、
FragmentManager#getFragments() で得られるリストの要素数は1だと思ったけど、残念2でした〜〜という話。
0番目には予想どおり Fragment A が、1番目にはnullが入っていた。removeしたって言ったじゃないか。解せぬ。

というわけでいつも通りFrameworkのコードを読んでいきます。まずはgetFragments() で何が返ってきているのか確認。

    ArrayList<Fragment> mActive;

    @Override
    public List<Fragment> getFragments() {
        return mActive;
    }

この mActive って名前のArrayListにactiveな状態のfragmentが管理されていそうなもんなのに、なんでactiveでなくなったfragmentを保持していたであろう要素がremoveされずにnullになっているのか?

activeじゃなくなる時のコードを見てみましょう。

    void makeInactive(Fragment f) {
        if (f.mIndex < 0) {
            return;
        }
        
        if (DEBUG) Log.v(TAG, "Freeing fragment index " + f);
        mActive.set(f.mIndex, null);
        if (mAvailIndices == null) {
            mAvailIndices = new ArrayList<Integer>();
        }
        mAvailIndices.add(f.mIndex);
        mHost.inactivateFragment(f.mWho);
        f.initState();
    }

ここでnullがセットされています。
気になるのはその下、 mAvailIndices にinactiveになったfragmentのindexがaddされているところですね。
mAvailIndices はIntergerのArrayListで、

    ArrayList<Integer> mAvailIndices;

使われるのはFragmentがactiveになった時。

    void makeActive(Fragment f) {
        if (f.mIndex >= 0) {
            return;
        }
        
        if (mAvailIndices == null || mAvailIndices.size() <= 0) {
            if (mActive == null) {
                mActive = new ArrayList<Fragment>();
            }
            f.setIndex(mActive.size(), mParent);
            mActive.add(f);
            
        } else {
            f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent);
            mActive.set(f.mIndex, f);
        }
        if (DEBUG) Log.v(TAG, "Allocated fragment index " + f);
    }

mAvailIndices Listの最後の要素をactiveにしたいfragmentのindexにセットしています。

つまり、 removeしたfragmentのindexを mAvailIndices に退避しておいて、 次にaddなりしてactiveになったfragmentにそのindexを割り当てるということをしています。

例えば、 3つのFragmentがActiveな場合、

index Fragment
0 A
1 B
2 C

ここで Fragment B をremoveすると、index 1 がnullになって、mAvailIndices にindex 1 が退避されます。

index Fragment
0 A
1 null
2 C

この状態で Fragment D をaddすると、退避されていたindex 1 が使用される。

index Fragment
0 A
1 D
2 C

indexの効率化が目的だったのかなー。

AsyncTaskのStatusを理解したくてFrameworkのコードを読んだメモ

RX全盛期のいま、こんなことに需要があるのか…ということは気にしない。

AsyncTaskのライフサイクルのstatusがぼんやりとしか理解できずググっても出てこなかったので調べたメモ。 具体的な疑問としては、taskの実行が終わったあと、初期状態(PENDING)に戻るのか?それともFINISH のまま?という点。 APIにもこのように書いてあるんだけどなんとなく釈然としない。taskのlifetimeってどういうことなんだろう。

Each status will be set only once during the lifetime of a task.

それではFrameworkのコードを読んでいきます。 読んだコードは6.0のもの。

初期値

当然ながら初期値は PENDING.
synchronizeよりコストの低いvolatileを使ってるんですね。へぇぇ。

    private volatile Status mStatus = Status.PENDING;

タスクの実行

タスクの実行命令がくるとまずは状態チェック。
PENDING 以外は許容しません。
チェックが終わるとRUNNING 状態に遷移して、onPreExecute() が呼ばれます。

    @MainThread
     public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec,
             Params... params) {
         if (mStatus != Status.PENDING) {
             switch (mStatus) {
                 case RUNNING:
                     throw new IllegalStateException("Cannot execute task:"
                             + " the task is already running.");
                 case FINISHED:
                     throw new IllegalStateException("Cannot execute task:"
                             + " the task has already been executed "
                             + "(a task can be executed only once)");
             }
         }
 
         mStatus = Status.RUNNING;
 
         onPreExecute();
 
         mWorker.mParams = params;
         exec.execute(mFuture);
 
         return this;
     }

タスクの完了・キャンセル

AsyncTaskは内部でhandlerを持っておき、タスクが完了もくしはキャンセルされた時に終了処理を実行するためのMessage(MESSAGE_POST_RESULT)を投げます。

    private static class InternalHandler extends Handler {
        public InternalHandler() {
            super(Looper.getMainLooper());
        }

        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
        @Override
        public void handleMessage(Message msg) {
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }

ここで呼ばれるfinish()の中で状態が FINISHED になります。 このあとはもう状態操作を行わないので、FINISH のまま。

    private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
        mStatus = Status.FINISHED;
    }

ということでわかったのは

  • onPreExecute()の直前にRUNNINGになって、onCancelled()onPostExecute()が完了したらFINISHEDになる
  • statusは循環するわけではなく、一方通行。一度FINISHになったらそこから遷移はしない
  • メンバ変数として持っておいて何度もそのままタスクを使い回すのも無理そう

まぁActivityのonPause()とかでcancelするためにメンバで参照保持しないとなんですけどね。
executeする前に状態チェックして FINISH だったらnewしなおしてやるということで。

最近読んだAsyncTaskのメモリリークの話も一緒にどうぞ。

medium.com

meta-dataで数値だけのStringを渡したい

前回manifestPlaceholders最高!٩( 'ω' )و みたいな記事かいといてアレですが、ハマりどころがあったので記事に残しておきます。 某SDKを使うためにAndroidManifestのmeta-dataにidを記載する必要があり、ここにmanifestPlaceholdersを使っていました。

 <meta-data android:name="hoge" android:value="${fuga}"/>

しかしこれだとなぜかnull扱い… ただし、 .foo を足すとなぜかfugaの中身も .foo も取れます。

<meta-data android:name="hoge" android:value="${fuga}.foo"/>

何が起こっていたかというと、あくまで予想なのですが、 おそらくSDKの中身が ActivityInfoオブジェクトの metaData に対して getString()してるんじゃないかなぁと思います。 だから .foo をつけると文字列になるので問題なくfugaの中身も取れたっぽい。

※予想コード

      ActivityInfo info = getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
      String fuga = info.metaData.getString("hoge");

型が違うものをgetしようとするとこんなエラーが出ます。

Bundle  : Key hoge expected String but value was a java.lang.Integer.  The default value <null> was returned.

自分でかいたコードなら getInt() に変えれば解決するけれどSDKの中身なので値を渡す側でどうにかするしかない。 ちょっとググってみたらバックスラッシュを一つ入れれば何故か解決するらしい。 回答つけた人もこんなこと言っちゃってます。

I have no idea quite how this works, if I'm honest.

stackoverflow.com

そんなわけでこれでうまく動きました٩( 'ω' )و

<meta-data android:name="hoge" android:value="\ ${fuga}"/>