2020年6月2日火曜日

androidx.test.ext:truth を使ったときに IllegalAccessError が出たらバージョンを 1.3.0 以降にする

androidx.test.ext.truth にある IntentSubject などを使うとき com.google.truth:truth:0.42 androidx.test.ext:truth:1.2.0 だと動くのですが、Truth のバージョンを以下のように 1.0.1 にすると com.google.truth:truth:1.0.1 androidx.test.ext:truth:1.2.0 java.lang.IllegalAccessError: tried to access method com.google.common.truth.Subject.actual()Ljava/lang/Object; from class androidx.test.ext.truth.content.IntentSubject

というエラーが出ます。

IntentSubject 内で Subject の actual() メソッドにアクセスしているのですが、これが 0.42 のときは protected だったのが package private に変わってアクセスできなくなったのが原因です。

そのため、この新しい Truth に対応した androidx.test.ext:truth のバージョンを使えば OK です。 com.google.truth:truth:1.0.1 androidx.test.ext:truth:1.3.0-rc01



2020年5月29日金曜日

Scroller を使う

Scroller というのは、スクロール時のアニメーションを実現するための x,y 位置を計算してくれるクラスです。

ScrollerOverScroller が用意されています。 OverScroller は行き過ぎて戻ってくるようなアニメーションができます。

Scroller にはアニメーションを開始するメソッドとして が用意されています。

使い方はこんな感じです。
  • 1. scroller.forceFinished() でアニメーションを止める
  • 2. scroller.fling() または scroller.startScroll() でアニメーションを開始する
  • 3. View.postInvalidateOnAnimation() を呼ぶ。これを呼ぶと View.computeScroll() が呼ばれる
  • 4. View.computeScroll() で scroller.computeScrollOffset() を呼ぶ。戻り値が true の場合アニメーションが終わっていないということ
  • 5. scroller.currX, scroller.currY を使って View の位置などを変える
setFriction() で摩擦を設定できます。デフォルトは ViewConfiguration.getScrollFriction() が設定されています。

class ScrollerSampleView : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) private val size = (100 * resources.displayMetrics.density).toInt() private val targetView: View = View(context).apply { layoutParams = LayoutParams(size, size) setBackgroundColor(Color.RED) } private val textView: TextView = TextView(context).apply { layoutParams = LayoutParams( LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT ) } private val scroller = OverScroller(context) init { addView(targetView) addView(textView) } fun scroll(dx: Int, dy: Int, duration: Int, friction: Float) { scroller.setFriction(friction) // scroll の前に今のアニメーションを止める scroller.forceFinished(true) targetView.translationX = 0f targetView.translationY = 0f val startX = 0 val startY = 0 // アニメーションを開始 scroller.startScroll( startX, // scroll の開始位置 (X) startY, // scroll の開始位置 (Y) dx, // 移動する距離、正の値だとコンテンツが左にスクロールする (X) dy, // 移動する距離、正の値だとコンテンツが左にスクロールする (Y) duration // スクロールにかかる時間 [milliseconds] ) // これにより computeScroll() が呼ばれる postInvalidateOnAnimation() } fun fling(velocityX: Int, velocityY: Int, overX: Int, overY: Int, friction: Float) { scroller.setFriction(friction) // fling の前に今のアニメーションを止める scroller.forceFinished(true) targetView.translationX = 0f targetView.translationY = 0f val startX = 0 val startY = 0 val minX = 0 val maxX = 800 val minY = 0 val maxY = 800 // アニメーションを開始 scroller.fling( startX, // scroll の開始位置 (X) startY, // scroll の開始位置 (Y) velocityX, // fling の初速 [px/sec] (X) velocityY, // fling の初速 [px/sec] (Y) minX, // X の最小値. minX - overX まで移動し、minX 未満のところは overfling 中になる maxX, // X の最大値. maxX + overX まで移動し、maxX を超えたところは overfling 中になる minY, // Y の最小値. minY - overY まで移動し、minY 未満のところは overfling 中になる maxY, // Y の最大値. maxY + overY まで移動し、maxY を超えたところは overfling 中になる overX, // overfling の範囲 (X). overfling の範囲は両端に適用される overY // Overfling の範囲 (Y). overfling の範囲は両端に適用される ) // これにより computeScroll() が呼ばれる postInvalidateOnAnimation() } override fun computeScroll() { super.computeScroll() // computeScrollOffset() の戻り値が true == まだアニメーション中 if (scroller.computeScrollOffset()) { textView.text = """ currVelocity: ${scroller.currVelocity} currX: ${scroller.currX} currY: ${scroller.currY} startX: ${scroller.startX} startY: ${scroller.startY} finalX: ${scroller.finalX} finalY: ${scroller.finalY} isFinished: ${scroller.isFinished} isOverScrolled: ${scroller.isOverScrolled} """.trimIndent() targetView.translationX = scroller.currX.toFloat() targetView.translationY = scroller.currY.toFloat() // アニメーション中なので再度呼ぶ postInvalidateOnAnimation() } } } 速度を 1000 [px/sec], 2000 [px/sec], 3000 [px/sec], 4000 [px/sec]、摩擦を ViewConfiguration.getScrollFriction(), ViewConfiguration.getScrollFriction() / 2、overfling 範囲を 0, 200 で上記の fling() を呼んだ結果が次の動画です。





摩擦を半分にすると同じ速度でも遠くまで移動し、overfling 範囲をつけると行き過ぎて戻ってくるようになります。



2020年5月27日水曜日

Dagger に Fragment と FragmentFactory の生成をまかせる

Master of Dagger の改定版にも入れる予定です。ただいま鋭意執筆中です。もう少々お待ちください。


ViewModelFactory と同じような感じで FragmentFactory および Fragment の生成をまかせることができます。

オブジェクトグラフに MyApi があるとします。 @Module object AppModule { @Provides fun provideMyApi(): MyApi { ... } } これを引数にとる Fragment があります。Dagger に生成をまかせたいのでコンストラクタに @Inject をつけます。 class MainFragment @Inject constructor(private val api: MyApi) : Fragment() { ... } Fragment の Map Multibindings 用の MapKey を用意します。 @Target( AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY_GETTER, AnnotationTarget.PROPERTY_SETTER ) @Retention(AnnotationRetention.RUNTIME) @MapKey annotation class FragmentKey(val value: KClass<out Fragment>) 用意した MapKey を使って MainFragment を Multibindings に追加します。 @Module interface FragmentModule { @Binds @IntoMap @FragmentKey(MainFragment::class) fun bindMainFragment(fragment: MainFragment): Fragment } Fragment の Multibindings を引数に取る FragmentFactory を用意します。 class MyFragmentFactory @Inject constructor( private val providers: Map<Class<out Fragment>, @JvmSuppressWildcards Provider<Fragment>> ) : FragmentFactory() { override fun instantiate(classLoader: ClassLoader, className: String): Fragment { val found = providers.entries.find { className == it.key.name } ?: throw IllegalArgumentException("unknown model class $className") val provider = found.value try { @Suppress("UNCHECKED_CAST") return provider.get() } catch (e: Exception) { return super.instantiate(classLoader, className) } } } 用意した MyFragmentFactory を取得するためのメソッドを Component に用意します。 @Component(modules = [AppModule::class, FragmentModule::class]) interface AppComponent { fun fragmentFactory(): MyFragmentFactory } class MyApplication : Application() { lateinit var appComponent: AppComponent override fun onCreate() { super.onCreate() appComponent = DaggerAppComponent.builder() .build() } } supportFragmentManager.fragmentFactory に Component から取得した MyFragmentFactory をセットします。 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportFragmentManager.fragmentFactory = (application as MyApplication).appComponent .fragmentFactory() setContentView(R.layout.activity_main) } } activity_main.xml <?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/mainFragment" android:name="net.yanzm.sample.MainFragment" android:layout_width="match_parent" android:layout_height="match_parent" />


