- 投稿日:2020-09-10T18:52:23+09:00
1. Android Studioインストール(Kotlin)
はじめに
DreamHanksのMOONです。
今回からKotlin言語を使用してAndroidプログラミングについて見てみます。
本来AndroidはJava言語を使って開発されていましたけど、最近はKotlinを使って開発することが多くなっています。
今回はKotlinを使用してみます。開発ツールをインストール
Androidを開発するためのAndroid Studioをインストールしてみます。
https://developer.android.com/studio?hl=jp
この公式サイトのリンクでアンドロイドスタジオインストールをします。Android Studioプロジェクト生成
インストール後、最初にアンドロイドスタジオを実行すると以下の画面が表示されます。
一番上の「Start a new Android Studio project」を押してプロジェクトを作成します。
プロジェクトテンプレートの中からempty Activityを選択します。
「Name」にはご希望のプロジェクト名を作成。
「Package name」には自動的にプロジェクトの名の以前ルートが生成されますが、
修正したい場合には修正します。
「Save location」も自動的に生成されます。
「Language」には今回はKotlinを使用していきたいと思いますのでKotlinを選択します。
「Minimum SDK」は最小のバージョンを選択します。15バージョンを選択する場合、
以下のメッセージのようにほとんどのデバイスで動作可能です。
上の作業が終わった場合、finishボタンをクリックすると、プロジェクト生成が完了されます。プロジェクトの基本要素確認
MainActivity.ktpackage com.example.practiceapplication import androidx.appcompat.app.AppCompatActivity import android.os.Bundle class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }Android Activityは、画面に表示されるUIを構成するための一番基本となる要素です。
Androidアプリは画面にUIを表示するため、最小1つのActivityを持つ必要があり、
アプリ実行時に指定されたActivityを実行してユーザーにUIを表示させます。activity_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>「res → layout」のパースに画面に表示されるxmlファイルがあります。
layoutのxmlファイルもアプリに画面を表示されるための一番基本となる要素です。終わりに
ここまでがアンドロイドスタジオインストールとプロジェクト生成しました。
次回は画面にボタンとテキストを追加し、イベントを設定してみます。
- 投稿日:2020-09-10T18:52:23+09:00
1. 【Android/Kotlin】Android Studioインストール
はじめに
DreamHanksのMOONです。
今回からKotlin言語を使用してAndroidプログラミングについて見てみます。
本来AndroidはJava言語を使って開発されていましたけど、最近はKotlinを使って開発することが多くなっています。
今回はKotlinを使用してみます。開発ツールをインストール
Androidを開発するためのAndroid Studioをインストールしてみます。
https://developer.android.com/studio?hl=jp
この公式サイトのリンクでアンドロイドスタジオインストールをします。Android Studioプロジェクト生成
インストール後、最初にアンドロイドスタジオを実行すると以下の画面が表示されます。
一番上の「Start a new Android Studio project」を押してプロジェクトを作成します。
プロジェクトテンプレートの中からempty Activityを選択します。
「Name」にはご希望のプロジェクト名を作成。
「Package name」には自動的にプロジェクトの名の以前ルートが生成されますが、
修正したい場合には修正します。
「Save location」も自動的に生成されます。
「Language」には今回はKotlinを使用していきたいと思いますのでKotlinを選択します。
「Minimum SDK」は最小のバージョンを選択します。15バージョンを選択する場合、
以下のメッセージのようにほとんどのデバイスで動作可能です。
上の作業が終わった場合、finishボタンをクリックすると、プロジェクト生成が完了されます。プロジェクトの基本要素確認
MainActivity.ktpackage com.example.practiceapplication import androidx.appcompat.app.AppCompatActivity import android.os.Bundle class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } }Android Activityは、画面に表示されるUIを構成するための一番基本となる要素です。
Androidアプリは画面にUIを表示するため、最小1つのActivityを持つ必要があり、
アプリ実行時に指定されたActivityを実行してユーザーにUIを表示させます。activity_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello World!" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout>「res → layout」のパースに画面に表示されるxmlファイルがあります。
layoutのxmlファイルもアプリに画面を表示されるための一番基本となる要素です。終わりに
ここまでがアンドロイドスタジオインストールとプロジェクト生成しました。
次回は画面にボタンとテキストを追加し、イベントを設定してみます。
- 投稿日:2020-09-10T18:46:29+09:00
Jetpack Compose (アルファ版) ハンズオン
Jetpack Compose がアルファリリースされたので遅まきながら触ってみました。
色々と新しいワードやコーディングばかりで学習コストがかかりそうかも...?
でも Kotlin で書けるのは嬉しかったりします?ひとまず簡単に ViewModel なんかを使いながら、リストやツールバーの表示を実装してみます。
Environment
今回使用した環境は以下になります。
Tools version Android Studio 4.2 Canary 9 Kotlin 1.4.0 Jetpack Compose 1.0.0-alpha02 Get Started
まずはプロジェクトを作成します。
Canary 版にはEmpty Compose Activity
のテンプレートがあるので開いてみましょう。作成すると自動で以下の様な Activity が出来上がります。
これだけでも結構色々出てきて困惑しますね。MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { JetpackComposeCodelabTheme { // A surface container using the 'background' color from the theme Surface(color = MaterialTheme.colors.background) { Greeting("Android") } } } } } @Composable fun Greeting(name: String) { Text(text = "Hello $name!") } @Preview(showBackground = true) @Composable fun DefaultPreview() { JetpackComposeCodelabTheme { Greeting("Android") } }setContent
Activity と ViewGroup の拡張関数として定義されている、レイアウトを定義していくスコープ
Wrapper.ktfun ComponentActivity.setContent( // Note: Recomposer.current() is the default here since all Activity view trees are hosted // on the main thread. recomposer: Recomposer = Recomposer.current(), content: @Composable () -> Unit ): Composition { FrameManager.ensureStarted() val composeView: AndroidOwner = window.decorView .findViewById<ViewGroup>(android.R.id.content) .getChildAt(0) as? AndroidOwner ?: AndroidOwner(this, this, this).also { setContentView(it.view, DefaultLayoutParams) } return doSetContent(composeView, recomposer, null, content) }content ブロック内に UI を記述する事で、ルートの View (AndroidOwner) にレイアウトしていきます。
@Composable
で定義された関数を呼び出す事で、レイアウトの定義を関数に切り出す事も可能です。
Composition
はsetContent
とdispose
を持つインタフェースで、setContent 内で宣言した UI をレイアウトする役割があります。
内部の処理は CompositionImpl がやってくれるので戻り値の Composition に対して何かする必要はありません。
dispose で空のレイアウトを上書きできる様ですが使いみちが思いつきません?Theme
JetpackComposeCodelabTheme
は端末のライトモード/ダークモードの設定を元に MaterialTheme のスタイルをセットアップしてくれます。
他にフォントやカラーの定義もしてくれています。こういったテーマを定義しておくと View のスタイルに適応できる様です。
MaterialTheme を指定しておくと Text 等でマテリアルデザインのスタイルを指定できる様になります。Theme.kt@Composable fun JetpackComposeCodelabTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable() () -> Unit) { val colors = if (darkTheme) { DarkColorPalette } else { LightColorPalette } MaterialTheme( colors = colors, typography = typography, shapes = shapes, content = content ) }Type.ktval typography = Typography( body1 = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp ) /* Other default text styles to override button = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.W500, fontSize = 14.sp ), caption = TextStyle( fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 12.sp ) */ )Shape.ktval shapes = Shapes( small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(4.dp), large = RoundedCornerShape(0.dp) )これからは colors や dimens なんかのリソースに定義していたものもコード管理していく事になるんでしょうか??
Surface
Jetpack Compose Surface
でググると Microsoft の Surface Duo が出てきます。
(ググラビリティ。。。)@Composable fun Surface( modifier: Modifier = Modifier, shape: Shape = RectangleShape, color: Color = MaterialTheme.colors.surface, contentColor: Color = contentColorFor(color), border: BorderStroke? = null, elevation: Dp = 0.dp, content: @Composable () -> Unit ) { val elevationPx = with(DensityAmbient.current) { elevation.toPx() } SurfaceLayout( modifier.drawLayer(shadowElevation = elevationPx, shape = shape) .zIndex(elevation.value) .then(if (border != null) Modifier.border(border, shape) else Modifier) .background( color = getBackgroundColorForElevation(color, elevation), shape = shape ) .clip(shape) ) { Providers(ContentColorAmbient provides contentColor, children = content) } }マテリアルデザインなレイアウトを作るための Layout コンポーネントです。
ボーダーやエレベーションを指定できますね。SurfaceLayout は左上にコンポーネントを配置する Layout の様です。
(FrameLayout かな??)Text
Text を表示する UI コンポーネントです。 TextView ですね。
引数に color や style を指定する事で今までの xml 上の textColor 等が設定できます。Modifier
サンプルには出て来ていませんでしたが、
Modifier
について触れておきます。
Modifier
は UI コンポーネントに対して Padding を設定したり、ドロー系の色々ができたり、クリックのイベント処理を指定したりできるインタフェースです。
今まで xml で指定していたレイアウト系の設定 (width, gravity, ... ) は Modifier で指定する事になりそうですね。MainActivity.ktText( text = "Hello World!", modifier = Modifier .fillMaxWidth() .fillMaxHeight() .padding(10.dp) .clickable { } )少し話がずれますが、すごいと思ったのが以下の記事の内容です。
Jetpack ComposeのConstraintLayoutなどがどのようにModifierクラスだけで様々なレイアウトを処理するか - Qiita
例えば ConstraintLayout での constraint も Modifier で指定しますが、普通の Layout スコープでは見えません。(使えたら困りますもんね。。。)
ちゃんと配慮されていてかっこいいなと思いました?
@ Preview
Android Studio で宣言した UI をプレビューするための関数を定義できる。
@Composable
で定義した物とは別にプレビュー用の関数を作成する必要があります。List 表示と State による画面更新
ここから、リスト表示とアイテムがクリックされた時に中のテキストを変更するまでを実装していきます。
まずはリストの表示から。LazyFor
Jetpack Compose でリスト表示する時は LazyFor 関数を使います。
関数 向き LazyColumnFor 縦 LazyRowFor 横 このような縦方向のリストを表示する場合は
MainActivity.ktLazyColumnFor(items = (0 until 100).map { it.toString() }, contentPadding = InnerPadding(8.dp)) { Text(text = it) }の様に記述できます。
↓の様にもできますが、LazyFor を使った方が
ネストが少なかったり、コンテンツ同士の Padding が指定できたりしていいですね。Column は UI コンポーネントを上から順に縦に並べていきます。
(LinearLayout: orientation = "vertical" みたいな感じでしょうか。)MainActivity.ktColumn { (0 until 100).forEach { Text(it.toString()) } }アイテム間のスペースを空ける場合は、Text に Modifier を設定しましょう。
State で画面更新
表示されたリストのアイテムがタップされた時に中のテキストを変更してみます。
視覚的に分かりやすい様に少しレイアウトを変更します。MainActivity.ktval items = (0 until 100).map { it.toString }.toMutableList() LazyColumnForIndexed(items = items) { index, item -> Card(modifier = Modifier.fillParentMaxWidth().padding(6.dp).clickable { items[index] = "Clicked!" }) { Text(text = item, textAlign = TextAlign.Start, modifier = Modifier.fillMaxWidth().padding(30.dp, 16.dp, 16.dp, 16.dp)) } }
Modifier.clickable
がクリックイベントの処理になります。
ただし、このままでは思った様に画面が更新されません。A stateless composable is a composable that cannot directly change any state.
Jetpack Compose での UI は基本的にはステートレスです。
そのためプロパティが変更されても再描画されず、スクロールして描画され直すまで更新されません。クリックのタイミングで更新するにはプロパティがステートを持つ必要があります。
ステートを持たせるには、今回はリストなのでtoMutableStateList()
が使えます。MainActivity.ktval items = (0 until 100).map { it.toString }.toMutableStateList() // <- LazyColumnForIndexed(items = items) { index, item -> Card(modifier = Modifier.fillParentMaxWidth().padding(6.dp).clickable { items[index] = "Clicked!" }) { Text(text = item, textAlign = TextAlign.Start, modifier = Modifier.fillMaxWidth().padding(30.dp, 16.dp, 16.dp, 16.dp)) } }こうすることでステートを持ったリストができるので
と、クリックと同時に更新ができました。
カウントアップのサンプル等で見かけた
state {}
は alpha 版では deprecated なのでremember { mutableStateOf() }
を使いましょうとの事です。ViewModel + LiveData で画面更新
今までの様に ViewModel + LiveData でデータソースの管理と通知による画面更新をしていきます。
LiveData を使う場合には拡張関数の
observeAsState()
が value にステートを持たせてくれるので、
使うために livedata 用のサブライブラリをインポートします。build.gradleimplementation "androidx.compose.runtime:runtime-livedata:1.0.0-alpha02"後はいつもの様に ViewModel を作り LiveData を持たせます。
MainViewModel.ktclass MainViewModel: ViewModel() { private val _items: MutableLiveData<MutableList<String>> = MutableLiveData() val items: LiveData<List<String>> = _items.map { it.toList() } init { _items.postValue((0 until 100).map { it.toString() }.toMutableList()) } fun click(index: Int) { _items.value?.let { it[index] = "Clicked!" _items.postValue(it) } } }いつもならこの items を observe して流れてきたデータに対して色々していましたが、
Composable 関数で使う時は observeAsState でステートを持った value を扱います。MainActivity.ktclass MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { JetpackComposeCodelabTheme { Surface(color = MaterialTheme.colors.background) { // observeAsState でステートを持たせた value を渡す Greeting(viewModel.items.observeAsState(listOf()).value) } } } } @Composable private fun Greeting(items: List<String>) { LazyColumnForIndexed(items, contentPadding = InnerPadding(8.dp)) { index, item -> val cardModifier = Modifier.fillParentMaxWidth().padding(6.dp).clickable { viewModel.click(index) } Card(modifier = cardModifier) { Text(text = item, textAlign = TextAlign.Start, modifier = Modifier.fillMaxWidth().padding(30.dp, 16.dp, 16.dp, 16.dp)) } } } }こうする事で今までの ViewModel から LiveData のデータの流れが出来ました。
ちょっと疑問なんですけど、Composable 関数はトップレベルに書くのがベターなんでしょうか?
Screen 用のファイルを作成してそのトップレベルに書いてあるサンプルが多いので... (JetNews とか)そうすると View -> ViewModel にイベントを上げる時に、どうするのがいいんでしょうね?
- Composable 関数に ViewModel を渡す?
- onClick みたいな関数リテラルを渡す?
今回は Activity 内に Composable 関数を書いてみましたが再利用性がないなぁ、と。
これはこれで一個の画面構成としては正しい気も。
Codelab では ViewModel を渡してたな...色々試してみて最適解を見つけていきたいですね。
こうやってみましたみたいな意見があれば教えていただけると嬉しいです。ツールバー (AppBar)
画面トップのツールバーを表示してメニューボタンを実装してみます。
ツールバーを表示する場合は
TopAppBar
を使います。
メニューボタンには Android Studio から VectorImage を drawable に吐き出してアイコンにします。MainActivity.ktclass MainActivity : AppCompatActivity() { private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { JetpackComposeCodelabTheme { Scaffold(topBar = { AppBar(title = "Codelab") }) { Greeting(viewModel.items.observeAsState(listOf()).value) } } } } @Composable private fun AppBar(title: String) { TopAppBar( title = { Text(text = title) }, actions = { IconButton(onClick = { viewMode.refresh() }) { Image(vectorResource(id = R.drawable.ic_refresh)) } } ) } }TopAppBar
ツールバーを表示するのに使うコンポーネントです。
AppBar.kt@Composable fun TopAppBar( title: @Composable () -> Unit, modifier: Modifier = Modifier, navigationIcon: @Composable (() -> Unit)? = null, actions: @Composable RowScope.() -> Unit = {}, backgroundColor: Color = MaterialTheme.colors.primarySurface, contentColor: Color = contentColorFor(backgroundColor), elevation: Dp = TopAppBarElevation ) { AppBar(backgroundColor, contentColor, elevation, RectangleShape, modifier) { val emphasisLevels = EmphasisAmbient.current if (navigationIcon == null) { Spacer(TitleInsetWithoutIcon) } else { Row(TitleIconModifier, verticalGravity = ContentGravity.CenterVertically) { ProvideEmphasis(emphasisLevels.high, navigationIcon) } } Row( Modifier.fillMaxHeight().weight(1f), verticalGravity = ContentGravity.CenterVertically ) { ProvideTextStyle(value = MaterialTheme.typography.h6) { ProvideEmphasis(emphasisLevels.high, title) } } ProvideEmphasis(emphasisLevels.medium) { Row( Modifier.fillMaxHeight(), horizontalArrangement = Arrangement.End, verticalGravity = ContentGravity.CenterVertically, children = actions ) } } }title にも Composable を渡すので柔軟にレイアウトを作れそうです。
actions に渡した Composable が右側のメニューのレイアウトになります。
今回はアイコンを表示するので IconButton を使っています。プレビュー版から AppBarIcon は削除され IconButton を使うように変わっています。
JetNews では navigationIcon に Drawer を実装していました。
TopAppBar の他に BottomAppBar もあり、BottomNavigation みたいな事もできそうです?
Image
Image == ImageView ですね。
TopAppBar の actions には Composable を渡す事になるので、アイコンを表示するだけなら Image を渡せば問題ないです。
ただし drawable から読み込む際は image か vector かを意識してロードしないと、間違えると NPE になります?
Scaffold
scaffold == 足場
AppBar を使う場合はそのまま TopAppBar を追加すると、バーの下にコンテンツが潜り込んでしまいます。
Scaffold で UI を定義しておくと引数に AppBar を渡せて、マテリアルデザインないい感じのレイアウトにしてくれます。Scaffold のトップのレイアウトは Surface でレイアウトされていました。
Conclusion
以上、今回試してみたハンズオンのリポジトリはこちらになります?
ダイアログ表示したりとかもしてみました。
個人的にはダイアログ表示のハンドリングの仕方がもう少しどうにかならないか悩んでます?GitHub - tick-taku/Jetpack_Compose_Codelab: Sample for Jetpack Compose.
間違いやご指摘等ありましたらコメントいただけると幸いです。
alpha 版なのでばんばん変更されるかもしれませんし、まだ参考が少ないですね。?
でも xml と行き来したりがなくなって管理しやすかったり、新しい書き方ができて楽しいです?関数リテラルをぽんぽん渡す事になるので、どうしたら可読性が上がるか考えないと。。。
一番の問題はまだ Navigation Component に対応してないみたいなので、Fragment をどう実装するかですね。
今のところ xml にも良さがあって勝手も分かっているので比較はあまり出来ませんでしたが、今後たくさん触って慣れていきたいと思います。
Gradle も含めほぼ全て Kotlin で Android アプリが書けるのはとても嬉しいです!参考
Layouts in Jetpack Compose | Android デベロッパー | Android Developers
[Published] Jetpack Compose basics
Using State in Jetpack Compose | Android デベロッパー | Android Developers
compose-samples/JetNews at master · android/compose-samples · GitHub
android - How to create recycler view in Compose Jetpack? - Stack Overflow
- 投稿日:2020-09-10T18:30:41+09:00
セキュリティの尻隠して頭隠さず...?Androidに実装されている「プライベートDNS」を利用しよう
本日のお題。
セキュリティの穴、DNSを何とかしましょう!自宅用DNSサーバーを構築してからDNSがマイブームな今日この頃、プライベートDNSについての記事に出会いました。
以前から有効にはしていましたが、どんな動作をしているのか気になって調査してみました。プライベートDNSとは?
Android 9 (Pie)
以降のスマートフォンに実装されている機能で、以下のような感じでアクセスできると思います。
設定
→その他の接続
→プライベートDNS
設定
→ネットワークとインターネット
→プライベートDNS
設定
→ネットワークとインターネット
→詳細設定
→プライベートDNS
取り敢えずは
プライベートDNSを設定
を選択してdns.google
やone.one.one.one
などと打ち込むと設定が完了します。
ほぼ同様の内容ですが、Google Public DNSに詳しい説明が記載されています。
なお、自動
に設定した場合は、設定中のDNSサーバーが本機能に対応している場合に有効となります。どういう機能?
Android Developers Blogによりますと...
Like HTTPS, DNS over TLS uses the TLS protocol to establish a secure channel to the server. Once the secure channel is established, DNS queries and responses can't be read or modified by anyone else who might be monitoring the connection. (The secure channel only applies to DNS, so it can't protect users from other kinds of security and privacy violations.)
DNSの問い合わせを安全にしようってことらしいです。
「DNSってなーに?」って人は、上記記事を参照するか、ググってください。
DNSが策定された1980年当時、インターネットは今より小規模で単純だったのでDNSに機密性は求められていなかったのですが、近年の著しい発達に伴いセキュアなプロトコルが必要になったため偉い人がRFC 7858で策定したのが
DNS over TLS
(以下DoT
)で、そのDoT
を適用してDNSの問い合わせを行うのがプライベートDNS
の機能です。DNS over TLS
DoT
の仕組みはHTTPS
と似たような感じとなっており、DNSサーバーとクライアント間で確立したセキュアなチャンネルを用いてDNSの通信を行い、第三者が閲覧・改竄できないようにしています。あたりまえですが、
DoT
で保護されるのはDNSの通信のみで他の通信は暗号化されませんのでHTTP
なんかで接続すれば内容は丸見えになってしまいます。
加えて、DoT
が保証するのはクライアント⇔DNSサーバー間の信頼性と機密性のみです。
こちらが詳しいですが、接続先のDNSサーバー自体が不正な場合などの安全性や、ホップ先の機密性は担保されていません。
DoT
はHTTPS
と同様にデジタル証明書で管理されますが、デジタル証明書は偽でないことを証明するだけで接続先が安全であるか否かは関知しません。
いたちごっこなのはセキュリティの性ですが、悪意のある第三者だけでなくフィルタリングなど監視活動の大半もDNSクエリを傍受して行われますし、適用しておくべきプロトコルだと思います。もっと知りたい方はJPNICさんで公開されている資料「DoH/DoT入門」が分かり易いのでおすすめです。
実際の通信を見てみよう
DNSがどのようにやり取りされているのかWiresharkというパケット監視ソフトで調査してみました。
Android
のパケット監視方法は色々あるようですが、今回は手軽に以下のような構成で通信を監視しました。
こうやってパケットを監視していると、公衆Wi-Fiなどが如何に危険かが身に沁みます。
一般人でも超手軽にパケットをのぞき見できるわけですから、平文通信なんて以ての外ですね。
未だにHTTP
しか対応していない企業HPなども見かけますが、早急に対応してほしいものです。
無知なユーザー(決して貶める意ではありません)を脅威から保護するために最善の努力を行うのは、私たちホスト側の責務だと思います。今回は調査目的で自分の通信を覗いていますが、くれぐれも他人の通信を覗いたり悪用することのないようにお願いします。
プライベートDNSが
無効
のときPCなどのDNSクエリストリームも同様の動作となっていると思います。
スマホ(192.168.137.149
)からGoogle Public DNS(8.8.8.8
)にquery
が投げられ、Google Public DNSからスマホにquery response
としてIPアドレス(黒塗り部分)が返ってきています。
問い合わせ(クエリ)と応答(レスポンス)で一セットになる標準的なDNSのやり取りで、DoT
でも秘匿されこそすれ同様の通信が行われます。
Google Public DNS
をPCのDNSサーバーに設定しているので8.8.8.8
に問い合わせていますが、環境によってはISPのDNSサーバー等に投げていると思います。
- クエリとレスポンスが平文でやり取りされており、第三者から丸見え
- 一回の問い合わせで発生する通信は上りと下りの一往復
DNSはスピード重視ですのでプロトコルには
UDP
が用いられています。
ステートレスなプロトコルですのでハンドシェイクなどは行われず、シンプルな通信が可能です。
仮にどこかでパケットが消えても再問合せすれば良いだけですので合理的だと思いますが、パケットが丸見えですので安全性もへったくれもありません。
特に無線通信ではプライバシーが侵害されたり、改竄されたりする可能性が高まります。
ちなみに、これらのDNSに関する通信の標準ポートは53
です。
dns.google
を設定したとき
Google Public DNS
でAndroid 9
以降向けに紹介されている方法です。
TSL
という暗号化方式を利用するにあたって、まずハンドシェイクという手順が行われます。ハンドシェイク
パケットに付したコメントはさほど正確でないのであくまでイメージだと思ってください。
まず、プライベートDNS
に入力できるのはホスト名のみなので、dns.google
を解決する必要があります。
もともとGoogle Public DNS (8.8.8.8)
がDNSサーバーなので分かりにくいですが、グレー部分より上の8.8.8.8
はネットワークに設定されていたDNSサーバーで、下はスマホに設定されたdns.google
を解決した結果のDNSサーバーです。
この部分はセキュアではない、伝統的なDNSのやり取りです。
dns.google
が解決されたらTLS
のハンドシェイクが始まります。
詳しい説明は省きますが、DNSサーバーとクライアントの間で公開鍵や乱数をやり取りして共通鍵を作成し、以後はその共通鍵を用いて暗号化を行い通信します。
TCP
やTLS
にそれほど詳しくないのですが、TCP
は3way ハンドシェイク
といって3回パケットをやり取りしなければ始まりません。
また、TLS
は基本的にClient Hello
というパケットが始まりの合図で、それはTCP
の接続が確立した後に行われるはずです。
ここでは3way ハンドシェイク
とClient Hello
が同時に行われているように見えますが実際どうなっているのでしょうか。
詳しい方がいましたらご教授下さい。DNSクエリストリーム
ハンドシェイクの後は以下のような通信が行われます。
通信内容は暗号化されているので付したコメントは想定ですが、仕組み上これで正しいと思われます。
通信の内容がTLSv1.2
で暗号化されているため、内容はApplication Data
と表示されています。
これで第三者はこれらの通信を傍受しても内容を確認することはできず、それぞれのパケットはハンドシェイク時の相手から送られてきていることが証明されていますので、一定の信頼性と機密性が保証されています。問い合わせ(クエリ)と応答(レスポンス)がペアで行われている基本的な点は伝統的なDNSと変わりませんが、使用しているプロトコルが投げ捨ての
UDP
から、確認付きのTCP
に代わったためACKパケット
が増えています。
このACKパケット
は必ず送られるものでもなく、効率化のためにバッファ分をまとめて送信することになっているので若干不規則な並びになっています。
TCP Keep-Alive
というパケットが目を引きますが、これはTCP
は一度接続が切れると再びハンドシェイクからやり直さなくてはならないため、一定期間通信が行われなかった時に切断されないよう送られるパケットです。
- 通信開始時にハンドシェイクという手続きが必要
- クエリとレスポンスが暗号化されており、第三者が確認できない
- 通信は、一回の問い合わせで発生する上りと下り +
TCP
のパケット伝統的なDNSに比べて複雑になっているものの、
TCP
やTLS
といった下層プロトコルに伝統的なDNSを載せているだけなので理解はし易いです。
一度ハンドシェイクが済んでしまえば、レイテンシーもそこまで増大せず安全に利用できるよくできた実装だと思います。弱点
すこし話はずれますが
DoT
の弱点にも触れておきたいと思います。まず、通信環境の悪いところなどでは
TCP
のコネクションが頻繁に切断され、ハンドシェイクが大量に行われ、それもなかなか上手くいかず…という感じでIPアドレスが取得できず接続エラーになってしまうことがあります。
自分も登山中に経験したことがあり、プライベートDNS
を無効
にすることで普通に接続できるようになりました。また、個々の負荷増大は軽微でもDNSサーバーにとっては大きな負荷になるのかと思います。
基本的にACK
でパケットが1.5倍ですからネットワークトラフィックも増大しますし、暗号化と復号化の処理も負荷になりそうです。
それでも(無料で)安定したPublic DNS
を提供して下さっているのですから、関係各社様には頭が上がりません。
DoT
で保護されるのはDNSサーバーとクライアント間の通信のみであることは前述のとおりですが、ハンドシェイク前のホスト名解決が伝統的なDNSである点が気になりました。
dns.google
に対して偽のIPアドレスを解決すれば以後の通信が不正になるのではと思いましたが問題ないのでしょうか?
とはいえ、なりすましやスコープ外の危険性はDoT
に固有のものではありませんので、導入を否定する要因にはなりません。自動を設定したとき
Google Public DNSによると、
自動
に設定した場合は、まず853
ポートへの接続を試行し、成功したらDoT
で、失敗したら53
ポートへ伝統的なDNSで問い合わせを行うようです。当方の環境では
dns.google
を明示した時と同じでした。
DNSサーバーが既定のままだったり、コネクションがうまくいかなかったりすると伝統的なDNSで問い合わせが行われると思われます。一度、
自動
に切り替えた後TCP
の接続に失敗して伝統的なDNSでやり取りしていた時がありました。
手動で再接続したらDoT
に切り替わりましたが、どうやらDoT
に執着するよりも接続性を優先する実装のようです。
one.one.one.one
を明示した時自称最速DNSサーバー、Cloudflareの
1.1.1.1
です。
前述の通り設定できるのはホスト名のみなのでone.one.one.one
と設定します。こちらも
DoT
で秘匿された通信ができています。当方ではベンチマークの結果が良かった
Google Public DNS
を採用しています。
他にも速いところはあるのですが安全性などの面も含めての決定です。
OpenDNS
を明示した場合国内にサーバーがあり優秀な部類に入ると思われる
OpenDNS
ですが、DoT
には対応してなさそうです。
ホスト名をnslookup
で検索してプライベートDNS
に明示してみましたが、853
ポートへアクセスできませんでした。
パケットを監視しているとエラーのパケットが返ってきているのが見えますが、スマホからはIPアドレスが取得できない状態が見えるだけです。
対応していない(または存在していない)ホスト名を打ち込むと通信ができなくなるのでよく確認する必要がありますね。おまけ(Windows)
Windowsでは
DNS over HTTPS
(以下DoH
)という技術でDNS通信を暗号化しようとしています。
これはHTTPS
の443
ポートを用いてDNSを行うもので、UDPパケット
とTCPパケット
の両方が用いられます。
2020年9月10日時点ではOSレベルの標準実装は行われていませんが、ChromeなどのブラウザはDoH
でDNSの問い合わせを行っています。
このように伝統的なDNSの問い合わせとDoH
が混在していて、ブラウザから投げられるDoH
はUDPプロトコル
を利用しています。
DoH
でもホスト名解決から始まるのは変わらないんですね。ちなみに
Windows insider Program (Dev Channel)
に登録していてBuild 19628
以降がインストールされていれば、レジストリを弄ることでOSレベルでもDoH
を適用することができます。
基本的に全ての通信がDoH
で行われているのが確認できます。
nslookup
はDNSサーバーを明示している扱いなのでDoH
の適用から外れているのだと思われます。
ブラウザレベルでの実装と違ってTCP
とTLSv1.2
が用いられているのが面白いですね。
Dev Channel
はだいぶ不安定になりますが気になる方は是非自己責任でどうぞ。
設定方法は公式ブログを参照してください。所感
悪意ある第三者は虎視眈々と我々の通信を狙っています。
コロナ禍で社内だけだったビジネス関係の通信環境も多種多様になったことと思いますが、セキュリティ対策まで手が回りにくいのも実情かと思います。
Wi-Fiやファイアウォール、VPNなど、セキュリティは多角的にアプローチする必要がありますが、全ての通信の起点といっても過言ではないDNSを意識することはあまりありませんでした。
手軽に、どこでも使えるスマートフォンだからこそ、少しでも安全な通信ができるよう適用しておきたい「プライベートDNS」、皆様もこの機会に是非設定してみてはいかがでしょうか。
- 投稿日:2020-09-10T17:59:29+09:00
【React Native】TextInputにselectionColorを設定するとAndroidで文字が見えなくなる
- 投稿日:2020-09-10T14:39:39+09:00
Android, Kotolin: CSVの読み書き
- Minimum SDK: API 23
- Android Studio 4.0.1
- Kotlin 1.4.0
- opencsv Version: 5.2
Android, KotlinでCSVの読み書きを、opencsvを使って実装します。
CSVの操作を説明するために、StringReader, StringWriterを使って実装していますので、必要に応じてFileReader, FileWriter, InputStreamReader, OutputStreamWriterに置き換えて実装してください。SDカードへのファイル読み書きはこちらを参照ください。→ Android: SDカード上のファイルへの読み書き
準備
opencsvライブラリ取り込み
build.gradle(app)dependencies { implementation 'com.opencsv:opencsv:5.2' }配列を使った方法
書き込み
実装val strWriter = StringWriter() val csvWriter = CSVWriter(strWriter) csvWriter.writeNext(arrayOf("三幸太郎", "45", "男")) csvWriter.writeNext(arrayOf("三幸次郎", "44", "男")) csvWriter.writeNext(arrayOf("三幸花子", "35", "女")) println(strWriter.toString())出力"三幸太郎","45","男" "三幸次郎","44","男" "三幸花子","35","女"読み込み
読み込み元は、先ほど作成したCSVテキスト(strWriter)
実装val strReader = StringReader(strWriter.toString()) val csvIter = CSVIterator(CSVReader(strReader)) for (row in csvIter) { for (col in row) { print(col) } }オブジェクト(Bean)を使った方法
データ定義
初期値はいるみたいです。
CSVヘッダー無しdata class User( @CsvBindByPosition(position = 0) val name: String = "", @CsvBindByPosition(position = 1) val age: Int = 0, @CsvBindByPosition(position = 2) val gender: String = "" )CSVヘッダーありdata class User( @CsvBindByName(column = "名前", required = true) val name: String = "", @CsvBindByName(column = "年齢", required = true) val age: Int = 0, @CsvBindByName(column = "性別", required = true) val gender: String = "" )書き込み
実装val strWriter = StringWriter() val beanWriter = StatefulBeanToCsvBuilder<User>(strWriter).build() beanWriter.write(User("三幸太郎", 45, "男")) beanWriter.write(User("三幸次郎", 44, "男")) beanWriter.write(User("三幸花子", 35, "女")) println(strWriter.toString())出力(ヘッダー無し)"三幸太郎","45","男" "三幸次郎","44","男" "三幸花子","35","女"出力(ヘッダーあり)"名前","年齢","性別" "三幸太郎","45","男" "三幸次郎","44","男" "三幸花子","35","女"読み込み
読み込み元は、先ほど作成したCSVテキスト(strWriter)
実装val strReader = StringReader(strWriter.toString()) val beanList = CsvToBeanBuilder<User>(strReader) .withType(User::class.java) .withIgnoreLeadingWhiteSpace(true) .build() .parse() for (user in beanList) { println(String.format("%s, %d, %s", user.name, user.age, user.gender)) }
- 投稿日:2020-09-10T14:39:39+09:00
Android, Kotlin: CSVの読み書き
- Minimum SDK: API 23
- Android Studio 4.0.1
- Kotlin 1.4.0
- opencsv Version: 5.2
Android, KotlinでCSVの読み書きを、opencsvを使って実装します。
CSVの操作を説明するために、StringReader, StringWriterを使って実装していますので、必要に応じてFileReader, FileWriter, InputStreamReader, OutputStreamWriterに置き換えて実装してください。SDカードへのファイル読み書きはこちらを参照ください。→ Android: SDカード上のファイルへの読み書き
準備
opencsvライブラリ取り込み
build.gradle(app)dependencies { implementation 'com.opencsv:opencsv:5.2' }配列を使った方法
書き込み
実装val strWriter = StringWriter() val csvWriter = CSVWriter(strWriter) csvWriter.writeNext(arrayOf("三幸太郎", "45", "男")) csvWriter.writeNext(arrayOf("三幸次郎", "44", "男")) csvWriter.writeNext(arrayOf("三幸花子", "35", "女")) println(strWriter.toString())出力"三幸太郎","45","男" "三幸次郎","44","男" "三幸花子","35","女"読み込み
読み込み元は、先ほど作成したCSVテキスト(strWriter)
実装val strReader = StringReader(strWriter.toString()) val csvIter = CSVIterator(CSVReader(strReader)) for (row in csvIter) { for (col in row) { print(col) } }オブジェクト(Bean)を使った方法
データ定義
初期値はいるみたいです。
CSVヘッダー無しdata class User( @CsvBindByPosition(position = 0) val name: String = "", @CsvBindByPosition(position = 1) val age: Int = 0, @CsvBindByPosition(position = 2) val gender: String = "" )CSVヘッダーありdata class User( @CsvBindByName(column = "名前", required = true) val name: String = "", @CsvBindByName(column = "年齢", required = true) val age: Int = 0, @CsvBindByName(column = "性別", required = true) val gender: String = "" )書き込み
実装val strWriter = StringWriter() val beanWriter = StatefulBeanToCsvBuilder<User>(strWriter).build() beanWriter.write(User("三幸太郎", 45, "男")) beanWriter.write(User("三幸次郎", 44, "男")) beanWriter.write(User("三幸花子", 35, "女")) println(strWriter.toString())出力(ヘッダー無し)"三幸太郎","45","男" "三幸次郎","44","男" "三幸花子","35","女"出力(ヘッダーあり)"名前","年齢","性別" "三幸太郎","45","男" "三幸次郎","44","男" "三幸花子","35","女"読み込み
読み込み元は、先ほど作成したCSVテキスト(strWriter)
実装val strReader = StringReader(strWriter.toString()) val beanList = CsvToBeanBuilder<User>(strReader) .withType(User::class.java) .withIgnoreLeadingWhiteSpace(true) .build() .parse() for (user in beanList) { println(String.format("%s, %d, %s", user.name, user.age, user.gender)) }
- 投稿日:2020-09-10T11:56:16+09:00
【Android/Kotlin】複数のWidgetが同期して同じ動作をしてしまう場合の解決方法
はじめに
この記事は【Android/Kotlin】Widgetを作ってクリック時の動作を設定する
の続きとなっていますすが,ウィジェットの基本が分かっている方はこの記事だけ見れば大丈夫です。また,執筆者は趣味レベルのAndroidアプリ開発者である為,説明間違いが含まれている可能性があります。お気づきの点がありましたら,是非コメントお願いいたします。また,アップデートにより,機能やコードが記事執筆時点と異なる場合があります
本記事の内容
本記事では,【Android/Kotlin】Widgetを作ってクリック時の動作を設定する
で作成した,ボタンを押すとカウンターが1ずつ増えていくウィジェットについて,複数が同じものとして動作してしまう問題を解消します。
修正毎のソースコード
特に複雑なことはしていませんが,ソースに関する詳しい説明は【Android/Kotlin】Widgetを作ってクリック時の動作を設定するを御覧ください。
ウィジェットのレイアウトファイル
sample_app_widget.xml<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" android:gravity="center" android:orientation="vertical" android:padding="@dimen/widget_margin"> <TextView android:id="@+id/counter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="0" android:textAppearance="@style/TextAppearance.AppCompat.Display1" /> <Button android:id="@+id/add_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="add" /> </LinearLayout>ウィジェットの動作を実装したファイル
SampleAppWidget.kt//com.example.android.stackwidgetの部分は自分のアプリのIDに修正する private const val COUNT_UP = "com.example.android.buttonwidgettest.COUNT_UP" class SampleAppWidget : AppWidgetProvider() { override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { // There may be multiple widgets active, so update all of them for (appWidgetId in appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId) } } override fun onReceive(context: Context?, intent: Intent?) { super.onReceive(context, intent) if (context == null || intent == null) return when (intent.action) { COUNT_UP -> { // カウント値を読み込み val dataStore = context.getSharedPreferences("widget", Context.MODE_PRIVATE) var clickCount = dataStore.getInt("clickCount", -1) clickCount++ // カウントアップ後のカウント値を書き込み dataStore.edit().putInt("clickCount", clickCount).commit() //TextViewにカウント値を適用 val views = RemoteViews(context.packageName, R.layout.sample_app_widget) views.setTextViewText(R.id.counter, clickCount.toString()) // ウィジェットを更新 val myWidget = ComponentName(context, SampleAppWidget::class.java) val manager = AppWidgetManager.getInstance(context) manager.updateAppWidget(myWidget, views) } } } } //onUpdateのappWidgetId毎の処理はこちらで実装する internal fun updateAppWidget( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int ) { // SharedPreferencesからデータを読み出し(登録されていない場合は0) val dataStore = context.getSharedPreferences("widget", Context.MODE_PRIVATE) var clickCount = dataStore.getInt("clickCount", -1) if (clickCount == -1) { dataStore.edit().putInt("clickCount", 0).apply() clickCount = 0 } // RemoteViews オブジェクトを作成 val views = RemoteViews(context.packageName, R.layout.sample_app_widget) //TextViewにカウント値を適用 views.setTextViewText(R.id.counter, clickCount.toString()) //Button押下通知用のPendingIntentを作成しに登録 val countIntent = Intent(context, SampleAppWidget::class.java).apply { action = COUNT_UP } val countPendingIntent = PendingIntent.getBroadcast(context, 0, countIntent, PendingIntent.FLAG_UPDATE_CURRENT) views.setOnClickPendingIntent(R.id.add_button, countPendingIntent) // ウィジェットを更新 appWidgetManager.updateAppWidget(appWidgetId, views) }修正毎のソースコード
ウィジェットのレイアウトファイルの変更はありません。
AppWidgetProviderを継承したクラスのファイルを以下のように修正しています。
SampleAppWidget.kt//com.example.android.stackwidgetの部分は自分のアプリのIDに修正する private const val COUNT_UP = "com.example.android.buttonwidgettest.COUNT_UP" private const val ARG_APP_WIDGET_ID = "appWidgetId" class SampleAppWidget : AppWidgetProvider() { override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { // There may be multiple widgets active, so update all of them for (appWidgetId in appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId) } } override fun onReceive(context: Context?, intent: Intent?) { super.onReceive(context, intent) if (context == null || intent == null) return when (intent.action) { COUNT_UP -> { // 修正:IntentからappWidgetIdを取得し,appWidgetIdを用いてにカウント値を読み書き val appWidgetId = intent.getIntExtra(ARG_APP_WIDGET_ID, 0) // カウント値を読み込み val dataStore = context.getSharedPreferences("widgetClickCount", Context.MODE_PRIVATE) var clickCount = dataStore.getInt(appWidgetId.toString(), -1) clickCount++ // カウントアップ後のカウント値を書き込み dataStore.edit().putInt(appWidgetId.toString(), clickCount).commit() //TextViewにカウント値を適用 val views = RemoteViews(context.packageName, R.layout.sample_app_widget) views.setTextViewText(R.id.counter, clickCount.toString()) // 修正:ウィジェット更新を通知する際に,appWidgetIdを指定 // ウィジェットを更新 val manager = AppWidgetManager.getInstance(context) manager.updateAppWidget(appWidgetId, views) } } } } //appWidgetId毎のonUpdateの処理はこちらで実装する internal fun updateAppWidget( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int ) { // 修正:SharedPreferencesにカウント値を保存する際,appWidgetId毎に保存するように修正G // SharedPreferencesからデータを読み出し(登録されていない場合は0) val dataStore = context.getSharedPreferences("widgetClickCount", Context.MODE_PRIVATE) var clickCount = dataStore.getInt(appWidgetId.toString(), -1) if (clickCount == -1) { dataStore.edit().putInt(appWidgetId.toString(), 0).apply() clickCount = 0 } // RemoteViews オブジェクトを作成 val views = RemoteViews(context.packageName, R.layout.sample_app_widget) //TextViewにカウント値を適用 views.setTextViewText(R.id.counter, clickCount.toString()) //Button押下通知用のPendingIntentを作成しに登録 val countIntent = Intent(context, SampleAppWidget::class.java).apply { action = COUNT_UP // 修正:IntentにappWidgetIdを付加 putExtra(ARG_APP_WIDGET_ID, appWidgetId) } // 修正:PendingIntentのrequestCodeにappWidgetIdを指定 val countPendingIntent = PendingIntent.getBroadcast( context, appWidgetId, countIntent, PendingIntent.FLAG_UPDATE_CURRENT ) views.setOnClickPendingIntent(R.id.add_button, countPendingIntent) // ウィジェットを更新 appWidgetManager.updateAppWidget(appWidgetId, views) }修正点は以下のとおりです。
- 修正1
- SharedPreferencesにカウント値を保存する際,keyをappWidgetId,valueをカウント値として,appWidgetId毎にカウント値を保存するように修正しました。
- 修正2
- Intentに,putExtraを用いてappWidgetIdを付加しました。 これにより,Intentを受信した際,onReceiveでappWidgetIdを参照することが可能になります。
- 修正3
- PendingIntentのrequestCodeにappWidgetIdを指定しました。
- 修正4
- onReceive内で,受け取ったIntentからappWidgetIdを取得し,appWidgetIdを用いてSharedPreferencesにカウント値を読み書きするようにしました
- 修正5
- onReceive内で,ウィジェット更新を通知する際に,appWidgetIdを指定するようにしました
修正1・3は,このアプリの特有の仕様によるものですのであまり重要ではありませんが,
修正2・4・5は,ウィジェット毎に各自の動作を行いたい場合には留意するべき点であると思います。おわりに
もし説明や実装におかしな点がありましたら是非コメントください。
修正させて頂きます。
- 投稿日:2020-09-10T11:56:16+09:00
【Android/Kotlin】同じWidgetを複数作成した際に,別々のものとして動作させる方法
はじめに
この記事は【Android/Kotlin】Widgetを作ってクリック時の動作を設定する
の続きとなっていますすが,ウィジェットの基本が分かっている方はこの記事だけ見れば大丈夫です。また,執筆者は趣味レベルのAndroidアプリ開発者である為,説明間違いが含まれている可能性があります。お気づきの点がありましたら,是非コメントお願いいたします。また,アップデートにより,機能やコードが記事執筆時点と異なる場合があります
本記事の内容
本記事では,【Android/Kotlin】Widgetを作ってクリック時の動作を設定する
で作成した,ボタンを押すとカウンターが1ずつ増えていくウィジェットについて,複数が同じものとして動作してしまう問題を解消します。
修正毎のソースコード
特に複雑なことはしていませんが,ソースに関する詳しい説明は【Android/Kotlin】Widgetを作ってクリック時の動作を設定するを御覧ください。
ウィジェットのレイアウトファイル
sample_app_widget.xml<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white" android:gravity="center" android:orientation="vertical" android:padding="@dimen/widget_margin"> <TextView android:id="@+id/counter" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="0" android:textAppearance="@style/TextAppearance.AppCompat.Display1" /> <Button android:id="@+id/add_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="add" /> </LinearLayout>ウィジェットの動作を実装したファイル
SampleAppWidget.kt//com.example.android.stackwidgetの部分は自分のアプリのIDに修正する private const val COUNT_UP = "com.example.android.buttonwidgettest.COUNT_UP" class SampleAppWidget : AppWidgetProvider() { override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { // There may be multiple widgets active, so update all of them for (appWidgetId in appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId) } } override fun onReceive(context: Context?, intent: Intent?) { super.onReceive(context, intent) if (context == null || intent == null) return when (intent.action) { COUNT_UP -> { // カウント値を読み込み val dataStore = context.getSharedPreferences("widget", Context.MODE_PRIVATE) var clickCount = dataStore.getInt("clickCount", -1) clickCount++ // カウントアップ後のカウント値を書き込み dataStore.edit().putInt("clickCount", clickCount).commit() //TextViewにカウント値を適用 val views = RemoteViews(context.packageName, R.layout.sample_app_widget) views.setTextViewText(R.id.counter, clickCount.toString()) // ウィジェットを更新 val myWidget = ComponentName(context, SampleAppWidget::class.java) val manager = AppWidgetManager.getInstance(context) manager.updateAppWidget(myWidget, views) } } } } //onUpdateのappWidgetId毎の処理はこちらで実装する internal fun updateAppWidget( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int ) { // SharedPreferencesからデータを読み出し(登録されていない場合は0) val dataStore = context.getSharedPreferences("widget", Context.MODE_PRIVATE) var clickCount = dataStore.getInt("clickCount", -1) if (clickCount == -1) { dataStore.edit().putInt("clickCount", 0).apply() clickCount = 0 } // RemoteViews オブジェクトを作成 val views = RemoteViews(context.packageName, R.layout.sample_app_widget) //TextViewにカウント値を適用 views.setTextViewText(R.id.counter, clickCount.toString()) //Button押下通知用のPendingIntentを作成しに登録 val countIntent = Intent(context, SampleAppWidget::class.java).apply { action = COUNT_UP } val countPendingIntent = PendingIntent.getBroadcast(context, 0, countIntent, PendingIntent.FLAG_UPDATE_CURRENT) views.setOnClickPendingIntent(R.id.add_button, countPendingIntent) // ウィジェットを更新 appWidgetManager.updateAppWidget(appWidgetId, views) }修正毎のソースコード
ウィジェットのレイアウトファイルの変更はありません。
AppWidgetProviderを継承したクラスのファイルを以下のように修正しています。
SampleAppWidget.kt//com.example.android.stackwidgetの部分は自分のアプリのIDに修正する private const val COUNT_UP = "com.example.android.buttonwidgettest.COUNT_UP" class SampleAppWidget : AppWidgetProvider() { override fun onUpdate( context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray ) { // There may be multiple widgets active, so update all of them for (appWidgetId in appWidgetIds) { updateAppWidget(context, appWidgetManager, appWidgetId) } } override fun onReceive(context: Context?, intent: Intent?) { super.onReceive(context, intent) if (context == null || intent == null) return when (intent.action) { COUNT_UP -> { // 修正:IntentからappWidgetIdを取得し,appWidgetIdを用いてにカウント値を読み書き val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, 0) // カウント値を読み込み val dataStore = context.getSharedPreferences("widgetClickCount", Context.MODE_PRIVATE) var clickCount = dataStore.getInt(appWidgetId.toString(), -1) clickCount++ // カウントアップ後のカウント値を書き込み dataStore.edit().putInt(appWidgetId.toString(), clickCount).commit() //TextViewにカウント値を適用 val views = RemoteViews(context.packageName, R.layout.sample_app_widget) views.setTextViewText(R.id.counter, clickCount.toString()) // 修正:ウィジェット更新を通知する際に,appWidgetIdを指定 // ウィジェットを更新 val manager = AppWidgetManager.getInstance(context) manager.updateAppWidget(appWidgetId, views) } } } } //appWidgetId毎のonUpdateの処理はこちらで実装する internal fun updateAppWidget( context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int ) { // 修正:SharedPreferencesにカウント値を保存する際,appWidgetId毎に保存するように修正G // SharedPreferencesからデータを読み出し(登録されていない場合は0) val dataStore = context.getSharedPreferences("widgetClickCount", Context.MODE_PRIVATE) var clickCount = dataStore.getInt(appWidgetId.toString(), -1) if (clickCount == -1) { dataStore.edit().putInt(appWidgetId.toString(), 0).apply() clickCount = 0 } // RemoteViews オブジェクトを作成 val views = RemoteViews(context.packageName, R.layout.sample_app_widget) //TextViewにカウント値を適用 views.setTextViewText(R.id.counter, clickCount.toString()) //Button押下通知用のPendingIntentを作成しに登録 val countIntent = Intent(context, SampleAppWidget::class.java).apply { action = COUNT_UP // 修正:IntentにappWidgetIdを付加 putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) } // 修正:PendingIntentのrequestCodeにappWidgetIdを指定 val countPendingIntent = PendingIntent.getBroadcast( context, appWidgetId, countIntent, PendingIntent.FLAG_UPDATE_CURRENT ) views.setOnClickPendingIntent(R.id.add_button, countPendingIntent) // ウィジェットを更新 appWidgetManager.updateAppWidget(appWidgetId, views) }修正点は以下のとおりです。
修正1・4は,このアプリの特有の仕様によるものですのであまり重要ではありませんが,
修正2・3・5は,ウィジェット毎に各自の動作を行いたい場合には必須の実装かと思います。修正1
SharedPreferencesにカウント値を保存する際,keyをappWidgetId,valueをカウント値として,appWidgetId毎にカウント値を保存するように修正しました。
修正2
Intentに,putExtraを用いてappWidgetIdを付加しました。
これにより,Intentを受信した際,onReceiveでappWidgetIdを参照することが可能になります。修正3
PendingIntentのrequestCodeにappWidgetIdを指定しました。
※因みに余談ですが,他の修正点を実装したうえでappWidgetIdを設定しない(=0を設定)場合はどうなるかというと,どのボタンを押しても最新のウィジェットの値だけが更新されるという現象が起こります。これは,FLAG_UPDATE_CURRENTフラグの挙動に関係あります。このフラグを設定した際,同じrequestCodeで新規にPendingIntentを作成する度,いままで作成したPendingIntentに設定したIntentが全て,新しく設定したIntentに置き換わってしまいます。(つまりCURRENTのものにUPDATEされている)
他のFLAGに関してはこちらの記事で分かりやすく解説されています。(他のFLAGを設定した場合にもそれぞれでおかしな動作が発生しますので興味ある方は試してみてください。)とりあえずどのFLAGを設定する場合でも,別々のPendingIntentを複数作成したい場合には,requestCodeにはユニークな値を設定したほうが良いです。修正4
onReceive内で,受け取ったIntentからappWidgetIdを取得し,appWidgetIdを用いてSharedPreferencesにカウント値を読み書きするようにしました
修正5
onReceive内で,ウィジェット更新を通知する際に,appWidgetIdを指定するようにしました
おわりに
もし説明や実装におかしな点がありましたら是非コメントください。
修正させて頂きます。
- 投稿日:2020-09-10T11:43:45+09:00
【Kotlin】AndroidでiOSライクな画面遷移アニメーションを実装する
簡単に言えばiOSで
UINavigationController
でshow
するような画面遷移を、
Androidで簡単に実装出来ます。手順1:res/animを作成する
手順2:画像のようにxmlファイルを4件追加する
close_enter.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:fromXDelta="-100%" android:toXDelta="0%" android:duration="300" android:fillAfter="true" android:fillEnabled="true"/> </set>close_exit.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:fromXDelta="0%" android:toXDelta="100%" android:duration="300" android:fillAfter="true" android:fillEnabled="true"/> </set>open_enter.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:fromXDelta="100%" android:toXDelta="0%" android:duration="300" android:fillAfter="true" android:fillEnabled="true"/> </set>open_exit.xml
<?xml version="1.0" encoding="utf-8"?> <set xmlns:android="http://schemas.android.com/apk/res/android"> <translate android:fromXDelta="0%" android:toXDelta="-100%" android:duration="300" android:fillAfter="true" android:fillEnabled="true"/> </set>手順3:アニメーションを行いたいA画面の画面遷移実行時に下記の記述を入れる。
val intent = Intent(this, MainActivity::class.java) startActivity(intent) overridePendingTransition(R.anim.open_enter, R.anim.open_exit);手順3:閉じるアニメーションを行いたいB画面の画面遷移実行時に下記の記述を入れる。
override fun finish() { super.finish() overridePendingTransition(R.anim.close_enter, R.anim.close_exit); }
- 投稿日:2020-09-10T00:31:10+09:00
[備忘録] MacBook Pro を使って Android 10 を Raspberry Pi 4 に焼く
Linux を使える環境だったら全く問題ないんですが、 macOS を搭載した MacBook Pro で Android 10 を Raspberry Pi に焼く時にいくつかハマるポイントがあったので備忘録がてら残しておく。
macOS で、とありますが Docker や VirtualBox で Linux は使いますので Linux を使わずに Android を焼く。という縛りプレイとかではないです。 Linux系OS の起動ディスク作ったりが面倒だったのでそれをやらずに焼く方法。という感じです。
主に以下で公開されている内容に沿って進めていきます。
https://github.com/android-rpi/device_arpi_rpi4この記事もわかりやすくまとめられています。 Linux が使える環境であればこちらを読んでその通りに進めていければいいと思います。
https://medium.com/@jpnurmi/android-10-on-raspberry-pi-4-5078901698ddAndroid のソースは以下のサイトからダウンロードしてきます。
https://source.android.google.cn/?hl=jaここに書いてあることが非常に重要で、macOS は多くの場合でファイルの大文字、小文字を区別しないファイルシステム上で動いているんですが、そのせいで macOS の環境で Android のビルドを始めると途中でコケます。以下のコマンドでどこかしらに
android.dmg.sparseimage
を作成してマウントし、そこで基本的に作業を進めるようにしてください。$ hdiutil create -type SPARSE -fs 'Case-sensitive Journaled HFS+' -size 250g ~/android.dmg.sparseimageそれでは、 Android のソースコードをダウンロードします。以下のコマンドを打ち込んでいってください。最後の
repo sync
コマンドは-j{任意の数字}
を入れると数字分並列して実行してくれるため高速化できることがあります。$ repo init -u https://android.googlesource.com/platform/manifest -b android-10.0.0_r41 $ git clone https://github.com/android-rpi/local_manifests .repo/local_manifests -b arpi-10 $ repo synchttps://github.com/android-rpi/local_manifests
repo
コマンドは Google 製のリポジトリ管理ツールとのことです。入れていなかったら以下を参考に入れてください。https://source.android.google.cn/setup/develop?hl=ja
次に、カーネルのビルドをします。上記でダウンロードしたソースに
kernel
ディレクトリが存在することを確認して進めてください。また、この作業は macOS だと上手くいかなかったので VirtualBox を使い Ubuntu 20.04 でやりました。$ sudo apt install gcc-arm-linux-gnueabihf libssl-dev build-essential bs python-mako $ cd kernel/arpi $ ARCH=arm scripts/kconfig/merge_config.sh arch/arm/configs/bcm2711_defconfig kernel/configs/android-base.config kernel/configs/android-recommended.config $ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make zImage $ ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make dtbs結構時間かかります。また環境によってはライブラリが足りずコケるかもしれませんので適宜追加して進めてください。
次に、以下にあるように一部ソースコードを書き換えます。
https://github.com/android-rpi/device_arpi_rpi4/wiki/Android-10-:-patch-framework-source
それが済んだらいよいよ Android のビルドです。 macOS 上で Android のソースコードのディレクトリで以下の通り作業をします。
macOS のバージョンによっては SDK のバージョンが新しすぎて途中でビルドがコケる。ということもあったのでその場合は古い SDK を入れて進めました。$ source build/envsetup.sh $ lunch rpi4-eng $ make ramdisk systemimage vendorimage次に、 Android を焼く SD カードのフォーマットをします。 macOS 10.15 でこの通りパーティションを以下の通りに分けることはできなかったので再び VirtualBox の Ubuntu 20.04 上でやりました。
Partitions of the card should be set-up like followings. p1 128MB for boot : Do fdisk, set W95 FAT32(LBA) & Bootable type, mkfs.vfat p2 768MB for /system : Do fdisk, new primary partition p3 128MB for /vendor : Do fdisk, new primary partition p4 remainings for /data : Do fdisk, mkfs.ext4 Set volume label of /data partition as userdataミソは p4 を userdata という label にすることと、 p1 にきちんと boot と lba のフラグをつけることです。
VirtualBox を使っているなら以下のページにある画像の通りに Gparted を使えばいけると思います。
https://medium.com/@jpnurmi/android-10-on-raspberry-pi-4-5078901698dd最後に以下の通り SD カードにビルドした Android をコピーしていけば完成です。
# Write system & vendor partition $ cd out/target/product/rpi4 $ sudo dd if=system.img of=/dev/<p2> bs=1M $ sudo dd if=vendor.img of=/dev/<p3> bs=1M # Copy kernel & ramdisk to BOOT partition device/arpi/rpi4/boot/* to p1:/ kernel/arpi/arch/arm/boot/zImage to p1:/ kernel/arpi/arch/arm/boot/dts/bcm2711-rpi-4-b.dtb to p1:/ kernel/arpi/arch/arm/boot/dts/overlays/vc4-kms-v3d-pi4.dtbo to p1:/overlays/ out/target/product/rpi4/ramdisk.img to p1:/だいぶ雑な感じですが自分用の備忘録ということで。
- 投稿日:2020-09-10T00:00:32+09:00
【Android開発】Notification Channelごとに通知をグループ化する方法
概要
下記画像のように、プッシュ通知をチャンネルごとにグループ化する方法を紹介します。
チャンネルごとに、プッシュ通知をグループ化して受信トレイに表示させたいという方には参考になると思います。また、Githubにソースコードをあげているので、参考にしてみてください。
https://github.com/YusukeSuzuki1213/notification-channel-group注意点としては、OS8以上でなければ通知チャンネルを使用することができないことと、OS7以上でなければ通知のグループ化を実装することができないことです。
実装方法
1. チャンネルの作成
まず下記のようにチャンネルを作成するメソッドを作成します。
fun createChannel(context: Context) { NotificationManagerCompat.from(context).createNotificationChannels( ChannelItem.values().map { NotificationChannel( it.channelId, context.getString(it.getTextStringRes()), NotificationManager.IMPORTANCE_HIGH ) } ) }
NotificationManagerCompat.from(context).createNotificationChannels
において、アプリの通知チャンネルを作成しています。引数にはList<NotificationChannel>
型のインスタンスを指定しています。NotificationChannelクラスはチャンネルの設定についてのクラスで、インスタンス化には、チャンネルのId、チャンネルの名前、Importanceを指定しています。Importanceとは、通知受信時に音を出すか、ヘッドアップ通知とするかなどの通知の重要度を指定することができます。詳しくはNotification importanceを参考にしてください。
チャンネル情報をまとめたクラスの作成
ChannelItem.values().map
としているように、今回は複数チャンネルを作成するため、下記のようにチャンネルの情報をまとめたChannelItem
というEnumクラスを作成します。enum class ChannelItem(val channelId: String) { CHANNEL_1("notification_channel_1"), CHANNEL_2("notification_channel_2"), CHANNEL_3("notification_channel_3"); companion object { @JvmStatic fun value(channelId: String) : ChannelItem? { return try { values().first {it.channelId == channelId} } catch (_: NoSuchElementException) { null } } } fun getTextStringRes(): Int { return when(this) { CHANNEL_1 -> R.string.channel_name_notification_1 CHANNEL_2 -> R.string.channel_name_notification_2 CHANNEL_3 -> R.string.channel_name_notification_3 } } }EnumクラスのプロパティにチャンネルのIdを持たせています。さらに、
getTextStringRes()
というメソッドでEnumクラスのインスタンスから、チャンネルの名前を取得できるようにしています。上記の実装でチャンネルの作成ができます。
2. プッシュ通知の作成
次に下記のように、実際にプッシュ通知を作成するメソッドを作成します。
fun localPush(context: Context, item: PushMessageItem) { val channelItem = ChannelItem.value(item.channelId) ?: return val builderParent = NotificationCompat.Builder(context, item.channelId) .setPriority(NotificationCompat.PRIORITY_HIGH) .setGroup(item.channelId) .setSmallIcon(R.drawable.ic_launcher_foreground) .setStyle( NotificationCompat.InboxStyle() .setSummaryText( context.getString(channelItem.getTextStringRes()) )) .setGroupSummary(true) .setAutoCancel(true) val builderChild = NotificationCompat.Builder(context, item.channelId) .setContentTitle(item.title) .setContentText(item.body) .setAutoCancel(true) .setDefaults(NotificationCompat.DEFAULT_SOUND) .setPriority(NotificationCompat.PRIORITY_HIGH) .setGroup(item.channelId) .setSmallIcon(R.drawable.ic_launcher_foreground) }実装のイメージは、
builderParent
と呼ばれる親通知の中に、builderChild
と呼ばれる複数の子通知が追加されていく感じです。builderParent について
画像のように、通知グループの概要を表示する通知になっています。
setGroup()
について
グループを識別する文字列を渡し、親通知をグループに追加しています。
setGruoupSummary()
について
引数にtrue
を渡すことで、builderParent
がグループ概要を表示する親通知であるということ示しています。
setStyle()
について
通知のレイアウトを指定しています。今回はNotificationCompat.InboxStyleを指定することで、通知の概要を追加しています。
詳しくは展開可能な通知を作成するを参考にしてください。builderChild について
画像のように、実際のプッシュ通知の情報を持ったインスタンスになります。通知音の設定や、タイトルの指定などを行なっています。
また
setGroup()
で親通知と同じグループを識別文字列を指定することで、グループに属する子通知であることを示しています。プッシュ通知の表示
下記コードにおいて、実際にプッシュ通知を表示しています。
NotificationManagerCompat.from(context).run { notify(channelItem.ordinal, builderParent.build()) notify(System.currentTimeMillis().toInt(), builderChild.build()) }
notify
の第一引数には、通知のIDを指定しています。このIDは通知を一意に識別するIDとなっています。このIDが重複してしまうと、通知が上書きされてしまいます。逆にIDが一意だと、異なる通知として表示されます。
親通知は通知が来るたびに、異なる通知として扱う必要がないため、
channelItem.ordinal
を指定して、通知グループごと同じIDをしてしています。子通知は、通知が上書きされないように、タイムスタンプをIDとして指定しています。
まとめ
Notification Channelごとに通知をグループ化する方法を紹介しました。
参考になればうれしいです。