開発者オプションメニューの値は3rd partyのアプリから書き換えるのが難しい
[2016.10.19 追記しました]
前回の続きです。
Quick Settings にTileを表示するところまで実装したので、次はそのTileをタップした時に何か便利な挙動をするのを目指してみます。
具体的には下記の二つどちらかできたら便利だなぁと思って調べてみました。
どちらもできませんでした:;(∩´﹏`∩);:
[↑できる方法があったので追記してます]
- USB debuggingのON/OFF
- 「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のアプリは設定できないようになっています。
この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のアプリでは設定できません。
というわけでどちらも3rd partyのアプリでは書き換えられないというお話でした(´・_・`)
[2016.10.19 追記]
GoogleのNick Butcherが同じことをやっていて、彼はadb commandで手動でpermissionをgrantする方法で解決していました。
この方法で解決することは手元でも確認済みです。すげえ!٩( 'ω' )و
Quick SettingsにCustom Tileを追加する
Android N からQuick Settingsに3rd Partyが好きな物を置けるようになりました。 これを使うと簡単にデバッグツール作れるのでは?と思ってやってみました٩( 'ω' )و
右下のやつ
やりたかったこと
- Quick SettingsにTileを追加する
- 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しているという仕組みでした。
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は困った。
- 🐱一覧のダイアログでスクロールするとき
- Foodを選択した後、Dialogが消えず端末全体がほぼ操作不能になる
何が悪かったか
1. 🐱一覧のスクロールが重い原因
まず、アプリ全体がどうやって集まっている🐱を保存しているか。
SharedPreferenceに seed
と name
を保存しています。
▼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さんのアプリを愛用していました。
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のメモリリークの話も一緒にどうぞ。
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.
そんなわけでこれでうまく動きました٩( 'ω' )و
<meta-data android:name="hoge" android:value="\ ${fuga}"/>