2020年5月21日木曜日

VelocityTracker の使い方

VelocityTracker はタッチイベントの速度計算を簡単にするためのクラスです。Fling など速度がジェスチャーの構成要素になっているものに対して便利です。

VelocityTracker.obtain() でインスタンスを取得します。
addMovement(ev) で MotionEvent を追加し、速度を取得するときは computeCurrentVelocity(int units) または computeCurrentVelocity(int units, float maxVelocity) を呼んだ後に getXVelocity(), getYVelocity() を呼びます。
obtain() で取得したインスタンスは不要になった時点で recycle() を呼びましょう。

computeCurrentVelocity() で maxVelocity を渡さない場合は Float.MAX_VALUE が使われます。 computeCurrentVelocity() で渡す units は getXVelocity(), getYVelocity() で取得する velocity の単位になります。1 を指定した場合は pixels per millisecond、1000 を渡した場合は pixels per second になります。 class SimpleDragView : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) private val targetView: View private var velocityTracker: VelocityTracker? = null init { val size = (100 * resources.displayMetrics.density).toInt() targetView = View(context).apply { layoutParams = LayoutParams(size, size).apply { gravity = Gravity.CENTER } setBackgroundColor(Color.RED) } addView(targetView) } private var lastVelocityX = 0f private var lastVelocityY = 0f override fun onTouchEvent(ev: MotionEvent): Boolean { when (ev.actionMasked) { MotionEvent.ACTION_DOWN -> { velocityTracker?.clear() velocityTracker = velocityTracker ?: VelocityTracker.obtain() velocityTracker?.addMovement(ev) } MotionEvent.ACTION_MOVE -> { velocityTracker?.let { it.addMovement(ev) val pointerId: Int = ev.getPointerId(ev.actionIndex) it.computeCurrentVelocity(1000) lastVelocityX = it.getXVelocity(pointerId) lastVelocityY = it.getYVelocity(pointerId) } } MotionEvent.ACTION_UP -> { velocityTracker?.let { ObjectAnimator .ofPropertyValuesHolder( targetView, PropertyValuesHolder.ofFloat( View.TRANSLATION_X, lastVelocityX / 4 ), PropertyValuesHolder.ofFloat( View.TRANSLATION_Y, lastVelocityY / 4 ) ) .apply { addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator?) { super.onAnimationEnd(animation) targetView.translationX = 0f targetView.translationY = 0f } }) } .setDuration(500) .start() } velocityTracker?.recycle() velocityTracker = null } MotionEvent.ACTION_CANCEL -> { velocityTracker?.recycle() velocityTracker = null } } return true } }


2020年5月7日木曜日

mockito-kotlin で lambda を mock + @RunWith(AndroidJUnit4::class) のときは work around が必要

以下のような @RunWith(AndroidJUnit4::class) を使わない Unit Test は問題なく動くのですが、 class HogeTest { @Test fun test() { val listener = mock<(Boolean) -> Unit>() ... verify(listener)(false) } } 次のように @RunWith(AndroidJUnit4::class) をつけるとエラーが発生します。 @RunWith(AndroidJUnit4::class) class HogeTest { @Test fun test() { val listener = mock<(Boolean) -> Unit>() ... verify(listener)(false) } }
org.mockito.exceptions.base.MockitoException:
ClassCastException occurred while creating the mockito mock :
class to mock : 'kotlin.jvm.functions.Function1', loaded by classloader : 'sun.misc.Launcher$AppClassLoader@18b4aac2'
created class : 'kotlin.jvm.functions.Function1$MockitoMock$1350680399', loaded by classloader : 'net.bytebuddy.dynamic.loading.MultipleParentClassLoader@7a2a2c83'
proxy instance class : 'kotlin.jvm.functions.Function1$MockitoMock$1350680399', loaded by classloader : 'net.bytebuddy.dynamic.loading.MultipleParentClassLoader@7a2a2c83'
instance creation by : ObjenesisInstantiator


この場合クッションになる interface を定義すると動きます。 @RunWith(AndroidJUnit4::class) class HogeTest { private interface Callback : (Boolean) -> Unit @Test fun test() { val listener = mock<Callback>() ... verify(listener)(false) } }

参考 : https://github.com/nhaarman/mockito-kotlin/issues/272



2020年3月18日水曜日

Kotlin メモ : repeat

repeat

指定回数だけ action を実行します。 val count = supportFragmentManager.backStackEntryCount repeat(count) { supportFragmentManager.popBackStack() }



2020年3月16日月曜日

Drag を実装する その2 : GestureDetector

GestureDetector を使うと onScroll() で移動距離を教えてくれる。ただし、GestureDetector は ACTION_UP や ACTION_CANCEL を通知してくれないのが難点である。 class SimpleDragView : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) private val targetView: View private val gestureDetector: GestureDetector init { val size = (100 * resources.displayMetrics.density).toInt() targetView = View(context).apply { layoutParams = LayoutParams(size, size) setBackgroundColor(Color.RED) } addView(targetView) gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() { override fun onScroll( e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float ): Boolean { targetView.translationX -= distanceX targetView.translationY -= distanceY return true } override fun onDown(e: MotionEvent): Boolean { val pointerIndex = e.actionIndex val x = e.getX(pointerIndex) val y = e.getY(pointerIndex) if (!(x.toInt() in 0..width && y.toInt() in 0..height)) { return false } val left = targetView.translationX val right = left + targetView.width val top = targetView.translationY val bottom = top + targetView.height if (!(x in left..right && y in top..bottom)) { return false } return true } }) gestureDetector.setIsLongpressEnabled(false) } override fun onTouchEvent(ev: MotionEvent): Boolean { return gestureDetector.onTouchEvent(ev) || super.onTouchEvent(ev) } }


