Ubuntu 14.04のApache2のconfigを見て「アレ?」となった話

私は、仕事で(プライベートでも?)よくGitlistっていう便利なgitビューワを使っています。
正直、自分一人だったら要らないんですが、共同作業するなら、必ず一人はいますよね。「わたしgit使えないんで(キリッ」みたいな訳の分からないことをいう人が。

まぁ事情はともかく、Gitllistは、Gitビューワとして非常にシンプルで最低限の機能はあるものなので、いろいろなサーバに建てるために

git clone https://github.com/klaussilveira/gitlist.git
cd gitlist/
curl -s http://getcomposer.org/installer | php
php composer.phar install
chmod 777 cache/
sudo a2enmod rewrite
sudo /etc/init.d/apache2 restart
くらいは、いつでもコピペできるようにしてあったりします。

環境によっては
sudo a2enmod userdir
sudo /etc/init.d/apache2 restart
も必要ですね。まぁそれくらいは誤差の範囲です。

ただ、最近になって、Ubuntu 14.04のサーバを手にしたのですが、「アレッ?」ってなりました。

最終的には
http://aoba.web-hack.org/gitlist/
にあるように、ちゃんと動いたのですが、ユーザディレクトリでRewriteがなかなか動かなくてちょっと戸惑いました。


Directory /home/*/public_html/ > AllowOverride Allとか書く場所は…

正直、これだけで躓きました。他はフィーリングで行けます。

今まで(Apache 2.2)だと、/etc/apache2/sites-available/defaultあたりに、それっぽい記載があったので、真似して
<Directory /home/*/public_html>
AllowOverride All
</Directory>

とかてきとーに書いていました。(セキュリティの云々はともかくとして…)

しかし、Apache 2.4になって、/etc/apache2/sites-available/defaultは見当たりません。
一体どこに行ったのか・・・。

じつは/etc/apache2/apache2.confにそれっぽい記載があります。
でも、本家confに書いちゃうのって微妙ですよね…。

というわけで、いろいろ試してみたところ、

/etc/apache2/conf-available/rewritable-userdir.conf
<Directory /home/*/public_html>
AllowOverride All
</Directory>
こんなかんじで1ファイル作って、
sudo a2enconf rewritable-userdir
sudo /etc/init.d/apache2 restart
ってやれば、本家のconfigをオーバーライドする感じで、設定が出来ました。(confにするかsiteにするかは悩ましいところですが…)
Apache2で「あれ?AllowOverrideの設定箇所どこ行った??ユーザディレクトリでRewrite効かないぞ?!」ってなった方は、ぜひお試しあれ。



(参考)
以下、完全に個人的な趣向の問題ですが、Gitlistをちょっとだけ手直しして使ってます。

nano src/GitList/Config.php
    public static function fromFile($file)
{
if (!file_exists($file)) {
die(sprintf('Please, create the %1$s file.', $file));
}

$data = parse_ini_file($file, true);
+ $repo = parse_ini_file($file.".repos.inc");
+ $data["git"]["repositories"] = $repo["repositories"];
$config = new static($data);
$config->validateOptions();

return $config;
}


これで、
config.ini
[git]
client = '/usr/bin/git' ; Your git executable path
default_branch = 'master' ; Default branch when HEAD is detached
;repositories[] = '/home/git/repositories/' ; Path to your repositories ★←コメントアウト
; If you wish to add more repositories, just add a new line

; WINDOWS USERS
;client = '"C:\Program Files (x86)\Git\bin\git.exe"' ; Your git executable path
;repositories[] = 'C:\Path\to\Repos\' ; Path to your repositories


config.ini.repos.inc
;QAEP
repositories[] = /home/yi01/mirror/LA.BF64.1.1-00110-8x94.0/

;AOSP
;repositories[] = /home/yi01/mirror/android-5.0.0_r2/

こうすることで、純粋なconfigと、リポジトリのメンテを別ファイルで行うことができるようになります。

Samsung SBrowserの小さな小さな工夫


ほんとうは、仕組みが解明できてから公開したかったのですが、もったいぶるメリットもないので、しょうもない内容なの承知でわかっている範囲だけ公開。

といっても、わかってるのは・・・

同じマルチタッチイベントを与えているにもかかわらず、SBrowserは中心軸がずれずに拡大縮小がされる!

以上。・・・分かり次第ちょびちょび付け足しで書いていきます…。

どんなタッチイベントを送ったか

マルチタッチと、その中心軸の遷移

点の座標をログに出してMatplotlibで可視化すると、こんなかんじで中心軸がぶれています。

そもそもAndroidってどうやってマルチタッチを解釈しているのか(書いてる途中・・・)

ScaleGestureDetectorというコンポーネントAndroidフレームワークに有ります。
マルチタッチズームの基本をちらっと図解します。

ScaleGestureDetectorのキモは3つです。
 ・[ScaleStart] 親指と人差指の間の距離をSpanといい、その距離が初期距離から16dp以上ずれるとズームを開始
 ・[ScaleBy] ズームの中心は、親指と人差指の中点(リアルタイムに更新)。倍率は、16ms前のSpanと現在のSpanの比率(リアルタイムに更新)
 ・[ScaleEnd] 指が2点じゃなくなるとズーム終了

中心の座標は、開始時のもの固定ではなくてリアルタイムにタッチの点の中心座標に更新するのがAndroid標準です。なので、冒頭で書いたとおりChromeはバカ正直にタッチの中心座標を更新するため、マルチタッチの中心軸がブレブレになって見えたりするのです。

SBrowserはこのようなタッチ中心の座標ブレブレ問題を解決しているようです。

SBrowserの工夫とは・・・(まだわからず)

オープンソースでその実装箇所を探ってる最中です。分かり次第続報をお届けします。



と、長く時間が空きましたが、結局実装箇所はわかりませんでした。
Zoomのジェスチャを判別する部分は、他の機種との差分は(ゼロではないですが)多分ない。 で、気になってSBrowser.apkを引っこ抜いて、apktool d SBrowser.apk とかなんとか探ってみようかなと思いましたが、答えっぽいものにはたどり着けませんでした。

org/chromium/content/browser/ZoomManager.smaliとかを見る限りだとAndroid標準のScaleGestureDetector使ってるし、 多分、MultiTabPinchUtilのperformPinchZoomあたりでなにかゴニョっとやってるんだろうなぁと思いつつ…
若干悔しいながら、今回はこれまで。。

Android標準のScaleGestureDetectorの動作とカスタマイズポイントが見えたくらいでよしとしておきましょう。完全に自己満足ですが。

"Androidのブラウザをカスタマイズする"ということ

めっちゃ会社でやってるネタなので企業秘密に触れるギリギリラインではありますが、

世の中、ブラウザはオープンソースの時代です( ー`дー´)キリッ ので、タイーホとならない程度に、書きたいことを書いてみようと思います。

そもそもなぜブラウザをカスタマイズするのか?

へたなカスタマイズはフラグメンテーション(改造によって互換性のないめちゃくちゃなものに・・・)のもとです。なので、やるべきではありません。
それなのに、なぜカスタマイズをするのか。

そこにはユーザの小さな小さな不満があるからです。

 ・タップしてからページが表示されるまでの時間が遅い。早くしたい。
 ・タッチスクロールの指に吸い付く度合いがヘボい。もっと指に気持ちよくついてきてほしい。
 ・電池めっちゃくう。ずっとブラウザ使ってても電池くわないようにしてほしい。

ほんとうに地味なことですが、こういうちょっとしたことがユーザの感覚に響くのです。

なくても困らないのだけども、あったほうが俄然いい

これがブラウザカスタマイズのモットーです。


で、ごちゃごちゃ論じるのはこのブログの趣旨には合いませんね。
オープンソースのブラウザコードを少しだけ読み解いてみましょう。


カスタマイズの説明の前に。

「ブラウザって、どうせタッチスクロールしたぶんだけ画面を動かしてるんでしょ?」というイメージをお持ちの方に、
ちょっとだけ認識を改めてほしいので、ブラウザのタッチスクロールの処理の仕組みを簡単に説明します。
まぁたしかにタッチスクロールした分だけ動かしてることには間違いないんですけど、登場人物が2人いて、それをここで明確にしておきたいのです。

登場人物1:タッチをひたすら監視する小人さん
Androidには、タッチイベントをリアルタイムに監視して、
「いまスクロールされたぞ!」「いまタップされたぞ!」って
都度言ってきてくれる小人さんがいます。


登場人物2:WebKit

WebKitの一番の役割は、
HTMLというフォーマットで書かれた文字列から
要求に応じてビットマップ画像を作り出すことです。




AndroidのWebViewは
前述のタッチイベント監視人さんとWebKitをうまく組み合わせて、
ユーザがタッチスクロールしたら画面を書きなおして
あたかも画面がスクロールしたかのように見せています。


ブラウザの「タッチスクロール」をすこしだけ改善してみよう

用意するもの:
 ・Android4.4.2か4.4.4の端末(Nexus 5)
 ・ページ内検索が速いPCブラウザ(IEはダメ、Google Chrome推奨)
 ・Androidのビルド環境


現在のAndroidのブラウザの多くに使われている"WebView"という部品は、じつは厳密にはスクロールした分だけ画面が移動してくれていません。
実際にスクロールされるのは、タッチスクロール量から数ミリメートルだけ引かれたぶんが、スクロールされます。

え?うそでしょ?とおもったあなた。実際にAndroid端末で以下のように動作を見てみてください。


"指に吸い付かないスクロール"を体感しよう


このブログをAndroidで見た時のスクリーンショットで説明してますが、YahooでもGoogleでもどんなページでも同じです。

どんなに頑張ってスクロールしても最終的に指の場所についてきてくれませんね?
今回はこれを、ちゃんと指についてきてくれるよう改善してみたいと思います。



 ・指に吸い付かない理由

正解から言ってしまうと、タッチイベントを監視している小人さんが"躊躇している"タイミングがあるからです。

専門用語で言うと、touch slopとよばれるもので、
「タップ」なのか「タッチスクロール」なのかを見分けるための、しきい値です。


どこまでがタップでどこからがスクロールか


ソースコードを見る前に、絵で説明します。

①は誰が見てもタップですね。これをスクロールだって言うと、手が震えがちな人はタップできませんね。
そして、③は誰が見てもスクロールですね。これをタップだって言われると、スクロールさせるにはどんだけ手を動かさんとダメなの?と思ってしまいますね。
じゃあ、②はどうでしょう?これは微妙ですね。

ただ、タッチ監視をしている小人さんは「微妙」という回答はできません。ユーザが画面に触れて何かアクションをした以上は、かならず「タップ」か「スクロール」かを決めてあげなければなりません。

そこでAndroidは1つバシッと指標を決めていて、
 ・8dp以上動いていない場合はタップ
 ・8dp以上動いている場合はタッチスクロール
としています。



ここでピンときたひとがいるかもしれません。

タッチスクロールしても8dp動いてない場合はタップと認識される。
タッチスクロールと認識され始めるのは、8dp目のスクロール部分から。

そう、まさにこの8dp捨てられているのがAndroidのWebViewの動作なのです。

さて、ソースを見てましょう。
http://tools.oesf.biz/android-4.4w_r1.0/xref/external/chromium_org/content/public/android/java/src/org/chromium/content/browser/third_party/GestureDetector.java

これがタッチを監視してる小人です。

    461     /**
462 * Analyzes the given motion event and if applicable triggers the
463 * appropriate callbacks on the {@link OnGestureListener} supplied.
464 *
465 * @param ev The current motion event.
466 * @return true if the {@link OnGestureListener} consumed the event,
467 * else false.
468 */
469 public boolean onTouchEvent(MotionEvent ev) {

535 case MotionEvent.ACTION_DOWN:

553 mDownFocusX = mLastFocusX = focusX;
554 mDownFocusY = mLastFocusY = focusY;


558 mCurrentDownEvent = MotionEvent.obtain(ev);


571 handled |= mListener.onDown(ev);
572 break;


574 case MotionEvent.ACTION_MOVE:
578 final float scrollX = mLastFocusX - focusX;
579 final float scrollY = mLastFocusY - focusY;


583 } else if (mAlwaysInTapRegion) {
584 final int deltaX = (int) (focusX - mDownFocusX);
585 final int deltaY = (int) (focusY - mDownFocusY);
586 int distance = (deltaX * deltaX) + (deltaY * deltaY);
587 if (distance > mTouchSlopSquare) {
588 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
589 mLastFocusX = focusX;
590 mLastFocusY = focusY;
591 mAlwaysInTapRegion = false;


595 }


599 } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) {
600 handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY);
601 mLastFocusX = focusX;
602 mLastFocusY = focusY;
603 }
604 break;


