tag:blogger.com,1999:blog-63212730991057074832024-03-05T13:40:07.377+09:00Yukiの枝折Androidに関する技術情報Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.comBlogger270125tag:blogger.com,1999:blog-6321273099105707483.post-14803010039279686162021-08-23T00:48:00.000+09:002021-08-23T00:48:00.107+09:00Maven Central Repositoryにライブラリを公開する<div class="markdown">
<p>公式手順はここで説明されています。 <br>
<a href="https://central.sonatype.org/publish/publish-guide/">The Central Repository Documentation - Getting started</a></p>
<p>本稿では必要な手順を端的に書いていきます。</p>
<h3 id="1-create-your-jira-account-issue">1. Create your JIRA account & Issue</h3>
<p>Maven Central Repositoryにあなたのレポジトリを作成するには申請する必要があります。 <br>
申請はJIRAチケットで行われますので、下記からアカウントを作ります。 <br>
<a href="https://issues.sonatype.org/secure/Signup!default.jspa">https://issues.sonatype.org/secure/Signup!default.jspa</a></p>
<p>アカウントを作ったら下記リンクからリポジトリ作成のIssueを作ります。 <br>
<a href="https://issues.sonatype.org/secure/CreateIssue.jspa?issuetype=21&pid=10134">https://issues.sonatype.org/secure/CreateIssue.jspa?issuetype=21&pid=10134</a></p>
<table>
<thead>
<tr>
<th>フィールド</th>
<th>値</th>
</tr>
</thead>
<tbody><tr>
<td>要約</td>
<td>プロジェクト名など 例:YukiMatsumura / koma</td>
</tr>
<tr>
<td>説明</td>
<td>READMEなどプロジェクトの概要</td>
</tr>
<tr>
<td>Group Id</td>
<td>あなたのプロジェクトであることを示す識別子. *後述</td>
</tr>
<tr>
<td>Project URL</td>
<td>プロジェクトページのURL. 例:<a href="https://github.com/YukiMatsumura/koma">https://github.com/YukiMatsumura/koma</a></td>
</tr>
<tr>
<td>SCM url</td>
<td>GitのURLなど. 例:<a href="https://github.com/YukiMatsumura/koma.git">https://github.com/YukiMatsumura/koma.git</a></td>
</tr>
<tr>
<td>Username</td>
<td>空</td>
</tr>
<tr>
<td>Already Synced to Central</td>
<td>No</td>
</tr>
</tbody></table>
<h4 id="group-id">Group Id</h4>
<p>まずはここを読んだようがいいです。 <br>
<a href="https://central.sonatype.org/publish/requirements/coordinates/">https://central.sonatype.org/publish/requirements/coordinates/</a></p>
<p>Group Idはよくある<code>implementation</code>指定で使われるもので, 下記でいうと <code>io.github.yukimatsumura</code> がGroup Idになります。</p>
<pre class="prettyprint"><code class=" hljs delphi"><span class="hljs-keyword">implementation</span> <span class="hljs-string">'io.github.yukimatsumura:koma:0.2'</span></code></pre>
<p>あなたが今後Maven Central RepositoryにリリースするであろうすべてのプロジェクトがこのGroup Idに紐づきます。 <br>
例えば、<code>example.com</code>を管理している場合、<code>com.example.domain</code>、 <code>com.example.testsupport</code>など、<code>com.example</code>で始まるGroup Idを使用することができます。</p>
<p><em>注意</em> <br>
ここで指定するGroupIdに紐づくドメインを所有または管理している必要があります。Issueで申請後、ドメインの所有/管理していることの証明を求められます。 <br>
ただし、GitHubやGitLabなど特定のコードホスティングサービスであればドメインの所有権がなくても、個人アカウントレベルのドメインをサポートしています。 <br>
<a href="https://central.sonatype.org/publish/requirements/coordinates/#supported-code-hosting-services-for-personal-groupid">https://central.sonatype.org/publish/requirements/coordinates/#supported-code-hosting-services-for-personal-groupid</a> <br>
例えば <code>github.com/yourusername</code> のアカウントであれば <code>io.github.yourusername</code> をGroup Idとして登録できます。</p>
<p><em>GitHubなどコードホスティングサービスの個人ページをGroup Idに指定した場合</em> <br>
指定のGroup Idがあなたの管理下にあることを証明する必要があります。 <br>
作成したIssueのチケット名で空のリポジトリを作成し、アカウントの所有権を証明しましょう。 <br>
例:<code>io.github.myusername</code>をGroupIdに指定し管理している場合、チケット名<code>OSSRH-*****</code>を名前にしたリポジトリ<code>github.com/myusername/OSSRH-*****</code>を作成します。</p>
<p>起票したIssueに最長でも2営業日以内に管理者からコメントで返信があるはずです。 <br>
反応があるまで待ちましょう。</p>
<p>ドメインの所有権確認などが済めば、リポジトリマネージャが利用できるようになります。 <br>
リポジトリマネージャにはJIRAの登録アカウントでログインできます。 <br>
<a href="https://oss.sonatype.org/">https://oss.sonatype.org/</a></p>
<h3 id="2-gpg">2. GPG</h3>
<p>Maven Central Repositoryに登録するaarなどのアーティファクトにはGnuPGなどによる署名が必要です。 <br>
下記の手順に従ってGPGを導入しましょう。 <br>
<a href="https://central.sonatype.org/publish/requirements/gpg/">https://central.sonatype.org/publish/requirements/gpg/</a></p>
<p>ざっくり手順を書いておきます。</p>
1. インストール<br/>
<pre class="prettyprint"><code class="language-bash hljs ">$ brew install gnupg</code></pre>
2. バージョン確認<br/>
<pre class="prettyprint"><code class="language-bash hljs ">$ gpg --version
gpg (GnuPG) <span class="hljs-number">2.2</span>.<span class="hljs-number">29</span></code></pre>
3. 鍵生成<br/>
<pre class="prettyprint"><code class="language-bash hljs ">$ LANG=C gpg --full-gen-key</code></pre>
<ul>
<li>Kind of key: <code>1</code> RSA and RSA.</li>
<li>Key size: <code>4096</code> 鍵のサイズ.</li>
<li>Expiration: <code>0</code> 0で無期限. 期限ありにしたいならそれを指定.</li>
<li>Real name, email: ご自由に</li>
<li>Comment: フリーテキスト. 空でもok.</li>
</ul>
<p>実行を終えるとキーを保護するためのパスワードを求められるので入力する。</p>
4. 生成した鍵IDを確認<br/>
<pre class="prettyprint"><code class="language-bash hljs ">$ gpg --list-keys
/Users/xxx/.gnupg/pubring.kbx
---------------------------------
pub rsa2048 <span class="hljs-number">2021</span>-xx-xx [SC]
ABCDEFG0123456789ABCDEFG0123456789ABCDEF
uid [ultimate] MatsumuraYuki <xxxx@xxx.xxx>
sub rsa2048 <span class="hljs-number">2021</span>-xx-xx [E]</code></pre>
<p>これで生成した公開鍵の情報が得られます。 <br>
<code>pub</code>にあるフィンガープリントの下8桁が鍵IDになります。(ここでは <code>89ABCDEF</code>) <br>
この8桁の鍵IDはあとで使うのでメモしておきます。</p>
5. 公開鍵を鍵サーバへ登録<br/>
<p>公開鍵があなたのものであることを確認できるように、鍵サーバーにアップロードします。</p>
<pre class="prettyprint"><code class="language-bash hljs ">$ gpg --keyserver keyserver.ubuntu.com --send-keys <先ほど生成した8桁の鍵ID></code></pre>
<p>現在Maven Central Repositoryがサポートしている鍵サーバは下記の3つです.</p>
<ul>
<li><code>keyserver.ubuntu.com</code></li>
<li><code>keys.openpgp.org</code></li>
<li><p><code>pgp.mit.edu</code></p>
</ul>
6. 秘密鍵のBase64エクスポート<br />
<p>署名する際に使う秘密鍵の情報をBase64エクスポートしてメモしておきます。</p>
<pre class="prettyprint"><code class="language-bash hljs ">$ gpg --export-secret-keys 89ABCDEF | base64</code></pre>
<h3 id="3-setup-gradle">3. Setup Gradle</h3>
<p>ここから先は下記のプロジェクトを参考に進めてみてください。動いている完成形で、これをベースに話を進めます。 <br>
<a href="https://github.com/YukiMatsumura/koma">https://github.com/YukiMatsumura/koma</a></p>
<p>公開に必要な設定はルートやモジュールの<code>build.gradle</code>とは別ファイルで管理するようにします(必須ではないですが、管理しやすくなるのでファイルを分けます) <br>
プロジェクトルートに <code>scripts</code> ディクトリを作成して、そこに <code>publish-module.gradle</code> と <code>publish-root.gradle</code> の空ファイルを作成しておきます。</p>
<h4 id="root-buildgradle">Root <code>build.gradle</code></h4>
<p>次にプロジェクトルートの <code>build.gradle</code> に下記を追加します。</p>
<pre class="prettyprint"><code class="language-gradle hljs r">buildscript {
repositories {
maven { url <span class="hljs-string">"https://plugins.gradle.org/m2/"</span> }
<span class="hljs-keyword">...</span>
}
dependencies {
<span class="hljs-keyword">...</span>
classpath <span class="hljs-string">'io.github.gradle-nexus:publish-plugin:1.1.0'</span>
classpath <span class="hljs-string">"org.jetbrains.dokka:dokka-gradle-plugin:1.5.0"</span>
}
}
apply plugin: <span class="hljs-string">'io.github.gradle-nexus.publish-plugin'</span>
apply from: <span class="hljs-string">"${rootDir}/scripts/publish-root.gradle"</span></code></pre>
<p>Maven Central Repositoryへの公開には <a href="https://github.com/gradle-nexus/publish-plugin/">gradle-nexus/publish-plugin</a>を使います。 <br>
また、参考プロジェクトはdokkaを使っているのでそのクラスパスも追加しています。</p>
<h4 id="publish-rootgradle">publish-root.gradle</h4>
<p><code>publish-root.gradle</code> の内容は次のとおりです。</p>
<pre class="prettyprint"><code class="language-gradle hljs livecodeserver">ext[<span class="hljs-string">"ossrhUsername"</span>] = <span class="hljs-string">''</span>
ext[<span class="hljs-string">"ossrhPassword"</span>] = <span class="hljs-string">''</span>
ext[<span class="hljs-string">"sonatypeStagingProfileId"</span>] = <span class="hljs-string">''</span>
ext[<span class="hljs-string">"signing.keyId"</span>] = <span class="hljs-string">''</span>
ext[<span class="hljs-string">"signing.password"</span>] = <span class="hljs-string">''</span>
ext[<span class="hljs-string">"signing.key"</span>] = <span class="hljs-string">''</span>
<span class="hljs-comment">
// CIとローカルビルド両方で動作するように秘匿情報の参照先を分けます</span>
File secretPropsFile = project.rootProject.<span class="hljs-built_in">file</span>(<span class="hljs-string">'local.properties'</span>)
<span class="hljs-keyword">if</span> (secretPropsFile.exists()) {
Properties p = <span class="hljs-built_in">new</span> Properties()
<span class="hljs-built_in">new</span> FileInputStream(secretPropsFile).withCloseable { is -> p.<span class="hljs-built_in">load</span>(is) }
p.<span class="hljs-keyword">each</span> { name, <span class="hljs-built_in">value</span> -> ext[name] = <span class="hljs-built_in">value</span> }
} <span class="hljs-keyword">else</span> {
ext[<span class="hljs-string">"ossrhUsername"</span>] = System.getenv(<span class="hljs-string">'OSSRH_USERNAME'</span>)
ext[<span class="hljs-string">"ossrhPassword"</span>] = System.getenv(<span class="hljs-string">'OSSRH_PASSWORD'</span>)
ext[<span class="hljs-string">"sonatypeStagingProfileId"</span>] = System.getenv(<span class="hljs-string">'SONATYPE_STAGING_PROFILE_ID'</span>)
ext[<span class="hljs-string">"signing.keyId"</span>] = System.getenv(<span class="hljs-string">'SIGNING_KEY_ID'</span>)
ext[<span class="hljs-string">"signing.password"</span>] = System.getenv(<span class="hljs-string">'SIGNING_PASSWORD'</span>)
ext[<span class="hljs-string">"signing.key"</span>] = System.getenv(<span class="hljs-string">'SIGNING_KEY'</span>)
}
nexusPublishing {
repositories {
sonatype {
stagingProfileId = sonatypeStagingProfileId
username = ossrhUsername
password = ossrhPassword
<span class="hljs-comment"> // 2021.02以降Maven Central Repositoryにリポジトリを新規作成する場合は下記の指定が必要です</span>
<span class="hljs-comment"> // https://central.sonatype.org/publish/publish-gradle/#metadata-definition-and-upload</span>
nexusUrl.<span class="hljs-built_in">set</span>(uri(<span class="hljs-string">"https://s01.oss.sonatype.org/service/local/"</span>))
snapshotRepositoryUrl.<span class="hljs-built_in">set</span>(uri(<span class="hljs-string">"https://s01.oss.sonatype.org/content/repositories/snapshots/"</span>))
}
}
}</code></pre>
<h4 id="module-buildgradle">Module <code>build.gradle</code></h4>
<p>公開するライブラリモジュールの <code>build.gradle</code> に下記を追加します。</p>
<pre class="prettyprint"><code class="language-gradle hljs cs">ext {
<span class="hljs-comment">// Provide your own coordinates here</span>
PUBLISH_GROUP_ID = <span class="hljs-string">'Group ID. 例:io.github.yukimatsumura'</span>
PUBLISH_VERSION = <span class="hljs-string">'ライブラリバージョン. 例:0.2'</span>
PUBLISH_ARTIFACT_ID = <span class="hljs-string">'アーティファクトID. 例:koma'</span>
}
apply <span class="hljs-keyword">from</span>: <span class="hljs-string">"${rootProject.projectDir}/scripts/publish-module.gradle"</span></code></pre>
<p>アーティファクトIDは <code>implementation "GroupID:ArtifactID:version"</code> で指定するアーティファクトIDになります。</p>
<h4 id="publish-modulegradle">publish-module.gradle</h4>
<p><code>publish-module.gradle</code> の内容は次のとおりです。</p>
<pre class="prettyprint"><code class="language-gradle hljs livecodeserver">apply plugin: <span class="hljs-string">'maven-publish'</span>
apply plugin: <span class="hljs-string">'signing'</span>
apply plugin: <span class="hljs-string">'org.jetbrains.dokka'</span>
task androidSourcesJar(type: Jar) {
archiveClassifier.<span class="hljs-built_in">set</span>(<span class="hljs-string">'sources'</span>)
<span class="hljs-built_in">from</span> android.sourceSets.main.java.srcDirs
<span class="hljs-built_in">from</span> android.sourceSets.main.kotlin.srcDirs
}
tasks.dokkaHtml.configure {
outputDirectory.<span class="hljs-built_in">set</span>(<span class="hljs-built_in">file</span>(<span class="hljs-string">"../documentation/html"</span>))
}
tasks.withType(dokkaHtml.getClass()).configureEach {
pluginsMapConfiguration.
<span class="hljs-built_in">set</span>([<span class="hljs-string">"org.jetbrains.dokka.base.DokkaBase"</span>: <span class="hljs-string">""</span><span class="hljs-string">"{ "</span>separateInheritedMembers<span class="hljs-string">": true}"</span><span class="hljs-string">""</span>])
}
task javadocJar(type: Jar, dependsOn: dokkaJavadoc) {
archiveClassifier.<span class="hljs-built_in">set</span>(<span class="hljs-string">'javadoc'</span>)
<span class="hljs-built_in">from</span> dokkaJavadoc.outputDirectory
}
artifacts {
archives androidSourcesJar
archives javadocJar
}
signing {
useInMemoryPgpKeys(rootProject.ext[<span class="hljs-string">"signing.keyId"</span>],
rootProject.ext[<span class="hljs-string">"signing.key"</span>],
rootProject.ext[<span class="hljs-string">"signing.password"</span>],)
sign publishing.publications
}
group = PUBLISH_GROUP_ID
<span class="hljs-built_in">version</span> = PUBLISH_VERSION
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
groupId PUBLISH_GROUP_ID
artifactId PUBLISH_ARTIFACT_ID
<span class="hljs-built_in">version</span> PUBLISH_VERSION
<span class="hljs-comment"> // Two artifacts, the `aar` (or `jar`) and the sources</span>
<span class="hljs-keyword">if</span> (project.plugins.findPlugin(<span class="hljs-string">"com.android.library"</span>)) {
<span class="hljs-built_in">from</span> components.release
} <span class="hljs-keyword">else</span> {
<span class="hljs-built_in">from</span> components.java
}
artifact androidSourcesJar
artifact javadocJar
pom {
name = PUBLISH_ARTIFACT_ID
description = <span class="hljs-string">'プロジェクトの概要'</span>
url = <span class="hljs-string">'プロジェクトのURL. 例:https://github.com/YukiMatsumura/koma'</span>
licenses {
license {
<span class="hljs-comment"> // ライセンス情報</span>
name = <span class="hljs-string">'The Apache License, Version 2.0'</span>
url = <span class="hljs-string">'http://www.apache.org/licenses/LICENSE-2.0.txt'</span>
}
}
developers {
developer {
id = <span class="hljs-string">'よしなに. 例:YukiMatsumura'</span>
name = <span class="hljs-string">'よしなに. 例:Matsumura Yuki'</span>
email = <span class="hljs-string">'よしなに. 例:xxxx@gmail.com'</span>
}
}
scm {
connection = <span class="hljs-string">'VCS情報. 例:scm:git:github.com/YukiMatsumura/koma.git'</span>
developerConnection = <span class="hljs-string">'VCS情報. 例:scm:git:ssh://github.com/YukiMatsumura/koma.git'</span>
url = <span class="hljs-string">'VCS情報. 例:https://github.com/YukiMatsumura/koma/tree/main'</span>
}
}
}
}
}
}</code></pre>
<h3 id="4-localproperties">4. local.properties</h3>
<p>外部公開できない秘匿情報を<code>local.properties</code>に定義しましょう。</p>
<pre class="prettyprint"><code class="language-text hljs avrasm">signing<span class="hljs-preprocessor">.keyId</span>=公開鍵の<span class="hljs-number">8</span>桁ID. 例:<span class="hljs-number">89</span>ABCDEF
signing<span class="hljs-preprocessor">.password</span>=PGPで生成した秘密鍵Base64情報. 例:PMxxxxxxxxxxxxxxxx........<span class="hljs-preprocessor">.xx</span>==
ossrhUsername=リポジトリマネージャログインID
ossrhPassword=リポジトリマネージャログインパスワード
sonatypeStagingProfileId=ステージングプロファイルID</code></pre>
<h4 id="ossrhusernamepassword">ossrhUsername/password</h4>
<p>そのままsonatypeのusername/passwordを指定することもできますが、よりセキュアにアクセストークンを発行して指定することもできます。 <br>
Sonatypeのリポジトリマネージャで、 <code>画面右上のログイン名 → Profile → User Token</code> からトークンを生成し、username/passwordと差し替えます。</p>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGorCom6bMfTdh5J-X-TeQwPVmw9CrTwlGISOt_9Z8sKL_67vQBv6a5O0vRKnM5-KJI8YZitrbmvjijdxr29qrxtBpw-HBMMordBbOMk_0VwuTEZkrkCABvqWLhtifI_xlU1uObwSl5Q1z/s0/1.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="416" data-original-width="909" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhGorCom6bMfTdh5J-X-TeQwPVmw9CrTwlGISOt_9Z8sKL_67vQBv6a5O0vRKnM5-KJI8YZitrbmvjijdxr29qrxtBpw-HBMMordBbOMk_0VwuTEZkrkCABvqWLhtifI_xlU1uObwSl5Q1z/s0/1.png"/></a></div>
<h4 id="staging-profile-id">Staging profile id</h4>
<p><a href="https://s01.oss.sonatype.org/">https://s01.oss.sonatype.org/</a> にログイン後, <code>Build Promotion → Staging Profiles</code> を選択し, 自分のプロファイルを選択するとURLの末尾にプロファイルIDが表示されます。 <br>
これを<code>sonatypeStagingProfileId</code>に指定します。</p>
<pre class="prettyprint"><code class=" hljs avrasm">例:https://s01<span class="hljs-preprocessor">.oss</span><span class="hljs-preprocessor">.sonatype</span><span class="hljs-preprocessor">.org</span>/<span class="hljs-preprocessor">#stagingProfiles;<profile id></span></code></pre>
<h3 id="5-release">5. Release</h3>
<p>これですべての設定は完了しました。 <br>
Gradleのタスクリストを見ると、ライブラリモジュールのタスクに<code>publishReleasePublicationToSonatypeRepository</code>がいるはずです。 <br>
コマンドを実行してライブラリをプレリリースしましょう。</p>
<pre class="prettyprint"><code class="language-gradhe hljs ruby">./gradlew <span class="hljs-symbol">:<</span>モジュール名><span class="hljs-symbol">:publishReleasePublicationToSonatypeRepository</span></code></pre>
<p>コマンドを実行すると、Sonatypeリポジトリマネージャの <code>Build Promotion → Staging Repositories</code> にライブラリがアップロードされているのがわかります。 </p>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMI4vvMUaUtjRyjVHnyka99guUt0ihl7_nPmT43nQTQw31l5cT9ghFiCsnpbc010x0efvGyJVPVxqF11ppaws5yVwCbgSphmyNor-ZIoXYU1_o5nLODp0Eru5h8I2tYjmejQbrHK50TdMr/s0/2.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="523" data-original-width="706" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgMI4vvMUaUtjRyjVHnyka99guUt0ihl7_nPmT43nQTQw31l5cT9ghFiCsnpbc010x0efvGyJVPVxqF11ppaws5yVwCbgSphmyNor-ZIoXYU1_o5nLODp0Eru5h8I2tYjmejQbrHK50TdMr/s0/2.png"/></a></div>
<p>ライブラリを選択し <code>Close</code> アクションを実行しましょう。 <br>
<code>Close</code>を実行するとしばらくの間バリデーションが実行されます。実行状況は同画面の <code>Activity</code> タブから確認できます。</p>
<p>リポジトリを閉じると<code>Drop</code>か<code>Release</code>のアクションが選択可能になります。 <br>
公開プロセスで問題があった場合は<code>Drop</code>でキャンセルできます。 <br>
<code>Release</code>を選択するとMaven Centralに公開します。<code>Release</code>後はステージングのアイテムは不要なのでDropできます。</p>
<p>公開には10~15分、長いと1時間以上かかります。 <br>
正常に公開されると <a href="https://repo1.maven.org/maven2/">https://repo1.maven.org/maven2/</a> であなたのリポジトリが参照できます。 <br>
さらに数時間後には <a href="https://search.maven.org/">https://search.maven.org/</a> で検索が可能になっているはずです。</p>
<p>以上です。</p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-54277928094862056782021-05-20T13:16:00.005+09:002021-05-20T13:16:49.652+09:00 Android: uses-permissionの追加・定義元を確認する<div class="markdown">
<p>ライブラリがパーミッションを定義していると, アプリのパーミッションとして自動で追加される. <br>
Android Studioで<code>AndroidManifest.xml</code>を開いて <code>Merged Manifest</code>タブを開けば最終的にアプリが使用するパーミッションを確認できる.</p>
<p>ここで, <code><uses-permission></code> として定義されたパーミッションをどのライブラリが追加・定義しているのかを調べたい場合, アプリを一度ビルドして<code>[module]/build/outputs/logs/manifest-merger-[build variant]-report.txt</code>の内容を確認すれば良い.</p>
<p>下記のような出力結果が得られるので, 「<code>READ_EXTERNAL_STORAGE</code> は <code>LeakCanary</code> が追加しているんだな」と知ることができる.</p>
<pre class="prettyprint"><code class=" hljs avrasm">uses-permission<span class="hljs-preprocessor">#android.permission.READ_EXTERNAL_STORAGE</span>
ADDED from [<span class="hljs-keyword">com</span><span class="hljs-preprocessor">.squareup</span><span class="hljs-preprocessor">.leakcanary</span>:leakcanary-android-core:<span class="hljs-number">2.4</span>] xxxleakcanary-android-core-<span class="hljs-number">2.4</span>/AndroidManifest<span class="hljs-preprocessor">.xml</span>:<span class="hljs-number">23</span>:<span class="hljs-number">5</span>-<span class="hljs-number">80</span></code></pre>
<p>以上.</p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-62810527126474802782021-05-20T13:08:00.003+09:002021-05-20T13:08:53.857+09:00non-SDK interfaces を veridex toolで検出する<div class="markdown">
<p>SDKに定義されていない非公開なインタフェース(non-SDK interface)であってもリフレクションを使うことでアプリから参照することができていましたが, Android 9(API Lv.28)以降は制限されるようになりました. 詳細は「<a href="https://android-developers.googleblog.com/2018/02/improving-stability-by-reducing-usage.html">Improving Stability by Reducing Usage of non-SDK Interfaces</a>」を参照してください. <br>
この変更により, アプリはTarget SDKのバージョンを変更する際にはアプリや依存するライブラリがnon-SDK interfaceを使っていないかをチェックする必要があります.</p>
<p>チェックする方法はいくつかありますが, 本稿ではveridex toolを使用する方法についてまとめます. 「<a href="https://developer.android.com/guide/app-compatibility/restrictions-non-sdk-interfaces#test-veridex-tool">Android Developers - Restrictions on non-SDK interfaces <br>
</a>」にも方法が書かれてありますveridex toolsの制限など知りたい方はそちらを参照してください.</p>
<h2 id="non-sdk-interface使用箇所の検出">non-SDK interface使用箇所の検出</h2>
<p>今回はmacOSでveridex toolsを実行します.</p>
<h3 id="veridex-toolsの実行">veridex toolsの実行</h3>
<ol>
<li><a href="https://android.googlesource.com/platform/prebuilts/runtime/+/refs/heads/master/appcompat/">git</a>から<a href="https://android.googlesource.com/platform/prebuilts/runtime/+archive/master/appcompat.tar.gz">appcompatのtar.gz</a>をDLします. </li>
<li>tar.gzを展開すると <code>veridex-mac.zip</code> があるのでこれを展開します</li>
<li><code>appcompat.sh</code> があるのでこれを実行します</li>
</ol>
<pre class="prettyprint"><code class="language-shell hljs fix"><span class="hljs-attribute">./appcompat.sh --dex-file</span>=<span class="hljs-string">[APKファイルパス]</span></code></pre>
<h3 id="出力結果の確認">出力結果の確認</h3>
<p><code>appcompat.sh</code>を実行すると下記のようなフォーマットで結果が出力されます.</p>
<div class="separator" style="clear: both;"><a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQU21JgSj3x_N36CSAuKjslo1Rx9HM3sqN5XTm3vitmanZ5WA3gcjHcEkaJmnFULcgYO55WRSoC6bi22hVgkhAeXTDkIFOp1f6BKj_El3ivpSj1Wlyqv4HtcNWN9aXmEAq4MHQctcegW2t/s0/report_desc.png" style="display: block; padding: 1em 0; text-align: center; "><img alt="" border="0" data-original-height="212" data-original-width="809" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgQU21JgSj3x_N36CSAuKjslo1Rx9HM3sqN5XTm3vitmanZ5WA3gcjHcEkaJmnFULcgYO55WRSoC6bi22hVgkhAeXTDkIFOp1f6BKj_El3ivpSj1Wlyqv4HtcNWN9aXmEAq4MHQctcegW2t/s0/report_desc.png"/></a></div>
<pre class="prettyprint"><code class="language-txt hljs lasso"><span class="hljs-variable">#1</span>: Linking unsupported Llibcore/io/Memory;<span class="hljs-subst">-></span>pokeByte(JB)V use(s):
Lcom/google/android/gms/internal/gtm/zztx<span class="hljs-variable">$zzb</span>;<span class="hljs-subst">-></span>zza(JB)V
<span class="hljs-variable">#2</span>: Reflection <span class="hljs-keyword">max</span><span class="hljs-attribute">-target</span><span class="hljs-attribute">-p</span> Landroid/widget/AutoCompleteTextView;<span class="hljs-subst">-></span>ensureImeVisible use(s):
Landroidx/appcompat/widget/SearchView<span class="hljs-variable">$PreQAutoCompleteTextViewReflector</span>;<span class="hljs-subst">-></span><span class="hljs-subst"><</span>init<span class="hljs-subst">></span>()V
<span class="hljs-attribute">...</span>
<span class="hljs-number">83</span> hidden API(s) used: <span class="hljs-number">28</span> linked against, <span class="hljs-number">55</span> through reflection
<span class="hljs-number">70</span> <span class="hljs-keyword">in</span> unsupported
<span class="hljs-number">0</span> <span class="hljs-keyword">in</span> blocked
<span class="hljs-number">1</span> <span class="hljs-keyword">in</span> <span class="hljs-keyword">max</span><span class="hljs-attribute">-target</span><span class="hljs-attribute">-o</span>
<span class="hljs-number">12</span> <span class="hljs-keyword">in</span> <span class="hljs-keyword">max</span><span class="hljs-attribute">-target</span><span class="hljs-attribute">-p</span>
<span class="hljs-number">0</span> <span class="hljs-keyword">in</span> <span class="hljs-keyword">max</span><span class="hljs-attribute">-target</span><span class="hljs-attribute">-q</span>
<span class="hljs-number">0</span> <span class="hljs-keyword">in</span> <span class="hljs-keyword">max</span><span class="hljs-attribute">-target</span><span class="hljs-attribute">-r</span></code></pre>
<p>結果の見方は下図の通りです.</p>
<h4 id="non-sdk-apiリストの種別">non-SDK APIリストの種別</h4>
<p>non-SDK APIリストの種別で <code>unsupported</code> は現在, 特に使用制限はなく, アプリが使用できるnon-SDK interfaceです. </p>
<p><code>max-target-p</code> はAndroid 9では制限されていなかったが, Android 10から制限されるようになったAPIです. Android 9では問題ありません. しかし, Android 10かつTarget SDKバージョン10のアプリはこのAPIを使うことができません. 同条件でこのAPIを呼び出すと実行時例外が発生します.</p>
<h4 id="検出されたnon-sdk-interface">検出されたnon-SDK interface</h4>
<p>制限の対象となり得るnon-SDK interfaceです.</p>
<h4 id="non-sdk-interfaceの使用元">non -SDK interfaceの使用元</h4>
<p>non-SDK interfaceをリフレクションを使って参照している参照元です.</p>
<h3 id="検出されたnon-sdk-interfaceの参照元をチェック">検出されたnon-SDK interfaceの参照元をチェック</h3>
<p>検出された参照元が自アプリのコードなら, 下記のようにSDKバージョンに応じて処理を分けるようにします.</p>
<pre class="prettyprint"><code class="language-java hljs "><span class="hljs-keyword">if</span> (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {...}</code></pre>
<p>検出された参照元が3rd-partyライブラリのコードならそのコードを参照し, SDKバージョンに応じて処理を分けているか確認します.</p>
<p>例えば, 下記のような出力結果が得られた場合,</p>
<pre class="prettyprint"><code class="language-txt hljs lasso"><span class="hljs-variable">#2</span>: Reflection <span class="hljs-keyword">max</span><span class="hljs-attribute">-target</span><span class="hljs-attribute">-p</span> Landroid/widget/AutoCompleteTextView;<span class="hljs-subst">-></span>ensureImeVisible use(s):
Landroidx/appcompat/widget/SearchView<span class="hljs-variable">$PreQAutoCompleteTextViewReflector</span>;<span class="hljs-subst">-></span><span class="hljs-subst"><</span>init<span class="hljs-subst">></span>()V</code></pre>
<p><code>androidx.appcompat.widget.SearchView$PreQAutoCompleteTextViewReflector</code> のコードを確認します.</p>
<pre class="prettyprint"><code class="language-java hljs "><span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> PreQAutoCompleteTextViewReflector PRE_API_29_HIDDEN_METHOD_INVOKER =
(Build.VERSION.SDK_INT < <span class="hljs-number">29</span>) ? <span class="hljs-keyword">new</span> PreQAutoCompleteTextViewReflector() : <span class="hljs-keyword">null</span>;</code></pre>
<p>SDKバージョンが考慮されているので, この出力結果は問題ないことがわかります.</p>
<p>もしライブラリ側に問題があった場合, ライブラリのバージョンを上げるか, コードオーナーに修正を依頼するなどして対応を待つ必要があります.</p>
<p>以上です.</p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-19255917442235252192021-02-27T13:14:00.002+09:002021-02-27T13:14:09.197+09:00NavHostFragmentをFragmentの入れ子にする時はsetPrimaryNavigationFragmentを指定する<div class="markdown">
<p><code>NavHostFragment</code> で <code>app:defaultNavHost=true</code>を指定すればバックキー制御をNavHostFragmentに任せることができます.</p>
<pre class="prettyprint"><code class="language-xml hljs "> <span class="hljs-tag"><<span class="hljs-title">androidx.fragment.app.FragmentContainerView
</span> <span class="hljs-attribute">android:name</span>=<span class="hljs-value">"androidx.navigation.fragment.NavHostFragment"</span>
<span class="hljs-attribute">app:defaultNavHost</span>=<span class="hljs-value">"true"</span>
<span class="hljs-attribute">...</span></span></code></pre>
<p><code>NavHostFragment</code>をアクティビティのレイアウトに指定した時の構造は次の通りです.</p>
<pre class="prettyprint"><code class="language-bash hljs ">Activity
|- NavHostFragment</code></pre>
<p>一方で, アクティビティ直下に<code>NavHostFragment</code>を配置せず, 下記のように間にフラグメントがいる場合は注意が必要です.</p>
<pre class="prettyprint"><code class="language-bash hljs ">Activity
|- Fragment
|- NavHostFragment</code></pre>
<p>この場合, フラグメントのレイアウトで<code>app:defaultNavHost=true</code>を指定しても, バックキー制御などナビゲーション周りで意図しない動作となります.</p>
<h3 id="解決策">解決策</h3>
<p><code>NavHostFragment</code>を持つフラグメントを <code>PrimaryNavigationFragment</code> に設定します.</p>
<pre class="prettyprint"><code class="language-kotlin hljs haskell"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-type">HostFragment</span> : <span class="hljs-type">Fragment</span> {
override fun onAttach<span class="hljs-container">(<span class="hljs-title">context</span>: <span class="hljs-type">Context</span>)</span> {
super.onAttach<span class="hljs-container">(<span class="hljs-title">context</span>)</span>
parentFragmentManager.commit {
setPrimaryNavigationFragment<span class="hljs-container">(<span class="hljs-title">this</span>@<span class="hljs-type">HostFragment</span>)</span>
}</span></code></pre>
<p><a href="https://developer.android.com/reference/androidx/fragment/app/FragmentTransaction?hl=en#setPrimaryNavigationFragment%28androidx.fragment.app.Fragment%29">FragmentTransaction.setPrimaryNavigationFragment</a></p>
<h3 id="appdefaultnavhost"><code>app:defaultNavHost</code></h3>
<p><code>NavHostFragment</code>は<code>app:defaultNavHost=true</code>を指定されると, 自身の<code>onAttach</code>で同様に <code>setPrimaryNavigationFragment(this)</code> を設定します.</p>
<p><a href="https://github.com/androidx/androidx/blob/6b9af68666080b59a1d58538925d19a4edb2b1ac/navigation/navigation-fragment/src/main/java/androidx/navigation/fragment/NavHostFragment.kt#L108">NavHostFragment.ktの該当行 - GitHub</a></p>
<h3 id="setprimarynavigationfragment"><code>setPrimaryNavigationFragment</code></h3>
<p>プライマリナビゲーションフラグメントに指定されると, バックナビゲーションなどをハンドリングできるようになります. <br>
<code>app:defaultNavHost=true</code>を指定するだけで, <code>NavHostFragment</code>がバックナビゲーションをうまく制御できるのはこのためです.</p>
<p>プライマリナビゲーションフラグメントはフラグメントマネージャのインスタンス毎に1つしか設定できません.</p>
<p><a href="https://github.com/androidx/androidx/blob/6b9af68666080b59a1d58538925d19a4edb2b1ac/fragment/fragment/src/main/java/androidx/fragment/app/FragmentManager.java#L3398">FragmentManager.setPrimaryNavigationFragment - GitHub</a></p>
<p><code>NavHostFragment</code>を複数管理する場合, <code>app:defaultNavHost=true</code> な<code>NavHostFragment</code>は1つにしなければならない理由でもあります.</p>
<h3 id="navhostfragmentをfragmentの入れ子にする">NavHostFragmentをFragmentの入れ子にする</h3>
<p>フラグメントがプライマリナビゲーションフラグメントと判定されるには, 親フラグメントがいる場合, 関連するフラグメントマネージャのプライマリナビゲーションフラグメントに指定されている必要があります.</p>
<p>つまり, 次の構造では<code>NavHostFragment</code>の親フラグメント/親フラグメントマネージャがいないので, <code>NavHostfragment</code>がプライマリナビゲーションフラグメントになります.</p>
<pre class="prettyprint"><code class="language-bash hljs ">Activity
|- NavHostFragment</code></pre>
<p>しかし, 次の構造では<code>NavHostFragment</code>に親フラグメントがおり, その親が<code>setPrimaryNavigationFragment</code>として指定されていない場合, 子である<code>NavHostFragment</code>もプライマリナビゲーションフラグメントの条件を満たしません.</p>
<pre class="prettyprint"><code class="language-bash hljs ">Activity
|- Fragment
|- NavHostFragment</code></pre>
<p>そのため, 親フラグメントは次のようなコードで自身をプライマリナビゲーションフラグメントとして指定する必要があります.</p>
<pre class="prettyprint"><code class="language-kotlin hljs haskell"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-type">HostFragment</span> : <span class="hljs-type">Fragment</span> {
override fun onAttach<span class="hljs-container">(<span class="hljs-title">context</span>: <span class="hljs-type">Context</span>)</span> {
super.onAttach<span class="hljs-container">(<span class="hljs-title">context</span>)</span>
parentFragmentManager.commit {
setPrimaryNavigationFragment<span class="hljs-container">(<span class="hljs-title">this</span>@<span class="hljs-type">HostFragment</span>)</span>
}</span></code></pre>
<h3 id="蛇足-navhostfragmentのバックナビゲーション周りの実装">蛇足: NavHostFragmentのバックナビゲーション周りの実装</h3>
<pre class="prettyprint"><code class=" hljs java">// デフォルトでフラグメントマネージャのOnBackPressedCallbackはenable=<span class="hljs-keyword">false</span>になっている
FragmentManager
<span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> OnBackPressedCallback mOnBackPressedCallback =
<span class="hljs-keyword">new</span> OnBackPressedCallback(<span class="hljs-keyword">false</span>) {
<span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">handleOnBackPressed</span>() {
FragmentManager.<span class="hljs-keyword">this</span>.handleOnBackPressed();
}
};
---
// OnBackPressedCallbackをenable=<span class="hljs-keyword">true</span>にするにはisPrimaryNavigationで<span class="hljs-keyword">true</span>を返す必要がある
<span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">updateOnBackPressedCallbackEnabled</span>() {
...
<span class="hljs-comment">// This FragmentManager needs to have a back stack for this to be enabled</span>
<span class="hljs-comment">// And the parent fragment, if it exists, needs to be the primary navigation</span>
<span class="hljs-comment">// fragment.</span>
mOnBackPressedCallback.setEnabled(getBackStackEntryCount() > <span class="hljs-number">0</span>
&& isPrimaryNavigation(mParent));
}
---
// PrimaryNavigationFragmentに変更があると...
Fragment
<span class="hljs-keyword">void</span> performPrimaryNavigationFragmentChanged() {
<span class="hljs-keyword">boolean</span> isPrimaryNavigationFragment = mFragmentManager.isPrimaryNavigation(<span class="hljs-keyword">this</span>); ⭐️
<span class="hljs-comment">// Only send out the callback / dispatch if the state has changed</span>
<span class="hljs-keyword">if</span> (mIsPrimaryNavigationFragment == <span class="hljs-keyword">null</span>
|| mIsPrimaryNavigationFragment != isPrimaryNavigationFragment) {
mIsPrimaryNavigationFragment = isPrimaryNavigationFragment;
onPrimaryNavigationFragmentChanged(isPrimaryNavigationFragment); 🍣
---
// 一方, NavHostFragmentでは...
NavHostFragment
🍣
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onPrimaryNavigationFragmentChanged</span>(<span class="hljs-keyword">boolean</span> isPrimaryNavigationFragment) {
<span class="hljs-keyword">if</span> (mNavController != <span class="hljs-keyword">null</span>) {
mNavController.enableOnBackPressed(isPrimaryNavigationFragment); 🌴
---
// BackPressedCallbackを有効にするにはisPrimaryNavigationFragmentが<span class="hljs-keyword">true</span>である必要がある.
NavController
🌴
<span class="hljs-keyword">void</span> enableOnBackPressed(<span class="hljs-keyword">boolean</span> enabled) {
mEnableOnBackPressedCallback = enabled;
updateOnBackPressedCallbackEnabled();
<span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">updateOnBackPressedCallbackEnabled</span>() {
mOnBackPressedCallback.setEnabled(mEnableOnBackPressedCallback
&& getDestinationCountOnBackStack() > <span class="hljs-number">1</span>);
---
// NavControllerはOnBackPressedCallbackを持っている. NavHostFragmentのバックキー制御はNavControllerの責務
<span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> OnBackPressedCallback mOnBackPressedCallback =
<span class="hljs-keyword">new</span> OnBackPressedCallback(<span class="hljs-keyword">false</span>) {
<span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">handleOnBackPressed</span>() {
popBackStack();
}
};</code></pre>
<p>以上.</p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-82052768741668703422020-05-13T11:57:00.000+09:002020-05-13T13:02:52.203+09:00Android: dropbox/Store<div class="markdown">
<p><a href="https://github.com/dropbox/Store">dropbox/Store :Github</a></p>
<h2 id="dropboxstoreとは">Dropbox/Storeとは</h2>
<p><code>Store</code>はデータロードのためのライブラリです. <br>
データの問合せに対して, ネットワーク越しにデータを取得するフェッチャーを定義し, 取得したデータをどのようにキャッシュするのかを決め, 指定したキャッシュポリシーにしたがって, その後のデータ取得を効率的に行うことができます. <br>
<code>Store</code>にアクセスするクラスは, データの所在(ネットワーク or ディスク or メモリ)を気にすることなくデータ取得することができるようになります.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhxdLI4-j8pTXfMsWmHrTJTbymptv0T3rY8O4pePskSm_LFRxgtlKGhfltBQhIJWVORudRjdeLYJYWT00bpgiobUdVjdgg5U-HUk5DkEJ4JLQHIg_bRaj8bLZqR85LSZLkSwWm121wRs6eK/s1600/getoncoldstart.gif" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhxdLI4-j8pTXfMsWmHrTJTbymptv0T3rY8O4pePskSm_LFRxgtlKGhfltBQhIJWVORudRjdeLYJYWT00bpgiobUdVjdgg5U-HUk5DkEJ4JLQHIg_bRaj8bLZqR85LSZLkSwWm121wRs6eK/s1600/getoncoldstart.gif" data-original-width="720" data-original-height="405" /></a>
<p><code>Store</code>が主に提供する機能は次の3つです.</p>
<ol>
<li>ネットワークを経由してデータをフェッチする方法の宣言(required)</li>
<li>取得したデータをメモリまたはディスクにキャッシュする方法の宣言(optional)</li>
<li>キャッシュのEvictionPolity(optional)</li>
</ol>
<p><code>Store</code>はデータを<code>Flow</code>で返すためマルチスレッド処理することが容易になるよう設計されています. <code>Flow/Coroutines</code>による構造化された同時実行性の性質によって, スコープが明確に定義され, メモリリークの減少, パフォーマンスの向上, クラッシュリスクの軽減が期待されます.</p>
<p><code>Store</code>によってデータのフェッチ/共有/キャッシュに関するロジックがカプセル化され, ビューで最新のデータを効率的に購読することができ, データをオフラインで使用することもできるようになります.</p>
<h2 id="store簡単まとめ">Store簡単まとめ</h2>
<ul>
<li>フェッチャーには単一レスポンスと複数レスポンス(<code>Flow</code>)のバリエーションがある</li>
<li>ディスクキャッシュする/しないを選べる. メモリキャッシュする/しないを選べる</li>
<li>メモリキャッシュのEviction Polityには最も過去に生成/更新されたものを破棄, 最終アクセスからn時間経過で破棄, キャッシュの上限個数を指定できる</li>
<li><code>Store</code>へのデータ問合せ時にはデータを一意に識別できる汎用キーが必要. これは同一リクエストかの判定やキャッシュヒットの判定に使われ, 汎用キーはKotlin Data Classが推奨される.</li>
<li><code>Store</code>へのデータ問合せによって <code>Loading</code>, <code>Data</code>, <code>Error</code>のレスポンスがエミットされる</li>
<li><code>Store</code>へのデータ問合せ中に発生したエラーは<code>Error</code>としてエミットされる</li>
<li>フェッチャーが使う<code>Flow</code>のスコープは<code>GlobalScope</code></li>
<li>In-flight debouncerが実装されており, 初回の複数同時リクエスト時にもうまくキャッシュが効く</li>
</ul>
<h2 id="ビルダーによるstoreの構築">ビルダーによるStoreの構築</h2>
<p><code>Store</code>は<code>StoreBuilder</code>によって構築されます.</p>
<pre class="prettyprint"><code class="language-kotlin hljs avrasm">StoreBuilder
<span class="hljs-preprocessor">.from</span>(
fetcher = nonFlowValueFetcher { api<span class="hljs-preprocessor">.fetchSubreddit</span>(it, <span class="hljs-string">"10"</span>)<span class="hljs-preprocessor">.data</span><span class="hljs-preprocessor">.children</span><span class="hljs-preprocessor">.map</span>(::toPosts) },
sourceOfTruth = SourceOfTrue<span class="hljs-preprocessor">.from</span>(
reader = db<span class="hljs-preprocessor">.postDao</span>()::loadPosts,
writer = db<span class="hljs-preprocessor">.postDao</span>()::insertPosts,
delete = db<span class="hljs-preprocessor">.postDao</span>()::clearFeed,
deleteAll = db<span class="hljs-preprocessor">.postDao</span>()::clearAllFeeds
)
)<span class="hljs-preprocessor">.cachePolicy</span>(
MemoryPolicy<span class="hljs-preprocessor">.builder</span>()
<span class="hljs-preprocessor">.setMemorySize</span>(<span class="hljs-number">10</span>)
<span class="hljs-preprocessor">.setExpireAfterAccess</span>(<span class="hljs-number">10.</span>minutes) // <span class="hljs-keyword">or</span> setExpireAfterWrite(<span class="hljs-number">10.</span>minutes)
<span class="hljs-preprocessor">.build</span>()
)<span class="hljs-preprocessor">.build</span>()</code></pre>
<p>これは次のことを宣言しています</p>
<ol>
<li>複数回呼び出された場合に備えるフェッチしたデータのメモリキャッシュ</li>
<li>ネットワークがオフラインの場合に備えたディスクキャッシュ</li>
</ol>
<p><code>Store</code>はネットワークへの過剰な呼び出しを防ぎ, ディスクキャッシュをSource of Truthとして使用することができます. Source of Truthの実装には <code>Room</code>, <code>SQLDelight</code> などの監視可能なソースを提供できるデータベースが利用できます.</p>
<h3 id="storebuilderfromnonflow">StoreBuilder.fromNonFlow</h3>
<pre class="prettyprint"><code class="language-kotlin hljs vbnet">fun <<span class="hljs-keyword">Key</span> : Any, Output : Any> fromNonFlow(
fetcher: suspend (<span class="hljs-keyword">key</span>: <span class="hljs-keyword">Key</span>) -> Output
): StoreBuilder<<span class="hljs-keyword">Key</span>, Output></code></pre>
<p><code>Flow</code>を返さないフェッチャーを持つ<code>StoreBuilder</code>を生成します. <br>
リクエストに対してHTTPのように単一の応答を返すフェッチャーを持つ<code>Store</code>を生成します.</p>
<h3 id="storebuilderfrom">StoreBuilder.from</h3>
<pre class="prettyprint"><code class="language-kotlin hljs vbnet">fun <<span class="hljs-keyword">Key</span> : Any, Output : Any> <span class="hljs-keyword">from</span>(
fetcher: (<span class="hljs-keyword">key</span>: <span class="hljs-keyword">Key</span>) -> Flow<Output>
): StoreBuilder<<span class="hljs-keyword">Key</span>, Output> = BuilderImpl(fetcher)</code></pre>
<p><code>Flow</code>を返すフェッチャーを持つ<code>StoreBuilder</code>を生成します. <br>
リクエストに対してWebsocketのように複数の応答を返すフェッチャーを持つ<code>Store</code>を生成します.</p>
<h3 id="storebuilderpersister">StoreBuilder.persister</h3>
<pre class="prettyprint"><code class="language-kotlin hljs coffeescript">fun <NewOutput : Any> persister(
<span class="hljs-attribute">reader</span>: <span class="hljs-function"><span class="hljs-params">(Key)</span> -></span> Flow<NewOutput?>,
<span class="hljs-attribute">writer</span>: suspend <span class="hljs-function"><span class="hljs-params">(Key, Output)</span> -></span> Unit,
<span class="hljs-attribute">delete</span>: <span class="hljs-function"><span class="hljs-params">(suspend (Key) -> Unit)</span>? = <span class="hljs-title">null</span>,
<span class="hljs-title">deleteAll</span>: <span class="hljs-params">(suspend () -> Unit)</span>? = <span class="hljs-title">null</span>
): <span class="hljs-title">StoreBuilder</span><<span class="hljs-title">Key</span>, <span class="hljs-title">NewOutput</span>></span></code></pre>
<p><code>Flow</code>なディスクキャッシュへアクセスするための <code>reader</code>, <code>writer</code>, <code>deleter</code> を定義します.</p>
<p>柔軟性を確保するため, <code>writer</code>のレコードタイプ(<code>Output</code>)と<code>reader</code>のレコードタイプ(<code>NewOutput</code>)は異なる型にすることができます. これによって, ネットワークから取得される型とローカルストレージのレコードタイプを分けることができます.</p>
<h3 id="storebuildernonflowingpersister">StoreBuilder.nonFlowingPersister</h3>
<pre class="prettyprint"><code class="language-kotlin hljs coffeescript">fun <NewOutput : Any> nonFlowingPersister(
<span class="hljs-attribute">reader</span>: suspend <span class="hljs-function"><span class="hljs-params">(Key)</span> -></span> NewOutput?,
<span class="hljs-attribute">writer</span>: suspend <span class="hljs-function"><span class="hljs-params">(Key, Output)</span> -></span> Unit,
<span class="hljs-attribute">delete</span>: <span class="hljs-function"><span class="hljs-params">(suspend (Key) -> Unit)</span>? = <span class="hljs-title">null</span>,
<span class="hljs-title">deleteAll</span>: <span class="hljs-params">(suspend () -> Unit)</span>? = <span class="hljs-title">null</span>
): <span class="hljs-title">StoreBuilder</span><<span class="hljs-title">Key</span>, <span class="hljs-title">NewOutput</span>></span></code></pre>
<p><code>Flow</code>ではないディスクキャッシュへアクセスするための <code>reader</code>, <code>writer</code>, <code>deleter</code> を定義します.</p>
<h3 id="storebuildercachepolicy">StoreBuilder.cachePolicy</h3>
<pre class="prettyprint"><code class="language-kotlin hljs erlang"><span class="hljs-keyword">fun</span> cache<span class="hljs-variable">Policy</span>(memory<span class="hljs-variable">Policy</span>: <span class="hljs-variable">MemoryPolicy</span>?): <span class="hljs-variable">StoreBuilder</span><<span class="hljs-variable">Key</span>, <span class="hljs-variable">Output</span>></code></pre>
<p><code>Store</code>のメモリキャッシュにおけるEvictionポリシーを指定できます. <br>
<code>MemoryPolicy.MemoryPolicyBuilder</code>で TTLまたは容量ベースのEvictionを設定できます. </p>
<p>ビルダーで特にポリシーの指定がない場合, 次のキャッシュポリシーが適用されます.</p>
<ul>
<li>キャッシュできるエントリーの上限個数 = 100</li>
<li>Evictionポリシー = キャッシュしたエントリーの作成/更新から24時間以上経過したものを破棄</li>
</ul>
<h3 id="storebuilderdisablecache">StoreBuilder.disableCache</h3>
<pre class="prettyprint"><code class="language-kotlin hljs erlang"><span class="hljs-keyword">fun</span> disable<span class="hljs-variable">Cache</span>(): <span class="hljs-variable">StoreBuilder</span><<span class="hljs-variable">Key</span>, <span class="hljs-variable">Output</span>></code></pre>
<p>キャッシュ機構を持たない<code>Store</code>になります.</p>
<h3 id="storebuilderscope">StoreBuilder.scope</h3>
<pre class="prettyprint"><code class="language-kotlin hljs d">fun <span class="hljs-keyword">scope</span>(<span class="hljs-keyword">scope</span>: CoroutineScope): StoreBuilder<Key, Output></code></pre>
<p><code>Store</code>が汎用キーに対応するデータをフェッチ(あるいはキャッシュヒット)して, 複数の購読者に結果をマルチキャストする際, そのスコープは<code>GlobalScope</code>となるのがデフォルトの挙動です. <br>
このマルチキャストのスコープを独自にハンドリングしたい場合はこの関数で<code>CoroutineScope</code>を指定します.</p>
<h2 id="memorypolicybuilder">MemoryPolicyBuilder</h2>
<p><code>Store</code>のメモリキャッシュポリシー(<code>MemoryPolicy</code>)を定義するためのビルダークラスです. <br>
いわゆるEvictionポリシーはここで指定することになります. <br>
このポリシーは最終的に<code>Cache</code>クラスを生成するパラメータとして利用されます.</p>
<h3 id="memorypolicybuildersetexpireafterwrite">MemoryPolicyBuilder.setExpireAfterWrite</h3>
<pre class="prettyprint"><code class="language-kotlin hljs bash">fun <span class="hljs-keyword">set</span>ExpireAfterWrite(expireAfterWrite: Duration): MemoryPolicyBuilder</code></pre>
<p>キャッシュエントリーが作成 or 置換/更新されてから一定時間後に自動削除するポリシーです. <br>
<code>Duration</code>が<code>0</code>で指定されるとキャッシュされなくなります.</p>
<h3 id="memorypolicybuildersetexpireafteraccess">MemoryPolicyBuilder.setExpireAfterAccess</h3>
<pre class="prettyprint"><code class="language-kotlin hljs bash">fun <span class="hljs-keyword">set</span>ExpireAfterAccess(expireAfterAccess: Duration): MemoryPolicyBuilder</code></pre>
<p>キャッシュエントリーが作成 or 置換/更新 or 最後にアクセスされてから一定時間後に自動削除するポリシーです. <br>
<code>Duration</code>が<code>0</code>で指定されるとキャッシュされなくなります.</p>
<h3 id="memorypolicybuildersetmemorysize">MemoryPolicyBuilder.setMemorySize</h3>
<pre class="prettyprint"><code class="language-kotlin hljs bash">fun <span class="hljs-keyword">set</span>MemorySize(maxSize: Long): MemoryPolicyBuilder</code></pre>
<p>キャッシュされるエントリー個数の上限を指定します. エントリー個数が上限を超えた場合, アクセスされた時間の最も古いエントリーが削除対象となります(LRU) <br>
<code>0</code>が指定されるとすぐにキャッシュを破棄するため, キャッシュされなくなります. <br>
特にビルダーで指定しなかった場合は個数の上限を設けません.</p>
<h2 id="storeの実装制約">Storeの実装制約</h2>
<p><code>Store</code>の唯一の実装制約は<code>flow</code>を返す関数, または特定の型を返すフェッチ用関数(フェッチャー)を実装する必要があることです.</p>
<pre class="prettyprint"><code class="language-kotlin hljs avrasm">val store = StoreBuilder<span class="hljs-preprocessor">.from</span> {
articleId -> api<span class="hljs-preprocessor">.getArticle</span>(articleId) //Flow<Article>
}
<span class="hljs-preprocessor">.build</span>() </code></pre>
<h3 id="データの識別子">データの識別子</h3>
<p><code>Store</code>はデータの識別子として汎用キーを使用します. <br>
<strong>この汎用キーは<code>toString()</code>, <code>equals()</code>, <code>hashCode()</code> を適切に実装した値オブジェクトにする必要があります.</strong> <br>
汎用キーにはKotlinの<code>data class</code>を使うことが強く推奨されます. </p>
<p>この汎用キーはフェッチ関数の引数として渡されます. また, キャッシュのプライマリ識別子としても利用されます. <br>
UIはこの汎用キーさえ知っていれば, いつでも<code>Store</code>からデータをネットワーク/キャッシュを気にせず再取得できるようになっています.</p>
<h2 id="stream-api">Stream API</h2>
<p><code>Store</code>が提供する主要なAPIとして<code>stream function</code>があります.</p>
<pre class="prettyprint"><code class="language-kotlin hljs xml">fun stream(request: StoreRequest<span class="hljs-tag"><<span class="hljs-title">Key</span>></span>): Flow<span class="hljs-tag"><<span class="hljs-title">StoreResponse</span>></span>Output>></code></pre>
<p><code>stream</code> の呼び出しに渡される <code>StoreRequest</code> には次の情報が格納されています.</p>
<ol>
<li>データを識別するための汎用キー</li>
<li>キャッシュの利用方針(ディスク/メモリキャッシュの利用有無)</li>
</ol>
<p><code>stream</code>の戻り値は<code>StoreResponse</code>が<code>Flow</code>で返されます.</p>
<h3 id="storeresponse">StoreResponse</h3>
<p><code>StoreResponse</code>は<code>seald class</code>でサブクラスには<code>Loading</code>, <code>Data</code>, <code>Error</code>が定義されています.</p>
<ul>
<li>それぞれのクラスには <code>ResponseOrigin</code> フィールドがあり, データの取得元がキャッシュ or ディスク or フェッチなのかを判別できるようになっています.</li>
<li><code>Loading</code>は<code>ResponseOrigin</code>のみを持ちます. このクラスはデータのロードをUIに反映するきっかけとして使うことができます.</li>
<li><code>Data</code>は<code>Store</code>から返される値を持ったクラスです</li>
<li><code>Error</code>は<code>ResponseOrigin</code>によって投げられた例外をフィールドに持ちます</li>
</ul>
<p>エラーが発生した場合でも<code>Store</code>は例外をスローしません. その代わり, <code>StoreResponse.Error</code>タイプによってこれが表現されます. <br>
これによって<code>Flow</code>が壊れることがなく, データへの問い合わせやデータの更新が継続して行われることになります. <br>
これによって, UIは<code>flow</code>の再起動/再接続を意識する必要がなくなります.</p>
<pre class="prettyprint"><code class="language-kotlin hljs vbscript">lifecycleScope.launchWhenStarted {
store.stream(StoreRequest.cached(key = key, refresh=<span class="hljs-literal">true</span>)).collect { <span class="hljs-built_in">response</span> ->
when(<span class="hljs-built_in">response</span>) {
<span class="hljs-keyword">is</span> StoreResponse.Loading -> showLoadingSpinner()
<span class="hljs-keyword">is</span> StoreResponse.Data -> {
<span class="hljs-keyword">if</span> (<span class="hljs-built_in">response</span>.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
updateUI(<span class="hljs-built_in">response</span>.value)
}
<span class="hljs-keyword">is</span> StoreResponse.<span class="hljs-keyword">Error</span> -> {
<span class="hljs-keyword">if</span> (<span class="hljs-built_in">response</span>.origin == ResponseOrigin.Fetcher) hideLoadingSpinner()
showError(<span class="hljs-built_in">response</span>.<span class="hljs-keyword">error</span>)
}
}
}
}</code></pre>
<h2 id="extention-function">extention function</h2>
<h3 id="suspend-fun-storegetkey-key-value">suspend fun Store.get(key: Key): Value</h3>
<p>渡された値に対応するデータを単発取得します. <br>
メモリ/ディスクキャッシュにヒットするデータがあればそこから取得されます. <br>
エラー(<code>StoreResponse.Error</code>)が発生した場合は例外がスローされ, データがない場合は<code>NullPointerException</code>がスローされます.</p>
<p>キャッシュされたデータもない初めての <code>Store.get</code> の呼び出しでは, ネットワークからデータを取得して, ディスク/メモリキャッシュにこれを格納します. <br>
再度, おなじ汎用キーで<code>Store.get</code>を呼び出したなら, キャッシュからデータを取得して, ネットワーク通信を最低限に抑えようとします.</p>
<h3 id="suspend-fun-storefreshkey-key-value">suspend fun Store.fresh(key: Key): Value</h3>
<p>フェッチャーによる問合せによって, ネットワークからデータを単発取得します. <br>
ディスク/メモリキャッシュをスキップしてデータを取得するため, 定期ジョブによるデータ(キャッシュ)更新や, Pull to Refreshによるデータの強制更新時などに利用されます. </p>
<h3 id="suspend-fun-storestreamkey-key-flow">suspend fun Store.stream(key: Key): Flow</h3>
<p>データを監視してリアルタイムにUI更新をしたい場合などでは <code>Store.stream</code> が利用できます. <br>
ディスクキャッシュの更新や, ネットワークからのロード/エラーイベントを監視するストリームを作成する方法と考えることができます.</p>
<h2 id="in-flight-debouncer">In-flight debouncer</h2>
<p><code>Store</code>は同じデータに対するリクエストの重複を避けるためにIn-flight debouncerの機能が組み込まれています. <br>
リクエストに対するデータがまだキャッシュされていない場合, 複数個の同じデータに対するリクエストが同時にあると, それぞれが並列に処理されてキャッシュヒットせず, 両方のリクエストがネットワーク通信に至ってしまう可能性があります. <br>
In-flight debouncerはこの不要なネットワーク通信を回避するために, 最初のリクエストはデータ取得のためにブロックされ, 他方の呼び出しはデータの到着を待たせます. </p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://github.com/dropbox/Store">dropbox/Store :Github</a></li>
<li><a href="https://dropbox.tech/mobile/store-grand-re-opening-loading-android-data-with-coroutines">Store grand re-opening: loading Android data with coroutines :Dropbox.Tech</a></li>
<li><a href="https://www.youtube.com/watch?v=raWdIwsDe-g&feature=youtu.be">KotlinConf 2019: Migrating a Library from RxJava To Coroutines by Mike Nakhimovich & Yiğit Boyar : YouTube</a></li>
</ul>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-39681890172813350352020-04-07T22:53:00.001+09:002020-04-07T22:53:37.143+09:00Android:バッテリー温度の確認方法<div class="markdown">
<p>下記コマンドでバッテリー状態をダンプできます.</p>
<pre class="prettyprint"><code class=" hljs livecodeserver">adb <span class="hljs-built_in">shell</span> dumpsys battery</code></pre>
<p>コマンドを実行すると, 次のような出力が得られます.</p>
<pre class="prettyprint"><code class=" hljs r">Current Battery Service state:
AC powered: false
USB powered: true
Wireless powered: false
Max charging current: <span class="hljs-number">0</span>
Max charging voltage: <span class="hljs-number">0</span>
Charge counter: <span class="hljs-number">3160971</span>
status: <span class="hljs-number">2</span>
health: <span class="hljs-number">2</span>
present: true
level: <span class="hljs-number">95</span>
scale: <span class="hljs-number">100</span>
voltage: <span class="hljs-number">4315</span>
temperature: <span class="hljs-number">358</span>
technology: Li-ion
batteryMiscEvent: <span class="hljs-number">0</span>
batteryCurrentEvent: <span class="hljs-number">32768</span>
mSecPlugTypeSummary: <span class="hljs-number">2</span>
<span class="hljs-keyword">...</span> 続く</code></pre>
<p>出力された中にある <code>temperature: 358</code> がバッテリー温度になります. <br>
数値は温度(摂氏)の10倍値になるので, <code>358</code> なら<code>35.8℃</code> ということになります.</p>
<p>バッテリー状態を管理するサービスクラスは<a href="https://cs.android.com/android/platform/superproject/+/master:frameworks/base/services/core/java/com/android/server/BatteryService.java">BatteryService</a>です. <br>
バッテリー状態のデータは<a href="out/soong/.intermediates/hardware/interfaces/health/1.0/android.hardware.health-V1.0-java/android_common/xref/srcjars.xref/android/hardware/health/V1_0/HealthInfo.java">HealthInfo</a>に定義されています.</p>
<p>以上です.</p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-942554088071323282019-01-09T22:50:00.001+09:002019-01-09T22:50:02.757+09:00Android: /data/data配下にadb push<div class="markdown">
<p><code>/data/data/<Application ID>/</code>配下にADBでファイル追加しようとするとpermission errorで失敗した✍</p>
<p>issueはこれ. <br>
<a href="https://issuetracker.google.com/issues/37138359">https://issuetracker.google.com/issues/37138359</a></p>
<p>Android Studioに付属してるFileExplorerを使えばファイルを追加できた. <br>
FileExplorerは次の手順を踏んでいた。</p>
<pre class="prettyprint"><code class="language-bash hljs "><span class="hljs-comment"># ファイルを一時領域へコピー </span>
$ adb push hoge /data/local/tmp
<span class="hljs-comment"># アプリユーザに切り替え </span>
$ adb shell
$ adb run-as <Application ID>
<span class="hljs-comment"># ファイルコピー </span>
$ cp /data/local/tmp/hoge /data/data/<ApplicationID>/hoge
<span class="hljs-comment">## ↑でエラーが出た場合はcatリダイレクトする</span>
$ cat /data/local/tmp/hohe > /data/data/<Application ID>/hoge</code></pre>
<p>FileExplorerのコードはこの辺.</p>
<p><a href="https://android.googlesource.com/platform/tools/adt/idea/+/studio-3.2.1/android/src/com/android/tools/idea/explorer/adbimpl/AdbDeviceDataDirectoryEntry.java#246">https://android.googlesource.com/platform/tools/adt/idea/+/studio-3.2.1/android/src/com/android/tools/idea/explorer/adbimpl/AdbDeviceDataDirectoryEntry.java#246</a></p>
<p><a href="https://android.googlesource.com/platform/tools/adt/idea/+/studio-3.2.1/android/src/com/android/tools/idea/explorer/adbimpl/AdbFileOperations.java#225">https://android.googlesource.com/platform/tools/adt/idea/+/studio-3.2.1/android/src/com/android/tools/idea/explorer/adbimpl/AdbFileOperations.java#225</a></p>
<p>以上.</p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-30229541803464990152018-12-23T17:14:00.002+09:002018-12-23T19:11:13.070+09:00Android: 擬似的に日本に夏時間を導入してテストする<div class="markdown">
<h2 id="はじめに">はじめに</h2>
<p>i18n対応で考えないといけないことの1つに夏時間(Daylight Saving Time)があります. <br>
夏時間のテストはいくつかの理由で難しい場合が多いです.</p>
<ul>
<li>サーバAPI開発中で, 海外へのサービス提供が蓋閉めされている</li>
<li>夏時間がくるまで待てない</li>
</ul>
<p>そこで, 日本でも夏時間が導入されていることにして, 好きなタイミングでJST(Japan Standard Time)↔️JDT(Japan Daylight Saving Time)を切り替えられればテストが捗りそうです.</p>
<p>本稿は, そのような環境を構築するためにTZDB(Time Zone Database)をテスト用に編集して, それをシステムに認識させる方法を紹介します.</p>
<p>本稿執筆時点でのTZDB Versionは<code>2018g</code>が最新です. 以降は最新が <code>2018g</code> の前提で話を進めます.</p>
<h2 id="threetenbp-と-threetenabp">ThreeTenBp と ThreeTenABP</h2>
<p>ThreeTenBpはTime Zone情報のロード周りでメモリ効率が悪いため, Android向けに<a href="https://github.com/JakeWharton/ThreeTenABP/blob/master/threetenabp/src/main/java/com/jakewharton/threetenabp/AssetsZoneRulesInitializer.java">ThreeTenABP</a>が提供されています.</p>
<p>ThreeTenBpを使用するためにはTime Zone情報を提供する必要があります. <br>
ThreeTenABPはその手続きを肩代わりしてくれるライブラリです.</p>
<p>ThreeTenABPは, ThreeTenBp(/IANA)が提供する <code>TZDB.dat</code> を Assetsに内包し, <code>AndroidThreeTen.init</code> でこれを <a href="https://docs.oracle.com/javase/jp/8/docs/api/java/time/zone/ZoneRulesProvider.html"><code>ZoneRulesProvider</code></a>に登録する <a href="https://github.com/JakeWharton/ThreeTenABP/blob/master/threetenabp/src/main/java/com/jakewharton/threetenabp/AssetsZoneRulesInitializer.java">AssetsZoneRulesInitializer</a>を実行します.</p>
<p>ThreeTenABPがやっていることはこれだけです. <br>
自前で<code>AssetsZoneRulesInitializer</code>と<code>TZDB.dat</code>を用意して, <code>ZoneRulesProvider</code> に登録すれば同じことが実現できるので, 次のようなクラスを用意しておけば, 好きな<code>TZDB.dat</code>を登録できるようになります.</p>
<pre class="prettyprint"><code class="language-kotlin hljs fsharp"><span class="hljs-keyword">class</span> AssetsZoneRulesInitializer(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> context: Context) : ZoneRulesInitializer() {
<span class="hljs-keyword">override</span> <span class="hljs-keyword">fun</span> initializeProviders() {
context.assets.<span class="hljs-keyword">open</span>(<span class="hljs-string">"TZDB.dat"</span>).<span class="hljs-keyword">use</span> {
ZoneRulesProvider.registerProvider(TzdbZoneRulesProvider(it))
}
}
}
<span class="hljs-comment">// Application.onCreateで下記を実行する</span>
ZoneRulesInitializer.setInitializer(AssetsZoneRulesInitializer(this))</code></pre>
<p>今回は, テスト用に定義した <code>TZDB.data</code> を作成・登録することで, 擬似的に日本にも夏時間があることにします.</p>
<h2 id="カスタム-tzdbdat-生成手順">カスタム TZDB.dat 生成手順</h2>
<ol>
<li><a href="https://github.com/ThreeTen/threetenbp">ThreeTenBP GitHub</a>をクローン</li>
<li>IANAから最新のTZDBを<a href="https://www.iana.org/time-zones">ダウンロード</a> </li>
<li>クローンしたソースの <code>src/tzdb/{tzdb-version}</code> に, 展開したTZDBファイルを移動</li>
<li>TZDBを編集</li>
<li><code>mvn clean package -Dtzdb-jar</code> を実行</li>
<li><code>target/threeten-TZDB-{version}.jar</code> から <code>TZDB.dat</code> を抽出</li>
</ol>
<h3 id="1-threetenbp-githubをクローン">1. <a href="https://github.com/ThreeTen/threetenbp">ThreeTenBP GitHub</a>をクローン</h3>
<p><a href="https://github.com/ThreeTen/threetenbp">ThreeTenBP GitHub</a> にはTZDBを読み込んでビルドし, <code>TZDB.dat</code>を生成するコンパイラ <a href="https://github.com/ThreeTen/threetenbp/blob/master/src/main/java/org/threeten/bp/zone/TzdbZoneRulesCompiler.java">TzdbZoneRulesCompiler</a> があります. <br>
<code>TzdbZoneRulesProvider</code>に読み込ませる<code>TZDB.dat</code>を生成するためにこのレポジトリをクローンします.</p>
<h3 id="2-ianaから最新のtzdbdata-only-distributionをダウンロード">2. IANAから最新のTZDBを<a href="https://www.iana.org/time-zones">ダウンロード</a></h3>
<p>TZDBを管理するInternet Assigned Numbers Authority(IANA)から最新のTZDBをダウンロードすることができます.</p>
<p>ダウンロードできる種類がいくつかありますが, 今回はタイムゾーン情報があればよいので “tzdata2018g.tar.gz - Data Only Distribution” を選びます.</p>
<h3 id="3-クローンしたソースの-srctzdbtzdb-version-に-展開したtzdbファイルを移動">3. クローンしたソースの <code>src/tzdb/{tzdb-version}</code> に, 展開したTZDBファイルを移動</h3>
<p>手順2でダウンロードした <code>tar.gz</code> を展開するとTZDBファイルが入っています. <br>
このTZDBファイルを, 手順1でクローンした<code>ThreeTenBp</code>の <code>src/tzdb/{tzdb-version}</code>ディレクトリに移動します.</p>
<p>クローン直後は <code>src</code>ディレクトリ直下に <code>tzdb</code> ディレクトリはないので作成しておきます. <br>
また, 注意点として <code>{tzdb-version}</code> の名前は下記の正規表現にマッチする必要があります</p>
<pre class="prettyprint"><code class="language-regex hljs markdown">[<span class="hljs-link_label">12</span>][<span class="hljs-link_reference">0-9</span>][<span class="hljs-link_label">0-9</span>][<span class="hljs-link_reference">0-9</span>][<span class="hljs-link_label">A-Za-z0-9._-</span>]+
OK: 2018g
NG: tzdb-2018g</code></pre>
<p><code>tar.gz</code>を展開してできるディレクトリ名には余計なプレフィックス <code>tzdb-</code> が入っているので注意が必要です.</p>
<p>最終的に, <code>asia</code> ファイルの場所は下記になります.</p>
<pre class="prettyprint"><code class=" hljs mathematica"><span class="hljs-list">{threetenbp-root}</span>/src/tzdb/<span class="hljs-number">2018</span>g/asia</code></pre>
<h3 id="4-tzdbを編集">4. TZDBを編集</h3>
<p>手順3で移動したTZDB情報を編集します. <br>
今回は日本に夏時間があった場合をシミュレーションするため “Asia/Tokyo” リージョンの情報が定義されている <code>{threetenbp-root}/src/tzdb/2018g/asia</code> ファイルを編集します.</p>
<p>日本(Asia/Tokyo)のタイムゾーン情報は <code>2018g</code> では次のように定義されています.</p>
<pre class="prettyprint"><code class=" hljs mathematica"># <span class="hljs-keyword">Rule</span> NAME FROM TO TYPE IN ON AT SAVE LETTER/S
<span class="hljs-keyword">Rule</span> Japan <span class="hljs-number">1948</span> only - May Sat>=<span class="hljs-number">1</span> <span class="hljs-number">24</span>:<span class="hljs-number">00</span> <span class="hljs-number">1</span>:<span class="hljs-number">00</span> <span class="hljs-keyword">D</span>
<span class="hljs-keyword">Rule</span> Japan <span class="hljs-number">1948</span> <span class="hljs-number">1951</span> - Sep Sat>=<span class="hljs-number">8</span> <span class="hljs-number">25</span>:<span class="hljs-number">00</span> <span class="hljs-number">0</span> S
<span class="hljs-keyword">Rule</span> Japan <span class="hljs-number">1949</span> only - Apr Sat>=<span class="hljs-number">1</span> <span class="hljs-number">24</span>:<span class="hljs-number">00</span> <span class="hljs-number">1</span>:<span class="hljs-number">00</span> <span class="hljs-keyword">D</span>
<span class="hljs-keyword">Rule</span> Japan <span class="hljs-number">1950</span> <span class="hljs-number">1951</span> - May Sat>=<span class="hljs-number">1</span> <span class="hljs-number">24</span>:<span class="hljs-number">00</span> <span class="hljs-number">1</span>:<span class="hljs-number">00</span> <span class="hljs-keyword">D</span></code></pre>
<p>日本でも過去に夏時間(夏時刻法)があったことがわかります.</p>
<p>TZDBのフォーマットは人間にも読めるようになっています. <br>
フォーマットルールは<a href="https://linuxjm.osdn.jp/html/LDP_man-pages/man8/zic.8.html"><code>zic</code> man page</a>に載っています. <br>
これに則り, 日本に夏時間を導入するため次の1行を追加してみましょう.</p>
<pre class="prettyprint"><code class=" hljs css"><span class="hljs-tag">Rule</span> <span class="hljs-tag">Japan</span> 2018 <span class="hljs-tag">only</span> <span class="hljs-tag">-</span> <span class="hljs-tag">Dec</span> 23 00<span class="hljs-pseudo">:00</span> 1<span class="hljs-pseudo">:00</span> <span class="hljs-tag">D</span></code></pre>
<p>これで, TZDB的には <code>2018/12/23 00:00:00(JST)</code> から日本では夏時間(JDT)が適用されるようになります.</p>
<h3 id="5-mvn-clean-package-dtzdb-jar-を実行">5. <code>mvn clean package -Dtzdb-jar</code> を実行</h3>
<p><code>TzdbZoneRulesCompiler</code>を使って編集したTZDBをもとに <code>TZDB.dat</code> を生成します. <br>
ThreeTenBpのルートで下記のコマンドを実行すると<code>TzdbZoneRulesCompiler</code>がビルドを始めます.</p>
<pre class="prettyprint"><code class="language-shell hljs lasso">mvn clean package <span class="hljs-attribute">-Dtzdb</span><span class="hljs-attribute">-jar</span></code></pre>
<p>実行するとビルドログが出力されます. <br>
下記のように <code>Source directory contains no valid source folders</code> のログが出力される場合はTZDBのディレクトリ名かパスが間違っており, <code>TzdbZoneRulesCompiler</code>がTZDBをうまく認識できていない可能性があります. その場合は手順2をやり直しましょう.</p>
<p><code>Source filenames not specified, using default set <br>
(africa antarctica asia australasia backward etcetera europe northamerica southamerica)</code> <br>
は, 今回特にファイル名を指定していないので出力されても問題ありません.</p>
<pre class="prettyprint"><code class="language-console hljs r"><span class="hljs-keyword">...</span>
[INFO] --- exec-maven-plugin:<span class="hljs-number">1.2</span><span class="hljs-number">.1</span>:java (default) @ threetenbp ---
Source filenames not specified, using default set
(africa antarctica asia australasia backward etcetera europe northamerica southamerica)
Source directory contains no valid <span class="hljs-keyword">source</span> folders: xxx
<span class="hljs-keyword">...</span></code></pre>
<p>編集したTZDBがうまく読み込まれなかった場合も <code>BUILD SUCCESS</code> となるので注意してください. その場合, 後述の <code>threeten-TZDB-2018g.jar</code> が出力されません.</p>
<h3 id="6-targetthreeten-tzdb-versionjar-から-tzdbdat-を抽出">6. <code>target/threeten-TZDB-{version}.jar</code> から <code>TZDB.dat</code> を抽出</h3>
<p><code>TzdbZoneRulesCompiler</code> のビルド結果はThreeTenBpプロジェクトルート直下の <code>target</code> ディレクトリに出力されます. <br>
ビルドが成功すると <code>target/threeten-TZDB-2018g.jar</code> が出力されます.</p>
<p>このJarファイルに目的の<code>TZDB.dat</code>が含まれているので, それを抽出します. <br>
下記のJarコマンドで内包されているファイルパスの一覧を取得します.</p>
<pre class="prettyprint"><code class="language-shell hljs mathematica">jar tf <span class="hljs-list">{threeten-TZDB-2018g.jar のパス}</span> </code></pre>
<p>今回のケースでは <code>org/threeten/bp/TZDB.dat</code> に<code>TZDB.dat</code>がありました. <br>
同じくJar コマンドでこれを抽出します.</p>
<pre class="prettyprint"><code class="language-shell hljs avrasm">jar -xvf {threeten-TZDB-<span class="hljs-number">2018</span>g<span class="hljs-preprocessor">.jar</span> のパス} org/threeten/bp/TZDB<span class="hljs-preprocessor">.dat</span></code></pre>
<p>コマンドを実行したディレクトリに <code>org/threeten/bp/TZDB.dat</code> が抽出されます.</p>
<h2 id="テスト">テスト</h2>
<p>日本にも夏時間が定義された<code>TZDB.dat</code> が作成できたので, これを<code>ZoneRulesProvider</code>に登録します.</p>
<pre class="prettyprint"><code class="language-kotlin hljs fsharp"><span class="hljs-keyword">class</span> AssetsZoneRulesInitializer(<span class="hljs-keyword">private</span> <span class="hljs-keyword">val</span> context: Context) : ZoneRulesInitializer() {
<span class="hljs-keyword">override</span> <span class="hljs-keyword">fun</span> initializeProviders() {
context.assets.<span class="hljs-keyword">open</span>(<span class="hljs-string">"TZDB.dat"</span>).<span class="hljs-keyword">use</span> {
ZoneRulesProvider.registerProvider(TzdbZoneRulesProvider(it))
}
}
}
<span class="hljs-comment">// Application.onCreateで下記を実行する.</span>
<span class="hljs-comment">// AndroidThreeTen.initは実行しない(ThreeTenABPは使わない)</span>
ZoneRulesInitializer.setInitializer(AssetsZoneRulesInitializer(this))</code></pre>
<p>この状態で, <code>ZonedDateTime</code>を使って日本時間表示してみると夏時間が適用されていることがわかります.</p>
<pre class="prettyprint"><code class="language-kotlin hljs avrasm">ZonedDateTime
<span class="hljs-preprocessor">.now</span>(ZoneId<span class="hljs-preprocessor">.of</span>(<span class="hljs-string">"Asia/Tokyo"</span>))
<span class="hljs-preprocessor">.format</span>(DateTimeFormatter<span class="hljs-preprocessor">.ISO</span>_DATE_TIME)
// 出力: <span class="hljs-number">2018</span>-<span class="hljs-number">12</span>-<span class="hljs-number">23</span>T15:<span class="hljs-number">26</span>:<span class="hljs-number">19.295</span>+<span class="hljs-number">10</span>:<span class="hljs-number">00</span>[Asia/Tokyo]
ZoneId<span class="hljs-preprocessor">.of</span>(<span class="hljs-string">"Asia/Tokyo"</span>)<span class="hljs-preprocessor">.rules</span><span class="hljs-preprocessor">.isDaylightSavings</span>(Instant<span class="hljs-preprocessor">.now</span>())
// 出力:true</code></pre>
<p>以上です.</p>
<p>参考:</p>
<ul>
<li><a href="https://linuxjm.osdn.jp/html/LDP_man-pages/man8/zic.8.html"><code>zic</code> man page</a></li>
<li><a href="https://www.threeten.org/threetenbp/update-tzdb.html">ThreeTen Backport - Update tzdb</a></li>
</ul>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-83176399432006293152018-12-14T02:25:00.002+09:002018-12-14T02:25:52.111+09:00不変条件とか, Nullabilityとか, Kotlin化とか<div class="markdown">
<p>備忘録. 走り書き. <br>
Kotlin化するときに苦しんだお話.</p>
<p>Java → Kotlin化する時によく困るのがNullabilityの判断. <br>
ある日, こんな感じのコードに出会った.</p>
<pre class="prettyprint"><code class="language-java hljs ">class Hoge {
<span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> String id;
<span class="hljs-keyword">protected</span> String foo;
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Hoge <span class="hljs-title">from</span>(proto HogeProto) {
<span class="hljs-keyword">if</span> (proto == <span class="hljs-keyword">null</span>) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalArgumentException(...);
Hoge hoge = Hoge(proto.id)
hoge.foo = Wire.get(proto.foo, HogeProto.DEFAULT_FOO)
<span class="hljs-keyword">return</span> hoge;
}
<span class="hljs-keyword">private</span> <span class="hljs-title">Hoge</span>(String id) {
<span class="hljs-keyword">this</span>.id = id;
}
...
}</code></pre>
<p>APIコールの応答として <code>HogeProto</code> を受け取り, それをモデル <code>Hoge</code> に変換させるコード. (Protocol Buffers と Wireライブラリを使ってる)</p>
<p>直したい部分がいくつかある.</p>
<p>まず, <code>proto.id</code> がJavaのString型なので <code>null</code> の可能性を捨てきれない. <br>
もし, とってもラッキーなことに, 全く正しく疑う余地のない最新のドキュメントが存在していて「idは絶対にnullにならない」って明記されていたり, サーバサイドのコードが <code>assert id != null</code> の不変条件を表明していたりする場合は, <code>requireNotNull(...)</code> の一文を事前条件として追加できるかもしれない. </p>
<pre class="prettyprint"><code class="language-java hljs "> <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Hoge <span class="hljs-title">from</span>(proto HogeProto) {
requireNotNull(proto, <span class="hljs-string">"..."</span>);
String id = requireNotNull(proto.id, <span class="hljs-string">"..."</span>);
Hoge hoge = Hoge(id);
....</code></pre>
<p>でも, 残念なことに今回はそんな状況じゃなかった.</p>
<p>ビジネス上, <code>proto.id</code> が <code>null</code> である可能性が限りなく乏しい状況だけれど, 数千万のユーザを抱えるサービスのエンジニアとしての責任を考えると「大丈夫でしょ♪」と根拠のない自信だけで例外を投げるチェックコードを追加する訳にもいかないし, そんなコードをリリースした夜はきっと眠れない(私は少し心配性).</p>
<p>サービスやコードの規模が大きくなった後で, こうしたチェックを追加するのはかなり苦労する. この問題は, Hogeクラスのコードを書いたプログラマがちょっと気を利かせて, Hogeクラスの不変条件をコードで表明しておいてくれれば助かるケースだった.</p>
<p>不変条件が追加できると判断できれば, Kotlin化もスムーズに滞りなくできる.</p>
<pre class="prettyprint"><code class="language-kotlin hljs r">class Hoge (
val id: String
) {
companion object {
fun from(HogeProto proto): Hoge {
requireNonNull(proto) {<span class="hljs-keyword">...</span>}
val id = requireNotNull(proto.id) {<span class="hljs-keyword">...</span>}
Hoge(id)
<span class="hljs-keyword">...</span>
}</code></pre>
<p>IDの不変条件の話はこれぐらいにして, Hogeクラスにはもう一つ問題があった. <br>
でもそれはIDの問題と比べればとっても小さい問題. 相手はフィールド <code>foo</code>.</p>
<pre class="prettyprint"><code class="language-java hljs ">class Hoge {
...
<span class="hljs-keyword">protected</span> String foo;
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Hoge <span class="hljs-title">from</span>(proto HogeProto) {
...
Hoge hoge = Hoge(proto.id)
hoge.foo = Wire.get(proto.foo, HogeProto.DEFAULT_FOO)
<span class="hljs-keyword">return</span> hoge;
}
...
}</code></pre>
<p><code>foo</code> のアクセス修飾子は <code>protected</code>. たぶん書いた当時はユニットテストからアクセスさせるためにスコープを広くとったんだと思う. <br>
だったら <code>@VisibleForTesting</code> をつけてほしいけど, まぁ本題じゃないのでそれは横に置いといて…(よくないけど)</p>
<p>実はHogeクラスはモデルというより POJO なクラス. ビジネスロジックを持っている訳でもないし, DTO 的な使われ方をする. Kotlin化で data class になるようなヤツ. <br>
なので, 実際に <code>foo</code> は一度初期化されれば, その後変更されない.</p>
<p>だけれども <code>foo</code> はコンストラクタで初期化されていない. <br>
なので, この状態で フィールド<code>foo</code> の宣言文に <code>@NonNull</code> アノテーションをつけるとIDEが <code>@Not-null fields must be initialized</code> ってWarningを表示する(そりゃそうだ). <br>
何も考えずKotlin化すると <code>lateinit</code> になっちゃうとこだけど, それはこのクラスの実態にあっていないから, そんなことはしたくない.</p>
<p>Kotlin化するときは引数やフィールドのNullabilityをはっきりとさせるためのチェックとして Javaコードに <code>@NonNull</code>, <code>@Nullable</code> を付けてからKotlin化するようにしているけれど, こういう <code>foo</code> のようなコードを書かれると, 問題解消のための一手間が必要になる.</p>
<p>でもこれはIDの問題と比べればとっても小さい問題. <code>foo</code> を <code>final</code> にすれば解消できる(もちろん, そうできるなら… だけど)</p>
<pre class="prettyprint"><code class="language-java hljs ">class Hoge {
<span class="hljs-annotation">@NonNull</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> String id;
<span class="hljs-annotation">@NonNull</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> String foo;
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Hoge <span class="hljs-title">from</span>(proto HogeProto) {
requireNotNull(proto, <span class="hljs-string">"..."</span>);
String id = requireNotNull(proto.id, <span class="hljs-string">"..."</span>);
<span class="hljs-keyword">return</span> Hoge(
id,
Wire.get(proto.foo, HogeProto.DEFAULT_FOO)
);
}
<span class="hljs-keyword">private</span> <span class="hljs-title">Hoge</span>(String id, String foo) {
<span class="hljs-keyword">this</span>.id = id;
<span class="hljs-keyword">this</span>.foo = foo;
}
...
}</code></pre>
<p>ここまでくれば, すぐにでもkotlin化に着手できる.</p>
<p>ちょっと <code>id</code> の話に戻るけど,,, <br>
<code>HogeProto</code> 側にある “デフォルト値” の意味を考えると <code>id</code> も <code>Wire.get(proto.id, HogeProto.DEFAULT_ID)</code> って形で取得した方がいいのかな〜って思ったりする. <br>
でも, 大抵 <code>DEFAULT_ID</code> って空文字だろうし, モデル <code>Hoge</code> としてはIDに空文字を許したくないので, 結局次のようなコードが必要になる.</p>
<pre class="prettyprint"><code class="language-java hljs ">String id = Wire.get(proto.id, HogeProto.DEFAULT_ID);
<span class="hljs-keyword">if</span> (StringUtil.isNullOrEmpty(id)) <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> IllegalArgumentException(...);</code></pre>
<p>“空文字を許さない” って部分をちゃんと事前条件として表明できているのはいいことだし, もし <code>DEFAULT_ID</code> が “空文字ではない何か” に置き換わるアップデートが <code>proto</code> にあったとしてもうまく対応できそうだ.</p>
<p>大切にしたいのは, <code>proto</code> はあくまで “APIレスポンスの仕様” ってだけで, クラス <code>Hoge</code> はアプリ内で使われるモデルやデータとして正しい形で存在してなきゃいけないってところ. <br>
<code>Hoge</code> クラスに「こうあってほしい」って考えをコードに落とし込むってところ.</p>
<p>“理解なんてものは概ね願望に基づくものだ” ってセリフがある. <br>
自分の理解をコードを通して他人にも理解させるってとこは, 願望のコード化でもあるなと思った.</p>
<p>閑話休題. 本題のkotlin化に戻る.</p>
<p>Nullabilityもハッキリして, 不変条件まで付いてくればKotlin化はかなり楽になる. <br>
ただ, <code>Hoge</code> のコードには現れていないけれど, Javaの頃によくやった <code>NullObject Pattern</code> が意外とKotlin化する上で厄介だったりする.</p>
<p>例えば, もし次のようなコードがあった場合にちょっと困る.</p>
<pre class="prettyprint"><code class="language-java hljs "><span class="hljs-keyword">final</span> Hoge EMPTY = Hoge(<span class="hljs-keyword">null</span>);</code></pre>
<p>だいたい, <code>UNKNOWN</code> とか <code>EMPTY</code> みたいな名前と一緒に <code>NullObject Pattern</code> が使われている. <br>
↑のケースだと, <code>Hoge.id</code> は <code>nullable (String?)</code> にしなきゃいけなくなるし, わざわざ <code>Hoge</code>が <code>EMPTY</code> かどうかを各所でチェックする必要が出てくる. <br>
<code>null</code>を許容して <code>Hoge?</code> なプロパティや引数をとる方が <code>isEmpty</code> チェック漏れを心配する必要もない.</p>
<p>Javaの頃はこういうオブジェクトがあればnull-safeなコードが書けたので重宝したけど, Kotlinだと言語レベルでnull-safeをサポートしているので, <code>NullObject Pattern</code>の必要性がかなり下がると思った. <br>
むしろ, あると邪魔なケースが多くて, <code>?</code> や <code>?:</code> で解決できるようなところをわざわざ<code>if (obj !== UNKNOWN)</code> みたいにしなきゃいけないし, <code>null</code> であってくれたほうが, 型チェック(<code>nullable</code> or <code>not-nullable</code>)の機構が働くので嬉しいことが多い.</p>
<p>特に何もしないデフォルトリスナーとかを <code>NullObject</code> にしているケースなんかは <code>nullable</code> にしてもさほど困らなさそう. <br>
ただ, <code>null</code>と<code>NullObject</code>を明確に区別する使い方をしているケースだと簡単に <code>nullable</code> にはできないので厄介だったりする.</p>
<p>以上, 走り書きでした.</p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-26646198938384424402018-10-23T18:47:00.000+09:002018-11-09T16:25:47.352+09:00Android: DownloadManagerのタスク管理と競合<div class="markdown">
<h3 id="タスクの状態">タスクの状態</h3>
<p>開発者はダウンローダへの要求をアクションとして表現します. アクションには <em>ダウンロード</em> と <em>削除</em> の2種類があり, ダウンローダはこれらをタスクとして処理していきます. <br>
ExoPlayer downloaderはタスクの状態を2種類定義しています. <br>
1つは<code>TaskState</code>として外部に公開される状態で下記の状態と遷移を持ちます.</p>
<p>状態リスト:</p>
<table>
<thead>
<tr>
<th>State</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>queued</td>
<td>開始待機状態</td>
</tr>
<tr>
<td>started</td>
<td>開始済み状態</td>
</tr>
<tr>
<td>completed</td>
<td>完了状態</td>
</tr>
<tr>
<td>canceled</td>
<td>キャンセル状態</td>
</tr>
<tr>
<td>failed</td>
<td>失敗状態</td>
</tr>
</tbody></table>
<p>状態遷移:</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiircHzsK_a-5c-6-93r7_bKc9tItipK-pft2KBhHxmGX9H5vLClyWU-g7NGaQINBpwaIkvFLGL1fZ6IdBC2gT29H3s857LqIYPwf6yzbdQAlkaIbXYCRZT36VMIaMP_Knq-f1FkxZLue-c/s1600/%25E3%2582%25B9%25E3%2582%25AF%25E3%2583%25AA%25E3%2583%25BC%25E3%2583%25B3%25E3%2582%25B7%25E3%2583%25A7%25E3%2583%2583%25E3%2583%2588+2018-10-23+19.14.12.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiircHzsK_a-5c-6-93r7_bKc9tItipK-pft2KBhHxmGX9H5vLClyWU-g7NGaQINBpwaIkvFLGL1fZ6IdBC2gT29H3s857LqIYPwf6yzbdQAlkaIbXYCRZT36VMIaMP_Knq-f1FkxZLue-c/s1600/%25E3%2582%25B9%25E3%2582%25AF%25E3%2583%25AA%25E3%2583%25BC%25E3%2583%25B3%25E3%2582%25B7%25E3%2583%25A7%25E3%2583%2583%25E3%2583%2588+2018-10-23+19.14.12.png" data-original-width="442" data-original-height="102" /></a>
<p>もう1つは外部に公開されない, DownloadManagerの内部管理用の状態です. <br>
主に, “キャンセル中”や”停止中”といった状態遷移中の状態が定義されています.</p>
<p>内部状態リスト:</p>
<table>
<thead>
<tr>
<th>State</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td>queued</td>
<td>開始待機状態</td>
</tr>
<tr>
<td>started</td>
<td>開始済み状態</td>
</tr>
<tr>
<td>completed</td>
<td>完了状態</td>
</tr>
<tr>
<td>canceled</td>
<td>キャンセル状態</td>
</tr>
<tr>
<td>failed</td>
<td>失敗状態</td>
</tr>
<tr>
<td>queued_canceling</td>
<td>開始待機キャンセル中状態</td>
</tr>
<tr>
<td>started_canceling</td>
<td>実行キャンセル中状態</td>
</tr>
<tr>
<td>started_stopping</td>
<td>実行停止中状態</td>
</tr>
</tbody></table>
<p>内部状態遷移:</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVkjLHt_UcHxHg8GsyLZSNYDW6ANrQ73kQCIOX163-rAuTsDhHIXOnPFQ3pMYYAs0JQ4kDR6d58IjK40tQLRNGKSILDcjcMqI-K2LkUI97DWA8a5v_DHKcbCGKutRbIn-1yQFhjGi4s73F/s1600/%25E3%2582%25B9%25E3%2582%25AF%25E3%2583%25AA%25E3%2583%25BC%25E3%2583%25B3%25E3%2582%25B7%25E3%2583%25A7%25E3%2583%2583%25E3%2583%2588+2018-10-23+19.14.20.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVkjLHt_UcHxHg8GsyLZSNYDW6ANrQ73kQCIOX163-rAuTsDhHIXOnPFQ3pMYYAs0JQ4kDR6d58IjK40tQLRNGKSILDcjcMqI-K2LkUI97DWA8a5v_DHKcbCGKutRbIn-1yQFhjGi4s73F/s1600/%25E3%2582%25B9%25E3%2582%25AF%25E3%2583%25AA%25E3%2583%25BC%25E3%2583%25B3%25E3%2582%25B7%25E3%2583%25A7%25E3%2583%2583%25E3%2583%2588+2018-10-23+19.14.20.png" data-original-width="429" data-original-height="165" /></a>
<p>ExoPlayer downloaderを使う上では前者の公開用状態を把握しておけば十分なのですが、ダウンローダの振る舞いを把握するには後者の内部状態を把握しておいた方が理解が進みます。</p>
<h3 id="downloadmanagerのタスク管理と競合">DownloadManagerのタスク管理と競合</h3>
<p>DownloadManagerはタスクの状態変更を <code>DownloadManager.onTaskStateChange</code> 検知します. <br>
ここでタスクの状態がアクティブではない場合, タスクの開始が試みられます. <br>
タスクが下記の条件を満たす場合にはアクティブであると判断されます.</p>
<pre class="prettyprint"><code class="language-java hljs "> <span class="hljs-javadoc">/** Returns whether the task is started. */</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isActive</span>() {
<span class="hljs-keyword">return</span> currentState == STATE_QUEUED_CANCELING
|| currentState == STATE_STARTED
|| currentState == STATE_STARTED_STOPPING
|| currentState == STATE_STARTED_CANCELING;
}</code></pre>
<p>要するにタスクが何かしらのアクション中であればアクティブと判断されます.</p>
<p>DownloadManagerは次の条件が全て満たされていることを確認してタスクを開始します.</p>
<ol>
<li>タスクがまだ開始されていないこと(taskstate == queued)</li>
<li>既にあるタスクと競合しないこと</li>
<li>ダウンロードタスクの場合, 先行するダウンロードタスクが保留中ではない. かつ, アクティブダウンロード数の上限に達していないこと</li>
</ol>
<p>1はわかりやすいですね. 既に開始済みのタスクを再び開始することはできないということです. <br>
DownloadManagerは, タスクの状態に関わらずタスクリストの先頭から順番に開始を試みます. 既に開始済みのタスクはここで除外されます. </p>
<p>3の”アクティブダウンロード数の上限”はDownloadManagerのコンストラクタ引数 <code>maxSimultaneousDownloads</code> が参照されます. <br>
また, これはダウンロードタスクに課せられる条件で, 削除タスクはこの条件に該当しません.</p>
<p>2は少し複雑です. DownloadManagerは次の2点をチェックしてタスク(アクション)の競合を検知します.</p>
<ol>
<li>同じコンテンツに対するアクションか</li>
<li>いずれかのアクションが削除アクションであるか</li>
</ol>
<p>DownloadManagerはタスクリストの先頭から順番に開始を試みる過程で, 同タスクリスト内の他タスクと競合していないかを検知するためにタスク同士を比較します. <br>
タスクが扱うコンテンツの同一性は <code>DownloadAction.isSameMedia</code> を使って判定され, タスクに紐づくアクションが持つ <code>uri</code> が同じかどうかで判断されます. </p>
<pre class="prettyprint"><code class="language-java hljs "> <span class="hljs-javadoc">/** Returns whether this is an action for the same media as the {@code other}. */</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">boolean</span> <span class="hljs-title">isSameMedia</span>(DownloadAction other) {
<span class="hljs-keyword">return</span> uri.equals(other.uri);
}</code></pre>
<p>同じコンテンツに対するアクションがタスクリスト内に複数見つかった場合, 比較元あるいは比較先のタスクのいずれかが削除アクションである場合は “競合した” と判定されます. <br>
例えば, とあるコンテンツAをダウンロード中に, 同コンテンツに対して削除アクションを投げるとこの状態になります.</p>
<p>タスクが競合すると, 比較元が削除アクションであれば比較対象のタスクがキャンセルされ, 比較元の削除タスクもスキップされます(キャンセルはされません). <br>
比較対象のタスクが削除アクションであれば比較元のタスクはスキップされます(キャンセルはされません).</p>
<p>タスクがキャンセルされるとタスクの状態が変化するので <code>DownloadManager.onTaskStateChange</code> が呼ばれます. <code>DownloadManager.onTaskStateChange</code> ではタスクの状態がアクティブではない場合にタスクの開始を試みるので, ここで再びタスクの開始が試みられます. スキップされた削除タスクはアクティブにはなっておらず, また競合していたタスクはキャンセルされているので競合は発生しなくなります.</p>
<p>これによって, とあるコンテンツAをダウンロード中に, 同コンテンツに対して削除アクションを投げるとダウンロード処理はキャンセルされて, ダウンロードのキャンセル処理が終了した後に削除処理が行われることになります.</p>
<h3 id="ダウンローダの開始と停止">ダウンローダの開始と停止</h3>
<p>DownloadServiceが起動されるとRequirementsHelperを登録してデバイス状態の監視を始めます. <br>
デバイスの状態がダウンロード開始条件を満たした場合, いよいよダウンロード処理が開始されます.</p>
<p>DownloadManagerはダウンローダ停止状態を示す <code>downloadsStopped</code> フィールドを内部に持っています. <br>
このフィールドが <code>false</code> の時, ダウンロードアクション/タスクが追加されたとしても新しくダウンロードが開始されることはありません. <br>
ダウンローダを開始するには <code>DownloadManager.startDownloads</code> を呼び出して, このフィールドを <code>true</code> にする必要があります.</p>
<p>ダウンローダの状態はダウンローダ停止状態がデフォルトですが, 開発者が明示的に <code>DownloadManager.startDownloads</code> を呼び出してダウンローダを開始する必要はありません. <br>
その理由として, まずDownloadServiceにダウンロードアクションを登録すると, タスクの開始を試みる処理が実行されます. <br>
しかし, このタイミングではダウンローダが停止状態なので, ダウンロードタスクは一旦スキップされます. <br>
スキップされたタスクがどのようにして実行されるのかというと, DownloadServiceがアクション/タスクを追加した後, RequirementsHelperを登録してデバイス状態の監視を始めます. <br>
デバイス状態がダウンロード開始の条件を満たした時, DownloadServiceはDownloadManager.startDownloadsでダウンローダを開始状態にします. <br>
ダウンローダの開始時にはタスクの実行を試みるようになっているので, ここでスキップされたタスクが実行されることになります.</p>
<p>“ダウンロードの開始/停止条件はRequirementsで表現される”と過去の投稿で言いましたが, 厳密には <strong>Requirementsはダウンローダの開始条件</strong> になります. <br>
ExoPlayerのダウンローダは開始済みであれば全てのタスクを処理しようとします. <br>
このダウンローダは全てのタスクが消化されたとしても, Requirementsの条件が満たされている限り停止しません. <br>
開発者が手動でDownloadManager.startDownload/stopDownloadを呼び出すときは, Requirementsとの整合性が崩れる可能性がある点に注意しなければいけません.</p>
<p>ちなみに, <code>downloadsStopped</code> はダウンロードアクションに対するものであり, 削除アクションはダウンローダの開始状態に依存しないため, このフィールドが <code>false</code> であっても削除アクションは実行されます. <br>
つまり, Requirementsを満たしていない状態でもコンテンツの削除は可能です.</p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-19196489873266433692018-10-19T14:16:00.000+09:002018-10-19T14:19:54.373+09:00Android: ExoPlayer DownloadManager, DownloadService<div class="markdown">
<p>ExoPlyerのDownloadManager, DownloadServiceを調べた時のメモ.</p>
<h3 id="中断されたアクションを読み込むタイミング">中断されたアクションを読み込むタイミング</h3>
<p>保存されたActionFileの読み込みタイミングはDownloadManagerを初期化したタイミングとなります.</p>
<p>プロセスの強制終了などでActionを完遂できなかった場合, 保存されたActionFileをプロセス再開後に読み込んでダウンロード処理を再開する必要があります. <br>
保存されたActionFileは <code>DownloadManager.loadActions</code> によってバックグラウンドスレッド上で読み込まれ, これはDownloadManagerのコンストラクタで実行されます. </p>
<h3 id="コンテンツの削除はバックグラウンド">コンテンツの削除はバックグラウンド?</h3>
<p>コンテンツを削除するにはremove flagをtrueにしたアクションを発行します. <br>
アクションはDownloadServiceで実行されるので, フォアグラウンドサービスとして実行することが可能です. <br>
削除中の通知も <code>DownloadService.getForegroundNotification</code> で返すNotification objectをカスタマイズすることができます. <br>
また, 通知の雛形として <code>DownloadNotificationUtil.buildProgressNotification</code> が用意されています. <br>
buildProgressNotification は引数のタスクステートに応じて通知の表示内容を変えるため, ダウンロード中通知や削除中通知もこのメソッドで生成できます. </p>
<h3 id="ダウンロード進捗率を取得する">ダウンロード進捗率を取得する</h3>
<p>DownloadManager.Listenerの <code>onTaskStateChanged</code> コールバック引数のTaskStateから間近のダウンロード進捗率が取得できます. <br>
<code>TaskState.downloadPercentage</code> がダウンロード進捗率を格納したフィールドです. <br>
ダウンロード進捗率が未定/不明 あるいは 削除タスクである場合は <code>com.google.android.exoplayer2.C#PERCENTAGE_UNSET</code> がセットされます. <br>
DownloadServiceでは, このコールバックを受けて通知の進捗率を更新するようになっています.</p>
<h3 id="タスクの実行順序とダウンロードのキャンセル">タスクの実行順序とダウンロードのキャンセル</h3>
<p>タスクはDownloadManagerによってArrayListで管理されており, 新しいタスクはタスクキュー(タスクリスト)の最後尾に追加され, 先頭から順に実行されます. <br>
<code>DownloadManager.handleAction</code> は新しいダウンロード/削除アクションアクションからタスクを生成してタスクキューの最後尾に追加します. <br>
新しく削除アクションがリクエストされた場合, 既に同じメディアファイルのダウンロードタスクがタスクキューに存在するなら, そのダウンロードを即座にキャンセルします. <br>
つまり, ダウンロード中やダウンロードリクエストをキューイングした後にこれをキャンセルしたい場合は削除アクションを投げるとキャンセルできます.</p>
<h3 id="ダウンロードの開始条件">ダウンロードの開始条件</h3>
<p>サービスによっては”従量制ネットワーク接続時にはダウンロードしたくない”といった要件があるかもしれません. <br>
あるいは, “NWが瞬断されてダウンロード中断されたけれど, NW接続が回復したら自動再開したい” 要件があるかもしれません. <br>
ExoPlayer Downloaderではデバイスの状態を監視して, こうした要件に応える機能があります.</p>
<p><code>RequirementsHelper</code> はデバイスの状態を監視し, 特定の条件を満たした場合にダウンロードを開始/再開するヘルパークラスです. ここで指定できる”特定の条件” は <code>Requirements</code> クラスで表現され, Requirementsに指定できる条件は次の通りです.</p>
<ol>
<li>ネットワーク種別 ( NW接続済み, 従量制NWに接続済み, ローミング中, etc. )</li>
<li>充電中かどうか</li>
<li>アイドル状態かどうか</li>
</ol>
<p>また, JobSchedulerによる監視もサポートされています. <br>
JobSchedulerを使用する場合は, Requirementsの情報がJobInfoに変換されてスケジューリングされます.</p>
<p>API Lv.によっては判定できるNW種別の種類や, アイドル状態と判定する条件に差異があるので, <code>JobScheduler</code>や<code>Requirements</code>のコードを確認した方がよいです.</p>
<h3 id="ダウンロードの開始プロセス">ダウンロードの開始プロセス</h3>
<p>ダウンロードを開始するにはDownloadManagerを初期化して, DownloadServiceを起動し, タスク(アクション)を追加する必要があります. <br>
DownloadServiceは起動されるとRequirementsHelperを起動してデバイス状態を監視し始めます. <br>
デバイスの状態がダウンロード開始条件を満たした場合, いよいよダウンロード処理が開始されます.</p>
<h3 id="requirementshelperとスケジューラの生存区間">RequirementsHelperとスケジューラの生存区間</h3>
<p>アプリのプロセスが生きている間はRequirementsHelperが動的ブロードキャストレシーバーを使ってデバイス状態を監視し, ダウンロードを開始/再開させます. <br>
デバイスの監視はDownloadServiceによって開始されますが, ダウンロードが中断されてDownloadServiceが停止してもこの監視は続きます. <br>
これは, デバイスの状態を監視するRequirementsHelperをDownloadServiceのstaticフィールドで保持しているためです.</p>
<p>DownloadServiceがgetSchedulerでスケジューラを指定している場合は, スケジューラでもデバイス状態が監視されます. <br>
スケジューラはAndroid標準のJobSchedulerを使用することができ, これによってアプリのプロセスが停止している場合にもダウンロードを開始/再開させることが可能になります. <br>
スケジューラはダウンロード開始条件が満たされていないと判断された場合にスケジューリングされます.</p>
<p>ダウンロード開始条件が満たされた場合, RequirementsHelperによる監視が続いている(アプリのプロセスが生きている)状態であればRequirementsHelperがダウンロードを開始/再開させて, スケジューラのスケジューリングをキャンセルします. <br>
RequiermentsHelperによる監視がされていない(アプリのプロセスが停止している)状態であればスケジューラによる開始/再開が行われます.</p>
<p>スケジューラだけでデバイス監視しないのは, RequirementsHelper(staticフィールドと動的ブロードキャストレシーバー)を使った方がデバイス状態の検知からダウンロードの開始/再開までを素早く行えるというメリットがあります.</p>
<h3 id="requirementsとschedulerとdownloadservice">RequirementsとSchedulerとDownloadService</h3>
<p>RequirementsとSchedulerは, DownloadServiceの<code>getScheduler</code>と<code>getRequirements</code>をオーバーライドして指定します.</p>
<pre class="prettyprint"><code class="language-java hljs "> <span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">protected</span> Requirements <span class="hljs-title">getRequirements</span>() {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> Requirements(Requirements.NETWORK_TYPE_ANY, <span class="hljs-keyword">false</span>, <span class="hljs-keyword">false</span>);
}
<span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">protected</span> PlatformScheduler <span class="hljs-title">getScheduler</span>() {
<span class="hljs-keyword">return</span> Util.SDK_INT >= <span class="hljs-number">21</span> ? <span class="hljs-keyword">new</span> PlatformScheduler(<span class="hljs-keyword">this</span>, JOB_ID) : <span class="hljs-keyword">null</span>;
}</code></pre>
<p><a href="https://developer.android.com/reference/android/app/job/JobScheduler">JobScheduler</a> を使ったデバイスの監視を実現するため <code>PlatformScheduler</code> クラスが用意されています. <code>PlatformScheduler</code>を使うにはAndroidManifest.xmlに次の定義を追加します.</p>
<pre class="prettyprint"><code class="language-xml hljs "><span class="hljs-tag"><<span class="hljs-title">uses-permission</span> <span class="hljs-attribute">android:name</span>=<span class="hljs-value">"android.permission.RECEIVE_BOOT_COMPLETED"</span>/></span>
<span class="hljs-tag"><<span class="hljs-title">service</span> <span class="hljs-attribute">android:name</span>=<span class="hljs-value">"com.google.android.exoplayer2.util.scheduler.PlatformScheduler$PlatformSchedulerService"</span>
<span class="hljs-attribute">android:permission</span>=<span class="hljs-value">"android.permission.BIND_JOB_SERVICE"</span>
<span class="hljs-attribute">android:exported</span>=<span class="hljs-value">"true"</span>/></span></code></pre>
<p>あるいはFirebaseJobDispatcherを使ったスケジューラ <code>JobDispatcherScheduler</code> も用意されています. <code>JobDispatcherScheduler</code>を使うにはAndroidManifest.xmlに次の定義を追加します.</p>
<pre class="prettyprint"><code class="language-xml hljs "> <span class="hljs-tag"><<span class="hljs-title">uses-permission</span> <span class="hljs-attribute">android:name</span>=<span class="hljs-value">"android.permission.RECEIVE_BOOT_COMPLETED"</span>/></span>
<span class="hljs-tag"><<span class="hljs-title">service
</span> <span class="hljs-attribute">android:name</span>=<span class="hljs-value">"com.google.android.exoplayer2.ext.jobdispatcher.JobDispatcherScheduler$JobDispatcherSchedulerService"</span>
<span class="hljs-attribute">android:exported</span>=<span class="hljs-value">"false"</span>></span>
<span class="hljs-tag"><<span class="hljs-title">intent-filter</span>></span>
<span class="hljs-tag"><<span class="hljs-title">action</span> <span class="hljs-attribute">android:name</span>=<span class="hljs-value">"com.firebase.jobdispatcher.ACTION_EXECUTE"</span>/></span>
<span class="hljs-tag"></<span class="hljs-title">intent-filter</span>></span>
<span class="hljs-tag"></<span class="hljs-title">service</span>></span></code></pre>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-19147905471111934892018-09-19T07:41:00.001+09:002018-09-19T07:41:39.419+09:00Android: Exoplayer DownloadManager<div class="markdown">
<p>Exoplayer r2.8.4のダウンローダ機能関連APIのメモ. </p>
<p>r2.6.0の頃はコンテンツをダウンロードするAPIだけが提供されていましたが, r2.8.0からはダウンロードタスクの管理やダウンロード処理の再開、サービスや通知といった部分までサポートされるようになっています.</p>
<p>関連記事:<a href="http://yuki312.blogspot.com/2017/12/android-exoplayer-downloader.html">Android: ExoPlayer - Downloader</a></p>
<h3 id="downloadmanager">DownloadManager</h3>
<p><code>DownloadManager</code> <br>
マルチダウンロードストリームの管理とダウンロードリクエストの削除をするクラスです. <br>
<strong>このクラスのメソッドはメインスレッド上から呼び出す必要があり, 複数スレッドからの呼び出しは想定されていません.</strong></p>
<p>ダウンロードマネージャは内部ハンドラを持ちます. もし<code>Looper</code>を持たないスレッドからの呼び出しがあった場合, <code>Looper.getMainLooper()</code>によってメインスレッドが取得されます. </p>
<p>次のコードはダウンロードマネージャーを生成します.</p>
<pre class="prettyprint"><code class="language-kotlin hljs r">// ダウンロードアクションファイルのデシリアライズに必要なクラスを定義
private val DOWNLOAD_DESERIALIZERS = arrayOf(
DashDownloadAction.DESERIALIZER,
HlsDownloadAction.DESERIALIZER)
fun initDownloadManager() {
// ダウンローダの生成に必要なコンストラクタヘルパー
val constructorHelper = DownloaderConstructorHelper(<span class="hljs-keyword">...</span>)
// ダウンロードマネージャを生成
DownloadManager(
constructorHelper,
DownloadManager.DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS,
DownloadManager.DEFAULT_MIN_RETRY_COUNT,
File( /* アクションファイルを保存するファイルパス */ ),
DOWNLOAD_DESERIALIZERS)
}</code></pre>
<p>ダウンロードマネージャのコンストラクタパラメータは下記.</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-keyword">public</span> <span class="hljs-title">DownloadManager</span>(
DownloaderConstructorHelper constructorHelper,
<span class="hljs-keyword">int</span> maxSimultaneousDownloads,
<span class="hljs-keyword">int</span> minRetryCount,
String actionSaveFile,
Deserializer... deserializers) {</code></pre>
<p><code>constructorHelper</code> <br>
ダウンローダを生成するためのコンストラクタヘルパー.</p>
<p><code>maxSimultaneousDownloads</code> <br>
最大同時ダウンロード本数. デフォルト値は1 (<code>DownloadManager.DEFAULT_MAX_SIMULTANEOUS_DOWNLOADS</code>) です.</p>
<p><code>minRetryCount</code> <br>
ダウンロードの最小再試行回数. デフォルト値は5 (<code>DownloadManager.DEFAULT_MIN_RETRY_COUNT</code>) です.</p>
<p><code>actionSaveFile</code> <br>
<code>DownloadAction</code>のシリアライズを保存するファイルパス. アクションを永続化することでプロセスを跨いでアクションを再開することができる. ただし, このファイルをSimpleCacheで使用したフォルダに保存しないこと. (ダウンローダは知らないファイルを削除するため)</p>
<p><code>deserializers</code> <br>
<code>actionSaveFile</code>に保存されている<code>DownloadAction</code>のデシリアライザ. <code>HlsDownloadAction.DESERIALIZER</code> etc.</p>
<h3 id="downloadmanagerlistener">DownloadManager.Listener</h3>
<p><code>DownloadManager.Listener</code>を使ってダウンロードイベントのリスナーを登録することができます. <br>
次のコールバックメソッドを定義してイベントを受け取ることができるようになります. </p>
<p><code>onInitialized</code> <br>
全てのアクションがリストアされたときに呼び出される.</p>
<p><code>onTaskStateChanged</code> <br>
タスクの状態が変わったときに呼び出される.</p>
<p><code>onIdle</code> <br>
アクティブなタスクがなくなったときに呼び出される.</p>
<h3 id="downloadaction">DownloadAction</h3>
<p><code>DownloadAction</code> <br>
コンテンツのダウンロードリクエストやコンテンツの削除リクエストを表現するクラス. <br>
ダウンロードやコンテンツ削除のために必要なパラメータ情報を保持している. <br>
このクラスは自前のシリアライザ/デシリアライザを持っており, 自身のアクションをシリアライズすることで, プロセスを跨いでも同アクションをでシリアライズして再開できるようになっている.</p>
<p>アクションが持つパラメータは下記.</p>
<p><code>type</code> <br>
アクションのタイプ. <br>
このタイプ値はシリアライズ情報に含まれ, アクションをデシリアライズする際に最適なデシリアライザーを選択するために使用される.</p>
<p><code>version</code> <br>
アクションのバージョン. <br>
アクションはシリアライザによって永続化される際にバージョン情報を記録する. <br>
これによって保存されたアクションのバージョンを判別でき, バリデーションやマイグレーション処理に使うことができる.</p>
<p><code>uri</code> <br>
ダウンロードまたは削除するURI. <br>
<code>SegmentDownloader</code>を継承したクラスであれば, URIフィールドもシリアライズの対象になる.</p>
<p><code>isRemoveAction</code> <br>
削除アクションであればtrue, ダウンロードアクションであればfalse. <br>
<code>SegmentDownloader</code>を継承したクラスであれば, URIフィールドもシリアライズの対象になる.</p>
<p><code>data</code> <br>
アクションのカスタムデータ. <br>
アクションファイルには任意の情報をカスタムデータとしてバイト配列形式で保存することができる. <br>
<code>SegmentDownloader</code>を継承したクラスであれば, URIフィールドもシリアライズの対象になる.</p>
<p>アクションファイルのフォーマットは次の通り.</p>
<pre class="prettyprint"><code class=" hljs avrasm">// type, version は共通フォーマット
output<span class="hljs-preprocessor">.writeUTF</span>(action<span class="hljs-preprocessor">.type</span>)<span class="hljs-comment">;</span>
output<span class="hljs-preprocessor">.writeInt</span>(action<span class="hljs-preprocessor">.version</span>)<span class="hljs-comment">;</span>
// 以下はSegmentDownloadAction系のフォーマット. keysについては後述
output<span class="hljs-preprocessor">.writeUTF</span>(uri<span class="hljs-preprocessor">.toString</span>())<span class="hljs-comment">;</span>
output<span class="hljs-preprocessor">.writeBoolean</span>(isRemoveAction)<span class="hljs-comment">;</span>
output<span class="hljs-preprocessor">.writeInt</span>(data<span class="hljs-preprocessor">.length</span>)<span class="hljs-comment">;</span>
output<span class="hljs-preprocessor">.write</span>(data)<span class="hljs-comment">;</span>
output<span class="hljs-preprocessor">.writeInt</span>(keys<span class="hljs-preprocessor">.size</span>())<span class="hljs-comment">;</span>
for (int i = <span class="hljs-number">0</span><span class="hljs-comment">; i < keys.size(); i++) </span>
writeKey(output, keys<span class="hljs-preprocessor">.get</span>(i))<span class="hljs-comment">;</span>
}</code></pre>
<p><code>SegmentDownloader</code> - <code>HlsDownloader</code>の関係と同じく, <code>HlsDownloadAction</code>は<code>SegmentDownloadAction</code>を継承しています. <br>
<code>SegmentDownloadAction</code>の生成方法は下記です.</p>
<pre class="prettyprint"><code class=" hljs java"><span class="hljs-keyword">protected</span> <span class="hljs-title">SegmentDownloadAction</span>(
Uri manifestUri,
<span class="hljs-keyword">boolean</span> isRemoveAction,
@Nullable String data,
K[] keys)</code></pre>
<p><code>manifestUri</code> <br>
ダウンロードしたいコンテンツのURL(Master/MediaPlaylist etc.).</p>
<p><code>isRemoveAction</code> <br>
ダウンロードするアクションの場合はfalse, ダウンロードコンテンツの削除アクションの場合はfalse.</p>
<p><code>data</code> <br>
カスタムデータを指定したい場合はここに指定する. <br>
<code>DownloadService</code>でも参照することができる.</p>
<p><code>keys</code> <br>
ダウンロードするトラックのキー(HLSであればレンディション. DASHであればレプリゼンテーション)を指定します. <code>keys</code>が空配列の場合はすべてのトラックがダウンロードされる. <br>
<strong>この引数をnullにすることはできず, また<code>removeAction</code>がtrueの場合は空配列である必要がある.</strong></p>
<h3 id="downloadhelper">DownloadHelper</h3>
<p>ダウンロードアクションを生成する際には, ダウンロード対象のプレイリスト/マニフェストURLと, トラックキー(レンディション / レプリゼンテーション)を指定する必要がある. <br>
トラックキーを取得するにはプレイリスト/マニフェストファイルをダウンロード・パースする必要がある. <br>
<code>DownloadHelper</code>はそうした前準備処理とトラックキー取得、ダウンロードアクションの生成を助けてくれる.</p>
<p><code>DownloadHelper</code>が提供するヘルパーメソッドは次の通り.</p>
<p><code>prepare</code> <br>
ヘルパーを初期化する. <br>
この操作にはプレイリストやマニフェストのダウンロードを伴う. <br>
引数<code>callback</code>に<code>DownloadHelper.Callback</code>を指定することで初期化の成功・失敗を受け取ることができる. <br>
<strong>初期化処理は別スレッドで実行され, コールバックはメインスレッド上で実行される.</strong></p>
<p><code>getPeriodCount</code> <br>
有効なピリオドの数を取得します. <br>
HLSコンテンツの場合は固定で1が返され, DASHコンテンツの場合はピリオド数が返されます. <br>
このメソッドはヘルパーを初期化した後で呼び出す必要があります.</p>
<p><code>getTrackGroups</code> <br>
指定ピリオドに含まれるトラックグループを取得します. <br>
HLSコンテンツの場合, Media playlistであれば空が返され, Master playlistであればvariants, audio, subtitleを含むグループを返します. <br>
DASHコンテンツの場合は, 引数<code>periodIndex</code>で指定されたピリオドに含まれるアダプションセットに含まれるレプリゼンテーションのフォーマット配列を返します. <br>
このメソッドはヘルパーを初期化した後で呼び出す必要があります.</p>
<p><code>getDownloadAction</code> <br>
指定のトラック(レンディション / レプリゼンテーション)をダウンロードするダウンロードアクションを構築します. <br>
引数<code>data</code>にはダウンロードアクションのコンストラクタ引数<code>data</code>を指定します. <br>
このメソッドはヘルパーを初期化した後で呼び出す必要があります.</p>
<p><code>getRemoveAction</code> <br>
コンテンツを削除するダウンロードアクションを構築します. <br>
<strong>このメソッドはヘルパーを初期化していない状態でも呼び出すことができます.</strong></p>
<p>次のコードはヘルパーを使ってダウンロードアクションを生成するものです.</p>
<pre class="prettyprint"><code class="language-kotlin hljs r">val mediaPlaylistUri = <span class="hljs-keyword">...</span>
val helper = HlsDownloadHelper(mediaPlaylistUri, dataSourceFactory)
helper.prepare(object : DownloadHelper.Callback {
override fun onPrepared(helper: DownloadHelper) {
helper.getDownloadAction( <span class="hljs-keyword">...</span> )
// TrackKeyのリストは下記の要領で構築できる
// val trackKeys = mutableListOf<TrackKey>()
// <span class="hljs-keyword">for</span> (i <span class="hljs-keyword">in</span> <span class="hljs-number">0</span> until helper.periodCount) {
// val trackGroups = helper.getTrackGroups(i)
// <span class="hljs-keyword">for</span> (j <span class="hljs-keyword">in</span> <span class="hljs-number">0</span> until trackGroups.length) {
// val trackGroup = trackGroups.get(j)
// <span class="hljs-keyword">for</span> (k <span class="hljs-keyword">in</span> <span class="hljs-number">0</span> until trackGroup.length) {
// // 必要ならtrackGroup.getFormat(k)でパラメータを確認してフィルタアウトできる
// trackKeys += TrackKey(i, j, k)
// }
// }
// }
}
override fun onPrepareError(helper: DownloadHelper, e: IOException) {
<span class="hljs-keyword">...</span>
}
})</code></pre>
<p>単純にHLSコンテンツのMedia playlistに含まれる全てのレンディションをダウンロードするのであれば, 事前にプレイリストをダウンロードして解析する必要もないので, ヘルパーを使わずに次のように生成します.</p>
<pre class="prettyprint"><code class=" hljs haskell"><span class="hljs-type">HlsDownloadAction</span>(uri, false, <span class="hljs-typedef"><span class="hljs-keyword">data</span>, emptyList<span class="hljs-container">()</span>)</span></code></pre>
<h3 id="donwloadservice">DonwloadService</h3>
<p><code>DownloadService</code> <br>
バックグラウンドでダウンロード処理を継続維持するための<code>Service</code>を継承した抽象クラス. <br>
アプリはこのクラスを継承して必要なメソッドをオーバーライドすることでサービスの管理をExoPlayerに任せることができる.</p>
<p>コンストラクタ引数には次のものがある.</p>
<p><code>foregroundNotificationId</code> <br>
フォアグラウンドサービス用のNotification ID.</p>
<p><code>foregroundNotificationUpdateInterval</code> <br>
フォアグラウンドノーティフィケーションをアップデートする間隔(ミリ秒).</p>
<p><code>channelId</code> <br>
フォアグラウンドノーティフィケーションで使用されるチャネルID. <br>
チャネルは低優先度のチャネルとして作成される. 自身でチャネルを作成する場合は<code>null</code>を指定する.</p>
<p><code>channelName</code> <br>
フォアグラウンドノティフィケーションで使用するチャネル名. <br>
自身でチャネルを作成する場合は特に使用されない.</p>
<p>定義されている抽象メソッドは下記.</p>
<p><code>getDownloadManager()</code> <br>
コンテンツのダウンロードで使用される<code>DownloadManager</code>インスタンスを返す. <br>
このメソッドはサービスのライフサイクルの中で1度しか呼ばれない.</p>
<p><code>getScheduler()</code> <br>
特定の条件を満たした時に<code>DownloadService</code>を初期化するジョブを持った<code>Scheduler</code>を返す. <br>
これによって, アプリが実行されていなくてもダウンロードを開始するスケジューリングが可能になる. <br>
スケジューリングが不要な場合はnullを返す.</p>
<p><code>getForegroundNotification</code> <br>
フォアグラウンドサービスに必要なNotificationを生成する. 引数<code>taskState[]</code>を使ってNotification情報を構築することができる. <br>
このメソッドは, タスクの状態が変化するか, アクティブなタスクがあれば定期的に呼び出される. <br>
呼び出し間隔はDownloadServiceのコンストラクタで調整可能. <br>
API Lv.26以降, このメソッドはサービスが停止する前に空の<code>TaskState[]</code>を引数に呼び出される.</p>
<p>抽象メソッドではないが, サブクラスが意識するべきメソッドは下記.</p>
<p><code>getRequirements</code> <br>
ダウンロード開始条件をカスタマイズすることができる. デフォルトではネットワーク接続の有無がダウンロード条件として設定される.</p>
<p><code>onTaskStateChanged</code> <br>
タスクの状態が変わった時に呼び出される.</p>
<h3 id="ダウンロードサービスの開始">ダウンロードサービスの開始</h3>
<p>アプリがバックグラウンドにいる状態でもダウンロード処理を継続したい場合は, ダウンロードサービスをフォアグラウンドサービスとして振る舞わせる必要がある. <br>
<code>DownloadService</code> は <code>DownloadService.startForeground(Notification)</code> を使って起動することができる.</p>
<pre class="prettyprint"><code class=" hljs php"><span class="hljs-comment">// DownloadService</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> void startWithAction(
Context context,
<span class="hljs-class"><span class="hljs-keyword">Class</span><? <span class="hljs-keyword">extends</span> <span class="hljs-title">DownloadService</span>> <span class="hljs-title">clazz</span>,
<span class="hljs-title">DownloadAction</span> <span class="hljs-title">downloadAction</span>.
<span class="hljs-title">boolean</span> <span class="hljs-title">foreground</span>)</span></code></pre>
<p><code>clazz</code> <br>
作成した<code>DownloadService</code>のサブクラスを指定します.</p>
<p><code>downloadAction</code> <br>
<code>DownloadAction</code>はダウンロードストリーム/コンテンツに対するアクション. <br>
対象のストリーム種別によって<code>ProgressiveDownloadAction</code>, <code>HlsDownloadAction</code>, <code>DashDownloadAction</code>などが用意されている.</p>
<p><code>foreground</code> <br>
フォアグラウンドサービスとして起動する場合はtrue.</p>
<p>ダウンロードサービスを開始するIntentだけが欲しい場合は次のメソッドを使用する.</p>
<p><code>DownloadService.buildAddActionIntent</code></p>
<p>生成されるIntentにはダウンロードアクションを格納する <code>download_action</code> と, フォアグラウンドサービスとして移動するかどうかのフラグ <code>foreground</code> が格納される.</p>
<p>ダウンロードサービスは次のメソッドを使うことでダウンロードアクションを指定せずに起動することもできる. </p>
<p><code>DownloadService.start</code> <br>
<code>DownloadService.startForeground</code></p>
<p>未完了のダウンロードアクションがある場合や, ダウンロード開始条件が満了された場合, サービスはそれらのダウンロードアクションを再開する. <br>
実行するアクションがなければサービスは即終了する.</p>
<h3 id="ダウンロードタスクの状態">ダウンロードタスクの状態</h3>
<p>ダウンロードタスクの状態は <code>DownloadManager.TaskState</code> で表現される. </p>
<p>定義:</p>
<p><code>STATE_QUEUED</code>:開始待ち <br>
<code>STATE_STARTED</code>:開始済み <br>
<code>STATE_COMPLETED</code>:完了済み <br>
<code>STATE_CANCELED</code>:キャンセル済み <br>
<code>STATE_FAILED</code>:失敗</p>
<p>状態遷移図:</p>
<pre class="prettyprint"><code class=" hljs haskell"><span class="hljs-title">queued</span> <-> started -> (canceled | completed | failed)</code></pre>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-43822771555248390632018-08-27T11:35:00.001+09:002018-08-27T11:35:55.238+09:00ボトムシートダイアログの背景を角丸にする<div class="markdown">
<p>ボトムシートダイアログの上辺だけを角丸にしたい.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzT_ZtyQ9NZrXB0TMNXPpmQi1HBgSpBJ_pEQyEWxuLvk2NQizvMTdNYipp6PxOckjc5EynpTiSPs9Y38Cg3492D9npfM1ZsvlX9dPuKJ72X7M-sE3gR8BWxCwCnmuwU8NysMjAzIdAsYX2/s1600/%25E3%2582%25B9%25E3%2582%25AF%25E3%2583%25AA%25E3%2583%25BC%25E3%2583%25B3%25E3%2582%25B7%25E3%2583%25A7%25E3%2583%2583%25E3%2583%2588+2018-08-27+11.17.26.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjzT_ZtyQ9NZrXB0TMNXPpmQi1HBgSpBJ_pEQyEWxuLvk2NQizvMTdNYipp6PxOckjc5EynpTiSPs9Y38Cg3492D9npfM1ZsvlX9dPuKJ72X7M-sE3gR8BWxCwCnmuwU8NysMjAzIdAsYX2/s320/%25E3%2582%25B9%25E3%2582%25AF%25E3%2583%25AA%25E3%2583%25BC%25E3%2583%25B3%25E3%2582%25B7%25E3%2583%25A7%25E3%2583%2583%25E3%2583%2588+2018-08-27+11.17.26.png" width="320" height="86" data-original-width="578" data-original-height="156" /></a>
<p>ボトムシートの背景画像を定義する.</p>
<pre class="prettyprint"><code class="language-xml hljs "><span class="hljs-tag"><<span class="hljs-title">shape
</span> <span class="hljs-attribute">xmlns:android</span>=<span class="hljs-value">"http://schemas.android.com/apk/res/android"</span>
<span class="hljs-attribute">android:shape</span>=<span class="hljs-value">"rectangle"</span>
></span>
<span class="hljs-tag"><<span class="hljs-title">corners
</span> <span class="hljs-attribute">android:bottomLeftRadius</span>=<span class="hljs-value">"0dp"</span>
<span class="hljs-attribute">android:bottomRightRadius</span>=<span class="hljs-value">"0dp"</span>
<span class="hljs-attribute">android:radius</span>=<span class="hljs-value">"1dp"</span>
<span class="hljs-attribute">android:topLeftRadius</span>=<span class="hljs-value">"12dp"</span>
<span class="hljs-attribute">android:topRightRadius</span>=<span class="hljs-value">"12dp"</span>
/></span>
<span class="hljs-tag"><<span class="hljs-title">solid</span> <span class="hljs-attribute">android:color</span>=<span class="hljs-value">"#fff"</span>/></span>
<span class="hljs-tag"></<span class="hljs-title">shape</span>></span></code></pre>
<p>ボトムシートダイアログのレイアウト背景に上記画像を設定する.</p>
<pre class="prettyprint"><code class=" hljs r"> <XxxLayout
<span class="hljs-keyword">...</span>
android:background=<span class="hljs-string">"@drawable/bg_bottomsheet"</span>
></code></pre>
<p>このままだと、ウィンドウ背景色が塗りつぶされてしまうので, これを透過するスタイルを用意する.</p>
<pre class="prettyprint"><code class=" hljs applescript"><style <span class="hljs-property">name</span>=<span class="hljs-string">"AppTheme.ShareDialog"</span> parent=<span class="hljs-string">"Theme.Design.Light.BottomSheetDialog"</span>>
<<span class="hljs-property">item</span> <span class="hljs-property">name</span>=<span class="hljs-string">"android:windowCloseOnTouchOutside"</span>><span class="hljs-constant">true</span></<span class="hljs-property">item</span>>
<<span class="hljs-property">item</span> <span class="hljs-property">name</span>=<span class="hljs-string">"android:windowIsTranslucent"</span>><span class="hljs-constant">true</span></<span class="hljs-property">item</span>>
<<span class="hljs-property">item</span> <span class="hljs-property">name</span>=<span class="hljs-string">"android:windowContentOverlay"</span>>@null</<span class="hljs-property">item</span>>
<<span class="hljs-property">item</span> <span class="hljs-property">name</span>=<span class="hljs-string">"android:colorBackground"</span>>@android:color/transparent</<span class="hljs-property">item</span>>
<<span class="hljs-property">item</span> <span class="hljs-property">name</span>=<span class="hljs-string">"android:backgroundDimEnabled"</span>><span class="hljs-constant">true</span></<span class="hljs-property">item</span>>
<<span class="hljs-property">item</span> <span class="hljs-property">name</span>=<span class="hljs-string">"android:backgroundDimAmount"</span>><span class="hljs-number">0.3</span></<span class="hljs-property">item</span>>
<<span class="hljs-property">item</span> <span class="hljs-property">name</span>=<span class="hljs-string">"android:windowFrame"</span>>@null</<span class="hljs-property">item</span>>
<<span class="hljs-property">item</span> <span class="hljs-property">name</span>=<span class="hljs-string">"android:windowIsFloating"</span>><span class="hljs-constant">true</span></<span class="hljs-property">item</span>>
</style></code></pre>
<p>スタイルを適用するダイアログを定義する.</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-keyword">private</span> <span class="hljs-keyword">class</span> <span class="hljs-title">HogeBottomSheetDialog</span>(
context: Context
) : <span class="hljs-title">BottomSheetDialog</span>(
context,
R.style.AppTheme_ShareDialog
) {</code></pre>
<p>done.</p>
<p>ダイアログのスタイルには <code>parent="Theme.Design.Xxx.BottomSheetDialog"</code> を継承したものを定義しないと, <br>
Robolectricのテストで <code>attr/bottomSheetStyle</code> が解決できずtest failする. <br>
<a href="https://github.com/robolectric/robolectric/issues/2941">Github robolectric/robolectric issue 2941</a></p>
<p>以上.</p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-40316466706715560992018-03-29T19:09:00.000+09:002018-03-29T19:09:04.154+09:00DataBinding v2: NullPointerException<div class="markdown">
<p>DataBinding v2にすると</p>
<pre class="prettyprint"><code class="language-bash hljs "> Caused by: java.lang.NullPointerException: Attempt to invoke interface method <span class="hljs-string">'void android.databinding.Observable.addOnPropertyChangedCallback(android.databinding.Observable$OnPropertyChangedCallback)'</span> on a null object reference
at android.databinding.BaseObservableField.<init>(BaseObservableField.java:<span class="hljs-number">16</span>)
at android.databinding.ObservableField.<init>(ObservableField.java:<span class="hljs-number">73</span>)
at ...</code></pre>
<p>問題のコードが下記.</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-keyword">private</span> val hoge = ObservableField<Foo?>(<span class="hljs-keyword">null</span>)</code></pre>
<p>これを, 次のように修正することで解決した.</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-keyword">private</span> val hoge= ObservableField<Foo?>()</code></pre>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-40793273305249613982018-03-28T15:50:00.000+09:002018-03-28T15:50:28.543+09:00Kotlin & StringFormatMatches lint<div class="markdown">
<pre class="prettyprint"><code class="language-kotlin hljs avrasm">val foo = <span class="hljs-number">1</span>
context<span class="hljs-preprocessor">.getString</span>(R<span class="hljs-preprocessor">.string</span><span class="hljs-preprocessor">.string</span>_format, foo)</code></pre>
<p>↑こういうコードだと, ↓こんなLintエラーが出る.</p>
<pre class="prettyprint"><code class="language-text hljs oxygene">Errors found:
/xxx/src/xxx/Hoge.kt:<span class="hljs-number">100</span>: Error: Wrong argument <span class="hljs-keyword">type</span> <span class="hljs-keyword">for</span> formatting argument <span class="hljs-string">'#1'</span> <span class="hljs-keyword">in</span> string_format: conversion <span class="hljs-keyword">is</span> <span class="hljs-string">'d'</span>, received <ErrorType> (argument <span class="hljs-string">#2</span> <span class="hljs-keyword">in</span> <span class="hljs-function"><span class="hljs-keyword">method</span> <span class="hljs-title">call</span>) [<span class="hljs-title">StringFormatMatches</span>]
<span class="hljs-title">setText</span><span class="hljs-params">(context.getString(R.string.string_format, foo)</span>)
/<span class="hljs-title">xxx</span>/<span class="hljs-title">src</span>/<span class="hljs-title">main</span>/<span class="hljs-title">res</span>/<span class="hljs-title">values</span>/<span class="hljs-title">strings</span>.<span class="hljs-title">xml</span>:</span><span class="hljs-number">100</span>: Conflicting argument declaration here</code></pre>
<pre class="prettyprint"><code class="language-kotlin hljs avrasm">val foo: Int = <span class="hljs-number">1</span>
context<span class="hljs-preprocessor">.getString</span>(R<span class="hljs-preprocessor">.string</span><span class="hljs-preprocessor">.string</span>_format, foo)</code></pre>
<p>これだとOK. <br>
静的解析で型推論できずにハマっているのかな?</p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-2672558991745793502017-12-21T22:43:00.001+09:002017-12-21T22:43:16.200+09:00Android: ExoPlayer - Downloader<div class="markdown">
<p>ExoPlayer 2.6.0からDownloaderが追加されたので実装を追った際のメモ書き.</p>
<h4 id="download">Download</h4>
<p>ダウンローダの構築に必要な情報を持つビルドパラメータクラス<code>DownloaderConstructorHelper</code> <br>
ダウンローダのコンストラクタ引数に使われる.</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-keyword">public</span> <span class="hljs-title">SegmentDownloader</span>(Uri manifestUri, DownloaderConstructorHelper constructorHelper) {</code></pre>
<p>ダウンロードメイン処理</p>
<pre class="prettyprint"><code class=" hljs java"><span class="hljs-comment">// SegmentDownloader#download</span>
<span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">synchronized</span> <span class="hljs-keyword">void</span> <span class="hljs-title">download</span>(@Nullable ProgressListener listener)
<span class="hljs-keyword">throws</span> IOException, InterruptedException {
priorityTaskManager.add(C.PRIORITY_DOWNLOAD);
<span class="hljs-keyword">try</span> {
getManifestIfNeeded(<span class="hljs-keyword">false</span>);
List<Segment> segments = initStatus(<span class="hljs-keyword">false</span>);
notifyListener(listener); <span class="hljs-comment">// Initial notification.</span>
Collections.sort(segments);
<span class="hljs-keyword">byte</span>[] buffer = <span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[BUFFER_SIZE_BYTES];
CachingCounters cachingCounters = <span class="hljs-keyword">new</span> CachingCounters();
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i < segments.size(); i++) {
CacheUtil.cache(segments.get(i).dataSpec, cache, dataSource, buffer,
priorityTaskManager, C.PRIORITY_DOWNLOAD, cachingCounters, <span class="hljs-keyword">true</span>);
downloadedBytes += cachingCounters.newlyCachedBytes;
downloadedSegments++;
notifyListener(listener);
}
} <span class="hljs-keyword">finally</span> {
priorityTaskManager.remove(C.PRIORITY_DOWNLOAD);
}
}</code></pre>
<p>ダウンロード済みのコンテンツは再ダウンロード時にスキップされる</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-comment">// CacheUtil#cache(DataSpec, Cache, CacheDataSource, byte[], PriorityTaskManager, int, CachingCounters, boolean)</span>
<span class="hljs-keyword">long</span> blockLength = cache.getCachedBytes(key, start,
left != C.LENGTH_UNSET ? left : Long.MAX_VALUE);
<span class="hljs-keyword">if</span> (blockLength > <span class="hljs-number">0</span>) {
<span class="hljs-comment">// Skip already cached data.</span></code></pre>
<h4 id="masterplaylist-or-mediaplaylist">MasterPlaylist or MediaPlaylist?</h4>
<p>ダウンロードするURLはMasterPlaylist or MediaPlaylist どちらかで, 内部ではMasterPlaylistではない(MediaPlaylistの)場合に<code>SingleVariantMasterPlaylist</code>として扱うようにしている.</p>
<pre class="prettyprint"><code class=" hljs java"><span class="hljs-comment">// HlsDownloader#getManifest</span>
<span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">protected</span> HlsMasterPlaylist <span class="hljs-title">getManifest</span>(DataSource dataSource, Uri uri) <span class="hljs-keyword">throws</span> IOException {
HlsPlaylist hlsPlaylist = loadManifest(dataSource, uri);
<span class="hljs-keyword">if</span> (hlsPlaylist <span class="hljs-keyword">instanceof</span> HlsMasterPlaylist) {
<span class="hljs-keyword">return</span> (HlsMasterPlaylist) hlsPlaylist;
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">return</span> HlsMasterPlaylist.createSingleVariantMasterPlaylist(hlsPlaylist.baseUri);
}
}</code></pre>
<h4 id="ダウンロードをとめる">ダウンロードをとめる.</h4>
<p>ダウンロードを停止するには <code>Thread.currentThread().interrupt();</code> を使う. <br>
割り込みをチェックする(停止できる)タイミングは</p>
<p>1 . 各セグメント毎の読み込み前 <br>
2. セグメントの指定バッファサイズ読み込みの都度</p>
<h4 id="ダウンロードコンテンツの永続化">ダウンロードコンテンツの永続化</h4>
<p><code>SegmentDownloader</code>はオンラインデータソース(<code>dataSource</code>)とオフラインデータソース(<code>offlineDataSource</code>)をそれぞれ持っている. <br>
それぞれのデータソースは<code>DownloadConstructorHelper</code>で生成される. <br>
オンラインデータソースはコンストラクタで指定されたファクトリから生成されるデータソースを持つ<code>CacheDataSource</code>が作られる. <br>
オフラインデータソースはデフォルトで<code>FileDataSource</code>を持つ<code>CacheDataSource</code>が作られる. <br>
また, オンラインデータソースにはキャッシュに情報を書き込む<code>CacheDataSink</code>がデフォルトで設定される.</p>
<pre class="prettyprint"><code class=" hljs java"><span class="hljs-comment">// DownloaderConstructorHelper#buildCacheDataSource</span>
<span class="hljs-keyword">public</span> CacheDataSource <span class="hljs-title">buildCacheDataSource</span>(<span class="hljs-keyword">boolean</span> offline) {
DataSource cacheReadDataSource = cacheReadDataSourceFactory != <span class="hljs-keyword">null</span>
? cacheReadDataSourceFactory.createDataSource() : <span class="hljs-keyword">new</span> FileDataSource();
<span class="hljs-keyword">if</span> (offline) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> CacheDataSource(cache, DummyDataSource.INSTANCE,
cacheReadDataSource, <span class="hljs-keyword">null</span>, CacheDataSource.FLAG_BLOCK_ON_CACHE, <span class="hljs-keyword">null</span>);
} <span class="hljs-keyword">else</span> {
DataSink cacheWriteDataSink = cacheWriteDataSinkFactory != <span class="hljs-keyword">null</span>
? cacheWriteDataSinkFactory.createDataSink()
: <span class="hljs-keyword">new</span> CacheDataSink(cache, CacheDataSource.DEFAULT_MAX_CACHE_FILE_SIZE);
DataSource upstream = upstreamDataSourceFactory.createDataSource();
upstream = priorityTaskManager == <span class="hljs-keyword">null</span> ? upstream
: <span class="hljs-keyword">new</span> PriorityDataSource(upstream, priorityTaskManager, C.PRIORITY_DOWNLOAD);
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> CacheDataSource(cache, upstream, cacheReadDataSource,
cacheWriteDataSink, CacheDataSource.FLAG_BLOCK_ON_CACHE, <span class="hljs-keyword">null</span>);
}</code></pre>
<p>オンラインデータソースでは<code>cacheWriteDataSource</code>の設定が行われる. <br>
<code>DownloaderConstructorHelper#buildCacheDataSource</code>で特に指定がない限り, <br>
<code>cacheWriteDataSource</code>には, オンラインデータソース(<code>upstream</code>)と<code>CacheDataSink</code>が設定された<code>TeeDataSource</code>が指定される. <br>
これで, オンラインデータソースの情報がCacheに保存される.</p>
<pre class="prettyprint"><code class=" hljs r">// CacheDataSource<span class="hljs-comment">#CacheDataSource(...)</span>
public CacheDataSource(Cache cache, DataSource upstream, DataSource cacheReadDataSource,
DataSink cacheWriteDataSink, @Flags int flags, @Nullable EventListener eventListener) {
<span class="hljs-keyword">...</span>
<span class="hljs-keyword">if</span> (cacheWriteDataSink != null) {
this.cacheWriteDataSource = new TeeDataSource(upstream, cacheWriteDataSink);</code></pre>
<p><code>CacheDataSource</code>は<code>writeDataSink</code>が指定されている場合は, <code>TeeDataSource</code>を<code>currentDataSource</code>として設定する.</p>
<pre class="prettyprint"><code class=" hljs lasso"><span class="hljs-comment">// CacheDataSource#openNextSource</span>
<span class="hljs-keyword">if</span> (cacheWriteDataSource <span class="hljs-subst">!=</span> <span class="hljs-built_in">null</span>) {
currentDataSource <span class="hljs-subst">=</span> cacheWriteDataSource;
lockedSpan <span class="hljs-subst">=</span> span;
} <span class="hljs-keyword">else</span> {
currentDataSource <span class="hljs-subst">=</span> upstreamDataSource;
<span class="hljs-keyword">cache</span><span class="hljs-built_in">.</span>releaseHoleSpan(span);
}</code></pre>
<p><code>TeeDataSource</code>はオンラインデータソースの情報を<code>dataSink</code>に書き込みながら読み込み処理を行う.</p>
<pre class="prettyprint"><code class=" hljs java"><span class="hljs-comment">// TeeDataSource#read</span>
<span class="hljs-annotation">@Override</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> <span class="hljs-title">read</span>(<span class="hljs-keyword">byte</span>[] buffer, <span class="hljs-keyword">int</span> offset, <span class="hljs-keyword">int</span> max) <span class="hljs-keyword">throws</span> IOException {
<span class="hljs-keyword">int</span> num = upstream.read(buffer, offset, max);
<span class="hljs-keyword">if</span> (num > <span class="hljs-number">0</span>) {
<span class="hljs-comment">// TODO: Consider continuing even if disk writes fail.</span>
dataSink.write(buffer, offset, num);
}
<span class="hljs-keyword">return</span> num;
}</code></pre>
<p>ここで書き込まれている<code>dataSink</code>は<code>DownloaderConstructorHelper</code>で生成された(デフォルトだと)<code>CacheDataSink</code>になる.</p>
<pre class="prettyprint"><code class=" hljs lasso"><span class="hljs-comment">// DownloaderConstructorHelper#buildCacheDataSource</span>
DataSink cacheWriteDataSink <span class="hljs-subst">=</span> cacheWriteDataSinkFactory <span class="hljs-subst">!=</span> <span class="hljs-built_in">null</span>
<span class="hljs-subst">?</span> cacheWriteDataSinkFactory<span class="hljs-built_in">.</span>createDataSink()
: <span class="hljs-literal">new</span> CacheDataSink(<span class="hljs-keyword">cache</span>, CacheDataSource<span class="hljs-built_in">.</span>DEFAULT_MAX_CACHE_FILE_SIZE);</code></pre>
<p><code>CacheDataSink.write</code>によってファイルへの書き込みが行われる.</p>
<pre class="prettyprint"><code class=" hljs lasso"><span class="hljs-comment">// CacheDataSink#openNextOutputStream</span>
<span class="hljs-keyword">cache</span><span class="hljs-built_in">.</span>startFile(dataSpec<span class="hljs-built_in">.</span>key, dataSpec<span class="hljs-built_in">.</span>absoluteStreamPosition <span class="hljs-subst">+</span> dataSpecBytesWritten,
maxLength);
underlyingFileOutputStream <span class="hljs-subst">=</span> <span class="hljs-literal">new</span> FileOutputStream(file);
<span class="hljs-keyword">if</span> (bufferSize <span class="hljs-subst">></span> <span class="hljs-number">0</span>) {
<span class="hljs-keyword">if</span> (bufferedOutputStream <span class="hljs-subst">==</span> <span class="hljs-built_in">null</span>) {
bufferedOutputStream <span class="hljs-subst">=</span> <span class="hljs-literal">new</span> ReusableBufferedOutputStream(underlyingFileOutputStream,
bufferSize);
} <span class="hljs-keyword">else</span> {
bufferedOutputStream<span class="hljs-built_in">.</span>reset(underlyingFileOutputStream);
}
outputStream <span class="hljs-subst">=</span> bufferedOutputStream;
<span class="hljs-comment">// CacheDataSink#write</span>
outputStream<span class="hljs-built_in">.</span>write(buffer, offset <span class="hljs-subst">+</span> bytesWritten, bytesToWrite);</code></pre>
<p>書き込まれる対象のファイルは<code>Chache.startFile</code>から取得できる. </p>
<pre class="prettyprint"><code class=" hljs avrasm">// SimpleCache<span class="hljs-preprocessor">#startFile</span>
return SimpleCacheSpan<span class="hljs-preprocessor">.getCacheFile</span>(cacheDir, index<span class="hljs-preprocessor">.assignIdForKey</span>(key), position,
System<span class="hljs-preprocessor">.currentTimeMillis</span>())<span class="hljs-comment">;</span></code></pre>
<p><code>SimpleCacheSpan.getCacheFile</code>でキャッシュするべきファイルパスを取得できる.</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-comment">// SimpleCacheSpan#getCacheFile</span>
<span class="hljs-keyword">return</span> <span class="hljs-keyword">new</span> File(cacheDir, id + <span class="hljs-string">"."</span> + position + <span class="hljs-string">"."</span> + lastAccessTimestamp + SUFFIX);</code></pre>
<p><code>SegmentDownloader.download</code>で各セグメントをキャッシュする.</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-comment">// SegmentDownloader#download</span>
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i < segments.size(); i++) {
CacheUtil.cache(segments.<span class="hljs-keyword">get</span>(i).dataSpec, cache, dataSource, buffer,
priorityTaskManager, C.PRIORITY_DOWNLOAD, cachingCounters, <span class="hljs-keyword">true</span>);</code></pre>
<p><code>CacheUtil.cache</code>により, オンラインストリームの情報が永続化される.</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-comment">// CacheUtil#cache(...)</span>
<span class="hljs-keyword">while</span> (left != <span class="hljs-number">0</span>) {
<span class="hljs-keyword">long</span> blockLength = cache.getCachedBytes(key, start,
left != C.LENGTH_UNSET ? left : Long.MAX_VALUE);
<span class="hljs-keyword">if</span> (blockLength > <span class="hljs-number">0</span>) {
<span class="hljs-comment">// Skip already cached data.</span></code></pre>
<p>キャッシュヒットした場合はオンラインソースからの情報取得をスキップする.</p>
<pre class="prettyprint"><code class=" hljs bash">// CacheUtil<span class="hljs-comment">#cache(...)</span>
<span class="hljs-keyword">if</span> (blockLength > <span class="hljs-number">0</span>) {
// Skip already cached data.
} <span class="hljs-keyword">else</span> {
// There is a hole <span class="hljs-keyword">in</span> the cache which is at least <span class="hljs-string">"-blockLength"</span> long.
blockLength = -blockLength;
long <span class="hljs-built_in">read</span> = <span class="hljs-built_in">read</span>AndDiscard(dataSpec, start, blockLength, dataSource, buffer,
priorityTaskManager, priority, counters);</code></pre>
<p>キャッシュヒットしなかった場合はデータソースから読み込む. <br>
このデータソースは<code>TeeDataSource</code>であるため, オンラインデータソースから読み込みながらキャッシュへ書き込むことになる.</p>
<pre class="prettyprint"><code class=" hljs perl">// CacheUtil<span class="hljs-comment">#readAndDiscard</span>
<span class="hljs-keyword">int</span> <span class="hljs-keyword">read</span> = dataSource.<span class="hljs-keyword">read</span>(buffer, <span class="hljs-number">0</span>,
<span class="hljs-keyword">length</span> != C.LENGTH_UNSET ? (<span class="hljs-keyword">int</span>) Math.min(buffer.<span class="hljs-keyword">length</span>, <span class="hljs-keyword">length</span> - totalRead)
: buffer.<span class="hljs-keyword">length</span>);</code></pre>
<hr>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-comment">// CacheUtil#cache(DataSpec, Cache, DataSource, CachingCounters)</span>
cache(dataSpec, cache, <span class="hljs-keyword">new</span> CacheDataSource(cache, upstream),
<span class="hljs-keyword">new</span> <span class="hljs-keyword">byte</span>[DEFAULT_BUFFER_SIZE_BYTES], <span class="hljs-keyword">null</span>, <span class="hljs-number">0</span>, counters, <span class="hljs-keyword">false</span>);</code></pre>
<p>キャッシュインデックスファイル: <code>cached_content_index.exi</code> <br>
キャッシュコンテンツファイル:下記パターンにマッチするファイル名</p>
<pre class="prettyprint"><code class=" hljs fix"><span class="hljs-attribute">Matcher matcher </span>=<span class="hljs-string"> CACHE_FILE_PATTERN_V3.matcher(name);</span></code></pre>
<p>キャッシュコンテンツのバージョンが古い場合(現時点だと<code>.v1.exo</code>, or <code>.v2.exo</code>なファイル)はアップグレード処理の機能が動く.</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-comment">// SimpleCacheSpan#upgradeFile</span>
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> File <span class="hljs-title">upgradeFile</span>(File file, CachedContentIndex index) {
String key;
String filename = file.getName();
Matcher matcher = CACHE_FILE_PATTERN_V2.matcher(filename);
<span class="hljs-keyword">if</span> (matcher.matches()) {
key = Util.unescapeFileName(matcher.<span class="hljs-keyword">group</span>(<span class="hljs-number">1</span>));
<span class="hljs-keyword">if</span> (key == <span class="hljs-keyword">null</span>) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
}
} <span class="hljs-keyword">else</span> {
matcher = CACHE_FILE_PATTERN_V1.matcher(filename);
<span class="hljs-keyword">if</span> (!matcher.matches()) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
}
key = matcher.<span class="hljs-keyword">group</span>(<span class="hljs-number">1</span>); <span class="hljs-comment">// Keys were not escaped in version 1.</span>
}
File newCacheFile = getCacheFile(file.getParentFile(), index.assignIdForKey(key),
Long.parseLong(matcher.<span class="hljs-keyword">group</span>(<span class="hljs-number">2</span>)), Long.parseLong(matcher.<span class="hljs-keyword">group</span>(<span class="hljs-number">3</span>)));
<span class="hljs-keyword">if</span> (!file.renameTo(newCacheFile)) {
<span class="hljs-keyword">return</span> <span class="hljs-keyword">null</span>;
}
<span class="hljs-keyword">return</span> newCacheFile;
}</code></pre>
<p>これらに該当しないファイル(ExoDownloader管理外ファイル)は不要ファイルとして<code>SimpleCache.initialize()</code>で削除される.</p>
<pre class="prettyprint"><code class=" hljs mel"><span class="hljs-comment">// SimpleCache#initialize</span>
File[] files = cacheDir.listFiles();
<span class="hljs-keyword">if</span> (files == null) {
<span class="hljs-keyword">return</span>;
}
<span class="hljs-keyword">for</span> (File <span class="hljs-keyword">file</span> : files) {
<span class="hljs-keyword">if</span> (<span class="hljs-keyword">file</span>.getName().equals(CachedContentIndex.FILE_NAME)) {
<span class="hljs-keyword">continue</span>;
}
SimpleCacheSpan span = <span class="hljs-keyword">file</span>.length() > <span class="hljs-number">0</span>
? SimpleCacheSpan.createCacheEntry(<span class="hljs-keyword">file</span>, index) : null;
<span class="hljs-keyword">if</span> (span != null) {
addSpan(span);
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">file</span>.<span class="hljs-keyword">delete</span>();
}
}</code></pre>
<h4 id="インデックス">インデックス</h4>
<p><code>CachedContentIndex</code>のコンストラクタパラメータ<code>encrypt</code>を<code>true</code>にすればインデックスファイルが暗号化される.</p>
<pre class="prettyprint"><code class=" hljs r">// CachedContentIndex<span class="hljs-comment">#CachedContentIndex(File, byte[], boolean)</span>
public CachedContentIndex(File cacheDir, byte[] secretKey, boolean encrypt) {
this.encrypt = encrypt;
<span class="hljs-keyword">if</span> (secretKey != null) {
Assertions.checkArgument(secretKey.length == <span class="hljs-number">16</span>);
<span class="hljs-keyword">try</span> {
cipher = getCipher();
secretKeySpec = new SecretKeySpec(secretKey, <span class="hljs-string">"AES"</span>);
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new IllegalStateException(e); // Should never happen.
}
<span class="hljs-keyword">...</span>
// CachedContentIndex<span class="hljs-comment">#getChipher</span>
// Workaround <span class="hljs-keyword">for</span> https://issuetracker.google.com/issues/<span class="hljs-number">36976726</span>
<span class="hljs-keyword">if</span> (Util.SDK_INT == <span class="hljs-number">18</span>) {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">return</span> Cipher.getInstance(<span class="hljs-string">"AES/CBC/PKCS5PADDING"</span>, <span class="hljs-string">"BC"</span>);
} catch (Throwable ignored) {
// ignored
}
}
<span class="hljs-keyword">return</span> Cipher.getInstance(<span class="hljs-string">"AES/CBC/PKCS5PADDING"</span>);
<span class="hljs-keyword">...</span>
// CachedContentIndex<span class="hljs-comment">#writeFile</span>
<span class="hljs-keyword">if</span> (encrypt) {
byte[] initializationVector = new byte[<span class="hljs-number">16</span>];
new Random().nextBytes(initializationVector);
output.write(initializationVector);
IvParameterSpec ivParameterSpec = new IvParameterSpec(initializationVector);
<span class="hljs-keyword">try</span> {
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec);
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new IllegalStateException(e); // Should never happen.
}
output.flush();
output = new DataOutputStream(new CipherOutputStream(bufferedOutputStream, cipher));
}
<span class="hljs-keyword">...</span>
</code></pre>
<p>インデックスファイルを読み込む</p>
<pre class="prettyprint"><code class=" hljs axapta"><span class="hljs-comment">// CachedContentIndex#readFile</span>
<span class="hljs-keyword">int</span> <span class="hljs-keyword">count</span> = input.readInt();
<span class="hljs-keyword">int</span> hashCode = <span class="hljs-number">0</span>;
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i < <span class="hljs-keyword">count</span>; i++) {
CachedContent cachedContent = <span class="hljs-keyword">new</span> CachedContent(input);
add(cachedContent);
hashCode += cachedContent.headerHashCode();
}</code></pre>
<p>キャッシュファイルはインデックス情報と紐づけて管理されている. <br>
インデックス情報のクラスは<code>SimpleCache</code>クラスで生成される.</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-comment">// SimpleCache#SimpleCache(File, CacheEvictor, byte[], boolean)</span>
<span class="hljs-keyword">this</span>(cacheDir, evictor, <span class="hljs-keyword">new</span> CachedContentIndex(cacheDir, secretKey, encrypt));</code></pre>
<p>インデックス情報は<code>SimpleCache</code>の初期化時に読み込まれる.</p>
<pre class="prettyprint"><code class=" hljs sql">// SimpleCache#initialize
index.<span class="hljs-operator"><span class="hljs-keyword">load</span>();</span></code></pre>
<p>インデックス情報が格納されたファイルは下記のような構造になっており, フォーマットにしたがって順番にロードされる. <br>
(暗号化されている場合はIV情報が格納されて, <code>number_of_CachedContent</code>以降が暗号化される)</p>
<pre class="prettyprint"><code class=" hljs axapta"> <span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> <span class="hljs-keyword">byte</span>[] testIndexV1File = {
<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">1</span>, <span class="hljs-comment">// version</span>
<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-comment">// flags</span>
(<span class="hljs-keyword">byte</span>) <span class="hljs-number">0xFA</span>, <span class="hljs-number">0x12</span>, ..., <span class="hljs-comment">// IV</span>
<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">2</span>, <span class="hljs-comment">// number_of_CachedContent</span>
<span class="hljs-comment">// number_of_CachedContentの分格納される</span>
<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">5</span>, <span class="hljs-comment">// cache_id</span>
<span class="hljs-number">0</span>, <span class="hljs-number">5</span>, <span class="hljs-number">65</span>, <span class="hljs-number">66</span>, <span class="hljs-number">67</span>, <span class="hljs-number">68</span>, <span class="hljs-number">69</span>, <span class="hljs-comment">// cache_key</span>
<span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">10</span>, <span class="hljs-comment">// original_content_length</span>
(<span class="hljs-keyword">byte</span>) <span class="hljs-number">0xF6</span>, (<span class="hljs-keyword">byte</span>) <span class="hljs-number">0xFB</span>, <span class="hljs-number">0x50</span>, <span class="hljs-number">0x41</span> <span class="hljs-comment">// hashcode_of_CachedContent_array</span>
};
<span class="hljs-comment">// CachedContentIndex#readFile</span>
DataInputStream inputStream = <span class="hljs-keyword">new</span> DataInputStream(<span class="hljs-keyword">new</span> BufferedInputStream(atomicFile.openRead()));
<span class="hljs-keyword">int</span> version = input.readInt();
<span class="hljs-keyword">int</span> flags = input.readInt();
<span class="hljs-keyword">if</span> ((flags & FLAG_ENCRYPTED_INDEX) != <span class="hljs-number">0</span>) input.readFully(initializationVector);
<span class="hljs-keyword">int</span> <span class="hljs-keyword">count</span> = input.readInt();
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i < <span class="hljs-keyword">count</span>; i++) {
CachedContent cachedContent = <span class="hljs-keyword">new</span> CachedContent(input);
add(cachedContent)
hashCode += cachedContent.headerHashCode();
}
<span class="hljs-keyword">if</span> (input.readInt() != hashCode) <span class="hljs-keyword">return</span> <span class="hljs-keyword">false</span>;</code></pre>
<p><code>CachedContent</code>の情報を読み込む際にインデックス情報をメモリにロードする. <br>
上記のインデックス情報にはコンテンツのメタ情報が格納されている.</p>
<ul>
<li>id: 元のストリームを識別するためのファイルID</li>
<li>key: 元のストリームを識別するためのキー</li>
<li>length: 元のストリームの長さ</li>
</ul>
<pre class="prettyprint"><code class=" hljs avrasm">// CachedContentIndex<span class="hljs-preprocessor">#add(CachedContent)</span>
private void <span class="hljs-keyword">add</span>(CachedContent cachedContent) {
keyToContent<span class="hljs-preprocessor">.put</span>(cachedContent<span class="hljs-preprocessor">.key</span>, cachedContent)<span class="hljs-comment">;</span>
idToKey<span class="hljs-preprocessor">.put</span>(cachedContent<span class="hljs-preprocessor">.id</span>, cachedContent<span class="hljs-preprocessor">.key</span>)<span class="hljs-comment">;</span>
}</code></pre>
<h4 id="鍵の保存">鍵の保存</h4>
<p><code>HlsDownloader.loadManifest</code>でマニフェストがパース・ロードされる.</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-comment">// HlsDownloader#loadManifest</span>
ParsingLoadable<HlsPlaylist> loadable = <span class="hljs-keyword">new</span> ParsingLoadable<>(dataSource, dataSpec,
C.DATA_TYPE_MANIFEST, <span class="hljs-keyword">new</span> HlsPlaylistParser());
loadable.load();</code></pre>
<p>ロード時には<code>TeeDataSource</code>が設定されたDataSourceInputStreamから読み込まれるため, <br>
マニフェストはこのタイミングで永続化される.</p>
<pre class="prettyprint"><code class=" hljs ocaml"> DataSourceInputStream inputStream = <span class="hljs-keyword">new</span> DataSourceInputStream(dataSource, dataSpec);
<span class="hljs-keyword">try</span> {
inputStream.<span class="hljs-keyword">open</span>();
result = <span class="hljs-keyword">parser</span>.parse(dataSource.getUri(), inputStream);</code></pre>
<p>さらに, パース処理ではマニフェストの先頭から各行ごとに解析される. <br>
鍵の情報は<code>Segment</code>インスタンスにも記録されていく.</p>
<pre class="prettyprint"><code class=" hljs lasso"><span class="hljs-comment">// HlsPlaylistParser#parse</span>
<span class="hljs-keyword">if</span> (line<span class="hljs-built_in">.</span>isEmpty()) {
<span class="hljs-comment">// Do nothing.</span>
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (line<span class="hljs-built_in">.</span>startsWith(TAG_STREAM_INF)) {
extraLines<span class="hljs-built_in">.</span>add(line);
<span class="hljs-keyword">return</span> parseMasterPlaylist(<span class="hljs-literal">new</span> LineIterator(extraLines, reader), uri<span class="hljs-built_in">.</span>toString());
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (line<span class="hljs-built_in">.</span>startsWith(TAG_TARGET_DURATION)
<span class="hljs-subst">||</span> line<span class="hljs-built_in">.</span>startsWith(TAG_MEDIA_SEQUENCE)
<span class="hljs-subst">||</span> line<span class="hljs-built_in">.</span>startsWith(TAG_MEDIA_DURATION)
<span class="hljs-subst">||</span> line<span class="hljs-built_in">.</span>startsWith(TAG_KEY)
<span class="hljs-subst">||</span> line<span class="hljs-built_in">.</span>startsWith(TAG_BYTERANGE)
<span class="hljs-subst">||</span> line<span class="hljs-built_in">.</span><span class="hljs-keyword">equals</span>(TAG_DISCONTINUITY)
<span class="hljs-subst">||</span> line<span class="hljs-built_in">.</span><span class="hljs-keyword">equals</span>(TAG_DISCONTINUITY_SEQUENCE)
<span class="hljs-subst">||</span> line<span class="hljs-built_in">.</span><span class="hljs-keyword">equals</span>(TAG_ENDLIST)) {
extraLines<span class="hljs-built_in">.</span>add(line);
<span class="hljs-keyword">return</span> parseMediaPlaylist(<span class="hljs-literal">new</span> LineIterator(extraLines, reader), uri<span class="hljs-built_in">.</span>toString());
<span class="hljs-comment">// HlsPlaylistParser#parseMediaPlaylist</span>
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (line<span class="hljs-built_in">.</span>startsWith(TAG_KEY)) {
<span class="hljs-built_in">String</span> method <span class="hljs-subst">=</span> parseStringAttr(line, REGEX_METHOD);
<span class="hljs-built_in">String</span> keyFormat <span class="hljs-subst">=</span> parseOptionalStringAttr(line, REGEX_KEYFORMAT);
encryptionKeyUri <span class="hljs-subst">=</span> <span class="hljs-built_in">null</span>;
encryptionIV <span class="hljs-subst">=</span> <span class="hljs-built_in">null</span>;
<span class="hljs-keyword">if</span> (<span class="hljs-subst">!</span>METHOD_NONE<span class="hljs-built_in">.</span><span class="hljs-keyword">equals</span>(method)) {
encryptionIV <span class="hljs-subst">=</span> parseOptionalStringAttr(line, REGEX_IV);
<span class="hljs-keyword">if</span> (KEYFORMAT_IDENTITY<span class="hljs-built_in">.</span><span class="hljs-keyword">equals</span>(keyFormat) <span class="hljs-subst">||</span> keyFormat <span class="hljs-subst">==</span> <span class="hljs-built_in">null</span>) {
<span class="hljs-keyword">if</span> (METHOD_AES_128<span class="hljs-built_in">.</span><span class="hljs-keyword">equals</span>(method)) {
<span class="hljs-comment">// The segment is fully encrypted using an identity key.</span>
encryptionKeyUri <span class="hljs-subst">=</span> parseStringAttr(line, REGEX_URI);
} <span class="hljs-keyword">else</span> {
<span class="hljs-comment">// Do nothing. Samples are encrypted using an identity key, but this is not supported.</span>
<span class="hljs-comment">// Hopefully, a traditional DRM alternative is also provided.</span>
}
<span class="hljs-attribute">...</span>
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-subst">!</span>line<span class="hljs-built_in">.</span>startsWith(<span class="hljs-string">"#"</span>)) {
<span class="hljs-built_in">String</span> segmentEncryptionIV;
<span class="hljs-keyword">if</span> (encryptionKeyUri <span class="hljs-subst">==</span> <span class="hljs-built_in">null</span>) {
segmentEncryptionIV <span class="hljs-subst">=</span> <span class="hljs-built_in">null</span>;
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (encryptionIV <span class="hljs-subst">!=</span> <span class="hljs-built_in">null</span>) {
segmentEncryptionIV <span class="hljs-subst">=</span> encryptionIV;
} <span class="hljs-keyword">else</span> {
segmentEncryptionIV <span class="hljs-subst">=</span> <span class="hljs-built_in">Integer</span><span class="hljs-built_in">.</span>toHexString(segmentMediaSequence);
}
segments<span class="hljs-built_in">.</span>add(<span class="hljs-literal">new</span> Segment(line, segmentDurationUs, relativeDiscontinuitySequence,
segmentStartTimeUs, encryptionKeyUri, segmentEncryptionIV,
segmentByteRangeOffset, segmentByteRangeLength));</code></pre>
<p>ここで追加されたSegmentはダウンローダが保存する形式のSegmentに変換される. <br>
プレイリストのセグメント: <br>
HlsMediaPlaylist.Segment#Segment</p>
<p>ダウンローダのセグメント: <br>
SegmentDownloader.Segment</p>
<p>変換は<code>HlsDownloader#addSegment</code>で行われる. <br>
Segmentに鍵情報が格納されているので, <code>fullSegmentEncryptionKeyUri != null</code>となる. <br>
<code>encryptionKeyUris</code>は<code>HashSet</code>なので, 新しい鍵Uriの場合に<code>encryptionKeyUris.add(keyUri)</code>が<code>true</code>を返し, <br>
その鍵のURI情報は<code>DataSpec</code>の<code>uri</code>として格納され, 一つのセグメントとしてコレクションに追加される.</p>
<pre class="prettyprint"><code class=" hljs avrasm">// HlsDownloader<span class="hljs-preprocessor">#addSegment</span>
if (hlsSegment<span class="hljs-preprocessor">.fullSegmentEncryptionKeyUri</span> != null) {
Uri keyUri = UriUtil<span class="hljs-preprocessor">.resolveToUri</span>(mediaPlaylist<span class="hljs-preprocessor">.baseUri</span>,
hlsSegment<span class="hljs-preprocessor">.fullSegmentEncryptionKeyUri</span>)<span class="hljs-comment">;</span>
if (encryptionKeyUris<span class="hljs-preprocessor">.add</span>(keyUri)) {
segments<span class="hljs-preprocessor">.add</span>(new Segment(startTimeUs, new DataSpec(keyUri)))<span class="hljs-comment">;</span>
}
}
Uri resolvedUri = UriUtil<span class="hljs-preprocessor">.resolveToUri</span>(mediaPlaylist<span class="hljs-preprocessor">.baseUri</span>, hlsSegment<span class="hljs-preprocessor">.url</span>)<span class="hljs-comment">;</span>
segments<span class="hljs-preprocessor">.add</span>(new Segment(startTimeUs,
new DataSpec(resolvedUri, hlsSegment<span class="hljs-preprocessor">.byterangeOffset</span>, hlsSegment<span class="hljs-preprocessor">.byterangeLength</span>, null)))<span class="hljs-comment">;</span></code></pre>
<p>セグメントのコレクションは<code>SegmentDownloader</code>によってキャッシュされるので, 結果的に鍵情報も同じように永続化される.</p>
<pre class="prettyprint"><code class=" hljs matlab"> <span class="hljs-keyword">for</span> (int <span class="hljs-built_in">i</span> = <span class="hljs-number">0</span>; <span class="hljs-built_in">i</span> < <span class="hljs-transposed_variable">segments.</span><span class="hljs-built_in">size</span>(); <span class="hljs-built_in">i</span>++) <span class="hljs-cell">{
CacheUtil.cache(segments.get(i).dataSpec, cache, dataSource, buffer,
priorityTaskManager, C.PRIORITY_DOWNLOAD, cachingCounters, true);</span></code></pre>
<h4 id="v3exo">.v3.exo</h4>
<p>ファイルサイズの上限は<code>CacheDataSource#DEFAULT_MAX_CACHE_FILE_SIZE</code>で定義されており, <br>
デフォルトで2MiB(2 * 1024 * 1024)が指定されている. <br>
これを変更するには<code>DownloaderConstructorHelper</code>のコンストラクタ引数<code>cacheWriteDataSinkFactory</code>に自前の<code>DataSink.Factory</code>を設定する.</p>
<pre class="prettyprint"><code class=" hljs oxygene">Cache cache = <span class="hljs-keyword">new</span> SimpleCache(dir, <span class="hljs-keyword">new</span> NoOpCacheEvictor());
DownloaderConstructorHelper <span class="hljs-function"><span class="hljs-keyword">constructor</span> =
<span class="hljs-title">new</span> <span class="hljs-title">DownloaderConstructorHelper</span><span class="hljs-params">(cache,
<span class="hljs-keyword">new</span> DefaultHttpDataSourceFactory("ExoPlayer", null)</span>,
<span class="hljs-title">null</span>,
<span class="hljs-title">new</span> <span class="hljs-title">CacheDataSinkFactory</span><span class="hljs-params">(cache, 20480)</span>,
<span class="hljs-title">null</span>);</span></code></pre>
<p><code>.v3.exo</code>の1ファイルあたりの上限サイズは2MiBがデフォルトで, これを超えると次の<code>.v3.exo</code>ファイルに分割保存される. <br>
<code>.v3.exo</code>は<code>SegmentDownloader.Segment</code>の単位で保存され, のファイル名は <br>
<code><id>.<ストリームの書き込みバイト位置>.<ファイル書き込み時のタイムスタンプ>.<バージョン>.exo</code> <br>
の形式で決まる.</p>
<p>idは<code>SegmentDownloader.Segment</code>の単位で管理されており, idが同じであれば同じセグメントを指す. <br>
(<code>SegmentDownloader.Segment</code>はPlaylistや.ts, 暗号キーといった単位を表現する) <br>
セグメントが変わればidも変わるため, .exoの書き込み容量が上限サイズを迎えていなくても次のファイル名に変わる.</p>
<p>分割保存された<code>.exo</code>ファイルを, 後に結合するためには前述の<code>cached_content_index.exi</code>にある <br>
インデックス情報(id, key, content length)を使って復元される.</p>
<h4 id="暗号化">暗号化</h4>
<p><code>.exi</code>は平文で保存されるのがデフォルトの挙動. <br>
これを暗号化して保存したい場合は<code>SimpleCache</code>に秘密鍵を渡すことで実現できる.</p>
<pre class="prettyprint"><code class=" hljs cs"><span class="hljs-keyword">byte</span>[] secretKey = <span class="hljs-string">"Bar12345Bar12345"</span>.getBytes(<span class="hljs-string">"UTF-8"</span>)
<span class="hljs-keyword">new</span> SimpleCache(cacheDir, <span class="hljs-keyword">new</span> NoOpCacheEvictor(), secretKey);</code></pre>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-64577061325560391472017-11-07T12:37:00.000+09:002017-11-07T12:38:25.927+09:00Android: デフォルトで@NonNull扱いにする<div class="markdown">
<p>JSR 305’s <code>@ParametersAreNonnullByDefault</code> を使うと, <code>@Nullable</code> でアノテートされていないメソッド, 引数, フィールドが <code>@NonNull</code> アノテートされているように解釈され, <code>@NonNull</code> アノテートされているのと同じ振る舞いになります. </p>
<p>プロジェクトによっては, <code>@NonNull</code> を明示することが煩わしく, <code>@Nullable</code> のみを定義することにして, それ以外は <code>@NonNull</code> 扱いとするルールを採用しているところもあるかと思います. <br>
ただ, これではIDEが提供するNull安全のインスペクションメッセージによる恩恵を受けることができず, 実装者が “<code>@Nullable</code>をつけ忘れていた” なんて悲劇を招く可能性もあります. </p>
<p><em>“Tool, not Rules”</em> ということで, デフォルトの振舞いを <code>@NonNull</code> にしたいときは, <code>@ParametersAreNonnullByDefault</code> が使えます. <br>
このアノテーションはクラス単位でつけることもできますが, 例えば次のようにパッケージ単位でも指定できるため, プロジェクトのデフォルト設定としても役に立ちます. </p>
<p><code>com.android.myapp</code>フォルダに↓のような <code>package-info.java</code> を用意すれば, パッケージ単位で <code>@NonNull</code> アノテーションが有効になります.</p>
<pre class="prettyprint"><code class=" hljs avrasm">// <span class="hljs-keyword">com</span><span class="hljs-preprocessor">.android</span><span class="hljs-preprocessor">.myapp</span><span class="hljs-preprocessor">.package</span>-info<span class="hljs-preprocessor">.java</span>
@javax<span class="hljs-preprocessor">.annotation</span><span class="hljs-preprocessor">.ParametersAreNonnullByDefault</span>
package <span class="hljs-keyword">com</span><span class="hljs-preprocessor">.android</span><span class="hljs-preprocessor">.myapp</span><span class="hljs-comment">;</span></code></pre>
<p>JavaからKotlin化する際には <code>@Nullable</code> / <code>@NonNull</code> が定義されていると, とても移行しやすいですのでおすすめです.</p>
<p>以上です.</p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-14449624555449643792017-10-16T02:03:00.001+09:002017-10-16T02:03:13.907+09:00Android Performance. UI Rendering<div class="markdown">
<p>レイアウトXMLはどのようなプロセスを経てピクセル情報に変換され, 画面に描画されるのでしょうか? <br>
Androidのパフォーマンスを改善するには, UIレンダリングの仕組みを理解しておく必要があります. </p>
<p><a href="http://yuki312.blogspot.jp/2017/10/android-performance-dropped-frame.html">Android Performance. Dropped frame</a>では画面のアップデートが16ms毎に行われ, これが遅延するとユーザ体験を悪くしてしまうことについて触れました. </p>
<p>アプリが60fpsを維持するためにはMainThreadでの処理を軽くし, 16msごとのリフレッシュレートを逃さないようにしなければなりません. <br>
60fpsを維持できなくする理由はたくさんありますが, 今回はViewの更新とレンダリングパイプラインについて見ていきます. </p>
<h4 id="layout-draw">Layout & Draw</h4>
<p>レイアウトXMLがパースされるとレイアウトツリー(ビューヒエラルキー)が作成されます. 描画はルートノードから始まり, ツリーを渡り歩きながらレイアウトと描画が行われます. <br>
複数のビューを持つ親ビュー(ビューグループ)の場合は, 子ビューにいくつかの制約や制限をつけて描画を要求します. 描画の順序は親ビューが先で子ビューが後になるので, 親が子より奥に描画され, 子ビューが親に重なる形で描画されることになります. </p>
<p>ビューのレイアウトにはメジャーとレイアウトのプロセスがあります. 親ビューは子ビューのサイズに依存するので, まずは子ビューのサイズを計測します. 計測が終わると親ビューが全ての子ビューを計算されたサイズで配置していきます. これはビューツリーからトップダウントラバーサルで処理されるため, ビュー階層が浅いほどパフォーマンスが良くなります. ビューのレイアウトが終わるとこれを描画します. </p>
<h4 id="rasterization">Rasterization</h4>
<p>Viewをディスプレイに描画するには, ボタンやテキストをピクセルに変換する必要があります. 例えば, ラスタ形式(ビットマップ, etc.)ではない文字列やボタン, ベクタードロワブルのようなオブジェクトはラスタライズと呼ばれるプロセスでピクセル形式に変換されてから画面に出力されます. <br>
Android3.0以降, レンダリングパイプラインはハードウェアアクセラレーションをサポートしました. ラスタライズはとても時間のかかるプロセスなので, 専用にデザインされたハードウェアユニット(アクセラレータ)で高速に処理されます. これがGPU(Graphics Processing Unit)です. </p>
<p>GPUはポリゴンやテクスチャといったいわゆる画像などのために設計されたハードウェアユニットです. CPUはそういった画像をGPUに供給する役割を果たします. この操作には OpenGL ES のAPIを使って行われています. </p>
<p>ボタンなどのUIオブジェクトを描画したい場合, まずはCPUでポリゴンやテクスチャ情報に変換し, これをGPUに送ってラスタライズします. CPUでポリゴンやテクスチャ情報に変換したり, GPUにこれを入力する処理は高速ではありません. </p>
<p>パフォーマンスのために, これらのオブジェクトに変換する回数を減らすことは効果があります. OpenGL ES のAPIはGPUに入力したオブジェクトをGPU上にそのままキャッシュさせることが可能です. 同じボタンやUIコンポーネントを使う場合は, 単にGPU上に残ったキャッシュを参照すればよいので, 余計なオーバーヘッドが起こりません. レンダリングの性能を最適化するには, GPU上にあるキャッシュを可能な限り長時間保持して, これを再利用するようにすることです. </p>
<h4 id="display-list">Display list</h4>
<p>標準UIコンポーネントのドロワブルなどはあらかじめGPUに入力されており, これらの描画は効率的に動きます. <br>
しかし, 実際のUIは複雑で, 例えば背景画像といったビットマップはCPUが画像をメモリにロードしてGPUに転送されます. また, ベクタードロワブルはパスを繋げてポリゴンを描画する必要があります. <br>
テキストにいたってはCPUで文字グリフをテクスチャにラスタライズしたあとGPUにこれを入力し, GPUメモリにグリフを参照する領域を描画します. <br>
アニメーションリソースはもっと複雑で, ビジュアルが変わればGPUリソースを1コマ, 1コマ何度も更新しなければなりません. </p>
<p>ハードウェアアクセラレーションが有効である場合, ディスプレイリストを使った新しい描画モデルで描画されます. ディスプレイリストにはGPUレンダリングに必要な情報アセットとOpenGLコマンドリストが格納されていて, 無駄なオーバーヘッドを抑えて効率的に描画することができます. </p>
<h4 id="draw-phase">Draw Phase</h4>
<p>ビューが実際にレンダリングされる前に, まずGPUに適した形式に変換するDrawフェーズがあります. これはJavaによるonDrawコマンドで行われますが, Canvasを使ってテッセレートされた複雑なオブジェクトかもしれません. <br>
この変換が終わると, システムによって結果がディスプレイリストとしてキャッシュされます. </p>
<p>Androidではその都度画面全体を再描画することはせず, 更新が必要な領域に絞って描画します. しかし, 多数のビューが無効化(<code>invalidate()</code>)されるとDrawフェーズに多くの時間を費やします. あるいは<code>onDraw</code>で非常に複雑なロジックを抱えているかもしれません. </p>
<h4 id="execute-phase">Execute Phase</h4>
<p>作成されたディスプレイリストは2Dレンダラーによって実行されます. ディスプレイリストはOpenGL ES APIを使ってドローされます. これによってGPUにデータが送られ, 最終的にピクセルを画面に送ります. <br>
複雑な描画をするカスタムビューでは, OpenGLが描画できるようにコマンドも複雑になる必要があります. 複雑なビューを描画することは2DレンダラーのExecuteフェーズに多くの時間を費やす原因になります. </p>
<p>画面上でUIオブジェクトの位置が変わった場合は, 同じディスプレイリストをもう1度Executeフェーズを実行するだけです. しかし, 画像のビジュアルが変化すると過去のディスプレイリストが無効になるかもしれません. その場合はDrawフェーズでディスプレイリストを再作成して, 再び実行する必要があります. 画像の描画内容が変わるたびにこのプロセスが繰り返されます. このパフォーマンスは画像の複雑さによって変わるため不正確です. </p>
<h4 id="process">Process</h4>
<p>DrawフェーズとExecuteフェーズが終わるとCPUはフレームのレンダリングが完了したことをGPU/グラフィックドライバーに伝えます. このアクションはブロッキングコールであるため, GPUがコマンドを受け付けたことの応答をCPUは待つことになります. <br>
GPUからのコマンド応答が長くなると, このプロセスも長くなります. プロセスが長くなるのは大抵GPUが多くの仕事をしていることが多いです. 多数の複雑なビューの結果, 多くのOpenGLレンダリングコマンドが必要になりGPUの仕事が増えるのです. </p>
<h4 id="16ms-frame">16ms / Frame</h4>
<p>16msの間に起こるレンダリングパイプラインは次の通りです. </p>
<ol>
<li>Input(ユーザからの入力)</li>
<li>Animation(アニメーション)</li>
<li>Measure&Layout</li>
<li>Drawing(Draw Phase)</li>
<li>Sync/Upload</li>
<li>Issuing Commands(Execute Phase)</li>
<li>Processing(Process)</li>
<li>Misc</li>
</ol>
<p>これらの時間はProfile GPU Renderingツールで見ることができます. 下図はフレームごとのレンダリングに要した時間を並べたもので, 緑色の水平線が16msを示すラインです. これを超えるとDropped Frameが発生します. </p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3vPiQAhWm3lVQ4izbEBlRSk2sf165GzoZAdlI47b7Nfa88e3aoZ8eGXWyCsY91pXZ8CC1tK9dHHc8QselC9Kt24Hwp5qSRXUNmxPKe9VofRGraMvkLFqbpyHx6qeP3cvWY69wze08C5Hc/s1600/bars.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj3vPiQAhWm3lVQ4izbEBlRSk2sf165GzoZAdlI47b7Nfa88e3aoZ8eGXWyCsY91pXZ8CC1tK9dHHc8QselC9Kt24Hwp5qSRXUNmxPKe9VofRGraMvkLFqbpyHx6qeP3cvWY69wze08C5Hc/s320/bars.png" width="320" height="208" data-original-width="360" data-original-height="234" /></a>
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj4aCJj7emQNo_f5l5hZ80O9urWy01TezdZliQ5_h77ZURCWWuoBVQGOfwhW5ahZULbL87o7byvie5je7IMvMgAfoa4lsF5TWEomeS3hLrqLGJlzacFXQ8CMBVz6pX-JK3Qi5zUSglzU0n8/s1600/s-profiler-legend.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj4aCJj7emQNo_f5l5hZ80O9urWy01TezdZliQ5_h77ZURCWWuoBVQGOfwhW5ahZULbL87o7byvie5je7IMvMgAfoa4lsF5TWEomeS3hLrqLGJlzacFXQ8CMBVz6pX-JK3Qi5zUSglzU0n8/s400/s-profiler-legend.png" width="400" height="15" data-original-width="730" data-original-height="27" /></a>
<p>実際にアプリケーションを作成すると, 16ms/フレーム・60fpsを維持することが大変であることを実感できるでしょう. パフォーマンスを改善するには計測して問題のある箇所を特定することを繰り返すことが重要です. </p>
<p>前回と合わせて, 最低限必要な知識は揃いましたので, アプリのパフォーマンスを悪くしている箇所を特定し, それを改善するアプローチについて次回以降に書きたいと思います. </p>
<p>次回に続く…</p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-77644903542559772572017-10-13T23:22:00.000+09:002017-10-13T23:56:43.983+09:00Android Performance. Dropped frame<div class="markdown">
<p>SystemEvents, Input Events, Application, Service, Alarm, UI Drawingといった多くの処理はMain Thread(UI Thread) で実行されます. <br>
重要なポイントは, 画面は16ミリ秒の間隔で再描画されているということです. </p>
<h4 id="why-16ms-why-60fps">Why 16ms, Why 60fps?</h4>
<p>人間は繋がりのある複数枚の絵が十分な速さで連続していると, それがあたかもアニメーションしているかのように錯覚します. パラパラ漫画やアニメGifの原理です. <br>
アニメーションをスムーズに見せるために, どれだけ素早く画像を表示できるかという点が重要で, 滑らかで流れるようなアニメーションには必要不可欠な要素です. </p>
<p>人間の脳がアニメーションしているように感じるためには, 最低でも12fps程度の速度が必要です. これよりも遅いとパラパラ漫画のようなぎこちない見た目になります. 12fpsという速度はアニメーションには見えてもあまりスムーズには映りません. <br>
24fpsは流れるようなアニメーションに見えますが, これはモーションブラーやビジュアルエフェクトの効果によるものです. <br>
60fpsはモーションブラーやエフェクトなしでスムーズに映ります. これ以上のfpsはほぼ感知できない領域です. </p>
<p>注意すべきは人間の目の明敏さで, フレームレートが60fpsから24fpsに落ちると, 途端にアニメーションのスムーズさを欠いたように感じ, よくない印象を与えることになります. </p>
<h4 id="vsync">VSYNC</h4>
<p>スムーズなアニメーションを実現するためにも, Androidがどのようにして60fpsを実現しているのかを理解しておきましょう. それには2つの用語を理解しておく必要があります. </p>
<h5 id="リフレッシュレート">リフレッシュレート</h5>
<p>1秒間に画面を何回リフレッシュできるかの値で, ハードウェアが定めた一定間隔で実行されます. <br>
単位はHz(ヘルツ)で, 例えば60Hzであれば1秒間に60回のリフレッシュが可能です. </p>
<h5 id="フレームレート">フレームレート</h5>
<p>GPUが一秒間で幾つのフレームを描画できるかの値です. <br>
単位はfpsで, 例えば60fpsであれば一秒間に60フレームの描画が可能です. </p>
<h5 id="synchronized">Synchronized</h5>
<p>GPUが画像データを出力し, ハードウェアがそれを画面に表示します. <br>
スクリーンの描画は, これを何度も繰り返しているので, GPUとハードウェアはできる限り一緒に働くことが望ましいのですが, リフレッシュレートとフレームレートは同じ頻度で起こることが保証されていません. </p>
<p>フレームレートがリフレッシュレートより早いと, ティアリングという現象が発生します. <br>
これは, GPUが新しいフレームをメモリに上書きしている最中に, 画面がリフレッシュされてしまい, まだ更新中の画像を描画してしまうことで, 画像が崩れる(部分的に古いフレームが残る)現象です. これを解決するのがダブルバッファリングです. </p>
<p>ダブルバッファリングでは, GPUがバックバッファにフレームを描画し, それが終わるとフレームバッファーと呼ばれる領域にコピーします. 画面をリフレッシュするときはこのフレームバッファから取り出してリフレッシュするわけです. これによって古いフレームへの上書きが行われないので, 中途半端に上書きされた状態にはなりません. </p>
<p>ここで注意しないといけないことのは, 画面のリフレッシュ中にバックバッファからフレームバッファへのコピー作業が発生しないようにすることです. そうしないと, 同じ問題が起こります. ここで登場するのがVSYNC(Vertical Synchronization)です. </p>
<p>通常はフレームレートがリフレッシュレートよりも高いことが望ましいです. なぜなら, 画面を読み込むよりもGPUのリフレッシュの方が早くなるからです. <br>
GPUはフレームをバックバッファに載せると, VSYNCによって次の画面リフレッシュまで処理を待つことになります. </p>
<p>しかし, 反対にフレームレートがリフレッシュレートよりも低い場合, 例えば30fpsに対して60Hzのディスプレイであった場合, フレームバッファのリフレッシュ作業には, 画面リフレッシュの倍の時間を要するため, 同じフレーム内容で2回ずつリフレッシュすることになります. <br>
問題は, これが断続的に起こった場合です. </p>
<p>十分に早いフレームレートで動作しても, 突然フレームレートが落ちると, ユーザはスムーズなアニメーションに続いて, ぶつ切りになったものを見ることになります. <br>
これらの事象は一般的に ラグ, ジャンク, ヒッチング, スタッター と呼ばれます. </p>
<p>アプリの開発者はこれらの事象を避けなければなりません. <br>
人間の目は明敏で, フレームレートが落ちると, 途端にアニメーションのスムーズさを欠いたように感じ, よくない印象を与えてしまうことを思い出してください. </p>
<p>アプリ開発者が目指すところは 常に60fpsのパフォーマンスを維持すること です. </p>
<pre class="prettyprint"><code class=" hljs fix"><span class="hljs-attribute">1000ms / 60frames </span>=<span class="hljs-string"> 16.666ms/frame</span></code></pre>
<p>MainThreadでは16msの間隔でUI Drawingイベントが発生します. 60fpsの滑らかなアニメーションを実現するためには16ms間隔の描画が必要になります. </p>
<h4 id="main-threadui-thread">Main Thread(UI Thread)</h4>
<p>単一スレッドの処理は逐次実行されるため, 順番に処理されていきます. MainThreadも例外ではありません. UI DrawingイベントもMainThreadで実行されるので, もしあなたの処理が長引くとUI Drawingイベントが遅延し, 次のリフレッシュレートのタイミングを逃してしまい, アニメーションで描画されるはずであったフレームが抜け落ちる ドロップフレーム が発生します. <br>
あなたが書いた処理の後には, 常にUI Drawingイベントが待ち構えていることを忘れないでください. </p>
<p>次回に続きます… </p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-65486932007305660912017-10-10T19:36:00.001+09:002017-10-10T19:36:49.886+09:00aapt:attr でリソースファイル数を節約する<div class="markdown">
<p><code>layer-list</code> や <code>selector</code> など, リソースがまた別のリソースを参照する場合があります. </p>
<pre class="prettyprint"><code class=" hljs r"><selector <span class="hljs-keyword">...</span>>
<item android:drawable=<span class="hljs-string">"@drawable/image01"</span> />
<item android:drawable=<span class="hljs-string">"@drawable/image02"</span> /></code></pre>
<p><code>image01</code> や <code>image02</code> がベクタードロワブルの場合は, 新たに <code>image01.xml</code>, <code>image02.xml</code> と2つのドロワブルリソースを用意する必要があります. </p>
<ul>
<li><code>selector</code> や <code>layer-list</code> の定義ファイル</li>
<li>image01.xml</li>
<li>image02.xml</li>
</ul>
<p><code>image01</code> や <code>image02</code> が他リソースでも使われている共通化されたリソースであれば良いのですが, 他では使われず, ここでしか参照されない場合は1つのリソースファイルとしてまとめて定義できた方が管理が楽です.</p>
<p>そうした場合は <code>aapt:attr</code> タグが使えます. </p>
<pre class="prettyprint"><code class=" hljs r"><?xml version=<span class="hljs-string">"1.0"</span> encoding=<span class="hljs-string">"utf-8"</span>?>
<selector
xmlns:android=<span class="hljs-string">"http://schemas.android.com/apk/res/android"</span>
xmlns:aapt=<span class="hljs-string">"http://schemas.android.com/aapt"</span>>
<item>
<aapt:attr name=<span class="hljs-string">"android:drawable"</span>>
<vector <span class="hljs-keyword">...</span> >
<path <span class="hljs-keyword">...</span> />
</vector>
</aapt:attr>
</item>
</selector></code></pre>
<p><code><aapt:attr></code> タグで指定したリソースは, aaptによってリソースファイルとして抽出・生成され, <code>name</code>属性名の値は, 親タグの同属性に指定のリソースを設定する動作となります. <br>
この機能は全てのAndroidバージョンで利用できます. </p>
<p>以上です. </p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-11769103678833968042017-10-09T19:31:00.000+09:002017-10-09T19:31:03.938+09:00DevFest2017<div class="markdown">
<iframe src="//www.slideshare.net/slideshow/embed_code/key/LVKjelqzCx8L3s" width="595" height="485" frameborder="0" marginwidth="0" marginheight="0" scrolling="no" style="border:1px solid #CCC; border-width:1px; margin-bottom:5px; max-width: 100%;" allowfullscreen> </iframe> <div style="margin-bottom:5px"> <strong> <a href="//www.slideshare.net/Yuki312/android1580-walkthrough" title="Android1.5~8.0 Walkthrough" target="_blank">Android1.5~8.0 Walkthrough</a> </strong> from <strong><a href="https://www.slideshare.net/Yuki312" target="_blank">Yuki Matsumura</a></strong> </div>
<p>Android1.5~8.0 Walkthrough のセッションに登壇した際のスライドとスピーカーノートメモ、あと喋った内容の文字起こし. </p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjr-1FrAovRn0KbRC7qCK6iqGDmRY-PZj5dYazvXwceRbngPAdaU5EbqLRyBSksn6RPGbCRvkmNNDqRS7sXh-eaj9WmRZ7TIqa1Q1sPcE2pm21TH8urt8KN_PdyUdrEgiqvy5d0PERByfWo/s1600/AndroidCtoO.001.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjr-1FrAovRn0KbRC7qCK6iqGDmRY-PZj5dYazvXwceRbngPAdaU5EbqLRyBSksn6RPGbCRvkmNNDqRS7sXh-eaj9WmRZ7TIqa1Q1sPcE2pm21TH8urt8KN_PdyUdrEgiqvy5d0PERByfWo/s400/AndroidCtoO.001.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>はじまり。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjEoeucYDCX_2VSpdDC8wDA_ONqu_2SXycOx7pxzeoPhxOQhefHsw15QIIyImtLwtni5A6ZiBsSbTyuKxPCBtELdXeyA81A_lYeyQQZ3YQz6bbGoL7MgK7btgyngDeHEbNw2tyYQ7Pexb1e/s1600/AndroidCtoO.002.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjEoeucYDCX_2VSpdDC8wDA_ONqu_2SXycOx7pxzeoPhxOQhefHsw15QIIyImtLwtni5A6ZiBsSbTyuKxPCBtELdXeyA81A_lYeyQQZ3YQz6bbGoL7MgK7btgyngDeHEbNw2tyYQ7Pexb1e/s400/AndroidCtoO.002.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>2017年8月に最新のOS Android8.0 コードネーム Oreoがリリースされました.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgwKE-amz7hFuWbtP4RQmpxdRoVoE3goHjPh_5HHcBLSdtuNuUZuRVLiuRBitXQVd576rSmnqsrNb5k0eaptxr5gY_Rfblljy0B7LMFW-3x0Zt5yjw-a6TYxG7UJTQghxY8Fk0P3p2AJmFC/s1600/AndroidCtoO.003.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgwKE-amz7hFuWbtP4RQmpxdRoVoE3goHjPh_5HHcBLSdtuNuUZuRVLiuRBitXQVd576rSmnqsrNb5k0eaptxr5gY_Rfblljy0B7LMFW-3x0Zt5yjw-a6TYxG7UJTQghxY8Fk0P3p2AJmFC/s400/AndroidCtoO.003.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>アプリを Oreo に最適化するには TargetSdkVersion を 26 に上げる必要があります。 <br>
TargetSdkVersion を上げることで、Oreoの新機能を十分に活かすことができます。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrVB2U5PdH2V51USjJxTS7SmJNc1BnHKjxq-1QW9YO0dyQHQdEDncA5N0vmm4AV-4RQYXTqVxZ3zZ-36371dLyLzM29FOuixHoDt3_rM-wUjZAmioLutHpyK7qbYwbBvHNrHYTiRO1ceaO/s1600/AndroidCtoO.004.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhrVB2U5PdH2V51USjJxTS7SmJNc1BnHKjxq-1QW9YO0dyQHQdEDncA5N0vmm4AV-4RQYXTqVxZ3zZ-36371dLyLzM29FOuixHoDt3_rM-wUjZAmioLutHpyK7qbYwbBvHNrHYTiRO1ceaO/s400/AndroidCtoO.004.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>ここ数年のアップデートでは システムリソースの消費を抑える DozeやAppStandby、バックグラウンド動作制限などがリリースされています。</p>
<p>これによって、ユーザは端末やアプリを使っていないときの バッテリー消費 を抑えることができます。 <br>
その一方で, 開発者は OSの仕様変更に対応する必要があります。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj2r6rTTH9lR19XSg9zPoEP57zHh80huPQ9DeK6Uu0YQKhSsEOGcu9Jg8ox15r6ODh5dYYgEF77NC_hx0iseHrsuZkLLj8YPGLVVQFWKt05LjlHeu1gUVn0tJI_HRnnknFPsdFDumXcc5vm/s1600/AndroidCtoO.005.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj2r6rTTH9lR19XSg9zPoEP57zHh80huPQ9DeK6Uu0YQKhSsEOGcu9Jg8ox15r6ODh5dYYgEF77NC_hx0iseHrsuZkLLj8YPGLVVQFWKt05LjlHeu1gUVn0tJI_HRnnknFPsdFDumXcc5vm/s400/AndroidCtoO.005.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>バックグラウンド活動のデザイン原則というものがあります。</p>
<ul>
<li>バックグラウンドの活動を減らすことができないのか?</li>
<li>デバイスが充電中の状態になるまで活動を遅らせることができないのか?</li>
<li>他の活動とまとめることができないのか?</li>
</ul>
<p>といったことを考える必要があります。</p>
<p>8.0で バックグラウンド活動が厳格化されたことで 開発者はこれらと “まじめに” 向き合っていく必要があります。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhU0Vby3KvJwHM1ZeWW0zrLGIvm1IN9NnU8eijAplq10IlC8nAlUGzq12LO6sLgjihDkwW4vT3J5BW_F3Uc-RPMq3MbzAiQS1tojLQRWNjIHTgCbnm9IQ2OP3I2YTAk3eEttnDZM-NXYrf7/s1600/AndroidCtoO.006.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhU0Vby3KvJwHM1ZeWW0zrLGIvm1IN9NnU8eijAplq10IlC8nAlUGzq12LO6sLgjihDkwW4vT3J5BW_F3Uc-RPMq3MbzAiQS1tojLQRWNjIHTgCbnm9IQ2OP3I2YTAk3eEttnDZM-NXYrf7/s400/AndroidCtoO.006.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>これらの機能を搭載したOSが市場にどれぐらい流通しているのかをグラフにしました。 <br>
一番左のグラフは、下から青がAndroid ヌガー, 緑がマシュマロ, 黄色がロリポップ, 赤がキットカット のシェア率を積み上げたものです。</p>
<p>Dozeは マシュマロ以降のOSに搭載されていますので 市場端末の およそ50% がこれを搭載しています。 <br>
Android ヌガーでリリースされた, 一部のBroadcastを無効にするProjectSvelteは 18% です。</p>
<p>この割合は DevelopersサイトのDashboardで公開されている 10月時点でのWorldWideなOSバージョンシェアの数字になります。国内に限定したり、ターゲットユーザ層やminSdkでそもそもサポートしていないOSがあると思いますので、みなさんのサービスと同じ数字にはならない点にご注意ください。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEht5lBgtxKwHWeWLFW5jmDEuSF8bPK5kg6eloSvm5OAPtJdbC-Zcg2gSu95cRVNNHjxa24zW0PmhwO7yqOv8QmjJ870k_stKfyelh8RygDTizd6nbyCb-KMnL6QDFa1DzN0d9KdwsnnGkQ6/s1600/AndroidCtoO.007.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEht5lBgtxKwHWeWLFW5jmDEuSF8bPK5kg6eloSvm5OAPtJdbC-Zcg2gSu95cRVNNHjxa24zW0PmhwO7yqOv8QmjJ870k_stKfyelh8RygDTizd6nbyCb-KMnL6QDFa1DzN0d9KdwsnnGkQ6/s400/AndroidCtoO.007.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>本日は、こういった仕様変更や動作制限の移り変わりを Android 1.5~8.0まで 振り返ります。 <br>
時間の都合上、厳選してピックアップしている点はご了承ください。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsnUcVQUYimybcLEYuWrHnzeGAhTp_3b8-1hZ9EH6mM_QazKakT0wx_AOldJZo09EVM8N5i773lGyoTUAgC4bjEloiMRZFUBuu0c_vSHOfu_x3dFqTbM0yPI7K9fh4n91FJnFvJ2HufyaC/s1600/AndroidCtoO.008.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsnUcVQUYimybcLEYuWrHnzeGAhTp_3b8-1hZ9EH6mM_QazKakT0wx_AOldJZo09EVM8N5i773lGyoTUAgC4bjEloiMRZFUBuu0c_vSHOfu_x3dFqTbM0yPI7K9fh4n91FJnFvJ2HufyaC/s400/AndroidCtoO.008.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>まず初めは2009年4月リリースのOS1.5 CUPCAKEです.</p>
<p>2009年といえば バラク・オバマ氏が アメリカ合衆国大統領に就任した年 になりますね。 <br>
その頃Androidは スクリーンキーボードのサポートやアプリウィジェットプロバイダーをリリースしていました。</p>
<p>リリース:2009年4月 Android1.5 - Api Lv.3</p>
<p>3rd party keyboards… サードパーティ製のキーボードはこの頃からサポート. <br>
Bluetooth A2DP… BluetoothプロファイルのA2DPがサポートされました. 当然まだBLEはサポートされていません. <br>
AppWidgetProvider… アプリウィジェット機能のAPIがリリースされ, 開発者はアプリウィジェットを作成することができるようになりました.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaW3uy8jJx5ZEDGLeCYSfAJIdqR61wgAFBAM44kKKp8e6sDyp44-tFkOKl4BdfpnyWdq_ErQ8-WIXl_zHl1jaLRoprs7FtkQl81W2alQxFu__Vkxt7RkV6rq_MtXNDFxrnDkOJK1EJVSKf/s1600/AndroidCtoO.009.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhaW3uy8jJx5ZEDGLeCYSfAJIdqR61wgAFBAM44kKKp8e6sDyp44-tFkOKl4BdfpnyWdq_ErQ8-WIXl_zHl1jaLRoprs7FtkQl81W2alQxFu__Vkxt7RkV6rq_MtXNDFxrnDkOJK1EJVSKf/s400/AndroidCtoO.009.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>次にリリースされたのが 2009年9月 OS1.6 DONUT です。 <br>
Cupcakeでは 320ピクセル x 480ピクセル の解像度のみをサポートしていましたが、Donutからは複数の解像度を扱えるようになりました。 <br>
また、バッテリー問題が今よりも はるかに深刻だった時代で, アプリ毎のバッテリー使用量をユーザが確認できる機能などが追加されました。</p>
<p>リリース:2009年9月 Android1.6 - Api Lv.4</p>
<p>Battery usage indicator… アプリごとの消費電力がわかる画面を搭載 <br>
当時は電力消費問題が深刻で朝満充電にしても夕方前にはバッテリー切れという状態.</p>
<p>New Android Market UI… 現Google PlayのUIが大幅刷新. <br>
当時のAndroidアプリは簡素なものが多かっただけに, Android Marketの多彩な表現は開発者の目をひくものだった</p>
<p>Text-to-speech engine… 多言語の音声合成エンジンでテキスト読み上げをサポート. ただし日本語は含まれていなかった.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPMlCykh-JdNiqxzxLVT16Rnqqi68TEA_o698nkXCpOIRtKUc9S9z2wtLNowJWHHv-wH0unVMQ0GEgFqzi9De-OKFKK_2vFMlebqAXyx2QEYbpKwAOtEU6sDMKyzW_PU6sJPZICnclkNGK/s1600/AndroidCtoO.010.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjPMlCykh-JdNiqxzxLVT16Rnqqi68TEA_o698nkXCpOIRtKUc9S9z2wtLNowJWHHv-wH0unVMQ0GEgFqzi9De-OKFKK_2vFMlebqAXyx2QEYbpKwAOtEU6sDMKyzW_PU6sJPZICnclkNGK/s400/AndroidCtoO.010.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>2009年10月 Donutリリースから わずか1ヶ月後には OS2.0 Eclair がリリースされました。 <br>
この頃はOSバージョンアップが 今よりも頻繁にあった時代です。 <br>
ここでサービス周りのアップデートがありましたので詳しくみてみます。</p>
<p>リリース:2009年10月 Android2.0~2.1 - Api Lv.5~7</p>
<p>Service.setForeground deprecated… Service.setForegroundが非推奨に. <br>
代わりにService.startForegroundを使う必要がある. さらにフォアグラウンドで動作していることをユーザに伝えるためにOngoing Notificationの登録が必須化された.</p>
<p>Key events executed on Key-up… Android2.0はHOMEやBackといったバーチャルキーをサポートするため, ユーザが誤ってキーダウンしてもドラッグすることでキーイベントをキャンセルすることができるように, キーアップでイベント発火されるように変更された.</p>
<p>Multi-touch… マルチタッチがサポートされて, キーボードで素早く文字入力しても抜けることが少なくなりました</p>
<p>その他… Live WallpaperのAPIリリースもこの時.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJk1EdDlgjoCGouFR8KV75Sktz581kjd6hmFFSErGVIMKSSb5MztKlT2VL5ICO4ng3BKbZ4BhUUHRmeWYZRwpG3uPzBDiXA8D-Q4ifHe1f5ci4Y2uIoaOQY2-bvCRGt4PG70011HcTq2nm/s1600/AndroidCtoO.011.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgJk1EdDlgjoCGouFR8KV75Sktz581kjd6hmFFSErGVIMKSSb5MztKlT2VL5ICO4ng3BKbZ4BhUUHRmeWYZRwpG3uPzBDiXA8D-Q4ifHe1f5ci4Y2uIoaOQY2-bvCRGt4PG70011HcTq2nm/s400/AndroidCtoO.011.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>まず、2.0のタイミングでService.setForegroundメソッドが非推奨になりました。 <br>
2.0未満のOSでは フォアグラウンドサービスを開始するのに 通知アイコン が不要でした。</p>
<p>通知アイコンが必須になったのは2.0からで、これによって、ユーザがバックグラウンドで活動しているアプリの存在に気づき、 <br>
無用なアプリを停止させることができるようになりました。</p>
<p>また、当時はバックグラウンドの活動に対する制限が緩かったので、バックグラウンドにいるアプリプロセスを片っ端からKillしていくタスクキラー系アプリが バッテリー寿命に効くということで流行りました。 <br>
アプリ開発者はそうしたキラー系アプリとも戦っていた時代です。</p>
<p>8.0ではサービスの在り方が大きく変わりました。原則、バックグラウンド状態から新しくサービスを起動できなくなったり、startForegroundServiceで起動する場合には5秒以内にフォアグラウンドへ昇格させないとANRが発生するなど厳格化されました。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjAICKVfbGduMgQh4lS05JeFXeIH2AdJEnMNmB8fSjt32Q7bfskIPdvrFSfKJBMoZaErd4cGkafcw8N6nQ4nu4omX0y7Ug_x2cIa-81WWybvox9Dr7w4Bqx8jTps_HM3tMLctB27zE9sdZr/s1600/AndroidCtoO.012.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjAICKVfbGduMgQh4lS05JeFXeIH2AdJEnMNmB8fSjt32Q7bfskIPdvrFSfKJBMoZaErd4cGkafcw8N6nQ4nu4omX0y7Ug_x2cIa-81WWybvox9Dr7w4Bqx8jTps_HM3tMLctB27zE9sdZr/s400/AndroidCtoO.012.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>バックグラウンド活動まわりで使えるAPIに、ロリポップでリリースされたJobScheduler APIがあります。 <br>
これの互換性ライブラリとして FirebaseJobDispatcherが API Lv.9から利用可能です。カバー率はほぼ100%です。 <br>
JobSchedulerは ロリポップ から使えるAPIなので 78% の端末で使うことができます。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiyaFpreg8zhiKJliB23HUVAciac4y7YKOKbWYwhpM23nUS1UoqjaP5Kmy42P5ly3uFuplsuiSg002YtQvS41YBd3r-0ZpUPHgCxOCjlK8UVexmTuLAUA23dckSPbauWPk6VnYYIph15xKf/s1600/AndroidCtoO.013.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiyaFpreg8zhiKJliB23HUVAciac4y7YKOKbWYwhpM23nUS1UoqjaP5Kmy42P5ly3uFuplsuiSg002YtQvS41YBd3r-0ZpUPHgCxOCjlK8UVexmTuLAUA23dckSPbauWPk6VnYYIph15xKf/s400/AndroidCtoO.013.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>2010年5月にはOS2.2 Froyoがリリースされました。 <br>
音声操作機能や、テザリング機能、GCMの前身にあたる C2DM がリリースされたのもこの時です。</p>
<p>リリース:2010年5月 Android2.2 - Api Lv.8</p>
<p>Install on external storage… アプリのインストール領域に外部ストレージを指定可能になった.</p>
<p>Backup Manager, C2DM… 新しい端末に乗換えした時に便利なアプリデータをクラウドへバックアップ/リストアを実現するAPI Backup Managerがリリース. <br>
アプリはBackup agentを実装することでこれを実現することができる. 現在のバックアップの仕組みとは少し異なる. またGCMやFCMの前身にあたるC2DMもこのOSからサポートされています. C2DMはGCMにリプレースされた時点で非推奨になっています.</p>
<p>JIT compiler… JITコンパイラサポートにより2~5倍高速化. マニフェストに <code>vmSafeMode=false</code> を指定することでJITコンパイラによる最適化を無効化することができます. このオプションは後々AOTコンパイラを無効化するオプションに置きかわります.</p>
<p>その他… PlayServiceはこれ以前のバージョンでは対応していない.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjePdPRGO9nTsg31H708jtcgE8f-WkxPZVQGbJCqQ2fRFZCxlzbTLjb_dCH4Ub1-c25OwxDeppngJEs5yXDxmpCNiUp4SiWCFEwbqpTHde97XC5o3LBAPP-fAXR8lVMmyzBeJ8lmS5Q51_K/s1600/AndroidCtoO.014.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjePdPRGO9nTsg31H708jtcgE8f-WkxPZVQGbJCqQ2fRFZCxlzbTLjb_dCH4Ub1-c25OwxDeppngJEs5yXDxmpCNiUp4SiWCFEwbqpTHde97XC5o3LBAPP-fAXR8lVMmyzBeJ8lmS5Q51_K/s400/AndroidCtoO.014.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>2010年12月には OS2.3 Gingerbreadがリリースされました。 <br>
電池が何に使われたかを計測するバッテリー管理機能などが強化されています。</p>
<p>Androidのイースターエッグが搭載されたのもGingerbreadからです。 <br>
Gingerbreadでは ゾンビ ジンジャーブレッドマン の絵がイースターエッグで表示されます。 <br>
実際、ジンジャーブレッドは ゾンビ な状態になります。</p>
<p>スマホ向けOSの最新版としての期間が長かったことと、スマホブームが重なったこともあって 一時期は全体の60%を超えるシェアにまでGingerbreadは普及しました。 <br>
その後は、2015年にマシュマロがリリースされて、ようやくGingerbreadのシェアが10%を切ったぐらいに ”ゾンビ” な状態でした。</p>
<p>リリース:2010年12月 Android2.3 - Api Lv.9/10</p>
<p>2010年… 東北新幹線全線開業した年.</p>
<p>1touch word selection & copy/paste… テキストのロングプレスで単語が選択されフリー選択モードに移行するようになった.</p>
<p>Improved Power management… アプリがバックグラウンドで消費したCPUタイムをユーザが見られるようになるなどバッテリー管理機能が強化された.</p>
<p>その他… StrictMode搭載. Apache Harmony 6.0ベース化. システムアプリやシステムUIの刷新. Google PlayServiceのサポートはここから.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiGIXRut7NxqRAxqoQgkZb2OrN7vQF5Lx7q3laRdDvypNySdkNSJKsN3xHTpt1TbE0KooSMp3ft9jIOjPOq40uoMlyJ99tO6EqkYXCNnsEgq0wkO9G4_2ViMTQPCIQBJmnxn2xB3-a9E_Lz/s1600/AndroidCtoO.015.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiGIXRut7NxqRAxqoQgkZb2OrN7vQF5Lx7q3laRdDvypNySdkNSJKsN3xHTpt1TbE0KooSMp3ft9jIOjPOq40uoMlyJ99tO6EqkYXCNnsEgq0wkO9G4_2ViMTQPCIQBJmnxn2xB3-a9E_Lz/s400/AndroidCtoO.015.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>ここで、パフォーマンスに関する仕様変更についてみてみます。 <br>
OS5.0から実行環境がARTに置き換わりましたが、それまではDalvikでした。</p>
<p>OS2.2でJITコンパイラが搭載されたことで CPU使用率の高いコードのパフォーマンスが 最大で5倍改善されました。 <br>
OS2.3ではコンカレントGCが採用され、いわゆる”Stop the world”が改善されています。 <br>
OS5.0でランタイムがARTに置き換わり、OS7.0ではARTにJITコンパイラが採用されています。</p>
<p>JITコンパイラの採用によってDEXを ジャストインタイム方式で 実行形式にコンバートすればよくなるので、 <br>
アプリのインストールやアップデート、OSバージョンアップの時間が大幅に短縮されています。</p>
<p>ランタイムやコンパイラやGCアルゴリズムの違いによってパフォーマンスに差がでる場合もありますので、 <br>
ランタイムの違いぐらいは覚えておいて損はないと思います。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7dc1E5jlYHRh6torTrFr3eqMQsUcXpbNuFFf2OHxTxPN450WT9WUTqt5HSV35JAjEl-_KsnIZ-Psua-kCm3yHT7XT7ImrCVjB-3WZIMFATT0CD6PoSEYYq63hhuBwnTuhU-Rlz3XhBayF/s1600/AndroidCtoO.016.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7dc1E5jlYHRh6torTrFr3eqMQsUcXpbNuFFf2OHxTxPN450WT9WUTqt5HSV35JAjEl-_KsnIZ-Psua-kCm3yHT7XT7ImrCVjB-3WZIMFATT0CD6PoSEYYq63hhuBwnTuhU-Rlz3XhBayF/s400/AndroidCtoO.016.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>ARTはキットカットでも利用できますが オプショナルです。 <br>
標準搭載されたのはロリポップ以降ですので 78% の端末に搭載されています。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgm4SJX44DPnmlpfqld5UNXP7yIEQ95EUrdYwtHLETf7INbdLpBBYYkjgGCdmSiYqJyu9AiRKBscb2BrwWi9-y-pvRB2N5wimrhf3qNJ7pmVZ06ad3wE3PbDoIt8XFNWA5yKW82Q-sXgqgy/s1600/AndroidCtoO.017.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgm4SJX44DPnmlpfqld5UNXP7yIEQ95EUrdYwtHLETf7INbdLpBBYYkjgGCdmSiYqJyu9AiRKBscb2BrwWi9-y-pvRB2N5wimrhf3qNJ7pmVZ06ad3wE3PbDoIt8XFNWA5yKW82Q-sXgqgy/s400/AndroidCtoO.017.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>2011年 2月には OS3.0 Honeycomb がリリースされました。 <br>
なかには 黒歴史 という人もいるハニカムですが、重要なアップデートが多くあった OS です。</p>
<p>ActionBar, Fragment, Loader, ハードウェアアクセラレーション, ホログラフィックUIがリリースされています。 <br>
ホログラフィックUIはこのスライドデザインのように 黒背景に水色のアクセントカラーをもつテーマで、白背景もバリエーションとしてありましたが、 <br>
黒背景が印象的なUIでした。ハニカムは大画面向けのOSで、スマホ向けには配信されていません。</p>
<p>リリース:2011年2月 Android3.0 - Api Lv.11/12/13</p>
<p>New UI design for tablets… Android3.0はタブレットデバイスのような大画面向けのアップデートですが, その内容は後々スマホ向けにも展開され非常に重要なアップデート内容が多く含まれている.</p>
<p>ActionBar, Fragment, Loader… アプリのUI要素にActionBarが導入されました. ActionBarにはMenuキーをエミュレートするオーバーフローメニューが導入されました. また, ActivityをFragmentというサブコンポーネントに分割してMaster-Detail Flowのような柔軟な画面デザインを提供することができる. 開発者は画面の大きさが異なるスマートフォンとタブレット両方で動作するアプリケーションを効率よく作成できるようになります. またActivityやFragmentからの非同期ロードをサポートするLoaderも追加.</p>
<p>Holographic UI… システム全体に新しいUIテーマが適用され, デザインが一新されました. アプリはTheme.Holoを指定することでこれを適用できるようになります. Notificationの表現がリッチになり始めたのもこの頃です.</p>
<p>その他… クリップボードへのコピー&ペースト対応. ハードウェアアクセラレーションサポート.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjuixwHsibhgmw6LZ7dHVxH9G6ydCoJQbVGtAZBKSdtd8sB6K5pX-0jy5wZy-Nyc_hdRBOmTFoLxAxKYvvBgQxuBQAjQTpVZWhazGFY-QXNgjWG4GXgE6NQZwY1bgzNzthcpyLMXS-6se5K/s1600/AndroidCtoO.018.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjuixwHsibhgmw6LZ7dHVxH9G6ydCoJQbVGtAZBKSdtd8sB6K5pX-0jy5wZy-Nyc_hdRBOmTFoLxAxKYvvBgQxuBQAjQTpVZWhazGFY-QXNgjWG4GXgE6NQZwY1bgzNzthcpyLMXS-6se5K/s400/AndroidCtoO.018.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>2011年10月には OS4.0 IceCreamSandwichがリリースされました。 <br>
ハニカムの大画面向けUI Frameworkがスマホ向けにも移植され、統一UIフレームワークとなりました。 <br>
また、ハードウェアにMenuキーを搭載することが必須でなくなったのもこのタイミングからです。</p>
<p>リリース:2011年10月 Android4.0 - Api Lv.14/15</p>
<p>Unified UI framework… Honeycombで追加されたタブレット向け要素がスマートフォン向けにも引き継がれた. スマホでは画面が小さいことからアクションアイテムがActionBarに収まらない場合, 上下に分割するSplit ActionBarの実装もここから始まります. <br>
ただし, SplitActionBarは現在では非推奨となっています.</p>
<p>MENUボタンがハードウェアに搭載されることは必須ではなくなり, オプションメニューを提供する場合はActionBarにオーバーフローメニューを配置する必要が出てきたのもこのバージョンからです.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiy474C08ezrAv8CUj25WVZ9lfeBkXa-sJRbVutkKM7SxukueodHu4tsarBPG1h7KbbQxkcYGnkZIhhjSmLQnguRhwVKnAPVPWXuCTst5STAhyphenhyphenp7Oq8QjbSbK03Zi91Z4qgl-kwDY_AjY_x/s1600/AndroidCtoO.019.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiy474C08ezrAv8CUj25WVZ9lfeBkXa-sJRbVutkKM7SxukueodHu4tsarBPG1h7KbbQxkcYGnkZIhhjSmLQnguRhwVKnAPVPWXuCTst5STAhyphenhyphenp7Oq8QjbSbK03Zi91Z4qgl-kwDY_AjY_x/s400/AndroidCtoO.019.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>2012年 6月には OS4.1 JellyBean がリリースされました。 <br>
16ms毎のvsyncやトリプルバッファリングによって、アニメーションやスクロールがより滑らかになりました。 <br>
Unicode6対応によって Unicode絵文字にも対応し、また, Google Play Service v1がリリースされたのもこの年です。</p>
<p>リリース:2012年6月 Android4.1~4.3 - Api Lv.16/17/18</p>
<p>Project Butter… 16ms毎のvsyncやグラフィクスのトリプルバッファリングにより, より早く, よりスムーズなユーザ体験を得られるようになりアニメーションやスクロール操作がより滑らかになりました. デバッグツールのsystraceがリリースされたのもこのタイミングです.</p>
<p>Unicode6.0… Unicode6.0絵文字がサポートされたのがこのOSからです. それまでの絵文字はキャリア絵文字でそれぞれ独自の文字コードが割り当てられていましたが, Unicode6.0絵文字がサポートされたことでキャリアを問わず絵文字が使えるようになりました.</p>
<p>Notification styles, GCM … NotificationにBigStyle/InBoxStyle/PictureStyleのスタイルが加わったのがこのバージョン. まだC2DMはGCMにリプレースされた.</p>
<p>その他… 2012年はAndroidMarketがGooglePlayに改名され, Androidアプリ以外のビデオや音楽も扱うストアサービスとして登場した. <br>
GooglePlayServiceライブラリもこの頃にリリースされた. API.18でBluetooth GATTプロファイルに対応も対応した.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhk8xZ9HTDToWd-XJdVGCoAyRMvNj6wnnvI9mtWdS0OQlQa47CUPMdqROxplZF4W6JWsBAgOpV19ZZfhP-3EUPmxzeJCiwXQr9w9gMHzmX7ZbsFS65kwD0Ozm6mzpybhEiBgQDolkez_jL6/s1600/AndroidCtoO.020.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhk8xZ9HTDToWd-XJdVGCoAyRMvNj6wnnvI9mtWdS0OQlQa47CUPMdqROxplZF4W6JWsBAgOpV19ZZfhP-3EUPmxzeJCiwXQr9w9gMHzmX7ZbsFS65kwD0Ozm6mzpybhEiBgQDolkez_jL6/s400/AndroidCtoO.020.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>Unicode6対応で うれしいことは Unicode絵文字が使えるようになったことですね。 <br>
プッシュ文言にUnicode絵文字を使うサービスも増えてきましたが、Unicode絵文字が使えるのはOS4.3からで、それ以前のOSでは文字化けするものがあります。 <br>
また, 絵文字に色がついてカラフルになったのはOS4.4からです。</p>
<p>OS5.0では、人間に関わる絵文字はスライドにあるような黄色いキャラクターのグリフに差し替えられました。 <br>
OS6.0でUnicode 7と8をサポートし、また「お父さんの絵文字+お母さんの絵文字+子供の絵文字」を <br>
Zero Width Joiner の文字コードで連結すると「家族」の絵文字、1文字に置き換わる仕様にも対応しています。</p>
<p>OS7.0ではUnicode9に対応し、5.0で対応されたnonhuman shapeのキャラクターが”人間”の見た目に戻りました。 <br>
絵文字には国や宗教、人種、思想に配慮した仕様になっていて複雑ですが、「human shape」な絵文字と Skin toneの文字コードを繋げることで <br>
絵文字の肌の色を変えることができるようになり、絵文字のバリエーションがグッと増えました。</p>
<p>一応、国内キャリア端末は標準絵文字グリフをキャリア絵文字のグリフで上書きしているので、 <br>
OSが同じでもキャリアによって絵文字の見た目に違いがでる問題があることも, ここに付け加えておきます。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhs1eIGBwn2AL63TV_zTZQYBYyyXDOXdx2MP-Lfg2JqoaGV-__wv3hhvCZhpIWUBRTUKvOW15lHY9gfWhdy1IPL8vTS79Q9zjL71ynaSWam9B6b_nfQv4j61KpcqKd2kPDWgjCPU0zuckAd/s1600/AndroidCtoO.021.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhs1eIGBwn2AL63TV_zTZQYBYyyXDOXdx2MP-Lfg2JqoaGV-__wv3hhvCZhpIWUBRTUKvOW15lHY9gfWhdy1IPL8vTS79Q9zjL71ynaSWam9B6b_nfQv4j61KpcqKd2kPDWgjCPU0zuckAd/s400/AndroidCtoO.021.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>それぞれのUnicodeバージョンを搭載している端末の割合はこちらの通りで、 <br>
Unicode6が 94%、Unicode 7&8が 50%、Unicode 9が 18%です。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEif_19dLNNX974_MHtDRX9gg2Baa4LJOOHzrBbcJ43MoaROeLEiBLFt19MRKujPlUfj9fv6fz_ONnZND4T6cDB5_EgQyLeS5ZDLW-3haNMDiEYn85bKCShO2r-IAAWkkggOkMCm5HfpZWFj/s1600/AndroidCtoO.022.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEif_19dLNNX974_MHtDRX9gg2Baa4LJOOHzrBbcJ43MoaROeLEiBLFt19MRKujPlUfj9fv6fz_ONnZND4T6cDB5_EgQyLeS5ZDLW-3haNMDiEYn85bKCShO2r-IAAWkkggOkMCm5HfpZWFj/s400/AndroidCtoO.022.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>2013年 10月にはOS4.4 Kitkatがリリースされました。 <br>
エントリーレベルのデバイスでも動作できるように設計されたOSでストレージアクセスフレームワークが搭載されたのもここからです。 <br>
WebViewのアップデートもありましたが そちらは後ほどお話しします。</p>
<p>リリース:2013年10月 Android4.4 - Api Lv.19</p>
<p>Support 512MB RAM device… エントリーレベルのデバイスであっても動作するように設計されたOSで, アプリもActivityManager.isLowRamDevice() APIを使うことで低スペックデバイス向けのコンフィグレーションが可能になりました.</p>
<p>Storage Access Framework… これまで端末内のファイルをユーザに選択させたり, 保存場所を指定させる場合に使われるファイルエクスプローラはOSから提供されていませんでした. ストレージアクセスフレームワークを使うことでユーザに一貫したファイルシステムへの参照方法を提供できるようになりました.</p>
<p>Chromium WebView… WebKitがChromiumベースに差し代わりました. これよりChrome Dev Toolsによるリモートデバッグもサポートされるようになった.</p>
<p>その他… RTLサポートが強化されました. それまではテキストの対応しかなく, リソースを重複して持つ必要がありました.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOdK9WZBQU_lpUunh8NJOCkAVPN3rcM5snlm5QxuK1pZnIHLwURcsNpMPKU1eLTGaCM1WVrO9IYV7k0EsB9USO6q8y_u-Hq9-0wMDf602J8szy7GHEzC1j_LH7hh3RhfSsV7z4cATqGJo7/s1600/AndroidCtoO.023.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhOdK9WZBQU_lpUunh8NJOCkAVPN3rcM5snlm5QxuK1pZnIHLwURcsNpMPKU1eLTGaCM1WVrO9IYV7k0EsB9USO6q8y_u-Hq9-0wMDf602J8szy7GHEzC1j_LH7hh3RhfSsV7z4cATqGJo7/s400/AndroidCtoO.023.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>Android4.4からは, バッテリー消費を抑えるためにアラームの発火タイミングが不正確になります。 <br>
4.4以降、どうしても正確なアラームが欲しい場合は AlarmManager の setWindow() か setExact() を使うことになります。</p>
<p>Android6.0は Dozeによるデバイスアイドル状態では アラームの発火が保留されます。 <br>
アイドル状態でもアラームを正確に発火させたい場合は setAndAllowWhileIdle() か setExactAndAllowWhileIdle() を使うことになります。</p>
<p>ちなみに、アラームはアプリごとに9分間に1回以上発火はされない仕様です。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicR5rbDrkOfolx7RaZgB45XNkK7Efad5abj9Wqa3mifm0564pdi5pg00NrBiKy7p2MNSPaVpHx9tkGjhq9bgr0k4bbTN1vUU-TCmRKvmFQA6y3-rZYWVFYw0zqaJvqBF-ypTLrz3LSpJi3/s1600/AndroidCtoO.024.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEicR5rbDrkOfolx7RaZgB45XNkK7Efad5abj9Wqa3mifm0564pdi5pg00NrBiKy7p2MNSPaVpHx9tkGjhq9bgr0k4bbTN1vUU-TCmRKvmFQA6y3-rZYWVFYw0zqaJvqBF-ypTLrz3LSpJi3/s400/AndroidCtoO.024.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>2014年10月にはOS5.0 Lollipop がリリースされました。 <br>
マテリアルデザインによってUI/UXが大きく変更され、ベクタードロワブル や マルチユーザのサポート、 <br>
ARTの標準搭載、CPUの64bitアーキテクチャサポートなど、幅広いアップデート内容になっています。</p>
<p>リリース:2014年10月 Android5.0 - Api Lv.21/22</p>
<p>Material Design, Project Volta… マテリアルデザインの導入でUI/UXが大きく刷新された. RecyclerViewやZ軸, シャドウの概念もここから. <br>
また, バッテリー消費を抑えて電池持ちを改善するプロジェクトProject Voltaが明らかにされました. ジョブスケジューラの機能が提供されたことにより, アプリの動作が最適化されバッテリー消費を抑えることに貢献しています.</p>
<p>Overview, Notification, Multi-user… OverviewはこれまでRecentsと呼ばれていた”最近使ったアプリーケション一覧”の機能に相当するものです. <br>
従来は使ったアプリケーションのリストが並ぶだけでしたが, ここに複数のActivityをドキュメントとして追加することができるようになり, マルチタスクにも使えるようになりました. また, Notificationにはプライオリティやカテゴリの概念が追加され, 重要な通知がヘッドアップ表示されるようになったのもこの頃です. </p>
<p>64bit, ART… また, パフォーマンス改善も行われ, ARTランタイム対応や64bit対応もここから始まりました.</p>
<p>その他… Chromium WebViewがPlayStoreからアップデートできるようになった. AndroidHttpClientのメンテナンスが終了・廃止されURLConnectionの使用が必須に. API Lv20はAndroid Wear向けのAPI Lvとして割り当てられた. またディベロッパープレビュー版という提供方法が始まったのもここから.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzkRHcYG4QjeswIXK0vZ3TbqTg1FxOaexOG-ZLlO68e-SwIYJ5igVu8nTLmWCzKTFEkKLLMflPMZTIqsXG3ZCICYfN8KKpuCGDiE2Apd7er5zao-MHv4Ifw4y8fcz2mk0GgaCcFVPVLlnK/s1600/AndroidCtoO.025.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgzkRHcYG4QjeswIXK0vZ3TbqTg1FxOaexOG-ZLlO68e-SwIYJ5igVu8nTLmWCzKTFEkKLLMflPMZTIqsXG3ZCICYfN8KKpuCGDiE2Apd7er5zao-MHv4Ifw4y8fcz2mk0GgaCcFVPVLlnK/s400/AndroidCtoO.025.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>WebView周りの変更についてみてみると、4.3までのWebViewは WebKit上で動作していましたが, 4.4以降はChromium上で動作します。 <br>
5.0では Google Play経由で アップデート可能になり、WebViewのセキュリティパッチが素早くユーザに届けられるようになりました。 <br>
WebViewに依存したアプリの開発者は WebViewのアップデート頻度が高くなったので注意する必要があります。</p>
<p>7.0 以降は Chrome APKから WebViewを提供する機能が搭載されています。これによって、メモリ消費が改善されました。 <br>
8.0では アプリのWebView がマルチプロセスモードで実行されます。ウェブコンテンツはアプリのプロセスとは別の独立したプロセスで処理されるので、 <br>
セキュリティが強化されています。</p>
<p>また、ここに記載していませんが Chrome custom Tab の機能がOS4.1以降で利用できるようになっています。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3K-7udchhqYq1N4g-Ai2KwJPPCOAo36999J_fh-oIGyMJn-GOzjC-xJnCPtkqqBqLDag9_Shf0paX0fKp2xhBCgCuiG9Fc7UFctfc1fGynmP-IxPbJoarOWmqBO1atTbC-hWK3NFu5VVs/s1600/AndroidCtoO.026.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg3K-7udchhqYq1N4g-Ai2KwJPPCOAo36999J_fh-oIGyMJn-GOzjC-xJnCPtkqqBqLDag9_Shf0paX0fKp2xhBCgCuiG9Fc7UFctfc1fGynmP-IxPbJoarOWmqBO1atTbC-hWK3NFu5VVs/s400/AndroidCtoO.026.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>Chromium版は 93%の端末 に搭載されていて, WebViewを個別にアップデート可能な端末は 78% です。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7QheP2sa5WG_Qx_0Fzhy3c_5Gc5XfQeoA8-oL7rUwgSsvESi6KZ4EEkmHjIEYBxQDcAhXO7y3QrB4QWhC3I9WkIXNzq502EzO-uy4nKJi6ar26Ab1Ok2yQbM1sjml0M6amcQWbfHnWhOI/s1600/AndroidCtoO.027.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7QheP2sa5WG_Qx_0Fzhy3c_5Gc5XfQeoA8-oL7rUwgSsvESi6KZ4EEkmHjIEYBxQDcAhXO7y3QrB4QWhC3I9WkIXNzq502EzO-uy4nKJi6ar26Ab1Ok2yQbM1sjml0M6amcQWbfHnWhOI/s400/AndroidCtoO.027.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>2015年 10月にはOS6.0 マシュマロがリリースされました。 <br>
このあたりからアプリの挙動を変えるアップデートが目立つようになりました。 <br>
RuntimePermission, Doze, AppStandbyなどです。</p>
<p>リリース:2015年10月 Android6.0 - Api Lv.23</p>
<p>RuntimePermission… パーミッションモデルに大きな変更が入りました. ユーザはアプリのパーミッションを管理できるようになり, 好きなタイミングで権限を付与/剥奪できるようになります. また, アプリインストール時にパーミッション許可を求めることはせず, アプリの任意のタイミングでユーザにパーミッション付与を求めるようになります.</p>
<p>Doze, App Standby… 電源に接続していない状態で, 一定時間端末を画面オフで放置していた場合にスリープ状態を維持するDozeや, アプリが長時間アイドル状態であった場合にアプリのネットワークアクセスが無効になり, 同期とジョブが保留されるようになりました.</p>
<p>AutoBackup, Do not disturb… アプリデータが自動でGoogle Driveへバックアップできるようになりました. 追加のコードは必要ありません. バックアップを無効にする場合はマニフェストに1行無効にするフラグを定義します. また, Do not disturbモードもこのバージョンからです.</p>
<p>その他… Apache HTTP clientが削除. OpenSSLからBoringSSLに移行. TextSelectionもそれまでの編集モードからpopup windowでアクションを選択するUIに変更されました. </p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSO8KJi7xxqTr-G5Y2kIUo8EUEsUwySq7quSVNxupEPqPZJzkTgbCpYyoqi8_KbiAvIr6MA8NBSFnILVn3M-NB63c6dJkG4MHU7fb_wXEHlswhrMAUyLAudIH_-aMcV_4_ChzijPry0fb/s1600/AndroidCtoO.028.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjhSO8KJi7xxqTr-G5Y2kIUo8EUEsUwySq7quSVNxupEPqPZJzkTgbCpYyoqi8_KbiAvIr6MA8NBSFnILVn3M-NB63c6dJkG4MHU7fb_wXEHlswhrMAUyLAudIH_-aMcV_4_ChzijPry0fb/s400/AndroidCtoO.028.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>6.0のDoze機能は, 画面OFF かつ 充電中ではない場合 かつ 端末をほとんど動かさない静止状態 にし続けると、 <br>
CPU と ネットワーク通信 を一時保留して バッテリーの寿命を延ばす 省電力機能が働きます。</p>
<p>7.0ではDoze状態になる条件が緩和されて、端末が静止していなくてもDoze状態に入ります。 <br>
これによって、ポケットにスマホを入れて持ち歩いているような状況でも バッテリー消費を抑えることができるようになりました。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi_0RA9k700AlUpoaZrEfnyxrwr8zON89fWb1wbL7hYKzJBQKJSJql4zPs2WX29VyPkvso-FmZ1yjsDwfS545T-3gRpzMRB4DALTQj6y5nua2ISCqgSfTAmrfRUD7bWLRwyk4I1_N8NrPP7/s1600/AndroidCtoO.029.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi_0RA9k700AlUpoaZrEfnyxrwr8zON89fWb1wbL7hYKzJBQKJSJql4zPs2WX29VyPkvso-FmZ1yjsDwfS545T-3gRpzMRB4DALTQj6y5nua2ISCqgSfTAmrfRUD7bWLRwyk4I1_N8NrPP7/s400/AndroidCtoO.029.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>Dozeがリリースされたのは Android マシュマロ以降なので 50%の端末 がこれを搭載しています。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsJ4fov2vjYZUz4ZUC0CFhtS9CCj43Mq4FwEvSzxI0XHp-qOjQCl3Yx-Mt7naRUCQ7yy0x9cQ5QeBfWsef7lIxTlUj8ze-zxtVSqZS_k3DLhlTutR8p_EH46hFA6nxW04MNIoN6A2bQ1lu/s1600/AndroidCtoO.030.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjsJ4fov2vjYZUz4ZUC0CFhtS9CCj43Mq4FwEvSzxI0XHp-qOjQCl3Yx-Mt7naRUCQ7yy0x9cQ5QeBfWsef7lIxTlUj8ze-zxtVSqZS_k3DLhlTutR8p_EH46hFA6nxW04MNIoN6A2bQ1lu/s400/AndroidCtoO.030.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>2016年8月には OS7.0 Nougat がリリースされました。 <br>
マルチウィンドウやRAMの使用量を削減するProject Svelteによって一部のブロードキャストが廃止されました。 <br>
また、ランチャーアイコンにまつわる変更もあります。</p>
<p>リリース:2016年8月 Android7.0/71 - Api Lv.24/25</p>
<p>Multi-window, Screen zoom… スマートフォンやタブレットで画面を分割して2つのアプリを並べて利用できるようになり, AndroidTVではピクチャーインピクチャーがサポートがサポートされました. また視力が低いユーザ向けの補助機能としてスクリーンズームが搭載され, 端末の画面密度設定が変更可能になりました.</p>
<p>Doze2, File security… 従来はDozeモードに突入するためには端末が静止状態である必要がありましたが, Doze2ではこの制限がなくなりました. <br>
また, プライベートディレクトリのアクセス権限が厳格化され, 他アプリにファイルを直接読み書きさせることができなくなりました. これに伴いfileスキームのURIを含むIntentを共有しようとするとセキュリティ例外が投げられるようになっています.</p>
<p>Project Svelte… アプリのバックグラウンド実行を最適化することでRAMの使用量を削減する取り組み. CONNECTIVITY_ACTION、ACTION_NEW_PICTURE、ACTION_NEW_VIDEOの暗黙的なブロードキャストが削除されました. これらのブロードキャストは複数のアプリが同時に起動するため, メモリが逼迫しシステムのパフォーマンスを低下させる要因になるためです.</p>
<p>その他… 3DレンダリングAPIのvulkanがプラットフォームに統合, データセーバ機能の搭載, WebViewがChrome APKから提供される, VRサポート, App Shortcutなど. またこのタイミングでApache HarmonyベースからOpenJDKベースに移行された.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhoEoGJlbXjgl0ZfFxyW-o6m2Fhn012vla86RJ4webfjJ140YhlrczvDMvfd-hwUv8z9r9fPxPT-fg3PW2vYvGC71NIbqCIniVPBfyi-E9X8d9cVZipa3YrLAbhViZHhsfEtmsmHkiqtu9b/s1600/AndroidCtoO.031.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhoEoGJlbXjgl0ZfFxyW-o6m2Fhn012vla86RJ4webfjJ140YhlrczvDMvfd-hwUv8z9r9fPxPT-fg3PW2vYvGC71NIbqCIniVPBfyi-E9X8d9cVZipa3YrLAbhViZHhsfEtmsmHkiqtu9b/s400/AndroidCtoO.031.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>OS4.3 では 一般的なサイズよりも大きく アプリアイコンを表示するランチャーアプリに対応するため, <br>
端末の抽象解像度ではなく、リクエストされたサイズに応じてリソースを返す mipmapリソースがサポートされました。</p>
<p>OS7.1では アプリアイコンを丸く表示するランチャーアプリが増えたため, アプリから丸いアイコンを提供するRound Iconリソースが追加されています。 <br>
OS8.0では さらに元のデザインを崩すことなく、自由にアプリアイコンの形を変えることができるAdaptive Iconがサポートされています。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBH4Q4o37FLjw5Xjn636eLxRn31OZaXI-9Ar5pWLXdFoaGdrqckQGQharagim0j2p9dAmIburr22w3RQtUQL_JvUMf9V2-PaAXg1zoYt-IssSJcGxdprkbYkFvR6Bp2Kf9Nfw8Q4PtVb8o/s1600/AndroidCtoO.032.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjBH4Q4o37FLjw5Xjn636eLxRn31OZaXI-9Ar5pWLXdFoaGdrqckQGQharagim0j2p9dAmIburr22w3RQtUQL_JvUMf9V2-PaAXg1zoYt-IssSJcGxdprkbYkFvR6Bp2Kf9Nfw8Q4PtVb8o/s400/AndroidCtoO.032.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>そして 2017年8月の Android8.0 現在に至ります。</p>
<p>リリース:2017年8月 Android8.0 - Api Lv.26</p>
<p>Background execution limits… バックグランドによる動作が大きく制限されました。サービスを開始してもアプリがバックグラウンドに遷移するとサービスは自動で停止されます。 <br>
バックグランドからサービス開始したい場合はContext.startForegroundServiceメソッドをコールし, 5秒以内にフォアグラウンドサービスに昇格させる必要があります。 <br>
また、暗黙的なブロードキャストも制限されはじめ、JobShcedulerへの移行が推奨されています。</p>
<p>Notification dots, XML font… アプリの通知がランチャーアイコンにドットで表現されるようになったり, フォントをリソースとして扱えるXMLフォントの機能が導入されました。</p>
<p>Alert windows… システムウィンドウより上にアラートウィンドウを表示できなくなりました。アプリはTYPE_APPLICATION_OVERLAYウィンドウを使うことができます。</p>
<p>その他… HttpsURLConnectionが古いTSLバージョンへフォールバックする動作をやめる, WebViewのマルチプロセスモード実行, ANDROID-IDの処理方法変更, クリッカブルなViewがデフォルトでフォーカス可能に変更, スマホ/タブレットでのピクチャーインピクチャーモード対応, AppShortcutの改善, アダプティブアイコン, 最大アスペクト比, マルティディスプレイ, JobScheduler改善など</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgOgcxXVpBGODTsmDsD_1PO3jWDJ9KVQ2-IN8LqWUHgC8n7DP-xdEHakjppnfW3Lu7UZHtGXID3f5VbYwYsJ3NvHum0m0HZ5RaTHfIIR2H-E_A_oYXatY79P_6zna-UqZirkVal-ZopUgK/s1600/AndroidCtoO.033.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEhgOgcxXVpBGODTsmDsD_1PO3jWDJ9KVQ2-IN8LqWUHgC8n7DP-xdEHakjppnfW3Lu7UZHtGXID3f5VbYwYsJ3NvHum0m0HZ5RaTHfIIR2H-E_A_oYXatY79P_6zna-UqZirkVal-ZopUgK/s400/AndroidCtoO.033.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>Android8.0 ではバックグラウンドで実行する動作を制限していますが、多くの場合 ジョブスケジューラに置き換えることができます。</p>
<p>これは ディベロッパーサイトの Intelligent Job-Scheduling というページからの引用で、 <br>
ジョブを賢くスケジューリングすることで、バッテリ寿命といったシステム状態とともに、アプリのパフォーマンスも向上できます。 <br>
と書かれています。</p>
<p>バッテリー寿命というのは、モバイルユーザ体験の重要なポイントです。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgS3wy9jH9lvuJcZh96gBY7Sh8Zbs7sFmo-HRhe382WpGwm-5buF-MNj6cWLQ5u5_ElfempMZENHeEJiotwTVS0zCD8JbBW6xOkQh_RkgCNO87wgN0b-mpu92RrhiLnqUDxgzSEVB5JbiNQ/s1600/AndroidCtoO.034.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgS3wy9jH9lvuJcZh96gBY7Sh8Zbs7sFmo-HRhe382WpGwm-5buF-MNj6cWLQ5u5_ElfempMZENHeEJiotwTVS0zCD8JbBW6xOkQh_RkgCNO87wgN0b-mpu92RrhiLnqUDxgzSEVB5JbiNQ/s400/AndroidCtoO.034.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>Android を よりスマートで、より早く、よりパワフルなプラットフォームに仕上げるには、OSのバージョンアップだけではなく、 <br>
アプリの最適化によるバージョンアップも必要不可欠です。</p>
<p>OSに最適化する作業は大変ですが、プラットフォームにも デバイスにも また, ユーザにも優しいアプリ開発を心がけたいものです。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7AhbJ2kG5pQGzZS0ass2N7-6nrV9NgjniKlsM3nvQKrY2tUvg_L6dqwqD97CF3TvOlgo7ZEW00QfU__5YQvNwD3TdSjsqY-gBH3rCHwi37AOn7l_ya49OrM9ZY6aI9msCGWrFpGdFfBjK/s1600/AndroidCtoO.035.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg7AhbJ2kG5pQGzZS0ass2N7-6nrV9NgjniKlsM3nvQKrY2tUvg_L6dqwqD97CF3TvOlgo7ZEW00QfU__5YQvNwD3TdSjsqY-gBH3rCHwi37AOn7l_ya49OrM9ZY6aI9msCGWrFpGdFfBjK/s400/AndroidCtoO.035.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<p>発表は以上ですが、本日紹介した内容は時間の都合上、細かな内容は省いています。 <br>
みなさんのサービスに関わる部分で気になるものがありましたら、これらのページを参考にしてみてください。</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-zgnEsUwOGV7vcGL4JAKt1Ud69q0U1QHZHmoP_j5lcyrZUVnv6B1KyrsYcZco1s7NaFI-gp_tYcdtei8E3sh8ilwoP8QOO_Enu6GoUE4H8XbYPjvEuCgta01M8r-2BET4LY1Ks5lfXgo8/s1600/AndroidCtoO.036.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-zgnEsUwOGV7vcGL4JAKt1Ud69q0U1QHZHmoP_j5lcyrZUVnv6B1KyrsYcZco1s7NaFI-gp_tYcdtei8E3sh8ilwoP8QOO_Enu6GoUE4H8XbYPjvEuCgta01M8r-2BET4LY1Ks5lfXgo8/s400/AndroidCtoO.036.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiRju1pYgc2v4z8kO2Exr0r-WjwyerXK4251uWc2HvEa3YGhcYG5ys71QTlUTrybe5vnEgYjtV35SzEWpN3HQR8S_gtDLuefGtOcZPVNa7Lk8Gm-E2ggpGKmTSMkCqy_ctZ-lRWymHmIc_Z/s1600/AndroidCtoO.037.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiRju1pYgc2v4z8kO2Exr0r-WjwyerXK4251uWc2HvEa3YGhcYG5ys71QTlUTrybe5vnEgYjtV35SzEWpN3HQR8S_gtDLuefGtOcZPVNa7Lk8Gm-E2ggpGKmTSMkCqy_ctZ-lRWymHmIc_Z/s400/AndroidCtoO.037.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEisjPpaWGtIjSEhEufqSFxNhtCKwny2CQdqzL6Ss6j-PiBDXkXos5J3iB454wHD0-eP2a4-xCh9UQMb65nrzpjHd8HZNR6nptQmloPR1QJ50fbZRrgQhaT6xMKKoBnrz91uPmPsKPvVekDU/s1600/AndroidCtoO.038.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEisjPpaWGtIjSEhEufqSFxNhtCKwny2CQdqzL6Ss6j-PiBDXkXos5J3iB454wHD0-eP2a4-xCh9UQMb65nrzpjHd8HZNR6nptQmloPR1QJ50fbZRrgQhaT6xMKKoBnrz91uPmPsKPvVekDU/s400/AndroidCtoO.038.png" width="400" height="225" data-original-width="800" data-original-height="450" /></a>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-45120489094277364912017-07-25T19:24:00.000+09:002017-07-25T19:31:21.872+09:00VisibleForTestingとRestrictTo<div class="markdown">
<p>昨日, メッセージの表示頻度を簡単に調整できるライブラリ<a href="https://github.com/YukiMatsumura/denbun">denbun</a>をリリースしました. </p>
<p><a href="https://github.com/YukiMatsumura/denbun"><img src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgbiTMdNsRZLHuLw6zU5BNIJG_wugY86oCN2EZcWmnjbbkkSrAOSF-ceyiVg6IRCh1GESeAHjc7rl84L1fKNgHz99v7NiYNZ_0enZWS7qKtTsmpy9bU2T20-Ew_ShfG5ELyDMuBjZlGo3q5/s1600/gotogithub.png" alt="Denbun" title=""></a></p>
<p>初めてのライブラリリリースなので色々と学びがありました. <br/>
本稿では<code>VisibleForTesting</code>と<code>RestrictTo</code>アノテーションについて書き留めます. </p>
<ul>
<li><a href="https://developer.android.com/reference/android/support/annotation/VisibleForTesting.html">VisibleForTesting</a></li>
<li><a href="https://developer.android.com/reference/android/support/annotation/RestrictTo.html">RestrictTo</a></li>
</ul>
<h3 id="visiblefortesting">VisibleForTesting</h3>
<p>フィールドやメソッドのスコープはできるだけ狭くすることが大切ですが, テスタビリティを確保するためにやむなくスコープを広くとる場合があります. <br>
<code>VisibleForTesting</code>は, スコープをテスタビリティのために広く定義していることを明示します. </p>
<p>例えば, Denbunライブラリでは情報の永続化先であるSharedPreferenceとのI/Oをフックできるようにしてテスタビリティを確保しています. </p>
<pre class="prettyprint"><code class="language-java hljs "><span class="hljs-annotation">@VisibleForTesting</span>(otherwise = PACKAGE_PRIVATE)
<span class="hljs-keyword">public</span> DenbunConfig <span class="hljs-title">daoProvider</span>(@NonNull Dao.Provider provider) { ... }</code></pre>
<p>このメソッドはプロダクションコードでは<code>Package Private</code>スコープで扱われることを想定し, テストコードでは<code>Public</code>スコープで扱われることを想定しています. <br>
そのため本来あるべきスコープは<code>Package Private</code>なのですが, テスタビリティのために<code>Public</code>としています. </p>
<p>メソッドが本来あるべきスコープは<code>VisibleForTesting</code>アノテーションの<code>otherwise</code>パラメータに指定します. <br>
こうすることで, プロダクションコードにおいて<code>Package Private</code>スコープ外からアクセスしてきた場合にインスペクションによる警告が表示されるようになります. </p>
<p>ただし, このアノテーションはクラスファイルに影響を及ぼすものではないので, インスペクションの警告を無視して無理やり要素にアクセスすることは可能です. </p>
<p><code>VisibleForTesting</code>の真価は, このアノテーションで指定された要素をプロダクションコードで呼び出すとインスペクションの警告によって使い方が間違っていることを教えてくれるところにあります. <br>
これは, javadocにコメントを残す対応よりもはるかに効果的で簡単です. また, 利用側に実装者の意図をインスペクションを通して伝えることができるので利用側にとっても嬉しい機能です. 実際のライブラリ開発では手軽に導入できてアクセス制御で悩むことも減るのでとても便利に使えます. </p>
<p>ただ, 実際にはアクセス制御できていないので, APIを公開することが致命的であるケースにおいてはイミュータブルインタフェースをかませるなどの対応が必要です. (そのようなケースはあまり思い浮かびませんが, セキュリティが必要なSDKなどでは該当しそうです)</p>
<h3 id="restrictto">RestrictTo</h3>
<p>次に<code>RestrictTo</code>アノテーションです. これはテストのために用意されたメソッドであることを明示するものです. <br>
<code>VisibleForTesting</code>はテスタビリティのための”スコープ”に着目しているので, そのメソッド自体は想定されるスコープ内であればプロダクションコードで呼ばれることが許されています. <br>
例えば, <code>VisibleForTesting(otherwise = private)</code>なメソッドであればプロダクションコードでもクラス内(privateスコープ内)からの呼び出しが想定されているということです. </p>
<p>一方で, <code>RestrictTo</code>はメソッド自体の存在に着目しています. <br>
<code>RestrictTo(TEST)</code>であれば, テストコードからの呼び出しのみを想定しており, プロダクションコードでの呼び出しは想定されません. <code>RestrictTo(LIBRARY)</code>であれば, ライブラリ内での用途に限った要素であることを明示しています. <br>
これはライブラリを作る側としてはとても強力です. これも<code>VisibleForTesting</code>と同じく, 呼び出し側が想定外の呼び出しを行なった場合にインスペクションの警告を表示します. </p>
<p>例えば, Denbunライブラリでは, <code>DenbunBox</code>の初期化は一度しか行えず, 2回目以降はno-opになるよう実装されています. <br>
しかし, UnitTestをする際にテストケースごとに<code>DenbunBox</code>を再初期化したくなる場合も想定して, <code>DenbunBox</code>の状態をリセットする<code>reset()</code>メソッドを用意しています. </p>
<pre class="prettyprint"><code class="language-java hljs "><span class="hljs-annotation">@RestrictTo</span>(TESTS) <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">reset</span>() { ... }</code></pre>
<p>このメソッドは, ライブラリ内部および, プロダクションコードからの呼び出しも想定していません. テストに限定した利用を想定したものです. </p>
<p>ライブラリを作る際には, こういったアノテーションも活用して, 利用する側に作り手の意図を明示するのも大切だなと感じました. </p>
<p>以上です. </p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-10425519144707187112017-07-25T02:57:00.000+09:002017-07-25T03:04:48.182+09:00Denbunライブラリでメッセージの表示頻度を調整する<div class="markdown">
<h3 id="tldr">tl;dr</h3>
<ul>
<li>メッセージの過度な表示はユーザを退屈させてしまう</li>
<li><a href="https://github.com/YukiMatsumura/denbun">Android向けライブラリ “Denbun” をリリース</a>した</li>
<li>Toast, Dialogなどのメッセージ表示頻度を簡単に調整できるライブラリ</li>
</ul>
<h3 id="はじめに">はじめに</h3>
<p>モバイルプラットフォームでは, ユーザ向けに何かしらのメッセージを表示することがよくあります. <br>
それは, イベントの発生を知らせるものであったり, ユーザのアクションが完了したことを知らせるものであったり, エラーの発生を知らせるものであったりと様々です. <br>
これらのメッセージは重要なものですが, 中には退屈と思われてしまうものもあります. </p>
<ul>
<li>ユーザがアプリケーションの振る舞いを学習するのに重要なメッセージが, アプリケーションを使い慣れた後になっては, ただのお節介なメッセージになってしまうケース</li>
<li>毎回閉じるだけの”お知らせダイアログ”といった類のもの</li>
<li>コンテンツの削除確認といった誤操作防止目的のもの</li>
<li>Backキーを押した際の「アプリケーションを終了しますか?」なもの</li>
</ul>
<p>ユーザを退屈させないためにも, メッセージの表示頻度を調節することが重要です. </p>
<h3 id="denbun">Denbun</h3>
<p>メッセージの表示頻度を調整するためのアプローチはいくつかあります. </p>
<ul>
<li>ダイアログに「今後表示しない」チェックボックスをつけてユーザ主動でダイアログ表示をやめさせる方法</li>
<li>一度しか表示しないような回数限定メッセージ</li>
<li>一週間のうち決まった曜日にだけ表示する定期的なメッセージ など… </li>
</ul>
<p>これらのアプローチをとるためには, 表示設定や表示回数といった内容を永続化して都度, 表示頻度を調整する必要があります. <br>
そこで, メッセージの前回表示時間や表示回数といった情報を保存し, 表示頻度の調整をサポートするDenbunライブラリをリリースしました. </p>
<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://github.com/YukiMatsumura/denbun" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://github.com/YukiMatsumura/denbun/raw/master/art/logo.png?raw=true" data-original-width="176" data-original-height="120" /></a><br />
<a href="https://github.com/YukiMatsumura/denbun">https://github.com/YukiMatsumura/denbun</a></div>
<br />
<p>このライブラリは, 次のようなメッセージ通知を実現したい場合に有効です. </p>
<ul>
<li>「今後表示しない」 オプション付きメッセージ</li>
<li>N回だけ表示するメッセージ</li>
<li>定期的に表示するメッセージ(1週間に1回の頻度で表示. 月曜日に1回だけ表示. etc.)</li>
<li>N回表示した後は, n時間経過するまで表示しないメッセージ</li>
</ul>
<p>メッセージの表現系(Dialog, Toast, Snackbar, etc.)は問いません. <br>
このライブラリは, メッセージの前回表示時間や表示回数をSharedPreferenceに保存しており, これらの情報を駆使して”今, メッセージを表示すべきかどうか” を判断することで, メッセージの表示頻度を調整します. </p>
<h4 id="使い方">使い方</h4>
<p>まず初めに, <code>Application.onCreate</code>などで, <code>DenbunBox</code>を初期化します. <br>
<code>DenbunBox</code>はこのライブラリの起点となる重要なクラスです. </p>
<pre class="prettyprint"><code class="language-java hljs ">DenbunBox.init(<span class="hljs-keyword">new</span> DenbunConfig(<span class="hljs-keyword">this</span>));</code></pre>
<p><code>DenbunBox</code>の初期化が終わったら, メッセージを表現する<code>Denbun</code>インスタンスを取得します. <br>
メッセージの表示頻度の調節はこの<code>Denbun</code>インスタンスを通して行います. </p>
<pre class="prettyprint"><code class="language-java hljs ">Denbun msg = DenbunBox.get(ID);</code></pre>
<p><code>Denbun</code>インスタンスの<code>show()</code>を呼び出すことで, 表示時間や表示回数の情報が更新され永続化されます. </p>
<pre class="prettyprint"><code class="language-java hljs ">Denbun msg = DenbunBox.get(ID);
msg.shown();</code></pre>
<p>メッセージの最適な表示頻度はメッセージ毎に異なりますので, <code>Denbun</code>インスタンスを取得する際に最適な表示頻度を算出できる<code>Frequency Adjuster</code>を指定します. <br>
例えば, 下記の例は1回限りのメッセージ通知を実現する例です. </p>
<pre class="prettyprint"><code class="language-java hljs "><span class="hljs-comment">// This message is displayed only once.</span>
Denbun msg = DenbunBox.get(ID, <span class="hljs-keyword">new</span> CountAdjuster(<span class="hljs-number">1</span>));
...
msg.isShowable(); <span class="hljs-comment">// true</span>
msg.shown();
msg.isShowable(); <span class="hljs-comment">// false</span></code></pre>
<p>あるいは, メッセージを直接的に今後表示しなくすることも可能です. </p>
<pre class="prettyprint"><code class=" hljs cs">Denbun msg = DenbunBox.<span class="hljs-keyword">get</span>(ID);
msg.suppress(<span class="hljs-keyword">true</span>);</code></pre>
<p>メッセージによっては表示頻度の計算が複雑になるものもあるでしょうから, Frequency Adjusterは自前のものを実装して<code>DenbunBox.get</code>に指定することもできます. </p>
<p>実際にDialogやToastを表示する際には, <code>Denbun</code>インスタンスの<code>isShowable()</code>の値を確認してから表示すると決められた頻度でメッセージを表示することができます. </p>
<h4 id="テスタビリティ">テスタビリティ</h4>
<p>Denbunライブラリを使ったコードをテストしたい場合は下記が参考になります. <br>
<code>DenbunConfig</code>にはDenbunライブラリとSharedPreferenceのI/Oを取り持つDAOのgetter/setterが用意されています(このメソッドは<code>@VisibleForTesting</code>です)</p>
<pre class="prettyprint"><code class=" hljs avrasm">DenbunConfig conf = new DenbunConfig(app)<span class="hljs-comment">;</span>
// spy original DaoProvider
Dao<span class="hljs-preprocessor">.Provider</span> origin = conf<span class="hljs-preprocessor">.daoProvider</span>()<span class="hljs-comment">;</span>
conf<span class="hljs-preprocessor">.daoProvider</span>(pref -> (spyDao = spy(origin<span class="hljs-preprocessor">.create</span>(pref))))<span class="hljs-comment">;</span>
DenbunBox<span class="hljs-preprocessor">.init</span>(conf)<span class="hljs-comment">;</span>
DenbunBox<span class="hljs-preprocessor">.find</span>(ID)<span class="hljs-preprocessor">.shown</span>()<span class="hljs-comment">;</span>
verify(spyDao, times(<span class="hljs-number">1</span>))<span class="hljs-preprocessor">.update</span>(any())<span class="hljs-comment">;</span></code></pre>
<h4 id="おわりに">おわりに</h4>
<p>Denbunライブラリを使い始めるには次の一文をbuild.gradleに追記するだけです. <br>
<code><latest version></code>には<a href="https://bintray.com/yuki312/maven/denbun/_latestVersion">最新のライブラリバージョン</a>を指定してください. </p>
<pre class="prettyprint"><code class=" hljs vbnet">compile <span class="hljs-comment">'com.yuki312:denbun:<span class="hljs-xmlDocTag"><latest version></span>'</span></code></pre>
<p>近々v1.0.0をリリース予定です. <br>
PRやIssueがあればGitHubの方に登録していただけると幸いです. </p>
<p>以上です. </p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-18270778914223618232017-07-20T21:04:00.001+09:002017-07-21T01:21:42.523+09:00Intentの共有先一覧から自アプリを除外する<div class="markdown">
<p>他アプリ起動周りでちょっとハマったのでメモ. </p>
<p>テキストやURIを暗黙Intentで共有する場合, 自アプリがそれに反応する<code>intent-filter</code>を持っていると, <code>ActivityChooser</code>に表示候補として含まれてしまう場合があります. <br>
自アプリで捌きたくないから他アプリに共有しているのに, そのリストに自アプリが載っているのはよろしくない. <br>
ということで, Intentは投げるけれど<code>ActivityChooser</code>に自アプリを含めない方法を探りました. </p>
<h3 id="tldr">TL;DR</h3>
<ul>
<li>createChooser, ChooserActivityまわりの挙動がOSバージョンで異なっている</li>
<li>API LV.23 前後で<code>PackageManager.MATCH_DEFAULT_ONLY</code>の振る舞いが変わる</li>
<li>API LV.23 前後でActivity選択ダイアログのレイアウトが変わる</li>
<li>結論<code>queryIntentActivities</code>からの自前ダイアログ生成のが楽そう</li>
</ul>
<p>シンプルに<code>queryIntentActivities</code>と<code>Intent.createChooser</code>を組み合わせればできるだろうと思っていたのですが, 古いOSで確認したところ意図した通りに動きませんでした. <br>
で, 古いOSでの動作もサポートすべく, 色々検討した結果を残しておきます. </p>
<h3 id="createchooser">createChooser</h3>
<pre class="prettyprint"><code class="language-java hljs ">Intent intent = <span class="hljs-keyword">new</span> Intent(Intent.ACTION_VIEW, Uri.parse(uri));
<span class="hljs-keyword">int</span> flag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PackageManager.MATCH_ALL
: PackageManager.MATCH_DEFAULT_ONLY;
List<ResolveInfo> launchers
= context.getPackageManager().queryIntentActivities(intent, flag); <span class="hljs-comment">// *a</span>
<span class="hljs-comment">// 自アプリを起動対象から除外する</span>
List<Intent> intents = <span class="hljs-keyword">new</span> ArrayList<>();
<span class="hljs-keyword">for</span> (ResolveInfo app : launchers) {
<span class="hljs-keyword">if</span> (context.getPackageName().equals(app.activityInfo.packageName)) {
<span class="hljs-keyword">continue</span>;
}
Intent target = <span class="hljs-keyword">new</span> Intent(intent);
target.setPackage(app.activityInfo.packageName);
intents.add(target);
}
<span class="hljs-keyword">if</span> (intents.isEmpty()) {
<span class="hljs-comment">// 起動対象のアプリが見つからなかった</span>
} <span class="hljs-keyword">else</span> {
<span class="hljs-comment">// createChooserの第一引数のIntentに反応できるアプリが存在しない場合は EXTRA_INITIAL_INTENTS</span>
<span class="hljs-comment">// の指定が無視されるため, 必ず反応できるIntentを設定する目的でremove(0)を指定する.</span>
Intent chooser = Intent.createChooser(intents.remove(<span class="hljs-number">0</span>), title); <span class="hljs-comment">// *1</span>
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(<span class="hljs-keyword">new</span> Parcelable[<span class="hljs-number">0</span>])); <span class="hljs-comment">// *1</span>
context.startActivity(chooser);
}</code></pre>
<p>ポイントは *1 の部分で, 下記のコードではAPI Lv.23未満だとうまく動作しませんでした. </p>
<pre class="prettyprint"><code class="language-java hljs "> Intent chooser = Intent.createChooser(<span class="hljs-keyword">new</span> Intent(), title); <span class="hljs-comment">// *1</span>
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(<span class="hljs-keyword">new</span> Parcelable[<span class="hljs-number">0</span>])); <span class="hljs-comment">// *2</span></code></pre>
<p><code>EXTRA_INITIAL_INTENTS</code>に目的のIntentを設定すればうまくいきそうなものですが, API Lv.23未満だと *1 の第一引数<code>Intent</code>に反応できるActivityの数が0であった場合に <code>EXTRA_INITIAL_INTENTS</code> が無視される挙動になります(つまりActivityNotFound) <br>
API Lv.23以上では<code>EXTRA_INITIAL_INTENTS</code>が評価されます. </p>
<p>API Lv.23未満で<code>createChooser</code>の第一引数に渡すIntentは, 少なくとも1つ以上のActivityが反応できる必要があるので下記のようなコードになりました. </p>
<pre class="prettyprint"><code class="language-java hljs ">Intent chooser = Intent.createChooser(intents.remove(<span class="hljs-number">0</span>), title); <span class="hljs-comment">// *1</span>
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, intents.toArray(<span class="hljs-keyword">new</span> Parcelable[<span class="hljs-number">0</span>])); <span class="hljs-comment">// *2</span></code></pre>
<h3 id="matchall">MATCH_ALL</h3>
<p>*a で, <code>PackageManager.MATCH_DEFAULT_ONLY</code> はAPI Lv.23から挙動が変わっています. <br>
API Lv.23未満だと, <code>Category.DEFAULT</code>に反応するActivityを抽出するものでしたが, <br>
API Lv.23以上だと, 「既定で開く」設定されたActivityがある場合はそのActivityしか返却されなくなりました. API Lv.23以上でAPI Lv.23未満と同じ挙動にするためにはAPI LV.23から追加された<code>PackageManager.MATCH_ALL</code>を指定する必要があります. </p>
<pre class="prettyprint"><code class="language-java hljs "><span class="hljs-keyword">int</span> flag = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M ? PackageManager.MATCH_ALL
: PackageManager.MATCH_DEFAULT_ONLY;
List<ResolveInfo> launchers
= context.getPackageManager().queryIntentActivities(intent, flag); <span class="hljs-comment">// *a</span></code></pre>
<p>より便利にいくなら, API Lv.23以上でも<code>MATCH_DEFAULT_ONLY</code>で<code>ResolveInfo</code>を拾って, 「既定で開く」設定が自アプリになっていなければそのまま起動, 自アプリであれば上記の処理を実行するとすればいけそうです. </p>
<p>この処理でうまくいきましたが, デバイスによってはシェアダイアログのレイアウトが下記のように残念な結果に :(</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-2Nf_owiPcFqt216Ia0tN-aXVrjbTtBsJIIacNkjSBD1xUzvicR08iv2sYKfGAosO5rUmh4Van88MsSqvgW0d2E0SYQUvFlsR501Vkt5_zhp3Z_0H-5fL_h85K_yFtTKW-_-cMNp0eg2h/s1600/device-2017-07-20-182436.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEj-2Nf_owiPcFqt216Ia0tN-aXVrjbTtBsJIIacNkjSBD1xUzvicR08iv2sYKfGAosO5rUmh4Van88MsSqvgW0d2E0SYQUvFlsR501Vkt5_zhp3Z_0H-5fL_h85K_yFtTKW-_-cMNp0eg2h/s320/device-2017-07-20-182436.png" width="320" height="264" data-original-width="1065" data-original-height="878" /></a>
<p>動作をみる限りでは, <code>createChooser</code>に渡したIntentが1行目に並び, <code>EXTRA_INITIAL_INTENTS</code>に渡したIntentが2行目に並んでいる様子. <br>
これを解決するならシェアダイアログを自前で組む必要がありそうです. <br>
(あるいはAPI Lv.23では<code>createChooser</code>の第一引数にどのActivityにもマッチしない<code>new Intent()</code>といったIntentを指定するなど…) </p>
<p>API Lv.24から<a href="https://developer.android.com/reference/android/content/Intent.html#EXTRA_EXCLUDE_COMPONENTS"><code>EXTRA_EXCLUDE_COMPONENTS</code></a>なる定数も追加されているので, API Lv.24以上はこれを使えということかもしれませんが, こんなことにOSバージョン分岐させるのも面倒なので, 手っ取り早くやるなら<code>queryIntentActivities</code>からの自前ダイアログ作成が安定しているという結論に落ち着きました. </p>
<p>以上です. </p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0tag:blogger.com,1999:blog-6321273099105707483.post-25709629100972361342017-07-10T15:09:00.000+09:002017-07-10T15:09:01.418+09:00Replace Dialog to BottomSheet<div class="markdown">
<p>従来はコンテンツを他アプリへ共有する際などにダイアログUIが使われていましたが, <br>
昨今では, マテリアルデザインの<a href="https://material.io/guidelines/components/bottom-sheets.html#bottom-sheets-modal-bottom-sheets">Modal bottom sheets</a>で説明されているように, ボトムシートUIにするのが一般的です. </p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi85PMh7xdzjq9z_GnEgpnETDivyVMFZTCv8shgkhBRet1B0-9wbYIi9lq0j6cEtFdyJ7sxZfUUTPQynbKNRkL8t7uy3Yd7vLNzCiK1FFpVxmQq8r5KN4wLXnp2xjY9loZletifE9jFbi3w/s1600/components_bottomsheets_modal2.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEi85PMh7xdzjq9z_GnEgpnETDivyVMFZTCv8shgkhBRet1B0-9wbYIi9lq0j6cEtFdyJ7sxZfUUTPQynbKNRkL8t7uy3Yd7vLNzCiK1FFpVxmQq8r5KN4wLXnp2xjY9loZletifE9jFbi3w/s1600/components_bottomsheets_modal2.png" data-original-width="300" data-original-height="448" /></a>
<p>ボトムシートを実装するにはいくつか方法がありますが, 既存のダイアログをボトムシートに変更したいだけであれば, <code>AppCompatDialog</code>を継承した<a href="https://developer.android.com/reference/android/support/design/widget/BottomSheetDialogFragment.html">BottomSheetDialogFragment</a>/<a href="https://developer.android.com/reference/android/support/design/widget/BottomSheetDialog.html">BottomSheetDialog</a>を使うだけで比較的容易に対応できます. </p>
<pre class="prettyprint"><code class=" hljs java"><span class="hljs-comment">// 継承元をDialogFragmentからBottomSheetDialogFragmentに変更</span>
<span class="hljs-comment">// public class MyDialogFragment extends DialogFragment</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">MyDialogFragment</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BottomSheetDialogFragment</span> {</span>
...
<span class="hljs-annotation">@Override</span> <span class="hljs-keyword">public</span> Dialog <span class="hljs-title">onCreateDialog</span>(Bundle savedInstanceState) {
...
View view = binding.getRoot();
MyBottomSheetDialog bottomSheet = <span class="hljs-keyword">new</span> MyBottomSheetDialog(getContext());
bottomSheet.setContentView(view);
<span class="hljs-comment">// ボトムシートダイアログを返却する</span>
<span class="hljs-keyword">return</span> bottomSheet;
}
}</code></pre>
<p>ボトムシートの幅はスクリーンサイズに合わせて最大幅を調節することが推奨されています.</p>
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgEalscBdVCixAHT-PKnYUyXMUFeFkcA-Std7SyHNH_BbCwhs5tJHTZJAUMLdbJsxI4mfYj-MTW8BPjOhq2BVXiHXRLfWv0EiufUaxvjlx51DPcd2vSt0PbIrB3ZsmXNKYpLLP-tzrpGKvc/s1600/components_bottomsheets_modal13+%25281%2529.png" imageanchor="1" ><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgEalscBdVCixAHT-PKnYUyXMUFeFkcA-Std7SyHNH_BbCwhs5tJHTZJAUMLdbJsxI4mfYj-MTW8BPjOhq2BVXiHXRLfWv0EiufUaxvjlx51DPcd2vSt0PbIrB3ZsmXNKYpLLP-tzrpGKvc/s1600/components_bottomsheets_modal13+%25281%2529.png" data-original-width="600" data-original-height="450" /></a>
<table>
<thead>
<tr>
<th align="left">Screen width</th>
<th align="left">Minimum distance from screen edge (in increments)</th>
<th align="left">Minimum sheet width (in increments)</th>
</tr>
</thead>
<tbody><tr>
<td align="left">960dp</td>
<td align="left">1 increment</td>
<td align="left">6 increments</td>
</tr>
<tr>
<td align="left">1280dp</td>
<td align="left">2 increments</td>
<td align="left">8 increments</td>
</tr>
<tr>
<td align="left">1440dp</td>
<td align="left">3 increments</td>
<td align="left">9 increments</td>
</tr>
</tbody></table>
<p><code>BottomSheetDialog</code>の横幅を決めるためには, ダイアログの場合と同じくウィンドウの幅を調整する必要があります. <br>
ウィンドウの幅は<code>BottomSheetDialog</code>のコンストラクタで指定することができます. </p>
<pre class="prettyprint"><code class=" hljs java"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ShareBottomSheetDialog</span> <span class="hljs-keyword">extends</span> <span class="hljs-title">BottomSheetDialog</span> {</span>
<span class="hljs-annotation">@Override</span> <span class="hljs-keyword">protected</span> <span class="hljs-keyword">void</span> <span class="hljs-title">onCreate</span>(Bundle savedInstanceState) {
<span class="hljs-keyword">super</span>.onCreate(savedInstanceState);
<span class="hljs-comment">// 横画面などでボトムシートが間延びしないように最大幅を設ける</span>
Optional.ofNullable(getWindow())
.ifPresent(window -> window.setLayout(
Math.min(displayWidth, maxWidth),
ViewGroup.LayoutParams.MATCH_PARENT); </code></pre>
<p>また, ボトムシート自体をどこまで引き出した状態で表示するかを<code>peekHeight</code>を使って指定できます. <code>peekHeight</code>は<code>BottomSheetBehavior</code>で指定することができます. </p>
<pre class="prettyprint"><code class=" hljs avrasm">bottomSheet<span class="hljs-preprocessor">.setContentView</span>(view)<span class="hljs-comment">;</span>
// 横画面などでもシェアアイコンが表示されるようにダイアログの高さ(peek)を確保する
BottomSheetBehavior behavior
= BottomSheetBehavior<span class="hljs-preprocessor">.from</span>((View) view<span class="hljs-preprocessor">.getParent</span>())<span class="hljs-comment">;</span>
behavior<span class="hljs-preprocessor">.setPeekHeight</span>(height)<span class="hljs-comment">;</span></code></pre>
<p>以上です. </p>
</div>Yuki_312http://www.blogger.com/profile/14694950176753892797noreply@blogger.com0