setIsLongpressEnabled(false) しないと下のように LongPress 判定されたときに onScroll() が呼ばれない。




assets 内のファイルの url

いつも忘れるのでメモっておく

src/main/assets/index.html の url は
file:///android_asset/index.html


2020年3月13日金曜日

Drag を実装する その1 : GestureDetector なし

Drag and scale | Android Developers (MotionEventCompat を使ってたりちょっと古い)を参考に変えたもの class SimpleDragView : FrameLayout { constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super( context, attrs, defStyleAttr ) private var activePointerId = INVALID_POINTER_ID private var lastTouchX = 0f private var lastTouchY = 0f private val targetView: View init { val size = (100 * resources.displayMetrics.density).toInt() targetView = View(context).apply { layoutParams = LayoutParams(size, size) setBackgroundColor(Color.RED) } addView(targetView) } override fun onTouchEvent(ev: MotionEvent): Boolean { when (ev.actionMasked) { MotionEvent.ACTION_DOWN -> { val pointerIndex = ev.actionIndex val x = ev.getX(pointerIndex) val y = ev.getY(pointerIndex) if (!(x.toInt() in 0..width && y.toInt() in 0..height)) { return false } val left = targetView.translationX val right = left + targetView.width val top = targetView.translationY val bottom = top + targetView.height if (!(x in left..right && y in top..bottom)) { return false } lastTouchX = x lastTouchY = y activePointerId = ev.getPointerId(0) } MotionEvent.ACTION_MOVE -> { if (activePointerId == INVALID_POINTER_ID) { return false } val pointerIndex = ev.findPointerIndex(activePointerId) val x = ev.getX(pointerIndex) val y = ev.getY(pointerIndex) if (!(x.toInt() in 0..width && y.toInt() in 0..height)) { return false } val diffX = x - lastTouchX val diffY = y - lastTouchY targetView.translationX += diffX targetView.translationY += diffY lastTouchX = x lastTouchY = y } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { activePointerId = INVALID_POINTER_ID } MotionEvent.ACTION_POINTER_UP -> { if (activePointerId == INVALID_POINTER_ID) { return false } val pointerIndex = ev.actionIndex if (ev.getPointerId(pointerIndex) != activePointerId) { return false } val newPointerIndex = if (pointerIndex == 0) 1 else 0 val x = ev.getX(newPointerIndex) val y = ev.getY(newPointerIndex) if (!(x.toInt() in 0..width && y.toInt() in 0..height)) { activePointerId = INVALID_POINTER_ID return false } lastTouchX = x lastTouchY = y activePointerId = ev.getPointerId(newPointerIndex) } } return true } }


2020年3月11日水曜日

dialogCornerRadius でダイアログの角丸具合を指定する

Android Pie(API Level 28)から ?android:attr/dialogCornerRadius でダイアログの角丸具合を指定できるようになりましたが、AppCompat や MaterialComponents では ?attr/dialogCornerRadius としてバックポートされています。 <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="dialogCornerRadius">12dp</item> </style> (<item name="android:dialogCornerRadius">12dp</item> だと Android Pie(API Level 28)以降だけ角丸になります。)



<item name="dialogCornerRadius">12dp</item>

API Level 21



API Level 27



API Level 28





<item name="android:dialogCornerRadius">12dp</item>

API Level 21



API Level 27



API Level 28





2020年3月10日火曜日

Material Design Components for Android 1.1.0 でボタンのデフォルトカラーが colorAccent から colorPrimary に変わった

Theme.AppCompat.Light.DarkActionBar



Theme.MaterialComponents.Light.DarkActionBar (1.0.0)



Theme.MaterialComponents.Light.DarkActionBar (1.1.0)





何もしてないのに(MDC の version を 1.1.0 に上げたけど...) 色が!変わった!

ピンクはどこの色かというと colorAccent に指定している色です。では緑はどこの色かというと colorPrimary に指定している色です。

ボタン系のデフォルトカラーが 1.1.0 から colorPrimary に変わったようです。



AlertDialog のボタンの色は

?attr/materialAlertDialogTheme に指定されている
ThemeOverlay.MaterialComponents.MaterialAlertDialog

Base.ThemeOverlay.MaterialComponents.MaterialAlertDialog

Base.V14.ThemeOverlay.MaterialComponents.MaterialAlertDialog の <item name="buttonBarPositiveButtonStyle">@style/Widget.MaterialComponents.Button.TextButton.Dialog</item> <item name="buttonBarNegativeButtonStyle">@style/Widget.MaterialComponents.Button.TextButton.Dialog</item> <item name="buttonBarNeutralButtonStyle">@style/Widget.MaterialComponents.Button.TextButton.Dialog.Flush</item>
Widget.MaterialComponents.Button.TextButton.Dialog の <item name="android:textColor">@color/mtrl_text_btn_text_color_selector</item>
@color/mtrl_text_btn_text_color_selector <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:alpha="1.00" android:color="?attr/colorPrimary" .../> <item android:alpha="0.60" android:color="?attr/colorOnSurface" .../> <item android:alpha="1.00" android:color="?attr/colorPrimary" .../> <item android:alpha="0.38" android:color="?attr/colorOnSurface"/> </selector> あー、colorPrimary と colorOnSurface になったのねぇ。

ちなみに 1.0.0 のときの @color/mtrl_text_btn_text_color_selector では colorAccent 使ってます。 <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:color="?attr/colorAccent" android:state_enabled="true"/> <item android:color="@color/mtrl_btn_text_color_disabled"/> </selector>


ボタンの色を変えたいときは 「AlertDialog の Negative ボタンの文字色を変える」 と同じ感じでやればOK <style name="ThemeOverlay.MyApp.MaterialAlertDialog" parent="ThemeOverlay.MaterialComponents.MaterialAlertDialog"> <item name="buttonBarPositiveButtonStyle">@style/Widget.MyApp.Button.TextButton.Dialog</item> <item name="buttonBarNegativeButtonStyle">@style/Widget.MyApp.Button.TextButton.Dialog</item> </style> <style name="Widget.MyApp.Button.TextButton.Dialog" parent="Widget.MaterialComponents.Button.TextButton.Dialog"> <item name="android:textColor">#1565C0</item> </style> AlertDialog.Builder(this, R.style.ThemeOverlay_MyApp_MaterialAlertDialog) .setTitle("Title") .setMessage("Message") .setPositiveButton(android.R.string.ok, null) .setNegativeButton(android.R.string.cancel, null) .show()