606 case MotionEvent.ACTION_UP:


615 } else if (mAlwaysInTapRegion) {

616 handled = mListener.onSingleTapUp(ev);
スクロール量の平方が、mTouchSlopSquareという値を超えているかどうかによって、
 ・onDown()→onScroll(),onScroll(),onScroll(),onScroll(),・・・
 ・onDown()→onSingleTapUp()
mListenerへのコールバックのされ方が2通りあります。

(いうまでもなく、前者はmTouchSlopを超えたタッチスクロール操作で、後者はmTouchSlopを超えないタップ操作。)


    413             final ViewConfiguration configuration = ViewConfiguration.get(context);
414 touchSlop = configuration.getScaledTouchSlop();
425 mTouchSlopSquare = touchSlop * touchSlop;
mTouchSlopSquareは↑で定義されていて、元をたどると http://tools.oesf.biz/android-4.4w_r1.0/search?q=config_viewConfigurationTouchSlop このへんから値を持ってきています。
Nexus 7 (2013)とかは8dpではなく、ちょっと大きめの12dpが指定されていますね。
# タッチパネルの精度が悪いからでしょうか…


ちなみに、ここまでのところ、「8dp捨てられている」という実装はありません。
タッチスクロール時、GestureDetector自体は
のように忠実に、判断結果と座標をmListenerへコールバックしています。
じゃあ、誰がonDown~初回onScrollのスクロール量を捨てているかというと、mListenerを使っている人です。
つまり、小人さんに監視を任せている、その司令塔が、捨てているのです。

http://tools.oesf.biz/android-4.4w_r1.0/xref/external/chromium_org/content/public/android/java/src/org/chromium/content/browser/ContentViewGestureHandler.java
司令塔はこいつです。

     30 class ContentViewGestureHandler implements LongPressDelegate {
31

69 private GestureDetector mGestureDetector;
このとおり、タッチ監視の小人であるGestureDetectorを配下に持っています。
    297     private void initGestureDetectors(final Context context) {
のなかに小人による監視結果をさばくための実装があります。

そこには、このように書かれています。

    304                     @Override
305 public boolean onDown(MotionEvent e) {

308 mTouchScrolling = false;
309 mSeenFirstScrollEvent = false;

316 if (sendMotionEventAsGesture(GESTURE_TAP_DOWN, e, null)) {

318 }
319 // Return true to indicate that we want to handle touch
320 return true;
321 }
323 @Override
324 public boolean onScroll(MotionEvent e1, MotionEvent e2,
325 float distanceX, float distanceY) {

327 if (!mSeenFirstScrollEvent) {
328 // Remove the touch slop region from the first scroll event to avoid a
329 // jump.
330 mSeenFirstScrollEvent = true;
331 double distance = Math.sqrt(
332 distanceX * distanceX + distanceY * distanceY);
333 double epsilon = 1e-3;
334 if (distance > epsilon) {
335 double ratio = Math.max(0, distance - scaledTouchSlop) / distance ;
336 distanceX *= ratio;
337 distanceY *= ratio;
338 }
339 }
359 // distanceX and distanceY is the scrolling offset since last onScroll.
360 // Because we are passing integers to webkit, this could introduce
361 // rounding errors. The rounding errors will accumulate overtime.
362 // To solve this, we should be adding back the rounding errors each time
363 // when we calculate the new offset.
364 int x = (int) e2.getX();
365 int y = (int) e2.getY();
366 int dx = (int) (distanceX + mAccumulatedScrollErrorX);
367 int dy = (int) (distanceY + mAccumulatedScrollErrorY);
368 mAccumulatedScrollErrorX = distanceX + mAccumulatedScrollErrorX - dx;
369 mAccumulatedScrollErrorY = distanceY + mAccumulatedScrollErrorY - dy;
370
371 mExtraParamBundleScroll.putInt(DISTANCE_X, dx);
372 mExtraParamBundleScroll.putInt(DISTANCE_Y, dy);
373 assert mExtraParamBundleScroll.size() == 2;
374
375 if ((dx | dy) != 0) {
376 sendGesture(GESTURE_SCROLL_BY,
377 e2.getEventTime(), x, y, mExtraParamBundleScroll);
378 }
上のコードででっかく太字にしたところが、「8dp捨てられている」実際のコードです。
初回にスクロールイベントを判定した時点で一気にばっこーーん!と8dpスクロールとんでスクロールされるのは不自然なので、滑らかに見せるために、8dp捨ててスクロール量を計算しているようです。

 ・指に吸い付かせる

まずは、馬鹿になって、AndroidのWebViewの心遣い(ばっこーーーん!とスクロールが飛んでしまうのを防ぐために)を無視してしまいましょう。
    335                                 double ratio = Math.max(0, distance - 0) / distance;
これで
mmm external/chromium_org/android_webview && mmm frameworks/webview/chromium/

cd out/target/product/hammerhead/
adb root
adb remount
adb shell stop
adb sync system
adb reboot
端末を焼き帰ると、ぱっこーーーーん!と飛びながらも追従するブラウザが出来上がりましたね?
(一気にステップとばしすぎ!と思われても仕方がない書き方w ビルドの説明まで書いてるとキリがないので・・・)

で・・・
こんなのが製品として成り立つわけがありませんね。

実際に製品にする人々はどういう工夫をしているかというと・・・(以下略

F-06Fのオープンソースを覗いてみてください。


歯切れ悪いですが、以上です(笑)


もっといい方法があるよ!という意見がありましたら、どしどしコメントください。