Quantcast
Channel: Yukiの枝折
Viewing all 146 articles
Browse latest View live

Android:バッテリー温度の確認方法

$
0
0

下記コマンドでバッテリー状態をダンプできます.

adb shell dumpsys battery

コマンドを実行すると, 次のような出力が得られます.

Current Battery Service state:
AC powered: false
USB powered: true
Wireless powered: false
Max charging current: 0
Max charging voltage: 0
Charge counter: 3160971
status: 2
health: 2
present: true
level: 95
scale: 100
voltage: 4315
temperature: 358
technology: Li-ion
batteryMiscEvent: 0
batteryCurrentEvent: 32768
mSecPlugTypeSummary: 2
...続く

出力された中にある temperature: 358がバッテリー温度になります.
数値は温度(摂氏)の10倍値になるので, 358なら35.8℃ということになります.

バッテリー状態を管理するサービスクラスはBatteryServiceです.
バッテリー状態のデータはHealthInfoに定義されています.

以上です.


Android: dropbox/Store

$
0
0

dropbox/Store :Github

Dropbox/Storeとは

Storeはデータロードのためのライブラリです.
データの問合せに対して, ネットワーク越しにデータを取得するフェッチャーを定義し, 取得したデータをどのようにキャッシュするのかを決め, 指定したキャッシュポリシーにしたがって, その後のデータ取得を効率的に行うことができます.
Storeにアクセスするクラスは, データの所在(ネットワーク or ディスク or メモリ)を気にすることなくデータ取得することができるようになります.

Storeが主に提供する機能は次の3つです.

  1. ネットワークを経由してデータをフェッチする方法の宣言(required)
  2. 取得したデータをメモリまたはディスクにキャッシュする方法の宣言(optional)
  3. キャッシュのEvictionPolity(optional)

StoreはデータをFlowで返すためマルチスレッド処理することが容易になるよう設計されています. Flow/Coroutinesによる構造化された同時実行性の性質によって, スコープが明確に定義され, メモリリークの減少, パフォーマンスの向上, クラッシュリスクの軽減が期待されます.

Storeによってデータのフェッチ/共有/キャッシュに関するロジックがカプセル化され, ビューで最新のデータを効率的に購読することができ, データをオフラインで使用することもできるようになります.

Store簡単まとめ

  • フェッチャーには単一レスポンスと複数レスポンス(Flow)のバリエーションがある
  • ディスクキャッシュする/しないを選べる. メモリキャッシュする/しないを選べる
  • メモリキャッシュのEviction Polityには最も過去に生成/更新されたものを破棄, 最終アクセスからn時間経過で破棄, キャッシュの上限個数を指定できる
  • Storeへのデータ問合せ時にはデータを一意に識別できる汎用キーが必要. これは同一リクエストかの判定やキャッシュヒットの判定に使われ, 汎用キーはKotlin Data Classが推奨される.
  • Storeへのデータ問合せによって Loading, Data, Errorのレスポンスがエミットされる
  • Storeへのデータ問合せ中に発生したエラーはErrorとしてエミットされる
  • フェッチャーが使うFlowのスコープはGlobalScope
  • In-flight debouncerが実装されており, 初回の複数同時リクエスト時にもうまくキャッシュが効く

ビルダーによるStoreの構築

StoreStoreBuilderによって構築されます.

StoreBuilder
.from(
fetcher = nonFlowValueFetcher { api.fetchSubreddit(it, "10").data.children.map(::toPosts) },
sourceOfTruth = SourceOfTrue.from(
reader = db.postDao()::loadPosts,
writer = db.postDao()::insertPosts,
delete = db.postDao()::clearFeed,
deleteAll = db.postDao()::clearAllFeeds
)
).cachePolicy(
MemoryPolicy.builder()
.setMemorySize(10)
.setExpireAfterAccess(10.minutes) // or setExpireAfterWrite(10.minutes)
.build()
).build()

これは次のことを宣言しています

  1. 複数回呼び出された場合に備えるフェッチしたデータのメモリキャッシュ
  2. ネットワークがオフラインの場合に備えたディスクキャッシュ

Storeはネットワークへの過剰な呼び出しを防ぎ, ディスクキャッシュをSource of Truthとして使用することができます. Source of Truthの実装には Room, SQLDelightなどの監視可能なソースを提供できるデータベースが利用できます.

StoreBuilder.fromNonFlow