2020年3月9日月曜日

Material Design Components for Android 1.1.0 から Checkbox で android:button を指定するなら app:useMaterialThemeColors="false" も必要(なことが多い)

Checkbox のマークをカスタマイズするときは android:button に drawable resource を指定します。

例えば以下のような drawable を用意して Checkbox の android:button に指定したのが次のスクリーンショットの下2つです。 <?xml version="1.0" encoding="utf-8"?> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:bottom="7dp" android:left="7dp" android:right="7dp" android:top="7dp"> <selector> <item android:state_checked="true"> <shape android:shape="oval"> <size android:width="18dp" android:height="18dp" /> <stroke android:width="6dp" android:color="#6666ff" /> </shape> </item> <item> <shape android:shape="oval"> <size android:width="18dp" android:height="18dp" /> <stroke android:width="2dp" android:color="#cccccc" /> </shape> </item> </selector> </item> </layer-list> <CheckBox ... android:button="@drawable/checkbox" ... />



全く同じコードで Activity の theme を Material Design Components for Android 1.0.0 に変えたのが次のスクリーンショットです。



同じようになってますね。

全く同じコードで Activity の theme を Material Design Components for Android 1.1.0 に変えたのが次のスクリーンショットです。



なんということでしょう!android:button で指定した drawable resource が Material Design の theme color で tint されるようになりました。tint で使われる色は colorControlActivated と colorOnSurface です。

この挙動を止めるには app:useMaterialThemeColors="false" を指定します。 <CheckBox ... android:button="@drawable/checkbox" app:useMaterialThemeColors="false" ... />



tint されなくなりました!



2020年3月6日金曜日

Facebook sdk で <meta-data> を設定しているのに初期化されていないと怒られる問題に対応した

いやー、まったくひどい落とし穴でしたよ。

もとの構成はこんな感じです(facebookAppId の値はダミーです)。

build.gradle android { ... defaultConfig { ... manifestPlaceholders = [facebookAppId: "1234567890000000"] } ... } dependencies { ... implementation "com.facebook.android:facebook-android-sdk:6.1.0" implementation "com.facebook.android:facebook-share:6.1.0" } <?xml version="1.0" encoding="utf-8"?> <manifest ...> ... <application ...> ... <meta-data android:name="com.facebook.sdk.ApplicationId" android:value="${facebookAppId}" /> <provider android:name="com.facebook.FacebookContentProvider" android:authorities="com.facebook.app.FacebookContentProvider${facebookAppId}" android:exported="true" /> </application> </manifest>
公式のドキュメント(https://developers.facebook.com/docs/android/getting-started#app_id)には meta-data を AndroidManifest に書けば勝手に初期化処理がされると書いてあるのに、実際に動かすと以下の初期化処理が走っていないというエラーで落ちます。

java.lang.ExceptionInInitializerError
     Caused by: The SDK has not been initialized, make sure to call FacebookSdk.sdkInitialize() first.


調べてみると FacebookSdk の loadDefaultsFromMetadata() の中で meta-data から読みんだ com.facebook.sdk.ApplicationId の値が Float になっていることが判明しました!(facebookAppId が全て数字だからそうなるのでしょうか...)

下記の Object appId が Float になってしまっていたのです。そのためその後の if 文に入らず、applicationId がセットされていませんでした。

FacebookSdk.java // Package private for testing only static void loadDefaultsFromMetadata(Context context) { if (context == null) { return; } ApplicationInfo ai = null; try { ai = context.getPackageManager().getApplicationInfo( context.getPackageName(), PackageManager.GET_META_DATA); } catch (PackageManager.NameNotFoundException e) { return; } if (ai == null || ai.metaData == null) { return; } if (applicationId == null) { Object appId = ai.metaData.get(APPLICATION_ID_PROPERTY); if (appId instanceof String) { String appIdString = (String) appId; if (appIdString.toLowerCase(Locale.ROOT).startsWith("fb")) { applicationId = appIdString.substring(2); } else { applicationId = appIdString; } } else if (appId instanceof Integer) { throw new FacebookException( "App Ids cannot be directly placed in the manifest." + "They must be prefixed by 'fb' or be placed in the string resource file."); } } ... } ここの処理を読むと、appId の最初に fb or FB がついているときはそれを省いた部分を applicationId にしていることがわかります。

そこで以下のように meta-data の value を "fb${facebookAppId}" にしたら怒られなくなりました! <?xml version="1.0" encoding="utf-8"?> <manifest ...> ... <application ...> ... <meta-data android:name="com.facebook.sdk.ApplicationId" android:value="fb${facebookAppId}" /> <provider android:name="com.facebook.FacebookContentProvider" android:authorities="com.facebook.app.FacebookContentProvider${facebookAppId}" android:exported="true" /> </application> </manifest>


ちなみに facebookAppId を string resource で用意した場合 <string name="facebook_app_id">1234567890000000</string> とか android { ... defaultConfig { ... resValue "string", "facebook_app_id", "1234567890000000" } ... } のときは、fb を付けなくても com.facebook.sdk.ApplicationId の値は String として読み込まれます。 <meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id" />



2020年2月26日水曜日

moshi-kotlin (1.9.2) の proguard 設定ではまった

moshi の proguard 設定は https://github.com/square/moshi#r8--proguard にあります。 この設定だけだと

java.lang.IllegalArgumentException: Cannot serialize Kotlin type XX. Reflective serialization of Kotlin classes without using kotlin-reflect has undefined and unexpected behavior. Please use KotlinJsonAdapter from the moshi-kotlin artifact or use code gen from the moshi-kotlin-codegen artifact.

というエラーが出て parse に失敗しました。

ちゃんと以下のように KotlinJsonAdapterFactory をセットしているので、関係ないエラー文言であり紛らわしいです。 val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build()

問題は moshi-kotlin を使うときに追加する proguard 設定にあります。
https://github.com/square/moshi/blob/master/kotlin/reflect/src/main/resources/META-INF/proguard/moshi-kotlin.pro
は 1.9.2 の時点で -keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl -keepclassmembers class kotlin.Metadata { public <methods>; } になっていますが、これを -keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl -keep class kotlin.Metadata { public <methods>; } にすれば正しく parse されるようになりました。


2020年2月16日日曜日

突如 Gmail が ACTION_SENDTO での EXTRA_TEXT を fill しなくなったので対応した

以前のコード val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:$address") .putExtra(Intent.EXTRA_SUBJECT, subject) .putExtra(Intent.EXTRA_TEXT, text) startActivity(intent) 対応したコード val intent = Intent(Intent.ACTION_SEND) // Intent.ACTION_SENDTO でもいけた .putExtra(Intent.EXTRA_EMAIL, arrayOf(address)) .putExtra(Intent.EXTRA_SUBJECT, subject) .putExtra(Intent.EXTRA_TEXT, text) .apply { selector = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:")) } startActivity(intent)


