言いたいことはそれだけか

KotlinとかAndroidとかが好きです。調べたことをメモします。٩( 'ω' )و

Modifier.verticalScroll() を親に設定すると子のfillMaxHeight() が無視される仕組みを調べた

Android ComposeのModifier.verticalScroll()が親のcomposableに設定されていた場合に、子に Modifier.fillMaxHeight()を設定してもこれが無視される現象に遭遇した。どういった仕組みでそうなっているのか内部実装を読んだ時のメモを残す。

知りたいこと

例えば、以下のように親に verticalScroll() がついていないと子のfillMaxHeight()は期待通りに動作する。

@Composable
fun Sample(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier
    ) {
        Text(
            modifier = Modifier.fillMaxHeight(),
            text = "Without verticalScroll",
        )
    }
}

しかし親に verticalScroll() をつけると挙動が変わりfillMaxHeight()が無視される。

@Composable
fun Sample(modifier: Modifier = Modifier) {
    Column(
        modifier = modifier.verticalScroll(rememberScrollState())
    ) {
        Text(
            modifier = Modifier.fillMaxHeight(),
            text = "With verticalScroll",
        )
    }
}

具体的にverticalScroll()が何をしてこの差が生まれるのか知りたい。

公式ドキュメント

developer.android.com

Modify element to allow to scroll vertically when height of the content is bigger than max constraints allow.

とあるので、コンテンツのサイズが親のサイズに依存しているとscrollするかどうかの計算ができないだろうから無視されるんだろうなーという空気を感じる。

内部実装

Modifier.verticalScroll() が中で何をやっているかを読んでいく。 色々省略するが、ScrollingLayoutModifierが使われるのがポイント。

