夕飯のカレーとコミットメントの話
先に言っておきますが、ただのポエムです。
エンジニアとして働いていると、「hoge機能を作りたいんだけど、今月中でできますか?」という質問をよくされます。ここでいうhogeがどれくらいしっかり固まっているかで話は変わってきます。
例えば、「今晩カレー食べたい!」というリクエストが来たとします。ここでいうカレーが
1. レトルトカレーを温めてご飯にかけるだけ
2. 市販のカレールーを使ってカレーをつくる
3. スパイスから拘ってインドカレーをつくる
4. おうちカレーじゃなくてカレー屋さんに行きたい
などなど無限に選択肢があります。
カレー食べたいと言い出した人が上のうちどれを想定しているのかによって、当然工数が変わってきます。困ったことに、カレー食べたいと言い出した人がどんなカレーを食べたいかわかっていないことがよくあります。
もしもカレーコンサルタントとして働いていて、それでお金を頂いているのであれば、手厚くヒアリングしましょう。でもそうじゃない場合、どうするのがいいのかなぁとよく考えます。
たとえば、上記4つの選択肢にかかる大体の工数をそれぞれ伝えてあげるのも一つの手段ですが、実際には1のレトルトカレーの選択肢でも、コンビニで買えるのか、Amazon Nowでポチってすぐ手に入るのか、Primeで1日かかるのか、それとも地方まで出かけないと手に入らないご当地カレーなのか様々です。
上のパラグラフのような話を長々としてしまっても、相談した人は困惑を浮かべるでしょう。エンジニアって面倒くさいな…と心の声が聞こえてきます。相談してきた人はできるかできないかの2択の答えを求めているからです。
かと言って適当に「ハイハイできます!」というのも違うなぁと思います。コミットメントした以上は約束を守る責任があるからです。(ここでいうコミットメントは、意識の高い人がよく使う「フルコミット」的な使い方ではなくて、「完遂すると約束すること」として使っています*1
付き合いが長い相手だと、「カレー食べたい!」って言われて、普通にお家カレーなんだろうなと予想がつくので「いいですね。今日のお夕飯はカレーにしますね」と言えます(=今晩の夕飯までにカレーを作るとコミットメントすることができる)が、ソフトウェア開発は斜め上のカレーをリクエストされる可能性が割と高い気がしています。
最近は一番可能性が高そうなケースを狙って、そこを一つの基準としておくのがいいのかなぁという気がしています。「どんなカレーを食べたいかによりますが、一旦週末目標で進めましょう。どんなカレーか具体的に決まったらもう一度話し合いましょうね。」って感じで。
何も決まってない以上、具体的なスケジュールを立てるのはもはや困難なので、相手の人を納得というか安心させてあげるだけでいいのかなという感じです。相談してきた人との信頼貯金を貯めていくというか、すぐに相談に来てもらえる関係性を作る点で有効かなと思っています。
「カレー」という文字を打ちすぎてカレー食べたくなってきたのでやめます。
本日のポエムは以上です。
*1:私がアンクルボブ信者なので。Clean Coderを読むとコミットメントの話題が出てきますね。
SnackbarでCustom Content を表示する
Support library revision25.1.0からSnackBarにCustom Contentを表示できるようになったらしいので試してみました٩( 'ω' )و
今回の変更概要
さて、release noteをよく読んでみましょう。
Snackbar has been refactored to allow apps to display custom content. BaseTransientBottomBar is the new base class that exposes the general sliding and animations behavior.
custom contentを表示するためにリファクタしたよって言ってますね。BaseTransientBottomBarが新しい基底クラスになるよって言ってます。誰よそれって感じなのでリファレンスやコードを読んでみましょう。
BaseTransientBottomBarって何よ
まずはSnackbarのクラス定義。確かにBaseTransientBottomBarをextendsしています。
public final class Snackbar extends BaseTransientBottomBar<Snackbar> {
このBaseTransientBottomBarが何をしてくれるクラスなのか、リファレンスを読んでみましょう。 主にやってくれそうなのは、この二つ。
- 画面下部から現れるViewの表示制御
- 表示・非表示のタイミングを取得するためのCallbackの提供
もともとSnackbarがになっていた機能の一部ですね。
Snackbarがどう変わったか
これらを踏まえて今度はSnackbarのコードを見てみましょう*1。Viewの表示制御をBaseTransientBottomBarへ移したからか、300行ほどの随分とスッキリしたクラスになりました。(以前は確か850行ほどありました。)
中身を読むと分かりますが、このクラスでやっていることは二つ。
- Snackbarのコンテンツ制御
- 表示・非表示のタイミングを取得するためのCallbackの提供
この通り、Viewの表示制御やアニメーションなどはやっていません。(Snackbar#make()の第3引数でdurationをとるので、ある意味ここだけアニメーションと言えなくはない。)
このようにコンテンツ制御というかSnackbar独自のviewの制御に特化したクラスになっています。
さらに言えばSnackbarのカスタムレイアウトはSnackbarContentLayout
としてhideな別クラスに切り出されています。
なのでSnackbar#setText()の中身も結構無理矢理な感じ。
/** * Update the text in this {@link Snackbar}. * * @param message The new text for this {@link BaseTransientBottomBar}. */ @NonNull public Snackbar setText(@NonNull CharSequence message) { final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0); final TextView tv = contentLayout.getMessageView(); tv.setText(message); return this; }
このように、VIewの表示制御とコンテンツ制御を切り離して実装することができるようになりました。 ということは、独自デザインのViewをSnackbarの用に表示・非表示するのが簡単にできるようになったってことです。
試してみた
今回は独自デザインの簡単なサンプルとして背景色が黒じゃないCustomSnackbarを作っていきます٩( 'ω' )و
まずはsupport libraryのrevisionを25.0.1にあげます。対象のlibraryをinstallしてbuild.gradle
をアップデートします。
compile 'com.android.support:design:25.1.0'
それではCustomSnackbarクラスを作っていきます。 BaseTransientBottomBarをextendsしてやって、
public class CustomSnackBar extends BaseTransientBottomBar<CustomSnackBar> {
Snackbarを参考に make()
メソッドを実装します。findSuitableParent()
の中身はSnackbarのそれのコピペです。
ちなみに、BaseTransientBottomBarのコンストラクタはprotectedなので、自前でコンストラクタを作ってやらないと"There is no default constructor available"って怒られます。
@NonNull public static CustomSnackBar make(@NonNull View view, @NonNull CharSequence text, int duration) { LayoutInflater inflater = LayoutInflater.from(view.getContext()); View content = inflater.inflate(R.layout.custom_snackbar, findSuitableParent(view), false); ((TextView) content.findViewById(R.id.custom_snackbar_textview)).setText(text); CustomSnackBar customSnackBar = new CustomSnackBar(findSuitableParent(view), content, new ViewCallBack()); return customSnackBar; } private CustomSnackBar(ViewGroup parent, View content, ContentViewCallback contentViewCallback) { super(parent, content, contentViewCallback); }
ここでinflateするレイアウトですが、下記のような適当な背景色をつけたTextViewを持つLinearLayoutを読み込みます。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_margin="10dp"> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:id="@+id/custom_snackbar_textview" android:background="@color/colorAccent" android:textColor="@android:color/white"/> </LinearLayout>
show()メソッドはBaseTransientBottomBarクラスの方ですでに実装されているので、たったこれだけで使うときは普通のSnackbarの様に使えます。
CustomSnackBar.make(view, "This is custom snackbar sample", Snackbar.LENGTH_LONG)
.show();
こんな感じで表示されます٩( 'ω' )و
Android/Kotlin関連の情報収集とか。
最近Android周りの情報をどうやって追っているか?について質問されることが何度かあったのでまとめました。自分向けに2016年末時点でのスナップショットも兼ねて。
知り合いのエンジニアさんや勉強会であった方とTwitterで繋がる機会が多いせいか、Android関連の情報が流れてくるのが多いのは自分にとって圧倒的にTwitterです。みなさんいつも有用な情報をつぶやいてくれたりニュースをretweetしてくれたり本当に助かっています。ありがとうございます。
また、Google公式アカウントも幾つかフォローしています。(@AndroidDevとか。)
Googleの有名なエンジニアの方とかSquareのJakeさんとかフォローしてる方も多いと思いますが、私は海外のエンジニアさんはフォローしていません。そこまで追い始めるとキリがなさそうなので。あと話題になるtweetは誰かがretweetしてくれるからどのみち目にとまるからいいかなと思ったりします。
blogの中の人や更新情報用アカウントをフォローしておりRSS代わりのような用途にも使っていたりします。
メルマガ
Android WeeklyとKotlin Weeklyをsubscribeしています。毎週月曜に一週間のトピックをまとめてくれるので便利です。英語のblogやVideoの情報はだいたいここから入ってきています。
Podcast
Podcastはdex.fmを聞いています。Androidの技術的な話だけでなくチーム開発とかサービスグロースの観点の話題も出てきて非常に参考になります。
AndroidのPodcastと言えばFragmentedなども有名かと思いますが、こちらは全然追えていません。
Advent Calendar
今の時期だとAdvent Calendarも楽しく読んでいます。今年はAndroidのAdvent Calendarが3つもできてていいですね!
Android Advent Calendar 2016, Androidその2 Advent Calendar 2016, Android その3 Advent Calendar 2016
社内Slack
社内はTwitterではなくFacebook文化なので社内のメンバーとTwitterで全く繋がっていません。そのためTwitterで情報を共有することはないのですが、代わりに社内Slackに専用channelを作ってそこで情報を流しています。
うちは分報文化*1が根付いているのですが、そのノリで#times_androidや#times_kotlinを作って興味がある人たちがそこを覗いて情報を流したりそこにコメントして議論に発展したりします。AndroidとKotlinを分けている理由は、サーバサイドの一部でKotlinを採用していおり、Androidを書かないエンジニアもKotlinの情報を必要としているからです。
そういえばpublicなAndroidのslack channelに怖くてjoinできていません。あそこって誰でも勝手に入っていいのかわからない…
というわけで2016年はこんな感じでした。来年の年末に振り返ってみると何か変わってるかなぁ。
Activity#getTitle()はActivity/Applicationのresourcesを使わない
このエントリは先日参加した「まったりAndroid Framework Code Reading #4」の成果です٩( 'ω’ )و
知りたかったこと
AppCompatActivity#setSupportActionBar()した時に、Frameworkのバグと思しき挙動を見つけてしまいました。
setするToolBarにtitleを設定していない場合、ActionBarのタイトルにアプリで設定したlocaleではなくdevice defaultのlocaleが適用されてしまうのです:;(∩´﹏`∩);:
回避策としてToolBarにapplicationのcontextからとってきたstringをsetすればいいんですけど、せっかくなので Frameworkがタイトルに表示するstringをどのように取得するのか知りたくてFrameworkのコードを読んでみました٩( 'ω’ )و
わかったこと
- setSupportActionBar()でsetするToolbarにtitleが設定されていればそれを、設定されていなかったらAppCompatDelegateImplV7のContextからgetTitle()したものが使われる
- AppCompatDelegateImplV7のContextはAppCompatActivity自身
- しかしgetTitle()の時に使われるresourcesはAppCompatActivity自身のものではなく、ActivityThreadがActivityをattachする時に使うresources
- 上記resourcesはApplicationPackageManager#getResourcesForApplication() で取得したもので、これがdevice defaultのlocaleになっている
- device defaultなlocaleになる理由は、引数のoverride configurationにapplicationのresourcesに設定されたconfigurationではなくnullを設定しているから。
1844 Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, 1845 String[] libDirs, int displayId, LoadedApk pkgInfo) { 1846 return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs, // ★最後から3番目の引数がoverride configurationだが、ここでnullを指定しているのでdevice defaultの物が設定される 1847 displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader()); 1848 }
ざっくりまとめると以上ですが、コードを読んだメモを置いておきます。
ToolBarについて
AppCompatActivity.java
public void setSupportActionBar(@Nullable Toolbar toolbar) { getDelegate().setSupportActionBar(toolbar); }
AppCompatDelegateImplV7.java
189 @Override 190 public void setSupportActionBar(Toolbar toolbar) { 212 if (toolbar != null) { 213 final ToolbarActionBar tbab = new ToolbarActionBar(toolbar, 214 ((Activity) mContext).getTitle(), mAppCompatWindowCallback); 215 mActionBar = tbab;
ToolbarActionBar.java
73 public ToolbarActionBar(Toolbar toolbar, CharSequence title, Window.Callback callback) { 74 mDecorToolbar = new ToolbarWidgetWrapper(toolbar, false); 75 mWindowCallback = new ToolbarCallbackWrapper(callback); 76 mDecorToolbar.setWindowCallback(mWindowCallback); 77 toolbar.setOnMenuItemClickListener(mMenuClicker); 78 mDecorToolbar.setWindowTitle(title); 79 }
ToolbarWidgetWrapper.java
90 public ToolbarWidgetWrapper(Toolbar toolbar, boolean style) { 91 this(toolbar, style, R.string.abc_action_bar_up_description, 92 R.drawable.abc_ic_ab_back_material); 93 }
95 public ToolbarWidgetWrapper(Toolbar toolbar, boolean style, 96 int defaultNavigationContentDescription, int defaultNavigationIcon) { 97 mToolbar = toolbar; 98 mTitle = toolbar.getTitle(); 99 mSubtitle = toolbar.getSubtitle(); 100 mTitleSet = mTitle != null; 105 if (style) { 106 final CharSequence title = a.getText(R.styleable.ActionBar_title); 107 if (!TextUtils.isEmpty(title)) { 108 setTitle(title); 109 }
237 @Override 238 public void setWindowTitle(CharSequence title) { 239 // "Real" title always trumps window title. 240 if (!mTitleSet) { 241 setTitleInt(title); 242 } 243 }
AppCompatDelegateImplBase.java
67 AppCompatDelegateImplBase(Context context, Window window, AppCompatCallback callback) { 68 mContext = context;
AppCompatDelegate.java
185 private static AppCompatDelegate create(Context context, Window window, 186 AppCompatCallback callback) { 187 final int sdk = Build.VERSION.SDK_INT; 188 if (BuildCompat.isAtLeastN()) { 189 return new AppCompatDelegateImplN(context, window, callback); 190 } else if (sdk >= 23) { 191 return new AppCompatDelegateImplV23(context, window, callback); 192 } else if (sdk >= 14) { 193 return new AppCompatDelegateImplV14(context, window, callback); 194 } else if (sdk >= 11) { 195 return new AppCompatDelegateImplV11(context, window, callback); 196 } else { 197 return new AppCompatDelegateImplV7(context, window, callback); 198 } 199 }
AppCompatActivity.java
509 public AppCompatDelegate getDelegate() { 510 if (mDelegate == null) { 511 mDelegate = AppCompatDelegate.create(this, this); 512 } 513 return mDelegate; 514 }
getTitle()で取得するStringについて
Activity.java
5635 public final CharSequence getTitle() { 5636 return mTitle; 5637 }
6593 final void attach(Context context, ActivityThread aThread, 6594 Instrumentation instr, IBinder token, int ident, 6595 Application application, Intent intent, ActivityInfo info, 6596 CharSequence title, Activity parent, String id, 6597 NonConfigurationInstances lastNonConfigurationInstances, 6598 Configuration config, String referrer, IVoiceInteractor voiceInteractor, 6599 Window window) { 6626 mTitle = title;
ActivityThread.java
2514 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { 2567 CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager()); 2580 activity.attach(appContext, this, getInstrumentation(), r.token, 2581 r.ident, app, r.intent, r.activityInfo, title, r.parent, 2582 r.embeddedID, r.lastNonConfigurationInstances, config, 2583 r.referrer, r.voiceInteractor, window);
ComponentInfo.java
92 @Override public CharSequence loadLabel(PackageManager pm) { 93 if (nonLocalizedLabel != null) { 94 return nonLocalizedLabel; 95 } 96 ApplicationInfo ai = applicationInfo; 97 CharSequence label; 98 if (labelRes != 0) { 99 label = pm.getText(packageName, labelRes, ai); 100 if (label != null) { 101 return label; 102 } 103 } 104 if (ai.nonLocalizedLabel != null) { 105 return ai.nonLocalizedLabel; 106 } 107 if (ai.labelRes != 0) { 108 label = pm.getText(packageName, ai.labelRes, ai); 109 if (label != null) { 110 return label; 111 } 112 } 113 return name; 114 }
ApplicationPackageManager.java
1491 @Override 1492 public CharSequence getText(String packageName, @StringRes int resid, 1493 ApplicationInfo appInfo) { 1494 ResourceName name = new ResourceName(packageName, resid); 1495 CharSequence text = getCachedString(name); 1496 if (text != null) { 1497 return text; 1498 } 1499 if (appInfo == null) { 1500 try { 1501 appInfo = getApplicationInfo(packageName, sDefaultFlags); 1502 } catch (NameNotFoundException e) { 1503 return null; 1504 } 1505 } 1506 try { 1507 Resources r = getResourcesForApplication(appInfo); 1508 text = r.getText(resid); 1509 putCachedString(name, text); 1510 return text;
1241 @Override 1242 public Resources getResourcesForApplication(@NonNull ApplicationInfo app) 1243 throws NameNotFoundException { 1244 if (app.packageName.equals("system")) { 1245 return mContext.mMainThread.getSystemContext().getResources(); 1246 } 1247 final boolean sameUid = (app.uid == Process.myUid()); 1248 try { 1249 return mContext.mMainThread.getTopLevelResources( 1250 sameUid ? app.sourceDir : app.publicSourceDir, 1251 sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs, 1252 app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY, 1253 mContext.mPackageInfo); 1254 } catch (Resources.NotFoundException cause) { 1255 final NameNotFoundException ex = 1256 new NameNotFoundException("Unable to open " + app.publicSourceDir); 1257 ex.initCause(cause); 1258 throw ex; 1259 } 1260 }
ActivityThread.java
1844 Resources getTopLevelResources(String resDir, String[] splitResDirs, String[] overlayDirs, 1845 String[] libDirs, int displayId, LoadedApk pkgInfo) { 1846 return mResourcesManager.getResources(null, resDir, splitResDirs, overlayDirs, libDirs, 1847 displayId, null, pkgInfo.getCompatibilityInfo(), pkgInfo.getClassLoader()); 1848 }
ResourcesManager.java
625 public @NonNull Resources getResources(@Nullable IBinder activityToken, 626 @Nullable String resDir, 627 @Nullable String[] splitResDirs, 628 @Nullable String[] overlayDirs, 629 @Nullable String[] libDirs, 630 int displayId, 631 @Nullable Configuration overrideConfig, 632 @NonNull CompatibilityInfo compatInfo, 633 @Nullable ClassLoader classLoader) { 634 try { 635 Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources"); 636 final ResourcesKey key = new ResourcesKey( 637 resDir, 638 splitResDirs, 639 overlayDirs, 640 libDirs, 641 displayId, 642 overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy 643 compatInfo); 644 classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader(); 645 return getOrCreateResources(activityToken, key, classLoader); 646 } finally { 647 Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); 648 } 649 }
517 private @NonNull Resources getOrCreateResources(@Nullable IBinder activityToken, 518 @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) { 519 synchronized (this) { 553 } else { 554 // Clean up any dead references so they don't pile up. 555 ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate); 556 557 // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl 558 ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key); 559 if (resourcesImpl != null) { 560 if (DEBUG) { 561 Slog.d(TAG, "- using existing impl=" + resourcesImpl); 562 } 563 return getOrCreateResourcesLocked(classLoader, resourcesImpl); 564 } 565 566 // We will create the ResourcesImpl object outside of holding this lock. 567 } 568 } 569 570 // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now. 571 ResourcesImpl resourcesImpl = createResourcesImpl(key);
開発者オプションメニューの値は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匹くらい集めるような使われ方するとも思ってなかったのかもしれない…