ありがとう https://stackoverflow.com/questions/59836984/email-body-empty-when-select-to-send-email-by-gmail


2020年2月7日金曜日

moshi で List や Map の Generics Type を指定する

いつも忘れるので val type = Types.newParameterizedType( Map::class.java, Hoge::class.java, Fuga::class.java ) val adapter: JsonAdapter<Map<Hoge, Fuga>> = moshi.adapter(type)

2020年1月30日木曜日

フレームワークの preference は API Level 29 (Android 10) で Deprecated になりました。

android.preference パッケージ全体が API Level 29 で Deprecated になりました。
https://developer.android.com/reference/android/preference/package-summary



AndroidX の Preference Library を使いましょう。
https://developer.android.com/reference/androidx/preference/package-summary

Android 10 (API Level 29) からは ClipboardManager の setPrimaryClip() にプロパティアクセスできない

API Level 29 未満はこう書けた val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboard.primaryClip = ClipData.newPlainText("ラベル", "クリップしたいメッセージ") API Level 29 からはプロパティアクセスできない。 val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager clipboard.setPrimaryClip(ClipData.newPlainText("ラベル", "クリップしたいメッセージ")) ClipboardManager で定義されている getPrimaryClip() は @Nullable な ClipData を返すが、setPrimaryClip() に渡す ClipData は @NonNull が要求されるようになったからです。 public @Nullable ClipData getPrimaryClip() { ... } public void setPrimaryClip(@NonNull ClipData clip) { ... }

2020年1月28日火曜日

Atom 1.40.0 に Juno を入れようとすると Error: Installing "julia-client@0.11.3" failed で失敗する

Atom に uber-juno パッケージ(Juno)を入れようとしたが
Error: Installing "julia-client@0.11.3" failed
で失敗してしまう。


結論としては、Atom を最新版(手元では 1.43.0)にすれば OK。




2020年1月19日日曜日

キリマンジャロ登山に持って行ったもの

旅行記はこちら
「キリマンジャロ登山 2020 : タンザニア旅行記」

登山用品

夏用の寝袋 x 2夏用の寝袋1枚だと絶対寒い。
2枚重ねで快適だった。
マットTHERMAREST Zライト ソル
ザック自分用
ザックポーターさんに預ける用
登山靴モンベル
ストック
ダウンジャケットバーグハウス*1
防風雨具上下バーグハウス
フリース x 2モンベルとシェルパギア*2
長袖シャツ x 2モンベルとマムート
ズボンモンベル
フリースパンツモンベル寝る用
タイツ x 3ジオライン x 2
メリノウール x 1
靴下 x 2モンベル普通用とアルパイン用
長袖インナー x 3ジオラインとメリノウール
下着 x 4ジオラインとメリノウール
毛糸の帽子山頂アタックのとき使った。
キャップ帽山頂アタック以外で使った。
ネックウォーマーモンベル
インナー手袋 x 2モンベルとバーグハウス
防風防水手袋モンベル
防寒テムレス*3
ヘッドライト
サングラス
折り畳みコップ歯磨きに使った。
ハイドレーションモンベル
ウォーターバッグ 2L
防水バッグ 10L x 2ザックの中で使った。
折り畳み傘モンベルテント場で雨の中トイレに行くときに使った。

*1 ダウンパンツ無しで問題なかった(持ってないし)。
*2 シェルパギアはネパールのブランド。ネパールのナムチェバザールで買った。
*3 山頂アタックのときはインナー手袋 + 防寒テムレスだったが指が冷たくなった。途中からカイロを手袋の中にいれたら快適になった。
* チェーンスパイクを持って行っていたが必要ないと言われたので山には持って行かなかった。実際必要なかった。

電子機器

スマホPixel 3
ビデオカメラOsmo Pocketよかった
モバイルバッテリーAnker PowerCore 10000 PD Redux容量大きくてよかった。
ソーラーチャージャーRAVPower ソーラーチャージャー ソーラー充電器 16W曇りでも発電できててすごかった。


食品

インスタント味噌汁ハナマルキ かるしお おいしい減塩 即席しじみ汁8食あってよかった
ゼリードリンク x 2アミノバイタル GOLD
アミノバイタル パーフェクトエネルギー
ダイエットはダメ。エネルギーが取れるやつ。
プロテインバー x 8ご飯がたくさん出たので行動食は全然減らなかった。
塩タブレット20個くらい


消耗品など

ダイソーの洗濯紐 + 洗濯バサミ山には持って行き忘れたが登山前後の宿で活躍した。
ワイヤー式ダイヤル錠使わなかった。
輪ゴムいろいろ活躍した。
セロテープそこそこ活躍した。
ダイソーの財布ぺらい財布がほしかったので
ポケットティッシュ普通のやつと鼻セレブ
トイレットペーパー1ロールシングルでよい
ポケットウェットティッシュ
ミニカイロ x 10桐灰のマグマ ミニ山は寒いので外に出していては暖かくならない。ポケットに入れておけば発熱してくる。
くつ下用カイロ x 5興和のホッカイロ くつ下用寝るとき用
ボディーシートエージー24 クリアシャワーシートシャワーなど無いので必須
日焼け止め好きなの使えばいいと思う
紫外線防止リップクリームNeutrogena lip moisturizer好きなの使えばいいと思う
ドライシャンプー資生堂 FRESSY ドライシャンプーよかった
虫除けサラテクトフレッシュミスト
歯ブラシ
100均のサンダルテント場で便利
エチオピア航空の機内でもらった靴下いろいろ活躍した。
ビニール袋10枚くらい


痒み止めムヒアルファSIIあってよかった
鎮痛剤バファリン高山病用に持って行ったが使わなかった
ダイアモックス効いたと思う。副作用の指先の痺れが出た。
絆創膏
鼻炎薬下山後にちょっと使った。
Protect J1必須!!!



2020年1月18日土曜日

キリマンジャロ登山 2020 : タンザニア旅行記



動画はこちら https://www.youtube.com/playlist?list=PLU-4gjX0n_0UgVM7Ca98uiiEcW53CrOFX



2019年12月31日

成田空港で仁川(インチョン)国際空港経由アディス・アベバ(Addis Ababa)国際空港行のエチオピア航空ET673に搭乗。初エチオピア航空。 搭乗時刻は20時半くらい。仁川国際空港までは2時間40分と短いフライトながら機内食(晩ごはん)が出た。機内食が出たのは21時半くらい。キムチがおいしかった。