fun Modifier.verticalScroll(
    state: ScrollState,
    enabled: Boolean = true,
    flingBehavior: FlingBehavior? = null,
    reverseScrolling: Boolean = false
) = scroll(
    state = state,
    isScrollable = enabled,
    reverseScrolling = reverseScrolling,
    flingBehavior = flingBehavior,
    isVertical = true
)
@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.scroll(
    state: ScrollState,
    reverseScrolling: Boolean,
    flingBehavior: FlingBehavior?,
    isScrollable: Boolean,
    isVertical: Boolean
) = composed(
    factory = {

// いろいろ省略

        val layout =
            ScrollingLayoutModifier(state, reverseScrolling, isVertical)
        semantics
            .clipScrollableContainer(orientation)
            .overscroll(overscrollEffect)
            .then(scrolling)
            .then(layout)
    },

で、このScrollingLayoutModifier()が子のconstraints を上書いてしまっていて、maxHeightを設定していたとしてもInfinityにしてしまっている。

private data class ScrollingLayoutModifier(
    val scrollerState: ScrollState,
    val isReversed: Boolean,
    val isVertical: Boolean
) : LayoutModifier {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints
    ): MeasureResult {
        checkScrollableContainerConstraints(
            constraints,
            if (isVertical) Orientation.Vertical else Orientation.Horizontal
        )

        val childConstraints = constraints.copy(
            maxHeight = if (isVertical) Constraints.Infinity else constraints.maxHeight, // ★ここ!!!
            maxWidth = if (isVertical) constraints.maxWidth else Constraints.Infinity
        )
        val placeable = measurable.measure(childConstraints)
        val width = placeable.width.coerceAtMost(constraints.maxWidth)
        val height = placeable.height.coerceAtMost(constraints.maxHeight)

// いろいろ省略

        return layout(width, height) {
            val scroll = scrollerState.value.coerceIn(0, side)
            val absScroll = if (isReversed) scroll - side else -scroll
            val xOffset = if (isVertical) 0 else absScroll
            val yOffset = if (isVertical) absScroll else 0
            placeable.placeRelativeWithLayer(xOffset, yOffset)
        }
    }

constraintsのmaxHeightがConstraints.Infinityに設定されてしまったせいで子の高さはUnboundedになるので親の影響を受けない。よって子であるText分の高さになる。

ComposeのLayoutやらConstraintについてはこちらが参考になる。

developer.android.com

Typescript Tupleの実態を知りたくてcompilerのコードを読んだ時のメモ

タイトルの通り。TypescriptにはTupleがあると知って、どうやって普通の配列と区別しているのか知りたくてcompilerのコードを読んだ時のメモ。 TS compilerのコードを読むのは初めてなので、間違っているかもしれない。変なことを書いていたらコメント等で教えてもらえると嬉しいです。

結論

TS compilerがsignatureを見ているところで、signatureの型が複数あったら(= Tupleだったら)SyntaxKind.TupleType というTuple用のメンバをnodeに設定して TupleTypeNode という型を使っっている。普通の配列は ArrayTypeNode になるので区別できる。

ちなみにJSにはTupleは用意されていないので、TSでTupleを書いてもtranspileされたJSのコードは普通の配列と同じ見た目をしている。

const thisIsTuple: [number, string, boolean]  = [1, "1", true];

↓これ(ts)がこう(js)なる

const thisIsTuple = [1, "1", true];

読んだコードのメモ

https://github.com/microsoft/TypeScript/blob/00dc0b6674eef3fbb3abb86f9d71705b11134446/src/services/refactors/convertOverloadListToSingleSignature.ts

    function getNewParametersForCombinedSignature(signatureDeclarations: (MethodSignature | MethodDeclaration | CallSignatureDeclaration | ConstructorDeclaration | ConstructSignatureDeclaration | FunctionDeclaration)[]): NodeArray<ParameterDeclaration> {
        const lastSig = signatureDeclarations[signatureDeclarations.length - 1];
        if (isFunctionLikeDeclaration(lastSig) && lastSig.body) {
            // Trim away implementation signature arguments (they should already be compatible with overloads, but are likely less precise to guarantee compatability with the overloads)
            signatureDeclarations = signatureDeclarations.slice(0, signatureDeclarations.length - 1);
        }
        return factory.createNodeArray([
            factory.createParameterDeclaration(
                /*modifiers*/ undefined,
                factory.createToken(SyntaxKind.DotDotDotToken),
                "args",
                /*questionToken*/ undefined,
                factory.createUnionTypeNode(map(signatureDeclarations, convertSignatureParametersToTuple))
            )
        ]);
    }
    function convertSignatureParametersToTuple(decl: MethodSignature | MethodDeclaration | CallSignatureDeclaration | ConstructorDeclaration | ConstructSignatureDeclaration | FunctionDeclaration): TupleTypeNode {
        const members = map(decl.parameters, convertParameterToNamedTupleMember);
        return setEmitFlags(factory.createTupleTypeNode(members), some(members, m => !!length(getSyntheticLeadingComments(m))) ? EmitFlags.None : EmitFlags.SingleLine);
    }
    function convertParameterToNamedTupleMember(p: ParameterDeclaration): NamedTupleMember {
        Debug.assert(isIdentifier(p.name)); // This is checked during refactoring applicability checking
        const result = setTextRange(factory.createNamedTupleMember(
            p.dotDotDotToken,
            p.name,
            p.questionToken,
            p.type || factory.createKeywordTypeNode(SyntaxKind.AnyKeyword)
        ), p);
        const parameterDocComment = p.symbol && p.symbol.getDocumentationComment(checker);
        if (parameterDocComment) {
            const newComment = displayPartsToString(parameterDocComment);
            if (newComment.length) {
                setSyntheticLeadingComments(result, [{
                    text: `*
${newComment.split("\n").map(c => ` * ${c}`).join("\n")}
 `,
                    kind: SyntaxKind.MultiLineCommentTrivia,
                    pos: -1,
                    end: -1,
                    hasTrailingNewLine: true,
                    hasLeadingNewline: true,
                }]);
            }
        }
        return result;
    }

https://github.com/microsoft/TypeScript/blob/44e8244dd96daadfafba75d1c1c2645a42a493ca/src/compiler/factory/nodeFactory.ts

    // @api
    function createTupleTypeNode(elements: readonly (TypeNode | NamedTupleMember)[]) {
        const node = createBaseNode<TupleTypeNode>(SyntaxKind.TupleType);
        node.elements = createNodeArray(parenthesizerRules().parenthesizeElementTypesOfTupleType(elements));
        node.transformFlags = TransformFlags.ContainsTypeScript;
        return node;
    }
    // @api
    function createNamedTupleMember(dotDotDotToken: DotDotDotToken | undefined, name: Identifier, questionToken: QuestionToken | undefined, type: TypeNode) {
        const node = createBaseDeclaration<NamedTupleMember>(SyntaxKind.NamedTupleMember);
        node.dotDotDotToken = dotDotDotToken;
        node.name = name;
        node.questionToken = questionToken;
        node.type = type;
        node.transformFlags = TransformFlags.ContainsTypeScript;

        node.jsDoc = undefined; // initialized by parser (JsDocContainer)
        node.jsDocCache = undefined; // initialized by parser (JsDocContainer)
        return node;
    }
    // @api
    function createUnionTypeNode(types: readonly TypeNode[]): UnionTypeNode {
        return createUnionOrIntersectionTypeNode(SyntaxKind.UnionType, types, parenthesizerRules().parenthesizeConstituentTypesOfUnionType) as UnionTypeNode;
    }
    function createUnionOrIntersectionTypeNode(kind: SyntaxKind.UnionType | SyntaxKind.IntersectionType, types: readonly TypeNode[], parenthesize: (nodes: readonly TypeNode[]) => readonly TypeNode[]) {
        const node = createBaseNode<UnionTypeNode | IntersectionTypeNode>(kind);
        node.types = factory.createNodeArray(parenthesize(types));
        node.transformFlags = TransformFlags.ContainsTypeScript;
        return node;
    }
    // @api
    function createNodeArray<T extends Node>(elements?: readonly T[], hasTrailingComma?: boolean): NodeArray<T> {
        if (elements === undefined || elements === emptyArray) {
            elements = [];
        }
        else if (isNodeArray(elements)) {
            if (hasTrailingComma === undefined || elements.hasTrailingComma === hasTrailingComma) {
                // Ensure the transform flags have been aggregated for this NodeArray
                if (elements.transformFlags === undefined) {
                    aggregateChildrenFlags(elements as MutableNodeArray<T>);
                }
                Debug.attachNodeArrayDebugInfo(elements);
                return elements;
            }

            // This *was* a `NodeArray`, but the `hasTrailingComma` option differs. Recreate the
            // array with the same elements, text range, and transform flags but with the updated
            // value for `hasTrailingComma`
            const array = elements.slice() as MutableNodeArray<T>;
            array.pos = elements.pos;
            array.end = elements.end;
            array.hasTrailingComma = hasTrailingComma;
            array.transformFlags = elements.transformFlags;
            Debug.attachNodeArrayDebugInfo(array);
            return array;
        }

        // Since the element list of a node array is typically created by starting with an empty array and
        // repeatedly calling push(), the list may not have the optimal memory layout. We invoke slice() for
        // small arrays (1 to 4 elements) to give the VM a chance to allocate an optimal representation.
        const length = elements.length;
        const array = (length >= 1 && length <= 4 ? elements.slice() : elements) as MutableNodeArray<T>;
        array.pos = -1;
        array.end = -1;
        array.hasTrailingComma = !!hasTrailingComma;
        array.transformFlags = TransformFlags.None;
        aggregateChildrenFlags(array);
        Debug.attachNodeArrayDebugInfo(array);
        return array;
    }

https://github.com/microsoft/TypeScript/blob/00dc0b6674eef3fbb3abb86f9d71705b11134446/src/compiler/factory/parenthesizerRules.ts

    function parenthesizeConstituentTypesOfUnionType(members: readonly TypeNode[]): NodeArray<TypeNode> {
        return factory.createNodeArray(sameMap(members, parenthesizeConstituentTypeOfUnionType));
    }
    function parenthesizeConstituentTypeOfUnionType(type: TypeNode) {
        switch (type.kind) {
            case SyntaxKind.UnionType: // Not strictly necessary, but a union containing a union should have been flattened
            case SyntaxKind.IntersectionType: // Not strictly necessary, but makes generated output more readable and avoids breaks in DT tests
                return factory.createParenthesizedType(type);
        }
        return parenthesizeCheckTypeOfConditionalType(type);
    }

https://github.com/microsoft/TypeScript/blob/90fb764a0f76b7d15f56c08a8bb98f37e8dd046f/src/compiler/emitter.ts

                case SyntaxKind.TupleType:
                    return emitTupleType(node as TupleTypeNode);


                case SyntaxKind.NamedTupleMember:
                    return emitNamedTupleMember(node as NamedTupleMember);
    function emitTupleType(node: TupleTypeNode) {
        emitTokenWithComment(SyntaxKind.OpenBracketToken, node.pos, writePunctuation, node);
        const flags = getEmitFlags(node) & EmitFlags.SingleLine ? ListFormat.SingleLineTupleTypeElements : ListFormat.MultiLineTupleTypeElements;
        emitList(node, node.elements, flags | ListFormat.NoSpaceIfEmpty, parenthesizer.parenthesizeElementTypeOfTupleType);
        emitTokenWithComment(SyntaxKind.CloseBracketToken, node.elements.end, writePunctuation, node);
    }
    function emitNamedTupleMember(node: NamedTupleMember) {
        emit(node.dotDotDotToken);
        emit(node.name);
        emit(node.questionToken);
        emitTokenWithComment(SyntaxKind.ColonToken, node.name.end, writePunctuation, node);
        writeSpace();
        emit(node.type);
    }

2022 - React Native - Project作成の時のメモ

毎回忘れそうなので自分用のメモです。

projectをtypescriptで作成

reactnative.dev

// setup react native env if you haven't
brew install node
brew install watchman
// install typescript
npm install -g typescript
npm link typescript

yarn global add expo-cli

expo init MyAwesomeProject
cd MyAwesomeProject
yarn start

なんだか雑にyarnとnpmが混在してしまった…

ディレクトリ構成

src/配下にコードをまとめたい場合、expoだと下記が必要。

docs.expo.dev

ESLint/Prettier

このあたりを参考にゴニョゴニョする。

zenn.dev

その他 3rd party libraries

Redux toolkit

redux-toolkit.js.org

yarn add react-redux
yarn add @reduxjs/toolkit

その後sliceとか作る。React Native向けはこちらを参照した。

hybridheroes.de

axios

yarn add axios

あとなんかlibrary思い出したら追記していく

技術書を読み切る技術 〜読書会のススメ〜

本棚に分厚い技術書が並んでいるエンジニアの皆さんこんにちは。その技術書の中には「いつか読もう」と思っている未読のものはないだろうか。いつか読みたいと思っているうちに数ヶ月、あるいは数年放置されているものはないだろうか。半年後の年末、その本を読了できたあなたと読了しなかったあなた、どちらになりたいだろう。前者である場合はぜひこの文章を読んでみてほしい。

私も本棚1に分厚い技術書が並んでいるが、そのうち何冊かはいわゆる積読である。仕事や育児が日々忙しかったり疲れているとなかなか手が伸びないものだ。しかし私には本当にちゃんと読まないとまずいなと思った場合の勝ちパターンがある。人を巻き込んでの読書会である。自分だけだとついついサボりがちになるので外的圧力を利用するという魂胆だ。これまでにいくつかのパターンを試してそれぞれいいところがあったので紹介していく。

当番制宿題タイプ

これをやったのはGoF本読書会

デザインパターンについての本なので、毎週ひとつかふたつずつくらいのパターンを使って担当者が簡単なプログラムを書いてくるという宿題形式だった。担当は当番制で、担当に当たらない週は何の準備も必要ないので負担は少ない。人が書いたパターンでも面白い例だと結構覚えているもので割とよかった。

全員宿題タイプ

これをやったのはアルゴリズム勉強会

プロコンの問題集みたいな感じなので、参加者各自が事前に解いてきてお互いどうやって解いたか発表し合うみたいな形式だった。こういう問題は自分でやらないとなかなか身につかないなと思ったので個人的には本当に全部コードを書いたが、今週は忙しくてやってきませんでした〜みたいな人も全然いるのでゆるい感じでもOK。とりあえず続くことが大事だと思う。

余談だけどお互いのコードを見せ合うときにPaiza.IOがめちゃめちゃ便利。クラウドテキストエディタやコード実行ツールはいくつかあるけれど、Paiza IOはtextでinputが入れられるので大変お世話になった。

paiza.io

宿題なしタイプ

これをやったのはReal World HTTP本読書会

当時は第2版がまだ出ておらず緑の方の表紙だった

時間になったら会議室とかに集まって、時間とどこまで読むか決めてその場で読む。読んでいる途中に気になった部分や面白かった箇所を付箋に書いていって、時間が来たらその付箋をお互いに見せあう。他の人の付箋を見ることで自分にはない目線が勉強になったりしたのでよかった。何よりいいのは事前の準備や宿題が一切ないので参加ハードルがめちゃめちゃ低いし、途中からふらっと参加したりもできるところ。

こんな人には向かない

以上3パターンを挙げてきたが、これらのやり方が向いている人と向いていない人がいる。向いていない人は他人の目がそこまで気にならない人だ。最初に書いた通り他者を巻き込むという外的要因によって強制力を持たせる狙いがあるので、そもそも他人にどう思われても気にしない人は気軽にドロップアウトするかもしれない。また、そもそも他人と何かをするのが全然好きじゃないという人にも向かないかもしれない。

終わりに

この文章を読んでちょっとやってみようかなと思ったらぜひ周囲の同僚やお友達やTwitterの知り合いやらに声をかけてみてほしい。皆さんの積読消化に少しでも役に立つと幸いです。


  1. 物理本棚だけでなく、Kindleや技術書典などバーチャル本棚にもたくさん本が積まれている

最近読んだジェンダー関連の本まとめ

はじめに

最近読んだジェンダー関連の本のメモ。

一般的なトピック

これは教科書とか研究をまとめたものではなくて、大学生がゼミで調べたことや話し合ったことをまとめたもの。ざっとジェンダー周りのトピックに目を通すのにいいと思う。一橋大学のゼミらしいが本の中で一橋大学アウティング事件にもふれられていた。

本書はジェンダーに限定した本ではなく、ステレオタイプ全般にまつわる脅威を対象としているがとても面白かったので紹介する。私たちはステレオタイプとして見ることも見られることからも逃れられない。しかしどういった影響があるかを学ぶことが対応への第1歩となると思う。

感想は以下のスレッドを参照。

女性を中心としたトピック

邦題がちょっと恥ずかしい感じなので本屋でレジに持っていくのが躊躇われるかもしれないが、内容はとてもよかった。

感想は以下のスレッドを参照。(めちゃめちゃ長いです)

この本についてはゲストとして呼んでもらったPodcastでも少し話した。

anchor.fm

プログラミングで世界を変えたアメリカの女の子二人の実話。プログラミングを学んできた元女の子としては共感する部分も多かった。

ジェンダー関連の実験を紹介しつつ、どうやって格差を克服していくかについての本。中でも興味深かったのはインドで実際に行われた社会実験。憲法で農村の議員の1/3を女性枠とし、さらに選挙毎に全国すべての村の3分の1を無作為抽出し議長になれるのを女性に限定した。日本だったら絶対にできないような実験なんだけど、結果も興味深いのでぜひ読んでみてほしい。

感想は以下のスレッドを参照。(めちゃめちゃ長いです)

男性を中心としたトピック

育児という場においては母親である自分はマジョリティに属しているのだが、マイノリティである父親の立場や考え方、バックグラウンドについて知りたかったので読んだ。もちろん人それぞれ考え方や事情は違うとは思うが、社会的な事情や男性の考え方の傾向など大変参考になった。

以下感想文スレ

おまけ:これから読もうと思っている本

Pending Intentのrequest codeは注意が必要(PendingIntentの仕組み)

Notificationに設定するaction buttonで使うPending Intentについて、よくわかってなかった部分があったのでメモ。

現象

Notificationにactionを設定する際、Pending Intentを渡す必要があるが、複数のPending Intentで同じrequest codeを使いまわしていたらintentが上書きされるという現象がおきた。上書きされたintentは起動先activityは同じクラスだが、extraに異なるものを設定していた。

例えば、下記のようなpending intentをそれぞれactionに登録したとして、

val requestCode = 0

// 同じrequest code
val intentFoo = getIntentFoo()
val pendingIntentFoo = PendingIntent.getActivity(this, requestCode, intentFoo, PendingIntent.FLAG_UPDATE_CURRENT)

val intentBar = getIntentBar()
val pendingIntentBar = PendingIntent.getActivity(this, requestCode, intentBar, PendingIntent.FLAG_UPDATE_CURRENT)

val notification = Notification.Builder(this, channelId)
            // いろいろ省略
            .addAction(Notification.Action.Builder(icon, getString(R.string.foo), pendingIntentFoo).build())
            .addAction(Notification.Action.Builder(icon, getString(R.string.bar), pendingIntentBar).build())
            .build()

これでfooの方のbuttonを押下した場合でも、起動先のactivityで取得されるのはintentBarだった。

公式のdocによると、

If you truly need multiple distinct PendingIntent objects active at the same time (such as to use as two notifications that are both shown at the same time), then you will need to ensure there is something that is different about them to associate them with different PendingIntents. This may be any of the Intent attributes considered by Intent.filterEquals, or different request code integers supplied to getActivity(Context, int, Intent, int), getActivities(Context, int, Intent[], int), getBroadcast(Context, int, Intent, int), or getService(Context, int, Intent, int).

とのことだったので、複数の異なるpending intentを同時に使いたい時はrequest codeを分ける必要があった。

Frameworkのコードを読む

上記のコードはPendingIntentを生成するときにPendingIntent#getActivity()を利用している。この getActivity()は内部でActivityManager#getIntentSender()をコールしている。

    public static PendingIntent getActivity(Context context, int requestCode,
            @NonNull Intent intent, @Flags int flags, @Nullable Bundle options) {
        String packageName = context.getPackageName();
        String resolvedType = intent != null ? intent.resolveTypeIfNeeded(
                context.getContentResolver()) : null;
        try {
            intent.migrateExtraStreamToClipData();
            intent.prepareToLeaveProcess(context);
            IIntentSender target =
                ActivityManager.getService().getIntentSender(
                    ActivityManager.INTENT_SENDER_ACTIVITY, packageName,
                    null, null, requestCode, new Intent[] { intent },
                    resolvedType != null ? new String[] { resolvedType } : null,
                    flags, options, context.getUserId());
            return target != null ? new PendingIntent(target) : null;
        } catch (RemoteException e) {
            throw e.rethrowFromSystemServer();
        }
    }

ここからActivityManagerService経由でPendingIntentController#getIntentSender()が呼ばれる。

    public PendingIntentRecord getIntentSender(int type, String packageName,
            @Nullable String featureId, int callingUid, int userId, IBinder token, String resultWho,
            int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle bOptions) {
        synchronized (mLock) {

// いろいろ省略

            PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName, featureId,
                    token, resultWho, requestCode, intents, resolvedTypes, flags,
                    SafeActivityOptions.fromBundle(bOptions), userId);
            WeakReference<PendingIntentRecord> ref;
            ref = mIntentSenderRecords.get(key);

ポイントはPendingIntentRecord.Keyを使ってIntentSenderRecordを取得していること。Key#equals()を見るとここでrequest codeが使われている。

        @Override
        public boolean equals(Object otherObj) {
            if (otherObj == null) {
                return false;
            }
            try {
                Key other = (Key)otherObj;
                if (type != other.type) {
                    return false;
                }
                if (userId != other.userId){
                    return false;
                }
                if (!Objects.equals(packageName, other.packageName)) {
                    return false;
                }
                if (!Objects.equals(featureId, other.featureId)) {
                    return false;
                }
                if (activity != other.activity) {
                    return false;
                }
                if (!Objects.equals(who, other.who)) {
                    return false;
                }
                if (requestCode != other.requestCode) {
                    return false;
                }
                if (requestIntent != other.requestIntent) {
                    if (requestIntent != null) {
                        if (!requestIntent.filterEquals(other.requestIntent)) {
                            return false;
                        }
                    } else if (other.requestIntent != null) {
                        return false;
                    }
                }
                if (!Objects.equals(requestResolvedType, other.requestResolvedType)) {
                    return false;
                }
                if (flags != other.flags) {
                    return false;
                }
                return true;
            } catch (ClassCastException e) {
            }
            return false;
        }

Package nameが同じ場合、起動先のintentのfilterEquals()が一致してrequest codeも同じだった場合は同様のものとみなされる。(公式docでfilterEquals()でもできるよって書いてあるのは、ここの条件のことっぽい。)

    public boolean filterEquals(Intent other) {
        if (other == null) {
            return false;
        }
        if (!Objects.equals(this.mAction, other.mAction)) return false;
        if (!Objects.equals(this.mData, other.mData)) return false;
        if (!Objects.equals(this.mType, other.mType)) return false;
        if (!Objects.equals(this.mIdentifier, other.mIdentifier)) return false;
        if (!(this.hasPackageEquivalentComponent() && other.hasPackageEquivalentComponent())
                && !Objects.equals(this.mPackage, other.mPackage)) {
            return false;
        }
        if (!Objects.equals(this.mComponent, other.mComponent)) return false;
        if (!Objects.equals(this.mCategories, other.mCategories)) return false;

        return true;
    }

そしてfilterEquals()はextraを見ていない。

さらに同じkeyだとみなされた場合はextraを上書きしている。

    public PendingIntentRecord getIntentSender(int type, String packageName,
            @Nullable String featureId, int callingUid, int userId, IBinder token, String resultWho,
            int requestCode, Intent[] intents, String[] resolvedTypes, int flags, Bundle bOptions) {
        synchronized (mLock) {

// いろいろ省略

            PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName, featureId,
                    token, resultWho, requestCode, intents, resolvedTypes, flags,
                    SafeActivityOptions.fromBundle(bOptions), userId);
            WeakReference<PendingIntentRecord> ref;
            ref = mIntentSenderRecords.get(key);
            PendingIntentRecord rec = ref != null ? ref.get() : null;
            if (rec != null) {
                if (!cancelCurrent) {
                    if (updateCurrent) {
                        if (rec.key.requestIntent != null) {
                            rec.key.requestIntent.replaceExtras(intents != null ?
                                    intents[intents.length - 1] : null);
                        }

Intent#replaceExtras()の中身はこんな感じ。コメントにある通り、本当にまるっと入れ替えている。

    /**
     * Completely replace the extras in the Intent with the extras in the
     * given Intent.
     *
     * @param src The exact extras contained in this Intent are copied
     * into the target intent, replacing any that were previously there.
     */
    public @NonNull Intent replaceExtras(@NonNull Intent src) {
        mExtras = src.mExtras != null ? new Bundle(src.mExtras) : null;
        return this;
    }

というわけで、extraだけ異なるintentでrequest codeを使いまわした場合は上書きされる。

おまけ:Notificationのactionがタップされた時のコード

addaAction() されたボタンの実態はRemoteViewである。こいつがNotificationに表示されるときにclick handlerが設定される。

RemoveView ※懐かしのAsyncTask

       @Override
        protected void onPostExecute(ViewTree viewTree) {

// 前後は省略
                try {
                    if (mActions != null) {
                        OnClickHandler handler = mHandler == null
                                ? DEFAULT_ON_CLICK_HANDLER : mHandler;
                        for (Action a : mActions) {
                            a.apply(viewTree.mRoot, mParent, handler);
                        }
                    }
                } catch (Exception e) {
                    mError = e;
                }
            }

実際にユーザがactionをclickした際にはRemoteView#handleViewClick()が呼ばれる。ここからいろいろ経由してNotificationRemoteInputManagerが持っているmOnClickHandlerでpending intentが使われる。

    private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {

        @Override
        public boolean onClickHandler(
                View view, PendingIntent pendingIntent, RemoteViews.RemoteResponse response) {
            mShadeController.get().wakeUpIfDozing(SystemClock.uptimeMillis(), view,
                    "NOTIFICATION_CLICK");

            if (handleRemoteInput(view, pendingIntent)) {
                return true;
            }

            if (DEBUG) {
                Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent);
            }
            logActionClick(view, pendingIntent);
            // The intent we are sending is for the application, which
            // won't have permission to immediately start an activity after
            // the user switches to home.  We know it is safe to do at this
            // point, so make sure new activity switches are now allowed.
            try {
                ActivityManager.getService().resumeAppSwitches();
            } catch (RemoteException e) {
            }
            return mCallback.handleRemoteViewClick(view, pendingIntent, () -> {
                Pair<Intent, ActivityOptions> options = response.getLaunchOptions(view);
                options.second.setLaunchWindowingMode(
                        WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY);
                return RemoteViews.startPendingIntent(view, pendingIntent, options);
            });
        }

またRemoteViewに帰ってくる

    /** @hide */
    public static boolean startPendingIntent(View view, PendingIntent pendingIntent,
            Pair<Intent, ActivityOptions> options) {
        try {
            // TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT?
            Context context = view.getContext();
            // The NEW_TASK flags are applied through the activity options and not as a part of
            // the call to startIntentSender() to ensure that they are consistently applied to
            // both mutable and immutable PendingIntents.
            context.startIntentSender(
                    pendingIntent.getIntentSender(), options.first,
                    0, 0, 0, options.second.toBundle());
        } catch (IntentSender.SendIntentException e) {
            Log.e(LOG_TAG, "Cannot send pending intent: ", e);
            return false;
        } catch (Exception e) {
            Log.e(LOG_TAG, "Cannot send pending intent due to unknown exception: ", e);
            return false;
        }
        return true;
    }

ここで、上で見てきたIntentSenderを取得してstartするコードになっていた。

言いたいことは以上です。

WorkManager.getInstance()に渡すContext

ちょっと気になったので調べたメモ。
Titleの通りWorkManager#getInstance()の引数のcontext1は何を渡すべきなのか、また何に使われるのか気になった。

developer.android.com

上記のAPI documentには

Context: A Context for on-demand initialization.

としか書いていない。よくわからない。これはapplication contextを渡すべき?それともactitivity? 複数の箇所でWorkManagerのinstanceを取得する場合、渡すcontextによって動作に差が出るか?など気になるポイントがいくつかある。

で、いつものようにコードを読む。まずWorkManager#getInstance()を読むと、Impl classを呼び出しているだけだとわかる。

   public static @NonNull WorkManager getInstance(@NonNull Context context) {
        return WorkManagerImpl.getInstance(context);
    }

Impl classを見にいく。

    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
    public static @NonNull WorkManagerImpl getInstance(@NonNull Context context) {
        synchronized (sLock) {
            WorkManagerImpl instance = getInstance();
            if (instance == null) {
                Context appContext = context.getApplicationContext();
                if (appContext instanceof Configuration.Provider) {
                    initialize(
                            appContext,
                            ((Configuration.Provider) appContext).getWorkManagerConfiguration());
                    instance = getInstance(appContext);
                } else {
                    throw new IllegalStateException("WorkManager is not initialized properly.  You "
                            + "have explicitly disabled WorkManagerInitializer in your manifest, "
                            + "have not manually called WorkManager#initialize at this point, and "
                            + "your Application does not implement Configuration.Provider.");
                }
            }

            return instance;
        }
    }

こんな感じで実際は引数として渡されたcontextから getApplicationContext() を呼んでそちらを使っているので、特に渡すcontextは気にしなくて良いことがわかる。


  1. 引数なしの同名methodも存在するがdeprecatedである。