fun <Key : Any, Output : Any> fromNonFlow(
fetcher: suspend (key: Key) -> Output
): StoreBuilder<Key, Output>

Flowを返さないフェッチャーを持つStoreBuilderを生成します.
リクエストに対してHTTPのように単一の応答を返すフェッチャーを持つStoreを生成します.

StoreBuilder.from

fun <Key : Any, Output : Any> from(
fetcher: (key: Key) -> Flow<Output>
): StoreBuilder<Key, Output> = BuilderImpl(fetcher)

Flowを返すフェッチャーを持つStoreBuilderを生成します.
リクエストに対してWebsocketのように複数の応答を返すフェッチャーを持つStoreを生成します.

StoreBuilder.persister

fun <NewOutput : Any> persister(
reader: (Key) -> Flow<NewOutput?>,
writer: suspend (Key, Output) -> Unit,
delete: (suspend (Key) -> Unit)? = null,
deleteAll: (suspend () -> Unit)? = null
): StoreBuilder<Key, NewOutput>

Flowなディスクキャッシュへアクセスするための reader, writer, deleterを定義します.

柔軟性を確保するため, writerのレコードタイプ(Output)とreaderのレコードタイプ(NewOutput)は異なる型にすることができます. これによって, ネットワークから取得される型とローカルストレージのレコードタイプを分けることができます.

StoreBuilder.nonFlowingPersister

fun <NewOutput : Any> nonFlowingPersister(
reader: suspend (Key) -> NewOutput?,
writer: suspend (Key, Output) -> Unit,
delete: (suspend (Key) -> Unit)? = null,
deleteAll: (suspend () -> Unit)? = null
): StoreBuilder<Key, NewOutput>

Flowではないディスクキャッシュへアクセスするための reader, writer, deleterを定義します.

StoreBuilder.cachePolicy

fun cachePolicy(memoryPolicy: MemoryPolicy?): StoreBuilder<Key, Output>

StoreのメモリキャッシュにおけるEvictionポリシーを指定できます.
MemoryPolicy.MemoryPolicyBuilderで TTLまたは容量ベースのEvictionを設定できます.

ビルダーで特にポリシーの指定がない場合, 次のキャッシュポリシーが適用されます.

  • キャッシュできるエントリーの上限個数 = 100
  • Evictionポリシー = キャッシュしたエントリーの作成/更新から24時間以上経過したものを破棄

StoreBuilder.disableCache

fun disableCache(): StoreBuilder<Key, Output>

キャッシュ機構を持たないStoreになります.

StoreBuilder.scope

fun scope(scope: CoroutineScope): StoreBuilder<Key, Output>

Storeが汎用キーに対応するデータをフェッチ(あるいはキャッシュヒット)して, 複数の購読者に結果をマルチキャストする際, そのスコープはGlobalScopeとなるのがデフォルトの挙動です.
このマルチキャストのスコープを独自にハンドリングしたい場合はこの関数でCoroutineScopeを指定します.

MemoryPolicyBuilder

Storeのメモリキャッシュポリシー(MemoryPolicy)を定義するためのビルダークラスです.
いわゆるEvictionポリシーはここで指定することになります.
このポリシーは最終的にCacheクラスを生成するパラメータとして利用されます.

MemoryPolicyBuilder.setExpireAfterWrite

fun setExpireAfterWrite(expireAfterWrite: Duration): MemoryPolicyBuilder

キャッシュエントリーが作成 or 置換/更新されてから一定時間後に自動削除するポリシーです.
Duration0で指定されるとキャッシュされなくなります.

MemoryPolicyBuilder.setExpireAfterAccess

fun setExpireAfterAccess(expireAfterAccess: Duration): MemoryPolicyBuilder

キャッシュエントリーが作成 or 置換/更新 or 最後にアクセスされてから一定時間後に自動削除するポリシーです.
Duration0で指定されるとキャッシュされなくなります.

MemoryPolicyBuilder.setMemorySize

fun setMemorySize(maxSize: Long): MemoryPolicyBuilder

キャッシュされるエントリー個数の上限を指定します. エントリー個数が上限を超えた場合, アクセスされた時間の最も古いエントリーが削除対象となります(LRU)
0が指定されるとすぐにキャッシュを破棄するため, キャッシュされなくなります.
特にビルダーで指定しなかった場合は個数の上限を設けません.

Storeの実装制約

Storeの唯一の実装制約はflowを返す関数, または特定の型を返すフェッチ用関数(フェッチャー)を実装する必要があることです.