同じ機体だし仁川国際空港での乗り換え時間が1時間なので降りないと思っていたら降ろされました。セキュリティゲートを通ってぐるっと回って降りたゲートの真上の搭乗ゲートまで移動。搭乗ゲートに着いたのが23時45分くらい。

2020年1月1日

搭乗ゲート前で新年を迎えた。騒ぐ人もおらず、皆でカウントダウンするでもなく、静かな新年。ゲートのディスプレイに happy new year :) の文字。



搭乗時刻は0時5分とか。仁川国際空港から出発して間もなくまた機内食(晩ごはん)が出たが、さすがにあんまり食べられず。その後けっこうぐっすり寝た。機内でもらった靴下が後々登山で役立つことに。

アディス・アベバ(Addis Ababa)国際空港到着前に再度機内食(朝ごはん)が出た。



仁川国際空港からアディス・アベバ(Addis Ababa)国際空港までは12時間40分くらい。アディス・アベバ(Addis Ababa)国際空港に着いたら下の階に降りてセキュリティゲートを通って上の階に移動。次のフライトまで2時間半くらいあるので地球の歩き方を読んだりして時間を潰した。



搭乗ゲート前の自動販売機で豆のようなものが売っていた。





空港WiFiもあった。搭乗時刻になったら搭乗ゲートを通ってバスに乗り飛行機のところまで移動し搭乗。



アディス・アベバ(Addis Ababa)国際空港からキリマンジャロ(Kilimanjaro)国際空港までは2時間35分とこれまた短いフライトながら機内食が出た。ライスのサラダ(冷たい)がアフリカっぽい。



晴れていてずっとアフリカの大地が見えていた。エチオピアの間はわりと茶色だったがケニアあたりから緑っぽくなっていった。

キリマンジャロ国際空港に着いたらステップを降りて徒歩で空港の建物まで移動。



入国書類を書いて入国審査を受ける。入国審査をパスしたら荷物を回収。



空港を出たら宿の迎えの人が来てました。ちょっと待ってもらって空港のATMでタンザニアシリングをキャッシング。とりあえず10万tzシリング(約5千円)。空港の駐車場に青い綺麗な鳥がいた。



迎えの車はTOYOTA車。タンザニアは日本の中古車だらけ。道は日本と同じく右ハンドル左側通行。



SIMを買いたいと伝えると Moshi 中心部のSIM屋に寄ってくれた。



向かいはバスターミナル



無造作にカゴにマニキュアが放り込まれている。売り物なのだろうか。



Moshi の宿(The Comfy Stay Hotel)に15時くらいに到着。ウェルカムドリンクにスイカジュースが出た。暑くて喉渇いていたのでうれしい。美味しかった。



とりあえずシャワーを浴びる。ぬるい水にしかならない。暑いから気にならないけど。

部屋のベッドには蚊帳がついている。カーテンがピンクなので部屋がピンク...



宿の2階に食堂(?)スペースがある。まったりできる。



宿のオーナーが紹介してくれたエージェント会社のボスが来てキリマンジャロ登山のプランを説明してくれた。マチャメ(Machame)ルートとレモショ(Lemosho)ルートで迷っていたが、マチャメルート7日間のプランにした。レモショルートだと8日間になってさらにスタート地点がマチャメルートより遠いので 400$ くらい高くなると言われたので。マチャメルート7日間で1人 1500$ になりました。このうち 700$ を手持ちから払い、残りはtzシリングで払うことに。

一番登山者の多いルートはマラングルートだが、当初から我々の選択肢にはなかった。短い日数で登れてハットが整備されている(マラングルート以外はテント泊)のが利点だが、景色がずっと同じで単調だし一気に登るので十分に高度順応することができないという欠点がある。我々は急ぐ旅でもなく登頂率を上げる方が大事だったので、4600mまで上がってから一度3900mに降りることで高度順応しやすくなるマチャメルートかレモショルートにしようと思っていた。

ボスが我々の装備をチェックしたいというので部屋の前に装備品を広げてチェックしてもらった。

19時くらいに宿を出てエージェント会社のオフィスに向かう。晴れてて宿からキリマンジャロの頂上が見えた。



ATMに寄って残りの180万tzシリングをキャッシング。オフィスで180万tzシリングを数え、申込書に記入。

↓ 180万tzシリング = 1万tzシリング札180枚


Fresh Restaurant で夕食を食べるつもりだと伝えていたのでオフィスからレストランまで送ってくれた。40分後に来てくれるように頼む。 Fresh Restaurant はカウンターで食べ物を選ぶ形式だった。選んだものを盛った皿をレンチンしていた。なるほど。盛りがいい。4人で25000tzシリング(1人300円くらい)だった。







エージェント会社の車で宿まで送ってもらう。

今回高山病予防薬のダイアモックスを処方してもらっていた(はじめて)。朝と夜に半錠ずつ飲む。高地にあがる前日から飲むのでこの日の夜から飲み始めた。

2020年1月2日 : 登山1日目

朝起きてパッキング。山に持っていかない荷物(街歩き用の靴とか)は宿にデポするので分ける。

宿の朝ごはんを食べる。コーヒー、ホットミルク、お湯がポットで用意されていて、紅茶のTバッグも用意されている。卵料理とパンケーキの種類が指定できて、私はスパニッシュオムレツとプレーンパンケーキにした。フルーツも出た。







宿に迎えがきてオフィスまで移動。



途中キリマンジャロがよく見えた。



オフィスで装備の内容をチェック。今回のガイドが紹介される。チーフガイドのフランクとガイドのジャスティンの2人。シェフも紹介される。ポーターに預ける荷物と自分の荷物を分けて、預ける荷物を渡す。ポーターさんたちがテントなどの荷物をマイクロバスの屋根に積んでいた。



我々の預ける荷物も積んで、ポーターさん含めみんなでオフィスからマイクロバスでゲートに向かう。途中で一度止った。どうやら食材などを買っていたようだ。ゲートまでの間にスワヒリ語で自己紹介したら、その後サシャという事務職っぽい女の子が隣にきてスワヒリ語をいろいろ教えてくれた。

10時50分くらいにマチャメゲート(Machame Gate)に着いた。



屋根のある休憩スペースのようなところがあり、そこで昼ごはんを食べろとランチボックスを渡された。まだ11時、あんまりお腹はすいていない。半分くらいだけ食べた。



トイレに行ったあと暇なのでポーターさんが集まってるところをちょっと見に行った。ポーターさんは担げる荷物の重さに制限があるらしく重さを図る列ができていた。 準備ができたのかガイドが呼びにきて、一緒にゲートのレセプションに行って一人ずつサイン(名簿帳みたいなノートに記入)した。

このクリスマスツリーはいつまで飾るのだろうか。



