Android Frameworkのコードにbreakpointを止めるメモ
Android開発をしていると、Frameworkがどういう挙動をしているか調べたくなる時がある。そういう時はFrameworkのコードにbreakpointを置くんだけど、止まってくれたり止まらなかったりすることがあるので困っていた。
完全に理解した!と言いたいけど、ViewRootImplの中でbreak pointおいても止まらないし、ログ埋め込むわけにもいかないので多分あってるくらいなのが歯がゆい…Ubuntu環境があればログ追加したfreamwork.jar作って挙動確かめたい🙄組み込み系の人、誰か気軽にできるなら代わりにやってほしい笑
— むーむー/Atsuko FUKUI (@muumuumuumuu) July 15, 2018
このTweetに対して神リプライがついたので流れないようにメモしておく。
完全に横からであれですけど、昔フレームワーク側コードにブレークポイント置いて止めたりwatchしてた記憶があるので(変わってなければ)おそらくできるはず?実機だとフレームワークのコードに手が入ってて行数がズレてて止まらないとかはありますが、エミュレータはいけるかと
— Yuki Fujisaki / tnj (@tnj) July 15, 2018
いくつかやり方がありますね。👀動かしている端末にコンパイルSDKをあわせれば止まります。あとはメソッドの宣言のところに貼れば止まるのと、どこかのサイトで動かしているAndroid OSのバージョンのソースコードを確認して、その行数と同じところに貼れば止まると言う感じでやっていますね
— takahirom (@new_runnable) July 15, 2018
実際試したところ、Compile SDK versionと合わせたEmulatorを作ってそこで動かすというのが良さそう。
StateListAnimatorを使ってXMLだけでAnimationをつける
こちらの記事を読んで「<selector>
の中にobject animator埋め込めるの知らなかった!!すげー!!!!」となったので遊んでみたメモ。
StateListAnimator
AndroidにはStateListAnimator
というクラスがあって、Viewのdrawable stateによってAnimationを書き分けることができる。何が最高かってこのAnimationはXMLでお手軽にかけるってところだ。1
ドキュメントはこちら
遊んでみた
アイコンに触っている間だけ大きくなるアニメーションを書いてみた。 xmlはこんな感じ
<?xml version="1.0" encoding="utf-8"?> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_pressed="true"> <set> <objectAnimator android:duration="200" android:propertyName="scaleX" android:valueTo="1.5" /> <objectAnimator android:duration="200" android:propertyName="scaleY" android:valueTo="1.5" /> <objectAnimator android:duration="200" android:propertyName="transitionZ" android:valueTo="10dp" /> </set> </item> <item> <set> <objectAnimator android:duration="200" android:propertyName="scaleX" android:valueTo="1" /> <objectAnimator android:duration="200" android:propertyName="scaleY" android:valueTo="1" /> <objectAnimator android:duration="200" android:propertyName="transitionZ" android:valueTo="0dp" /> </set> </item> </selector>
android:state_pressed="true"
の時とそうでない場合でObjectAnimatorの振る舞いを変えることができる。上記公式ドキュメントのリンク先を参照するとpress以外のstateもたくさんある。
xmlのobjectAnimator
タグで使えるattribute一覧はこちら
Animation resources | Android Developers
<objectAnimator android:propertyName="string" android:duration="int" android:valueFrom="float | int | color" android:valueTo="float | int | color" android:startOffset="int" android:repeatCount="int" android:repeatMode=["repeat" | "reverse"] android:valueType=["intType" | "floatType"]/>
このanimationとviewを紐づける時はこんな感じでandroid:stateListAnimato
を使う。
android:stateListAnimator="@animator/fav_animator"
こんな感じでハートがドキドキする。
サンプルコードはこちら。
KotlinのRegexとDestructuredで文字列からdata classに変換する
Androidでアプリを書いていて、正規表現を扱うときにjava.util.regex.Matcher
を使うこと多いが、あれは個人的には好きではない。もっとスッキリかけるんじゃないかなぁといつも思ってしまう。
例えば、URLからprotocolとdomainを正規表現を使って取り出すコードを書こうと思うとこんな感じになるかと思う。
companion object { private const val REGEX: String = "(.*)://(.*)" } fun hoge() { val urlString = "http://www.com" val url = generateUrlFromString(urlString) } private fun generateUrlFromString(url: String): Url? { val matcher = Pattern.compile(REGEX).matcher(url) return if (matcher.find()) { val protocol = matcher.group(1) val domain = matcher.group(2) return Url(protocol, domain) } else { null } } data class Url(val protocol: String, val domain: String)
再帰の場合に注意が必要で、上記の場合だとmatcher.group()
が0ではなく1と2だったり何かとやらかしてしまったりする。(matcher.group(0)
にはhttp://www.com
が入ってくる)
そんなときに、この記事見て最高では????と思ったので自分でも試してみることにした。一行でまとめると Destructured
クラスが最高では?という話です。やっぱりKotlinは可愛い。
上記のgenerateUrlFromString()
をJavaの正規表現からKotlinの正規表現に書き換えたコードがこちら。
private fun generateUrlFromString(url: String): Url? = REGEX.toRegex().matchEntire(url) ?.destructured ?.let { (protocol, domain) -> Url(protocol, domain) }
これだけで十分可愛さが伝わる気がするが、蛇足ながら可愛いポイントを書いていく。
- 文字列REGEXをtoRegex() でRegexクラスに変換
- matchEntire()で引数url stringを渡すことによりMatchResultを取得
- 2で正規表現にマッチしなかった場合はnullになるので、
?.
でdestructuredを取得。これは正規表現にマッチしたgroupをDestructured
クラスで返してくれる - 3ですでにnullの可能性があるので引き続き
?.
でletを呼ぶ。ポイントはここでラベルをつけることができるという点。サンプルコードだとprotocol
とdomain
をすぐに使っているが、let blockのなかで複雑なことをしている場合にわざわざ名前をつけるために変数に代入しなくてもよくて可読性がグッと上がる。
これらをワインラインでスッキリかけるところがまた可愛い!
当然ながらKotlinのRegexもJavaのMatcherとかをwrapしているので、めんどくさいところは全てやってくれていて最高。
言いたいことは以上です。
kotlin.concurrent.threadは新規threadが作成されるけどcurrent threadのgroupで動く
[2018.7.15 ご指摘いただき記事の最後に追記しました]
あらまし
先日おもしろいTweetを見つけたのでちょっと調べてみたメモ。
別ActivityでUI操作の必要なマルチスレッド処理をしたいとき、runOnUiThreadを使わないとMarshmallowならクラッシュするけど、Oreoだとthreadだけで動作するという謎知見を得た
— omega (@equal_001) 2018年7月13日
残念ながら手元で上記の現象は再現しなくて、MashmallowでもCrashが発生しなかったんだけど、
Kotlinのthreadってmain threadじゃない別threadで動くんじゃなかったっけ…?🤔
と思ったので調査。
ちなみにMashmallowの端末が手元になかったのでEmulator環境でやってます。
まずは調査用の簡単なコードを準備。Activityの onResume()
でUI操作を行う。(textという名前のTextViewに"hogehoge"というtextをセットしています。)
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_thread) Timber.plant(Timber.DebugTree()) // main threadのログ出力結果を確認するためにログを入れる Timber.d("main thread: ${Looper.getMainLooper().thread}") } override fun onResume() { super.onResume() thread { Timber.d("current thread: ${Thread.currentThread()}") text.text = "hogehoge" } }
ThreadのtoString()
で表示しているのは下記の三つ。 thread name
と priority
と group name
public String toString() { ThreadGroup group = getThreadGroup(); if (group != null) { return "Thread[" + getName() + "," + getPriority() + "," + group.getName() + "]"; } else { return "Thread[" + getName() + "," + getPriority() + "," + "" + "]"; } }
ログとるときの操作は、単純に画面を表示させるだけ。結果はこんな感じ。
07-14 08:31:01.772 5497-5497/? D/ThreadActivity: main thread: Thread[main,5,main] 07-14 08:31:01.777 5497-5513/? D/ThreadActivity$onResume: current thread: Thread[Thread-266,5,main]
thread blockの中でログを出力した方は確かにthread nameを見ると別のthreadになっている。しかし3つ目のgroup nameはmainになっている。
kotlin.concurrent.threadを読んでみよう
Kotlinのthreadは気軽にサンプルコードのように書いている人が多いと思うが、じつはたくさんのdefault引数が用意されている
public fun thread(start: Boolean = true, isDaemon: Boolean = false, contextClassLoader: ClassLoader? = null, name: String? = null, priority: Int = -1, block: () -> Unit): Thread { val thread = object : Thread() { public override fun run() { block() } } if (isDaemon) thread.isDaemon = true if (priority > 0) thread.priority = priority if (name != null) thread.name = name if (contextClassLoader != null) thread.contextClassLoader = contextClassLoader if (start) thread.start() return thread }
で、そんなdefault引数を置いておいて、メソッドの頭でJavaのjava.lang.Thread
をnewしているんだけど、このときcurrent threadをparentにとるようになっているので、main threadからthread()
を呼び出すとmain threadのgroupで新規threadが作られるのだ。
private void init(ThreadGroup g, Runnable target, String name, long stackSize) { Thread parent = currentThread(); if (g == null) { g = parent.getThreadGroup(); } g.addUnstarted(); this.group = g;
UI操作できるのは一つのthreadからだけ
ちなみに上記のサンプルコードで、一度backgroundに行って再びonResume()
に帰ってくるとアプリがcrashする。
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
backgroundから復帰した時にonResume()
でもう一度threadから新規threadを作成しているので最初にさわったthreadではない別のthreadからUIを触ろうとしてcrashする。
ログを見てもthread nameが変わっていることがわかる
// Activity起動 07-14 08:31:01.772 5497-5497/? D/ThreadActivity: main thread: Thread[main,5,main] 07-14 08:31:01.777 5497-5513/? D/ThreadActivity$onResume: current thread: Thread[Thread-266,5,main] // Backgroundから復帰(2回目のonResume) 07-14 08:31:06.983 5497-5518/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current thread: Thread[Thread-269,5,main]
ここでthreadを新規作成するのではなくmain threadに固定することでこのcrashは避けられる。
override fun onResume() { super.onResume() // この部分を // thread { // Timber.d("current thread: ${Thread.currentThread()}") // text.text = "hogehoge" // } // こう変える runOnUiThread { Timber.d("current thread: ${Thread.currentThread()}") text.text = "hogehoge" } }
ログを見たら全てmain threadで固定されていることがわかる。
// Activity起動 07-14 08:07:18.328 5242-5242/com.example.muumuu.playgroundapp D/ThreadActivity: main thread: Thread[main,5,main] 07-14 08:07:18.329 5242-5242/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current thread: Thread[main,5,main] // Backgroundから復帰(2回目のonResume) 07-14 08:07:27.811 5242-5242/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current thread: Thread[main,5,main]
ちなみにもう一つおもしろい挙動を見つけたのでメモ。 こんな感じでthreadとrunOnUiThread両方書いてやるとCrashは発生しない。
override fun onResume() { super.onResume() thread { Timber.d("current thread from thread: ${Thread.currentThread()}") text.text = "hogehoge" } runOnUiThread { Timber.d("current thread from ui thread: ${Thread.currentThread()}") text.text = "hogehoge" } }
ログを見ると別threadよりも先に runOnUiThread
の中が実行されているのがわかる。先にmain threadでさわっているので以降main threadのgroupで動いている別threadでさわってもcrashしないということなのかな。 // この部分間違ってそうなので追記で捕捉しました(2018.7.15)
// 1回目のonResume() 07-14 08:40:11.749 5693-5693/? D/ThreadActivity$onResume: current thread from ui thread: Thread[main,5,main] 07-14 08:40:11.749 5693-5709/? D/ThreadActivity$onResume: current threa from thread: Thread[Thread-278,5,main] // 2回目のonResume() 07-14 08:40:49.488 5693-5693/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current thread from ui thread: Thread[main,5,main] 07-14 08:40:49.489 5693-5717/com.example.muumuu.playgroundapp D/ThreadActivity$onResume: current threa from thread: Thread[Thread-280,5,main]
[2018.7.15ここから追記]
ブログ公開ごにhydrakecatさんにご指摘いただいたのでさらに追加調査。
自分もちょっと調べただけなのですが、たぶんこのサンプルだとたまたまうまくいっているだけな気がします。thread { } の中で Thread.sleep(1000) とか呼んでみるとクラッシュすると思います。見当違いなことを言っていたらすみません m(_ _)m
— Hiroshi Kurokawa (@hydrakecat) July 14, 2018
確かに thread { } の中で Thread.sleep(1000)を入れるとcrashするようになった。 (emulator環境Mashmallow / 実機環境Oreo共にcrashすることを確認 )
override fun onResume() { super.onResume() thread { text.text = "hogehoge" // sleep以前にtextをさわってもcrashしないが、 Thread.sleep(1000) // 1000msecの遅延後 text.text = "hogehoge" // textをさわるとcrashする }
例外を投げているのはViewRootImpl.java
のこの部分。
Cross Reference: /frameworks/base/core/java/android/view/ViewRootImpl.java
7311 void checkThread() { 7312 if (mThread != Thread.currentThread()) { 7313 throw new CalledFromWrongThreadException( 7314 "Only the original thread that created a view hierarchy can touch its views."); 7315 } 7316 }
mThread
に何が入るのかとういうと、constructorでThread.currentThread()
を代入している。mThread
はfinalなのでsleep前後でも変わらない。
235 final Thread mThread; 478 public ViewRootImpl(Context context, Display display) { 479 mContext = context; 480 mWindowSession = WindowManagerGlobal.getWindowSession(); 481 mDisplay = display; 482 mBasePackageName = context.getBasePackageName(); 483 mThread = Thread.currentThread();
mThread
も Thread.currentThread()
もsleep前後で変化しないはずなのでおかしいなぁと思っていたら
たぶんですが、親Viewがレイアウト中だとうまく無視されるんじゃないかと思っています。そしてTextViewにテキストを反映するときにうまく変更後の文字列を拾ってくれるんじゃないかと推測しています。mainスレッドで触った直後に別スレッドで触ったらクラッシュしなかったのもたぶん同じ理由かとー。
— Hiroshi Kurokawa (@hydrakecat) July 15, 2018
というわけでcheckThread()
が呼ばれない可能性を考える。
ViewRootImpl
はmHandlingLayoutInLayoutRequest
というフラグを持っていて、Layout実行時にこのフラグがtrueになる。
1158 @Override 1159 public void requestLayout() { 1160 if (!mHandlingLayoutInLayoutRequest) { 1161 checkThread(); 1162 mLayoutRequested = true; 1163 scheduleTraversals(); 1164 } 1165 }
で、requestLayout()
にこのフラグが立っていたら checkThread()
が実行されない(というか何もしない)ので、threadのcheckも行われず例外も投げられないということのようだ。sleep後はlayoutが完了してフラグがfalseになっているのでthreadのcheckが行われて例外が発生している。
runOnUiThread()
と thread
両方書いたケースでも、runOnUiThread()
の方でlayout要求が出ているのでmHandlingLayoutInLayoutRequest
がtrueになり、そのタイミングでthread
でUIをさわったのでcheckThread()
が呼ばれず例外が発生しなかっただけだと思われる。
ViewRootImpl
はAndroid FrameworkのクラスなのでDebuggerが効かず本当にそうか?というのが確認できないので歯がゆいが、多分あってるんじゃ無いかなぁ。多分。
完全に理解した!と言いたいけど、ViewRootImplの中でbreak pointおいても止まらないし、ログ埋め込むわけにもいかないので多分あってるくらいなのが歯がゆい…Ubuntu環境があればログ追加したfreamwork.jar作って挙動確かめたい🙄組み込み系の人、誰か気軽にできるなら代わりにやってほしい笑
— むーむー/Atsuko FUKUI (@muumuumuumuu) July 15, 2018
[2018.7.15 追記ここまで]
RxTextView#textChanges()でdistinctUntilChangedする時にeventが流れない時がある
みんな大好きRxBinding1 ですが、ちょっとハマることがあったのでメモ。
RxTextView#textChanges()
で EditText
の入力イベントを監視する。その時に重複した入力をdistinctUntilChanged()
で削ろうとしたら以降何もeventが流れない現象に遭遇した。
RxTextView.textChanges(editText) .distinctUntilChanged() // ここでevnetが止められるから .observeOn(AndroidSchedulers.mainThread()) .subscribe { Log.d("SampleApp", it.toString()) // ここは実行されない }
textChanges()
は内部ではTextWatcher
を使っていて、これがTextの変更を検出するとCharSequence
を返してくれるのだが、こいつはmutableで毎度同じinstanceを返すのだ。同じinstanceなのでchangeしたと見なされずいつまでたっても次のeventが流れてこない。
なので、CharSequence
からStringを取り出してやれば想定していた挙動になる。
RxTextView.textChanges(editText) .map { it.toString() } // ここでStringに変換すると .distinctUntilChanged() .observeOn(AndroidSchedulers.mainThread()) .subscribe { Log.d("SampleApp", it.toString()) // ここが実行される }
そもそもなんで RxTextView#textChanges()
で入力の重複を防ぎたかったかというと、機種依存でSoftware Keyboardのsearch button
をtapした時にtextChanges
のeventがemitされちゃう場合があって、これを防ぎたかったから。これはRxTextView
というよりは、内部で使われているTextWatcher
でeventが発火しているからなんだけど、keyboardの機種依存周りつらすぎてなんか仕様とか作って統一して欲しい…
@JvmOverloadsなfunctionをmockできないケースがある
@JvmOverloads
を付与したKotlinのfunctionを、Javaから呼び出すコードを書いた。ここのテストを書こうと思ってmockito-kotlinでmockしようとしてできなかったときのメモ。ちなみにmockしたいクラスはDagger2でDIしている前提。
やりたかったこと
例えばこんなKotlinで書かれたfunctionがあったとして、
// Hoge.kt class Hoge { @JvmOverloads fun equal(a: String, b: String = "0"): Boolean = a == b }
それをJavaから呼び出しをするコードに対して
// Fuga.java public class Fuga { @Inject Hoge hoge = new Hoge(); @VisibleForTesting public Fuga(Hoge hoge) { this.hoge = hoge; } boolean callHogeEquals() { return hoge.equal("1"); } }
mockするようなテストコードを書こうと思ったらfailする
class FugaTest { @Mock lateinit var hoge: Hoge @Before fun setUp() { MockitoAnnotations.initMocks(this) } @Test fun testCallHogeEquals() { val fuga = Fuga(hoge) whenever(hoge.equal("1")).thenReturn(true) val result = fuga.callHogeEquals() Assert.assertEquals(true, result) } }
junit.framework.AssertionFailedError: Expected :true Actual :false
mockできずに本体コード通っちゃったのかな?と思ったが、こういうコードに対して同じようなテストを書いたところ
@JvmOverloads fun returnConcatString(a: String, b: String = "0") = a + b
@Test fun testCallHogeConcatString() { val fuga = Fuga(hoge) whenever(hoge.returnConcatString("1")).thenReturn("hoge") val result = fuga.callHogeCancatString() Assert.assertEquals("hoge", result) }
junit.framework.ComparisonFailure: Expected :hoge Actual :null
という結果になったので、どうやら本体コードを通っているのではなく、その型のdefault値が返って来ているように見える。
どうやって回避する?
1. 全てKotlinで書く
今回のようにTest対象のクラスだけがJavaで、mock対象とTestコードがKotlinで書かれた場合にこの問題が発生する。Test対象のクラスもKotlinで書いた場合には再現しないので、Test対象のクラスをKotlinで書き直すと回避できる。
2. 全てのParameterを指定する
別の方法として、Javaで書かれたTest対象のクラスからmock対象のコードを呼び出す時に、全てのParameterを指定するように変更するとこの問題は回避できる。
先ほどの例でいうと、Test対象のJavaをこんな感じに変更して、
boolean callHogeEquals() { return hoge.equal("1", "0"); // default parameterと同じものを2nd parameterに指定 }
Testコードも2nd paramterをつけてやると
@Test fun testCallHogeEquals() { val fuga = Fuga(hoge) whenever(hoge.equal("1", "0")).thenReturn(true) val result = fuga.callHogeEquals() Assert.assertEquals(true, result) }
無事にtestがpassする。ちなみにmock対象のコードは@JvmOverloads
をつけたままで問題ない。
なぜmockできないのか?
@JvmOverloads
がついたfunctionをJavaのbyte codeで見てみると、それぞれのシグネチャのパターン分のメソッドだけでなく、ブリッジメソッドも作られていた。
// access flags 0x1049 public static synthetic bridge equal$default(Lcom/example/muumuu/playgroundapp/Hoge;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Z @Lkotlin/jvm/JvmOverloads;() // invisible ILOAD 3 ICONST_2 IAND IFEQ L0 L1 LINENUMBER 8 L1 LDC "0" ASTORE 2 L0 ALOAD 0 ALOAD 1 ALOAD 2 INVOKEVIRTUAL com/example/muumuu/playgroundapp/Hoge.equal (Ljava/lang/String;Ljava/lang/String;)Z IRETURN MAXSTACK = 3 MAXLOCALS = 5
で、mockする箇所で引数一つで呼び出すとそのブリッジメソッドをINVOKESTATICしている。
L3 LDC "1" ACONST_NULL ICONST_2 ACONST_NULL INVOKESTATIC com/example/muumuu/playgroundapp/Hoge.equal$default (Lcom/example/muumuu/playgroundapp/Hoge;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Z INVOKESTATIC java/lang/Boolean.valueOf (Z)Ljava/lang/Boolean; INVOKESTATIC com/nhaarman/mockito_kotlin/MockitoKt.whenever (Ljava/lang/Object;)Lorg/mockito/stubbing/OngoingStubbing; ICONST_1 INVOKESTATIC java/lang/Boolean.valueOf (Z)Ljava/lang/Boolean; INVOKEINTERFACE org/mockito/stubbing/OngoingStubbing.thenReturn (Ljava/lang/Object;)Lorg/mockito/stubbing/OngoingStubbing; POP
自分の理解ではmockitoは@Mock
を付与したインスタンスはdummy objectを生成して、when等で定めた振る舞いをstubとして生成している。そのため、今回のようにブリッジメソッドをstubとして生成しても実際にJavaのコードからコールされるのはこのブリッジメソッドではないのでstubが存在しないため戻り値のクラスのデフォルト値が返ってしまったようだ。
それでは上記回避策をとった場合は、どのようなbyte codeが生成されていて、なぜ問題が起きないのか見ていこう。
まず、全てKotlinで書かれた場合、mockする時だけでなく本体側も@JvmOverloads
なfunctionを同じシグネチャでコールするとブリッジメソッドが呼ばれるので、stubする対象が一致するので正常にmockできる。
次にstub生成時に引数をフルで指定する場合は、ブリッジメソッドではなく引数がフルに揃ったシグネチャのメソッドのスタブが作成されるため正常にmockできる。
L3 LDC "1" LDC "1" INVOKEVIRTUAL com/example/muumuu/playgroundapp/Hoge.equal (Ljava/lang/String;Ljava/lang/String;)Z INVOKESTATIC java/lang/Boolean.valueOf (Z)Ljava/lang/Boolean; INVOKESTATIC com/nhaarman/mockito_kotlin/MockitoKt.whenever (Ljava/lang/Object;)Lorg/mockito/stubbing/OngoingStubbing; ICONST_1 INVOKESTATIC java/lang/Boolean.valueOf (Z)Ljava/lang/Boolean; INVOKEINTERFACE org/mockito/stubbing/OngoingStubbing.thenReturn (Ljava/lang/Object;)Lorg/mockito/stubbing/OngoingStubbing; POP
はー、フルKotlinな世界はよ〜〜〜〜〜
アメリカでUberにiPhone置き忘れて無事に回収した。SIMは通話付きを買おうな!
[2018/06/13 追記しました]
事の顛末
一週間くらい出張でUS(CA, Palo Alto)に来ている。Palo AltoはSan Fransiscoと異なり移動は車が基本になる。なので出勤はUberを利用しているのだが、車内にiPhoneを置き忘れる事件が発生。先日のGoogle I/OでiPhoneを置き忘れた話を見かけたがまさか自分が同じ事になるとは思わなかった 😇
私がラッキーだったのは、メインで使っていた端末が無くしたiPhoneではなく、別のAndroid端末だった事。日本から持って来た端末は2台だが、現地でSIMを1枚だけ買ってAndroidの方に挿して使っていた。つまり、iPhoneを無くしても常にネットワークに接続できるし、帰りのUberも呼べるしで緊急で困ることはなかった。あとiPhoneはプライベートで使っていただけだったので会社の情報とかも入っていなかったので一安心。とはいえまたiPhone買い直して$800とか払うのもバカバカしいので取り戻す事にした。ちなみにiPhoneはSIMさしてないとオフラインなので「iPhoneを探す」が使えない😇😇
Uberアプリは無くしたものを取り戻すのも便利。ただし電話番号とSMSが必須。
ドライバーとなんとか連絡をとりたくてUberアプリを色々さわる。乗車情報より「持ち物を紛失した」のメニューを発見。ここまではいいんだけど、Uberはプライバシーとかの観点からかドライバーと乗客が直接やりとりするのを極力減らす設計になっており、Uberを経由した電話で連絡を取る事になる。
どういうことかと言うと、こちらが連絡してほしい番号をUberにアプリ経由で送信し、Uberがドライバーに連絡するのだが、ドライバーはUberから連絡された番号にかけても直接乗客の番号に繋がるのではなくUberの番号に繋がり、そこから自動で通話が転送される。通話終了後、どちらかが掛け直しても相手に繋がらないような仕組みになっていた。
私の場合は電話番号を送信したらすぐにかかって来た。1 "Are you an Uber driver?"って聞いたらそうだって言われたので、「自分が今朝の乗客であること 」と「iPhoneを無くしたらしいこと」を伝えると、「今日は何人か乗せたから誰かわかんないけど、とりあえずiPhoneには気がつかなかった。でも調べてまた折り返し連絡するね」って言ってくれて通話終了。この時点では気がついてなかったけど、向こうから折り返すことがUberの仕組み上できなかった。
ずっと待ってても連絡が来ないのでどうしたもんかなぁと次の日の朝周りに相談したら「アメリカはグイグイpushしないと無くしたものは戻って来ない」と言われたのでPushすることにする。
もう一度同じメニューから連絡をとってみたけど今度は繋がらず。別の問い合わせ先を探していたら "I couldn't reach my driver about a lost item" 2 があったのでこちらからも連絡してみる事にする。先ほどと同様電話番号を送信する仕組みになっていた。今度はサポートからメッセージが来て、「ドライバーと連絡がついてiPhone見つかったらしい。向こうから連絡くると思うからちょっと待っててくれ」3 と言われたので待つ。
しばらくするとSMSがドライバーより届いて、「どこに住んでるの?」と聞かれて住所を教えると「$20でそこまで持っていくか、San JoseのUberのオフィスに預けるかどっちがいいか選んで」と言われたので持って来てもらう事にした。そこから時間とかのやりとりを全てSMS上で行い、その日の晩4にドライバーが来てくれて無事iPhoneを受け取った。彼曰く、「本当はもっと早く(最初の電話で約束した通り)掛け直してあげたかったんだけど繋がらなかったんだよねー」とのこと。ちなみに受け取る時に$20をキャッシュで支払ったのだが、どうやらUberを通じて返すと$15がアカウントに課金されるので$5で私はSan Joseにいく手間と時間を買って、ドライバーは$20をポケットに入れることができたっぽい。5
[2018/06/13 追記 ここから]
後日Uberのサポートから「落し物見つかったって聞いたから$15がライド料金に含まれるように調整しておいたよ、$15は全額ドライバーに支払われるよ」という連絡が来ました。
[2018/06/13 追記 ここまで]
タイムライン
- 6/6 9:00
- 6/6 18:30
- 6/6 19:00
- 6/7 9:00
- 一晩明けてもドライバーから連絡がないので再度連絡を試みるが繋がらない
- Uberサポートに連絡
- ドライバーからSMSが来て$20で持って来てもらうことを約束する
- 6/7 20:00
- ドライバーからiPhoneを受け取る
雑感
- 今回の出張でたまたまSIMは通話付きのもの6を買っていたのでとてもスムーズにいった。通話なしのSIMを買っていたら相当面倒だったと思う。
- 日本だとSMSはMFAくらいにしか使っていないけど意外と他の国では使われるのだろうか。
- Uberのドライバーと乗客を直接繋がないプライバシーに配慮したサービス設計は参考になった。
- 今回ドライバーに私た$20が出張中支払った唯一のキャッシュだった。念のため空港で換金しておいてよかった
言いたいことは以上です。