val store = StoreBuilder.from {
articleId -> api.getArticle(articleId) //Flow<Article>
}
.build()

データの識別子

Storeはデータの識別子として汎用キーを使用します.
この汎用キーはtoString(), equals(), hashCode()を適切に実装した値オブジェクトにする必要があります.
汎用キーにはKotlinのdata classを使うことが強く推奨されます.

この汎用キーはフェッチ関数の引数として渡されます. また, キャッシュのプライマリ識別子としても利用されます.
UIはこの汎用キーさえ知っていれば, いつでもStoreからデータをネットワーク/キャッシュを気にせず再取得できるようになっています.

Stream API

Storeが提供する主要なAPIとしてstream functionがあります.

fun stream(request: StoreRequest<Key>): Flow<StoreResponse>Output>>

streamの呼び出しに渡される StoreRequestには次の情報が格納されています.

  1. データを識別するための汎用キー
  2. キャッシュの利用方針(ディスク/メモリキャッシュの利用有無)

streamの戻り値はStoreResponseFlowで返されます.

StoreResponse

StoreResponseseald classでサブクラスにはLoading, Data, Errorが定義されています.

  • それぞれのクラスには ResponseOriginフィールドがあり, データの取得元がキャッシュ or ディスク or フェッチなのかを判別できるようになっています.
  • LoadingResponseOriginのみを持ちます. このクラスはデータのロードをUIに反映するきっかけとして使うことができます.
  • DataStoreから返される値を持ったクラスです
  • ErrorResponseOriginによって投げられた例外をフィールドに持ちます

エラーが発生した場合でもStoreは例外をスローしません. その代わり, StoreResponse.Errorタイプによってこれが表現されます.
これによってFlowが壊れることがなく, データへの問い合わせやデータの更新が継続して行われることになります.
これによって, UIはflowの再起動/再接続を意識する必要がなくなります.