レセプションには Merry Christmas と Happy New Year の旗。この旗は配られてでもいるのか旅の間何回も見かけることになる。



12時くらいにようやく出発。

最初はジャスティンだけ。今日はずっとジャングル。



途中トイレが2箇所くらいある。ぼっとんトイレ。鍵が閉まらなかったりする。



途中白黒の鳥が道にいたが写真を撮る前に飛び立ってしまった。残念。

滝があった。



5時間半くらい歩いて Machame Camp に到着。標高は2835m。



ポーターさんが先に着いてテントを設営してくれている。まずはレセプションでサイン。



ポーターさんから荷物をうけとってテントで一休みしていると、洗面器にお湯を入れて持ってきてくれた。ご飯を食べる用の高さのあるテントが設営されていて、中にテーブルと椅子が置いてある。晩ごはんまでの間ポップコーンとお茶がでた。お湯が入った大きいポットが来て、紅茶のティーバッグ、インスタントコーヒー、ミロ、チョコレートドリンク、粉ミルクがあって好きなものが飲める。私は持ってきたインスタントみそ汁を飲んだ。みそ汁うまい。



シェフがいるテントで料理を作っている。



晩御飯はキューカンバースープ、茹でたじゃがいも、魚のフライ、トマトベースのベジタブルソース。じゃがいもが多い!





2020年1月3日 : 2日目

6時くらいに起きて外でまったり。昨日は見えなかった山頂がはっきり見えた。



明け方から鳥の鳴き声がけっこうしていた。



7時くらいにお湯を持ってきてくれた。



7時半くらいから朝ごはん。お粥がおいしい。他にパン、パンケーキ、プレーンオムレツ、ウィンナーとピーマンの炒めものも。







朝ごはんを食べ終わったらパッキング。

我々のテントが片付けられていく。



8時半くらいに出発。

すぐにジャングルを抜け草原帯に入る。そこそこ傾斜のある岩場を登ったり。

振り返ると Machame Camp やメルー山が見える。





洞窟がある。





Shira Cave Camp 直前の岩登り



4時間半くらい歩いて13時くらいに Shira Cave Camp に到着。標高は3750m。



首の後が白いカラスが何羽もいて登山者の残り物を狙っていた。



大きい新しそうなトイレ。



レセプションでサインしてテントでひと休み。

2人で使う寝る用テントはけっこう広い。



13時45分くらいに昼ごはん。昼ごはんはナポリタンと揚げたチキン。ナポリタンは茹で足りない感じだったが美味しかった。



16時くらいにポップコーンが出た。



ポーターさんとかが集まって輪になって歌い踊っていた。

18時半くらいに晩ご飯。晩御飯はキューカンバースープ、ライス、キャベツの炒めたやつ、ビーフのハヤシライスみたいなやつ、ベジタブルソース(人参、ナス、カリフラワーとか)だった。やっぱりキューカンバースープ美味しい。





2020年1月4日 : 3日目

6時くらいに起きて着替えとか準備とか。

7時過ぎくらいに朝ごはん。フルーツ、お粥、トースト、パンケーキ、オムレツ。



8時半くらいに出発。草原帯が終わってここからは岩ばかりになる。まずは Lava Tower Campに向かう。





右に見えるのはトイレ。



途中から雲が出てきて真っ白になってしまう。12時半くらいに Lava Tower Campに到着。標高は4600m。



雲の中に入っていて風が強くて寒かった。ポーターさんが先に行ってご飯を食べる用のテントを設置してくれている。13時くらいから昼ごはん。 昼ごはんはパン、フルーツ(パイナップルとはっさくみたいなやつ(チュンガ))、チキンとかじゃがいもとかが入ったトマトベースっぽいシチュー。じゃがいもの他にサツマイモみたいなちょっと甘い芋も入っていた。





昼ごはんを食べ終わるとちょっと雲が晴れて Lava Tower(だと思う岩)が見えた。



14時前くらいに出発。ここからは下り。雨がパラパラ降っていて、チーフガイドのフランクはタンザニア国旗カラーの傘を差していた。



途中にたくさん Dendrosenecio kilimanjari が生えていた。



15時15分くらいに Baranco Camp に到着。標高は3900m。



まずはレセプションでサイン。



テントで一休みしたあとお茶を飲みながらトランプで時間を潰す。



18時くらいから晩御飯。揚げたドーナツのようなものが出た。味はサーターアンダギーみたいな感じ。



さらにキューカンバースープとトマト味付けの山盛りマカロニと牛肉の炒めたやつ。



2020年1月5日 : 4日目

4時過ぎにトイレに行った時にきれいに晴れていたので、Pixel 3の星空撮影モードにチャレンジしてみた。



6時くらいに起きて着換えとかパッキングの準備とか。

朝はよく晴れて山頂が見えた。



7時前にお湯をもってきてくれた。



7時半くらいから朝ごはん。朝ごはんはフルーツ、お粥、トースト、オムレツ、炒めたウィンナー、甘くないバナナの揚げたものだった。甘くないバナナは食べた感じは完全に芋。以後勝手に芋バナナと呼ぶことにする。





出発準備中



8時半くらいに出発。すぐに Baranco Wall という急登の岩登りエリアに入る。頭に荷物を載せてこの岩を登るポーターさんはすごい。







途中メルー山がよく見える。



Baranco Wall のてっぺんでちょっと休憩。ここの標高は4200mくらい。



ここからはだらだら下ったあと少し登り返してトラバースをやや歩く。



途中にロべリアという葉の内側に花が咲いている植物があった。



陽気なイングランド人団体と遭遇。一緒に写真を撮った。

最後がっと降りて川を渡り、がっと登る。





11時45分くらいに Karanga Camp に到着。標高は3995m。



左はアシスタントガイドのマイコ。



12時50分くらいから昼ごはん。昼ごはんは山盛りのフライドポテト、フライドチキン、サラダ、キューカンバースープ、フルーツ(パイナップルとはっさくみたいなやつ)。





パイナップルがまじで美味しいんだわー。ちなみにスワヒリ語でパイナップルは nanasi。



14時くらいに出発。Karanga Camp から Barafu Camp まではずっと登り。途中みぞれが降ってきて雷がゴロゴロいい、風も強く顔に当たる雨が痛かった。





16時45分くらいに Barafu Camp に到着。標高は4673m。レセプションでサインして17時くらいにテントに移動。



テント場が斜面でトイレまでの道のりが大変。今までの Camp 場ではスリッパで移動していたがさすがにスリッパでは無理。

↓右側の青い屋根がトイレ


19時くらいから晩ごはん。晩ごはんは麸を揚げたようなやつ、キューカンバースープ、塩パスタにベジタブルソース。