lifecycleScope.launchWhenStarted {
store.stream(StoreRequest.cached(key = key, refresh=true)).collect { response ->
when(response) {
is StoreResponse.Loading -> showLoadingSpinner()
is StoreResponse.Data -> {
if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
updateUI(response.value)
}
is StoreResponse.Error -> {
if (response.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
showError(response.error)
}
}
}
}

extention function

suspend fun Store.get(key: Key): Value

渡された値に対応するデータを単発取得します.
メモリ/ディスクキャッシュにヒットするデータがあればそこから取得されます.
エラー(StoreResponse.Error)が発生した場合は例外がスローされ, データがない場合はNullPointerExceptionがスローされます.

キャッシュされたデータもない初めての Store.getの呼び出しでは, ネットワークからデータを取得して, ディスク/メモリキャッシュにこれを格納します.
再度, おなじ汎用キーでStore.getを呼び出したなら, キャッシュからデータを取得して, ネットワーク通信を最低限に抑えようとします.

suspend fun Store.fresh(key: Key): Value

フェッチャーによる問合せによって, ネットワークからデータを単発取得します.
ディスク/メモリキャッシュをスキップしてデータを取得するため, 定期ジョブによるデータ(キャッシュ)更新や, Pull to Refreshによるデータの強制更新時などに利用されます.

suspend fun Store.stream(key: Key): Flow

データを監視してリアルタイムにUI更新をしたい場合などでは Store.streamが利用できます.
ディスクキャッシュの更新や, ネットワークからのロード/エラーイベントを監視するストリームを作成する方法と考えることができます.

In-flight debouncer

Storeは同じデータに対するリクエストの重複を避けるためにIn-flight debouncerの機能が組み込まれています.
リクエストに対するデータがまだキャッシュされていない場合, 複数個の同じデータに対するリクエストが同時にあると, それぞれが並列に処理されてキャッシュヒットせず, 両方のリクエストがネットワーク通信に至ってしまう可能性があります.
In-flight debouncerはこの不要なネットワーク通信を回避するために, 最初のリクエストはデータ取得のためにブロックされ, 他方の呼び出しはデータの到着を待たせます.

参考

NavHostFragmentをFragmentの入れ子にする時はsetPrimaryNavigationFragmentを指定する

$
0
0

NavHostFragmentapp:defaultNavHost=trueを指定すればバックキー制御をNavHostFragmentに任せることができます.

<androidx.fragment.app.FragmentContainerView
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
...

NavHostFragmentをアクティビティのレイアウトに指定した時の構造は次の通りです.

Activity
|- NavHostFragment

一方で, アクティビティ直下にNavHostFragmentを配置せず, 下記のように間にフラグメントがいる場合は注意が必要です.

Activity
|- Fragment
|- NavHostFragment

この場合, フラグメントのレイアウトでapp:defaultNavHost=trueを指定しても, バックキー制御などナビゲーション周りで意図しない動作となります.

解決策

NavHostFragmentを持つフラグメントを PrimaryNavigationFragmentに設定します.

classHostFragment : Fragment {
override fun onAttach(context: Context) {
super.onAttach(context)
parentFragmentManager.commit {
setPrimaryNavigationFragment(this@HostFragment)
}

FragmentTransaction.setPrimaryNavigationFragment

app:defaultNavHost

NavHostFragmentapp:defaultNavHost=trueを指定されると, 自身のonAttachで同様に setPrimaryNavigationFragment(this)を設定します.

NavHostFragment.ktの該当行 - GitHub

setPrimaryNavigationFragment

プライマリナビゲーションフラグメントに指定されると, バックナビゲーションなどをハンドリングできるようになります.
app:defaultNavHost=trueを指定するだけで, NavHostFragmentがバックナビゲーションをうまく制御できるのはこのためです.

プライマリナビゲーションフラグメントはフラグメントマネージャのインスタンス毎に1つしか設定できません.

FragmentManager.setPrimaryNavigationFragment - GitHub

NavHostFragmentを複数管理する場合, app:defaultNavHost=trueNavHostFragmentは1つにしなければならない理由でもあります.

フラグメントがプライマリナビゲーションフラグメントと判定されるには, 親フラグメントがいる場合, 関連するフラグメントマネージャのプライマリナビゲーションフラグメントに指定されている必要があります.

つまり, 次の構造ではNavHostFragmentの親フラグメント/親フラグメントマネージャがいないので, NavHostfragmentがプライマリナビゲーションフラグメントになります.

Activity
|- NavHostFragment

しかし, 次の構造ではNavHostFragmentに親フラグメントがおり, その親がsetPrimaryNavigationFragmentとして指定されていない場合, 子であるNavHostFragmentもプライマリナビゲーションフラグメントの条件を満たしません.

Activity
|- Fragment
|- NavHostFragment

そのため, 親フラグメントは次のようなコードで自身をプライマリナビゲーションフラグメントとして指定する必要があります.

classHostFragment : Fragment {
override fun onAttach(context: Context) {
super.onAttach(context)
parentFragmentManager.commit {
setPrimaryNavigationFragment(this@HostFragment)
}

蛇足: NavHostFragmentのバックナビゲーション周りの実装

// デフォルトでフラグメントマネージャのOnBackPressedCallbackはenable=falseになっている

FragmentManager
privatefinal OnBackPressedCallback mOnBackPressedCallback =
new OnBackPressedCallback(false) {
@Override
publicvoidhandleOnBackPressed() {
FragmentManager.this.handleOnBackPressed();
}
};

---

// OnBackPressedCallbackをenable=trueにするにはisPrimaryNavigationでtrueを返す必要がある

privatevoidupdateOnBackPressedCallbackEnabled() {
...
// This FragmentManager needs to have a back stack for this to be enabled
// And the parent fragment, if it exists, needs to be the primary navigation
// fragment.
mOnBackPressedCallback.setEnabled(getBackStackEntryCount() > 0
&& isPrimaryNavigation(mParent));
}

---

// PrimaryNavigationFragmentに変更があると...

Fragment
void performPrimaryNavigationFragmentChanged() {
boolean isPrimaryNavigationFragment = mFragmentManager.isPrimaryNavigation(this); ⭐️
// Only send out the callback / dispatch if the state has changed
if (mIsPrimaryNavigationFragment == null
|| mIsPrimaryNavigationFragment != isPrimaryNavigationFragment) {
mIsPrimaryNavigationFragment = isPrimaryNavigationFragment;
onPrimaryNavigationFragmentChanged(isPrimaryNavigationFragment); 🍣

---

// 一方, NavHostFragmentでは...

NavHostFragment
🍣
publicvoidonPrimaryNavigationFragmentChanged(boolean isPrimaryNavigationFragment) {
if (mNavController != null) {
mNavController.enableOnBackPressed(isPrimaryNavigationFragment); 🌴

---

// BackPressedCallbackを有効にするにはisPrimaryNavigationFragmentがtrueである必要がある.

NavController
🌴
void enableOnBackPressed(boolean enabled) {
mEnableOnBackPressedCallback = enabled;
updateOnBackPressedCallbackEnabled();

privatevoidupdateOnBackPressedCallbackEnabled() {
mOnBackPressedCallback.setEnabled(mEnableOnBackPressedCallback
&& getDestinationCountOnBackStack() > 1);

---

// NavControllerはOnBackPressedCallbackを持っている. NavHostFragmentのバックキー制御はNavControllerの責務

privatefinal OnBackPressedCallback mOnBackPressedCallback =
new OnBackPressedCallback(false) {
@Override
publicvoidhandleOnBackPressed() {
popBackStack();
}
};

以上.

non-SDK interfaces を veridex toolで検出する

$
0
0

SDKに定義されていない非公開なインタフェース(non-SDK interface)であってもリフレクションを使うことでアプリから参照することができていましたが, Android 9(API Lv.28)以降は制限されるようになりました. 詳細は「Improving Stability by Reducing Usage of non-SDK Interfaces」を参照してください.
この変更により, アプリはTarget SDKのバージョンを変更する際にはアプリや依存するライブラリがnon-SDK interfaceを使っていないかをチェックする必要があります.

チェックする方法はいくつかありますが, 本稿ではveridex toolを使用する方法についてまとめます. 「Android Developers - Restrictions on non-SDK interfaces
」にも方法が書かれてありますveridex toolsの制限など知りたい方はそちらを参照してください.

non-SDK interface使用箇所の検出

今回はmacOSでveridex toolsを実行します.

veridex toolsの実行

  1. gitからappcompatのtar.gzをDLします.
  2. tar.gzを展開すると veridex-mac.zipがあるのでこれを展開します
  3. appcompat.shがあるのでこれを実行します
./appcompat.sh --dex-file=[APKファイルパス]

出力結果の確認

appcompat.shを実行すると下記のようなフォーマットで結果が出力されます.

#1: Linking unsupported Llibcore/io/Memory;->pokeByte(JB)V use(s):
Lcom/google/android/gms/internal/gtm/zztx$zzb;->zza(JB)V

#2: Reflection max-target-p Landroid/widget/AutoCompleteTextView;->ensureImeVisible use(s):
Landroidx/appcompat/widget/SearchView$PreQAutoCompleteTextViewReflector;-><init>()V

...

83 hidden API(s) used: 28 linked against, 55 through reflection
70in unsupported
0in blocked
1inmax-target-o
12inmax-target-p
0inmax-target-q
0inmax-target-r

結果の見方は下図の通りです.

non-SDK APIリストの種別

non-SDK APIリストの種別で unsupportedは現在, 特に使用制限はなく, アプリが使用できるnon-SDK interfaceです.

max-target-pはAndroid 9では制限されていなかったが, Android 10から制限されるようになったAPIです. Android 9では問題ありません. しかし, Android 10かつTarget SDKバージョン10のアプリはこのAPIを使うことができません. 同条件でこのAPIを呼び出すと実行時例外が発生します.

検出されたnon-SDK interface

制限の対象となり得るnon-SDK interfaceです.

non -SDK interfaceの使用元

non-SDK interfaceをリフレクションを使って参照している参照元です.

検出されたnon-SDK interfaceの参照元をチェック

検出された参照元が自アプリのコードなら, 下記のようにSDKバージョンに応じて処理を分けるようにします.

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {...}

検出された参照元が3rd-partyライブラリのコードならそのコードを参照し, SDKバージョンに応じて処理を分けているか確認します.

例えば, 下記のような出力結果が得られた場合,

#2: Reflection max-target-p Landroid/widget/AutoCompleteTextView;->ensureImeVisible use(s):
Landroidx/appcompat/widget/SearchView$PreQAutoCompleteTextViewReflector;-><init>()V

androidx.appcompat.widget.SearchView$PreQAutoCompleteTextViewReflectorのコードを確認します.

staticfinal PreQAutoCompleteTextViewReflector PRE_API_29_HIDDEN_METHOD_INVOKER =
(Build.VERSION.SDK_INT < 29) ? new PreQAutoCompleteTextViewReflector() : null;

SDKバージョンが考慮されているので, この出力結果は問題ないことがわかります.

もしライブラリ側に問題があった場合, ライブラリのバージョンを上げるか, コードオーナーに修正を依頼するなどして対応を待つ必要があります.

以上です.

Android: uses-permissionの追加・定義元を確認する

$
0
0

ライブラリがパーミッションを定義していると, アプリのパーミッションとして自動で追加される.
Android StudioでAndroidManifest.xmlを開いて Merged Manifestタブを開けば最終的にアプリが使用するパーミッションを確認できる.

ここで, <uses-permission>として定義されたパーミッションをどのライブラリが追加・定義しているのかを調べたい場合, アプリを一度ビルドして[module]/build/outputs/logs/manifest-merger-[build variant]-report.txtの内容を確認すれば良い.

下記のような出力結果が得られるので, 「READ_EXTERNAL_STORAGELeakCanaryが追加しているんだな」と知ることができる.

uses-permission#android.permission.READ_EXTERNAL_STORAGE
ADDED from [com.squareup.leakcanary:leakcanary-android-core:2.4] xxxleakcanary-android-core-2.4/AndroidManifest.xml:23:5-80

以上.

Maven Central Repositoryにライブラリを公開する

$
0
0

公式手順はここで説明されています。
The Central Repository Documentation - Getting started

本稿では必要な手順を端的に書いていきます。

1. Create your JIRA account & Issue

Maven Central Repositoryにあなたのレポジトリを作成するには申請する必要があります。
申請はJIRAチケットで行われますので、下記からアカウントを作ります。
https://issues.sonatype.org/secure/Signup!default.jspa

アカウントを作ったら下記リンクからリポジトリ作成のIssueを作ります。
https://issues.sonatype.org/secure/CreateIssue.jspa?issuetype=21&pid=10134

フィールド
要約プロジェクト名など 例:YukiMatsumura / koma
説明READMEなどプロジェクトの概要
Group Idあなたのプロジェクトであることを示す識別子. *後述
Project URLプロジェクトページのURL. 例:https://github.com/YukiMatsumura/koma
SCM urlGitのURLなど. 例:https://github.com/YukiMatsumura/koma.git
Username
Already Synced to CentralNo

Group Id

まずはここを読んだようがいいです。
https://central.sonatype.org/publish/requirements/coordinates/

Group Idはよくあるimplementation指定で使われるもので, 下記でいうと io.github.yukimatsumuraがGroup Idになります。

implementation'io.github.yukimatsumura:koma:0.2'

あなたが今後Maven Central RepositoryにリリースするであろうすべてのプロジェクトがこのGroup Idに紐づきます。
例えば、example.comを管理している場合、com.example.domaincom.example.testsupportなど、com.exampleで始まるGroup Idを使用することができます。

注意
ここで指定するGroupIdに紐づくドメインを所有または管理している必要があります。Issueで申請後、ドメインの所有/管理していることの証明を求められます。
ただし、GitHubやGitLabなど特定のコードホスティングサービスであればドメインの所有権がなくても、個人アカウントレベルのドメインをサポートしています。
https://central.sonatype.org/publish/requirements/coordinates/#supported-code-hosting-services-for-personal-groupid
例えば github.com/yourusernameのアカウントであれば io.github.yourusernameをGroup Idとして登録できます。

GitHubなどコードホスティングサービスの個人ページをGroup Idに指定した場合
指定のGroup Idがあなたの管理下にあることを証明する必要があります。
作成したIssueのチケット名で空のリポジトリを作成し、アカウントの所有権を証明しましょう。
例:io.github.myusernameをGroupIdに指定し管理している場合、チケット名OSSRH-*****を名前にしたリポジトリgithub.com/myusername/OSSRH-*****を作成します。

起票したIssueに最長でも2営業日以内に管理者からコメントで返信があるはずです。
反応があるまで待ちましょう。

ドメインの所有権確認などが済めば、リポジトリマネージャが利用できるようになります。
リポジトリマネージャにはJIRAの登録アカウントでログインできます。
https://oss.sonatype.org/

2. GPG

Maven Central Repositoryに登録するaarなどのアーティファクトにはGnuPGなどによる署名が必要です。
下記の手順に従ってGPGを導入しましょう。
https://central.sonatype.org/publish/requirements/gpg/

ざっくり手順を書いておきます。

1. インストール
$ brew install gnupg
2. バージョン確認
$ gpg --version
gpg (GnuPG) 2.2.29
3. 鍵生成
$ LANG=C gpg --full-gen-key
  • Kind of key: 1 RSA and RSA.
  • Key size: 4096鍵のサイズ.
  • Expiration: 0 0で無期限. 期限ありにしたいならそれを指定.
  • Real name, email: ご自由に
  • Comment: フリーテキスト. 空でもok.

実行を終えるとキーを保護するためのパスワードを求められるので入力する。

4. 生成した鍵IDを確認
$ gpg --list-keys
/Users/xxx/.gnupg/pubring.kbx
---------------------------------
pub rsa2048 2021-xx-xx [SC]
ABCDEFG0123456789ABCDEFG0123456789ABCDEF
uid [ultimate] MatsumuraYuki <xxxx@xxx.xxx>
sub rsa2048 2021-xx-xx [E]

これで生成した公開鍵の情報が得られます。
pubにあるフィンガープリントの下8桁が鍵IDになります。(ここでは 89ABCDEF
この8桁の鍵IDはあとで使うのでメモしておきます。

5. 公開鍵を鍵サーバへ登録

公開鍵があなたのものであることを確認できるように、鍵サーバーにアップロードします。

$ gpg --keyserver keyserver.ubuntu.com --send-keys <先ほど生成した8桁の鍵ID>

現在Maven Central Repositoryがサポートしている鍵サーバは下記の3つです.

  • keyserver.ubuntu.com
  • keys.openpgp.org
  • pgp.mit.edu

6. 秘密鍵のBase64エクスポート

署名する際に使う秘密鍵の情報をBase64エクスポートしてメモしておきます。

$ gpg --export-secret-keys 89ABCDEF | base64

3. Setup Gradle

ここから先は下記のプロジェクトを参考に進めてみてください。動いている完成形で、これをベースに話を進めます。
https://github.com/YukiMatsumura/koma

公開に必要な設定はルートやモジュールのbuild.gradleとは別ファイルで管理するようにします(必須ではないですが、管理しやすくなるのでファイルを分けます)
プロジェクトルートに scriptsディクトリを作成して、そこに publish-module.gradlepublish-root.gradleの空ファイルを作成しておきます。

Root build.gradle

次にプロジェクトルートの build.gradleに下記を追加します。

buildscript {
repositories {
maven { url "https://plugins.gradle.org/m2/" }
...
}
dependencies {
...
classpath 'io.github.gradle-nexus:publish-plugin:1.1.0'
classpath "org.jetbrains.dokka:dokka-gradle-plugin:1.5.0"
}
}

apply plugin: 'io.github.gradle-nexus.publish-plugin'
apply from: "${rootDir}/scripts/publish-root.gradle"

Maven Central Repositoryへの公開には gradle-nexus/publish-pluginを使います。
また、参考プロジェクトはdokkaを使っているのでそのクラスパスも追加しています。

publish-root.gradle

publish-root.gradleの内容は次のとおりです。

ext["ossrhUsername"] = ''
ext["ossrhPassword"] = ''
ext["sonatypeStagingProfileId"] = ''
ext["signing.keyId"] = ''
ext["signing.password"] = ''
ext["signing.key"] = ''

// CIとローカルビルド両方で動作するように秘匿情報の参照先を分けます

File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
Properties p = new Properties()
new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
p.each { name, value -> ext[name] = value }
} else {
ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
ext["signing.key"] = System.getenv('SIGNING_KEY')
}

nexusPublishing {
repositories {
sonatype {
stagingProfileId = sonatypeStagingProfileId
username = ossrhUsername
password = ossrhPassword
// 2021.02以降Maven Central Repositoryにリポジトリを新規作成する場合は下記の指定が必要です
// https://central.sonatype.org/publish/publish-gradle/#metadata-definition-and-upload
nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
}
}
}

Module build.gradle

公開するライブラリモジュールの build.gradleに下記を追加します。

ext {
// Provide your own coordinates here
PUBLISH_GROUP_ID = 'Group ID. 例:io.github.yukimatsumura'
PUBLISH_VERSION = 'ライブラリバージョン. 例:0.2'
PUBLISH_ARTIFACT_ID = 'アーティファクトID. 例:koma'
}

apply from: "${rootProject.projectDir}/scripts/publish-module.gradle"

アーティファクトIDは implementation "GroupID:ArtifactID:version"で指定するアーティファクトIDになります。

publish-module.gradle

publish-module.gradleの内容は次のとおりです。

apply plugin: 'maven-publish'
apply plugin: 'signing'
apply plugin: 'org.jetbrains.dokka'

task androidSourcesJar(type: Jar) {
archiveClassifier.set('sources')
from android.sourceSets.main.java.srcDirs
from android.sourceSets.main.kotlin.srcDirs
}

tasks.dokkaHtml.configure {
outputDirectory.set(file("../documentation/html"))
}

tasks.withType(dokkaHtml.getClass()).configureEach {
pluginsMapConfiguration.
set(["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""])
}

task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
archiveClassifier.set('javadoc')
from dokkaJavadoc.outputDirectory
}

artifacts {
archives androidSourcesJar
archives javadocJar
}

signing {
useInMemoryPgpKeys(rootProject.ext["signing.keyId"],
rootProject.ext["signing.key"],
rootProject.ext["signing.password"],)
sign publishing.publications
}

group = PUBLISH_GROUP_ID
version = PUBLISH_VERSION

afterEvaluate {
publishing {
publications {
release(MavenPublication) {
groupId PUBLISH_GROUP_ID
artifactId PUBLISH_ARTIFACT_ID
version PUBLISH_VERSION

// Two artifacts, the `aar` (or `jar`) and the sources
if (project.plugins.findPlugin("com.android.library")) {
from components.release
} else {
from components.java
}

artifact androidSourcesJar
artifact javadocJar

pom {
name = PUBLISH_ARTIFACT_ID
description = 'プロジェクトの概要'
url = 'プロジェクトのURL. 例:https://github.com/YukiMatsumura/koma'

licenses {
license {
// ライセンス情報
name = 'The Apache License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
developers {
developer {
id = 'よしなに. 例:YukiMatsumura'
name = 'よしなに. 例:Matsumura Yuki'
email = 'よしなに. 例:xxxx@gmail.com'
}
}
scm {
connection = 'VCS情報. 例:scm:git:github.com/YukiMatsumura/koma.git'
developerConnection = 'VCS情報. 例:scm:git:ssh://github.com/YukiMatsumura/koma.git'
url = 'VCS情報. 例:https://github.com/YukiMatsumura/koma/tree/main'
}
}
}
}
}
}

4. local.properties

外部公開できない秘匿情報をlocal.propertiesに定義しましょう。

signing.keyId=公開鍵の8桁ID. 例:89ABCDEF
signing.password=PGPで生成した秘密鍵Base64情報. 例:PMxxxxxxxxxxxxxxxx.........xx==

ossrhUsername=リポジトリマネージャログインID
ossrhPassword=リポジトリマネージャログインパスワード
sonatypeStagingProfileId=ステージングプロファイルID

ossrhUsername/password

そのままsonatypeのusername/passwordを指定することもできますが、よりセキュアにアクセストークンを発行して指定することもできます。
Sonatypeのリポジトリマネージャで、 画面右上のログイン名 → Profile → User Tokenからトークンを生成し、username/passwordと差し替えます。

Staging profile id

https://s01.oss.sonatype.org/にログイン後, Build Promotion → Staging Profilesを選択し, 自分のプロファイルを選択するとURLの末尾にプロファイルIDが表示されます。
これをsonatypeStagingProfileIdに指定します。

例:https://s01.oss.sonatype.org/#stagingProfiles;<profile id>

5. Release

これですべての設定は完了しました。
Gradleのタスクリストを見ると、ライブラリモジュールのタスクにpublishReleasePublicationToSonatypeRepositoryがいるはずです。
コマンドを実行してライブラリをプレリリースしましょう。

./gradlew :<モジュール名>:publishReleasePublicationToSonatypeRepository

コマンドを実行すると、Sonatypeリポジトリマネージャの Build Promotion → Staging Repositoriesにライブラリがアップロードされているのがわかります。

ライブラリを選択し Closeアクションを実行しましょう。
Closeを実行するとしばらくの間バリデーションが実行されます。実行状況は同画面の Activityタブから確認できます。

リポジトリを閉じるとDropReleaseのアクションが選択可能になります。
公開プロセスで問題があった場合はDropでキャンセルできます。
Releaseを選択するとMaven Centralに公開します。Release後はステージングのアイテムは不要なのでDropできます。

公開には10~15分、長いと1時間以上かかります。
正常に公開されると https://repo1.maven.org/maven2/であなたのリポジトリが参照できます。
さらに数時間後には https://search.maven.org/で検索が可能になっているはずです。

以上です。

Viewing all 146 articles
Browse latest View live