明日は山頂アタックで朝4時出発なので晩ごはん後すぐに就寝。23時半くらいにトイレに行こうとしたら、ウェイターもしているポーターさんが起きていて、まだ出発じゃないよと慌てて言ってきた。トイレに行くんだといったら安心していた。元々 Karanga Camp で一泊して次の日の早い時間に Barafu Camp に着いて寝て午前1時出発だったのを、我々が元気で健脚なので Karanga Camp をスキップして1日早く Barafu Camp に来て午前4時出発に変えたので、私が間違って起きたのかと思ったようだった。トイレから帰ってくると、途中で会ったイングランド人グループがちょうど山頂に向けて出発していった。

2020年1月6日 : 5日目

2時半くらいに起きて着替えやら準備。ハイドレーションのチューブが凍るという話を読んでたので、エチオピア航空でもらった靴下をチューブに巻きつけてみた。

星がきれいだったので Pixel 3 の星空撮影モードで撮ってみた。



3時半くらいの朝ごはんは飲み物とビスケット。前日にチーフガイドのフランクから山頂アタックのときはたくさん食べると吐いてしまうから朝ごはんは飲み物とビスケットだよと言われていた。アタックの気合を入れるため(?)持ってきたインスタントみそ汁を飲んだ。やっぱりみそ汁はうまい。

4時くらいに出発。最初は日の出前なのでヘッドライトをつけて登る。6時前から徐々に空が明るくなってくる。



6時半前にはもうヘッドライトいらないよと言われた。





早く動くと心拍数が上がり過ぎてしまう。意識してすごーくゆっくり登る。できる限り腹式呼吸するように。

8時半くらいに朝見かけたイングランド人グループと思われる人たちとすれ違った。



途中でエナジーゼリーを一パック半飲んだ。



Stella Point 直前から雪が出てきた。



10時に 5758m の Stella Point に到着。いやー、空気が薄い!



ここからお鉢に沿って150mくらい登ると山頂。ちょっとだけ休憩して出発。ここからは雪がある。







ゴールの看板が見える



左下の斜面に氷河が見える。空気が薄くて常に全力疾走みたいな感じですぐに息が上がってしまう。途中から雪の上を歩いて11時15分についに山頂に到着。



途中で何回か会ったアメリカ人カップルが直前に着いていた。写真を撮ったり、おめでとうと言い合ったり、残ってたエナジーゼリー半パックを飲んだりした。 天気のコンディションがとても良く、風もそれほどひどくなかったので5分くらい手袋なしでも大丈夫だった。

雪があってすごく眩しい。やばそう。日焼け止めを塗り直す。



下山は登りと違ってサクサク降りていく。下山ルートは登りと別で、途中から富士山の砂走りみたいな感じだった。足がけっこう疲れていて踏ん張りが効かなくて大変だった。





14時過ぎにテント場に戻ってきた。7時間かけて登ったのを3時間で降りてきた。さすがに疲れたー。ポーターさんがくれたマンゴージュースがめっちゃおいしかった!そしてとてもお腹がすいた。



昼ごはんを用意してくれるという。寝るテントまで行く力が湧かなかったので、ご飯を食べるテントで座って待つことにした。昼ごはんは具沢山シチュー、フルーツだった。パイナップルがおいしかったー。



チーフガイドのフランクから1時間下の High Camp まで移動するのはどうかという提案があった。3950m まで高度が下がるので体が楽になるし、Barafu Camp よりも天気が安定している。他のメンバーと相談した結果、もう動きたくないという人がいたのと、そもそも今からパッキングするなら休む時間がないのでは、ということでそのまま Barafu Camp に泊まることになった。

テントで1時間半くらい寝たらだいぶ疲れが回復した。19時から晩ごはん。晩ごはんは具無しインスタントラーメン、クミンご飯に中華っぽいあんかけソースだった。インスタントラーメンがとても美味しかった。







ご飯の後チーフガイドのフランクに Song of Kilimanjaro の歌詞をノートに書いてもらった。 疲れていたのでご飯を食べたらすぐにテントに戻って寝た。

2020年1月7日 : 6日目

5時半に起きてパッキングとか着替えとかもろもろ準備。朝焼けがきれい。



6時半くらいから朝ごはん。朝ごはんは、フルーツ、お粥、オムレツ、トースト、パンケーキ、焼いたソーセージ。





8時前くらいに出発。Barafu Camp に到着したときは天気が悪くて看板で写真を撮れなかったのでしばし写真を撮る。



Barafu Camp からしばらくは風が強い。



途中ダウンした登山者を運ぶ手押し車が置いてあった。



少し下ると風が当たらなくなって一気に暑くなる。High Camp の直前にヘリポートがあった。病人を搬送する用らしい。



9時過ぎに High Camp に到着。標高は 3950m。



もともと今日は Mweka Camp に泊まる予定だったが、フランクから今日ゲートまで降りないかと提案され、支払済みだけど使わなかった2日〜3日の宿代を今日の分に当てられるならOKだよと伝えた。フランクからエージェント経由で宿に話をしてくれることになった。

9時半くらいに High Camp を出発。ここからは草原帯。



Mwaka Camp の直前にジャングルに入り、10時半くらいに Mweka Camp に到着。標高は3100m。3000m超えているのにすごく酸素を感じる!



Mweka Camp に着く直前にフランクが宿代の話が通ったことを教えてくれた。ここでしばし休憩。私は水が少なくなっていたのでフランクに水が欲しいといったらシェフの人に言って用意してくれた。



11時くらいに Mweka Camp を出発。ジャングルの中をひたすら下る。





別々の木がくっ付いたトンネルを抜ける(木の名前を忘れた)。



ゲートから20分くらいのところでレンジャーの小屋を工事していた。ここまでは車が入れるようになっている。





13時半くらいにゲートに到着。お疲れ様でした!



レセプションでサインする。見たことのある Merry Christmas と Happy New Year の旗。そしてクリスマスツリーもある。



マイクロバスに乗るとみんなにキリマンジャロビールが振る舞われる。おいしい。



タンザニアでは飲酒は18歳からだ。



そのあとみんなで song of kilimanjaro を歌ってくれた。コーヒー畑を横目に見ながら Moshi へ向かう。Moshi のオフィスに2時過ぎに到着。



荷物を降ろして近くのレストランに行って昼ごはんを食べる。ライスと芋バナナのカレーみたいなやつだった。



オフィスに戻って預けていた自分たちの荷物を確認。その間にフランクが登頂証明書に記入していた。オフィスの前の広場にみんなで集まって登頂証明書の受賞式をしました。



その後サプライズで用意されていたケーキを切って互いに食べさせ合うのをやりました。最後に全員で写真を撮ってオフィスを後にし宿に向かいました。



とてもとても楽しかった。