20190506のAndroidに関する記事は13件です。

【Android】外部から取得した画像をデコードする時に気をつけること

APIなどを叩いて外部から取得した画像データを利用する場合、エラー等のデコードできない可能性や大きすぎる画像である可能性などを考慮する必要がある。
かなり大きな画像をデコードしてしまうと、Out of Memoryでクラッシュする可能性もあるため、直接ピクセルデータをデコードするのではなく、最初にサイズ情報のみをデコードし、必要なサンプリング数を設定してからデコードすることが必要になる。
こうしたことを考慮すると、以下のようなメソッドをutilメソッドとして定義しておき、使い回すのが好ましい。

    fun decodeBitmapFromByteArray(
        data: ByteArray,
        @Dimension requestWidth: Int,
        @Dimension requestHeight: Int
    ): Bitmap {
        val options = BitmapFactory.Options()

        options.inJustDecodeBounds = true
        BitmapFactory.decodeByteArray(data, 0, data.size, options)

        options.inSampleSize = sampleSize(
            options.outWidth, options.outHeight, requestWidth, requestHeight
        )
        options.inJustDecodeBounds = false
        return BitmapFactory.decodeByteArray(data, 0, data.size, options)
    }

    private fun sampleSize(
        sourceWidth: Int, sourceHeight: Int,
        requestWidth: Int, requestHeight: Int
    ): Int {
        if (requestWidth <= 0 || requestHeight <= 0) {
            return 1
        }
        if (sourceWidth * requestHeight < requestWidth * sourceHeight) {
            return if (sourceWidth > requestWidth) sourceWidth / requestWidth else 1
        }
        return if (sourceHeight > requestHeight) sourceHeight / requestHeight else 1
    }

inJustDecodeBoundsをtrueにしてdecodeByteArrayを呼び出すと、画像をメモリに展開せずに画像サイズを取得することができる。これを利用して画像サイズからサンプリングのサイズを決定し、optionsのinSampleSizeに設定した後に、inJustDecodeBoundsをfalseにして実際にdecodeを行う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Canvasを使ってあるBitmapから正円のBitmapを作成する処理を書いた話

やりたいこと

ある画像から、サムネイルのアイコンを作って表示する処理が必要になったので、Canvasを使って実装してみました。

スクリーンショット 2019-05-06 18.38.46.png
例えばこういうネクタイの画像があったとすると、欲しいのは以下のようなアイコンになります。
スクリーンショット 2019-05-06 18.39.05.png
拡大縮小はせず、中心部分をうまく丸く切り取る処理をする場合、以下のような実装になります。

実際のソースコード

    private fun getCroppedBitmap(bitmap: Bitmap): Bitmap {
        val size = Math.min(bitmap.width, bitmap.height)
        val output = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888)
        val canvas = Canvas(output)

        val paint = Paint()

        paint.isAntiAlias = true
        canvas.drawARGB(0, 0, 0, 0)
        canvas.drawCircle(size / 2f, size / 2f, size / 2f, paint)
        paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN)
        val max = Math.max(bitmap.width, bitmap.height)
        val start = (size - max) / 2
        val left = if (bitmap.width < bitmap.height) 0 else start
        val top = if (bitmap.height < bitmap.width) 0 else start
        val rect = Rect(left, top, bitmap.width + left, bitmap.height + top)
        canvas.drawBitmap(bitmap, Rect(0, 0, bitmap.width, bitmap.height), rect, paint)
        return output
    }

解説

やっていることはそんなに難しくはありません。
まず、出力先のBitmapをsize指定で作成します。この時、sizeは元画像の幅/高さのうち小さい方を指定します。
次にCanvasとPaintを用意します。
今回は円形に画像を切り抜くので、Paintにはジャギーが目立たなくなるようにisAntiAliasをtrueに設定します(詳細はアンチエイリアスを参照のこと)。
canvasは透明で塗りつぶしてから、drawCircle(中心点のx座標,中心点のy座標,半径,paint)でsizeの半分の半径でちょうど中央に円を描きます。
paint.xfermodeを設定する前のdrawCircleがDST、その後のdrawBitmapがSRCなので、画像が丸の中に入るようにするにはPorterDuffXfermode(PorterDuff.Mode.SRC_IN)paint.xfermodeに設定します。
最後に切り抜くrectを決めてdrawBitmapすることで、描画処理は完了です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

KotlinMPPでAndroid/iOS両対応のHTTPクライアントライブラリを作る

はじめに 

 Android/iOS、両プラットフォーム上で同じようなアプリを開発する際、明らかに同じような処理をKotlin/Swiftで実装することがあり、共通化したいなと思うところがありました。
例えば、通信部分やEntity、ValueObjectなど、OSが違っても共通になる処理については尚更です。こういった部分を二度も違う言語で実装するのは明らかに二度手間で余計な工数がかかりますし、バグが発生する可能性も増します。
なんとかこういった部分を共通化し、一度コードを書けば両デバイスで動かせる、それによって工数の削減をしたい。そういったモチベーションがあったため、KotlinMPPについて調査を行い、実際にそれを用いて両プラットフォームに一度で対応できるHTTPクライアントを作ってみました。

KotlinMPPとは

正式名称 Kotlin Multiplatform Project
Kotlin1.2及び1.3で実験的に提供されている機能です。Gradleでターゲットとなるプラットフォームにどんな方法でビルドをするかを記述することで、一つのKotlinのソースを各プラットフォーム向けに動作する形でビルドすることが出来ます。
例えば、AndroidではJVMで動作するようにビルドする、iOSではネイティブで動作するようにビルドする、といった次第です。
Anrdoid,iOSはもちろん、mac,Windows,JSをターゲットとすることが出来ます。

KotlinMPPを使ったHTTPクライアントライブラリ

サンプルプロジェクトを作ってみました。
5hyn3/KotlinMPPHttpClientSample

以下のような言語やビルドツール、ライブラリを組み合わせて作られています

  • kotlin

    • 1.3.31
  • gradle

    • 5.4.1
  • ktor-client

    • 1.1.5
  • kotlinx.serialization

    • 0.11.0
  • kotlinx.coroutin

    • 1.2.1

テストが一つ記述されており、実際にAPIからjsonを取得し、パースしてkotlinのオブジェクトとして取得する部分までの動作を確認することが出来ます。実際にアプリに組み込んで動作を確認したい場合は以下を参考にしてください。

Multiplatform Project: iOS and Android

ビルド成果物としてjarとframeworkができるので、それらを扱う形になります。

現状、AndroidとiOSのみをターゲットに作っていますが、gradleに手を入れることでJSなどにも対応させることが出来ます。

苦労した点

以下サンプルプロジェクトを作った際に遭遇した大変だった点を書いていきます

バージョンごとにビルド時の挙動が違っており適切なバージョンの組み合わせを見つけるのが大変だった

主にGradleによるものです。色々なプロジェクトを参考にしてサンプルプロジェクトを作りましたが、バージョンがちょっとでも違うと全くビルドすら出来ませんでした。また、ライブラリが要求するGradleのバージョンが食い違っていたりして悩んだこともありました。
最終的に、現時点での最新版である5.4.1を採用することで解決しました。本プロジェクト作成に費やした時間の8割くらいはここで適切なライブラリを選定することで消費されています。まだexperimentalなのでこのあたりはどうしてもしょうがないですね。

suspendなmethodをSwiftから呼び出せない

現状Kotlin/Nativeはsuspendなmethodをswiftから呼び出せるインターフェースを作ってくれないようです。suspendをやめ、Defferdインスタンスを返すようにすることでSwiftからもきちんと扱えるようになりました。こちらであればSwift側からも扱えるインターフェースが提供されており、問題なく呼び出すことが出来ます。

Kotlin/Nativeでビルドした際にランタイムでIncorrectDereferenceExceptionが発生する

 本プロジェクトでは非プリミティブ型のトップレベル変数をメインスレッド外から触ろうとしたために発生しました。具体的にどこで発生したかというと、ここです。対処法としては@ThreadLocalを付与して、スレッドローカルとして扱うことで対応を行いました。

終わりに

 これでKotlinでコードを一つ書けばAndroid/iOS両方で動くライブラリを作ることが出来ました。これでより業務を加速し、同じことを二回も書くようなつまらない作業は減らして、もっと本質の部分に時間を使うことが出来るようになるといいなぁと思います。

参考

JetBrains/kotlin-mpp-example
Multiplatform Project: iOS and Android
kotlin-native /IMMUTABILITY.md
DroidKaigi/conference-app-2019
SimonSchubert/Newsout

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android の DatePickerDialog で日時範囲(開始日時~終了日時)の選択

目的

Android の DatePickerDialog を使って、日付範囲(開始日~終了日)を選択させる。

  • DatePickerDialogで日付を選んで、Buttonのtextに表示させる。
    • Buttonじゃなくて他のViewに表示させてもOK。TextViewとか。
  • 選択できる日付を制限する。
    • 開始日に終了日より後の日付を選択させない。
    • 終了日に開始日より前の日付を選択させない。

その他の仕様(詳しい説明はこちらの記事に書いてます)

すでに日付が選択されており、DatePickerDialogで日付を選ばなかったとき、選択された日付を削除する。

  • キャンセルを押したときにButton.textにセットされていた値を変更するということ。
  • ここでは日付が未入力の場合、"指定なし"というテキストをセットすることにします。

動作環境

  • Kotlin 1.3.21
  • Android 8.0 (Oreo)
  • AndroidStudio 3.3.2

動作イメージ

dataRangeTest.gif

実装

layoutファイル

開始日付と終了日付を選択・表示するButtonとTextViewの構成とします。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        tools:context=".MainActivity">
    <TextView
            android:text="日付範囲"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:id="@+id/date_range_text"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent"
            android:layout_marginLeft="8dp" android:layout_marginStart="8dp" android:layout_marginTop="16dp"/>
    <Button
            android:text="指定なし"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:id="@+id/begin_date_button"
            app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/date_range_text" android:layout_marginLeft="64dp"
            android:layout_marginStart="64dp" android:layout_marginTop="16dp"/>
    <TextView
            android:text="~"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:id="@+id/range"
            app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/begin_date_button" android:layout_marginLeft="8dp"
            android:layout_marginStart="8dp" android:layout_marginTop="16dp"/>
    <Button
            android:text="指定なし"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" android:id="@+id/end_date_button"
            app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toEndOf="@+id/range" android:layout_marginLeft="8dp"
            android:layout_marginStart="8dp" android:layout_marginTop="16dp"/>
</android.support.constraint.ConstraintLayout>

activityファイル

以下は、MainActivity.onCreateメソッド内のコードですが、分かりやすくするために2つに分けます。

  • dateSetOnClickListener.kt : ButtonのViewを取得とButton.setOnClickListenerの実装を行う。
  • DateDialogFragment.kt : DateDialogFragmentを表示させるclass。
dateSetOnClickListener.kt
val mBeginDate = findViewById<Button>(R.id.begin_date_button)
val mEndDate = findViewById<Button>(R.id.end_date_button)

/* 開始日付を指定するボタンを押したとき */
mBeginDate.setOnClickListener {
//    DialogFragmentを生成し、表示
    DateDialogFragment(mBeginDate).show(supportFragmentManager, mBeginDate::class.java.simpleName)
}
/* 終了日付を指定するボタンを押したとき */
mEndDate.setOnClickListener {
    DateDialogFragment(mEndDate).show(supportFragmentManager, mEndDate::class.java.simpleName)
}
DateDialogFragment.kt
/* 日付を入力する時に使用するinnerクラス
   DialogFragmentで日付を入力できるカレンダーを表示する
   DialogFragmentを呼び出したボタンを取得したいので、コンストラクタの引数にButtonを定義する */
class DateDialogFragment(val button: Button) : DialogFragment() {
    /* DatePickerDialogを返却するメソッド
       このメソッドで、日付を選択した後の処理や、日付範囲、Dialogのタイトルを設定する */
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
//        現在日付を取得
        val calendar = Calendar.getInstance()
        return DatePickerDialog(
            context,
            theme,
//            DialogFragmentで日付を選択し、OKを押したときの処理
            DatePickerDialog.OnDateSetListener { _, year, month, dayOfMonth ->
                val inputDate = Calendar.getInstance()
//                選択された日付を取得
                inputDate.set(year, month, dayOfMonth)
                val dfInputeDate = SimpleDateFormat("yyyy-MM-dd", Locale.US)
//                CalendarからStringへ変換
                val strInputDate = dfInputeDate.format(inputDate.time)
//                DialogFragmentを呼び出したボタンのテキストに日付をセット
                button.text = strInputDate
            },
            calendar.get(Calendar.YEAR),
            calendar.get(Calendar.MONTH),
            calendar.get(Calendar.DAY_OF_MONTH))
            .also {
                /* 選択可能な日付の上限を設定
                   終了日付が指定されている場合、開始日付は終了日付より後を選べないようにする */
                if (button === mBeginDate && mEndDate.text != "指定なし"){
                    val maxDate = Calendar.getInstance()
                    val dfMaxDate = SimpleDateFormat("yyyy-MM-dd", Locale.JAPAN)
//                    指定されている終了日付を取得
                    val endInputDate = dfMaxDate.parse(mEndDate.text.toString())
                    maxDate.time = endInputDate
//                    選択可能な開始日付の上限に指定されている終了日付を設定
                    it.datePicker.maxDate = maxDate.timeInMillis
                } else {
//                    選択可能な開始日付の上限に現在日付を設定
                    it.datePicker.maxDate = calendar.timeInMillis
                }
                /*  選択可能な日付の下限を指定 */
                val minDate = Calendar.getInstance()
                /* 開始日付が指定されている場合、終了日付は開始日付より前を選べないようにする */
                if (button === mEndDate && mBeginDate.text != "指定なし"){
                    val dfMinDate = SimpleDateFormat("yyyy-MM-dd", Locale.JAPAN)
                    val beginInputDate = dfMinDate.parse(mBeginDate.text.toString())
                    minDate.time = beginInputDate
//                    選択可能な終了日付の下限に指定されている開始日付を設定
                    it.datePicker.minDate = minDate.timeInMillis
                } else {
//                    選択可能な終了日付の下限を設定、ここでは2018/2/1とします
                    minDate.set(2018, 2, 1)
                    it.datePicker.minDate = minDate.timeInMillis
                }
//                タイトルが勝手に表示されるのを防ぐために空文字をセット
                it.setTitle("")
            }
    }

    /* DialogFragmentでcancelを押した際の処理 */
    override fun onCancel(dialog: DialogInterface?) {
        super.onCancel(dialog)
//        cancelを押した際はセットされていた日付を削除し、"指定なし"をセット
        button.text = "指定なし"
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SQLiteで昇順・降順にソートする方法

概要

データベースから値を取得するとき、昇順・降順にソートしてから取得ができる
※前提条件として、SQLiteOpenHelperを使用してデータベースに値が入っていること

使い方

number title
15 ABCD
67 EFGH
22 IJKL
12 MNOP

例えば上記のようにデータベース(DB)に値があったとする

        if(helper == null){
            helper = new TestOpenHelper(getActivity().getApplicationContext());
        }

        if(db == null){
            db = helper.getReadableDatabase();
        }
        Cursor cursor = db.query(
                "testdb",
                new String[] { "number","title" },
                null, //selection
                null, //selectionArgs
                null, //groupBy
                null, //having
                null, //orderBy
        );

登録順にDBを取得するだけならこの方法で良い。

order Byに指定する(ソート)

        if(helper == null){
            helper = new TestOpenHelper(getActivity().getApplicationContext());
        }

        if(db == null){
            db = helper.getReadableDatabase();
        }

        String order_by = "number ASC"; //ソートしたい値,昇順(ASC)か降順(DESC)か

        Cursor cursor = db.query(
                "testdb",
                new String[] { "number","title" },
                null, //selection
                null, //selectionArgs
                null, //groupBy
                null, //having
                order_by
        );

Cursorに値を代入する時にorder Byに指定する
→昇順はASC、降順はDESCを指定するだけ!

その後(おまけ)

        //データリストを回す
        cursor.moveToFirst();
        for (int i = 0; i < cursor.getCount(); i++) {
       //ここに任意の処理を書く
            cursor.moveToNext();
        }
        cursor.close();

よく使う、ソート後のデータを取得する方法をメモ。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

SharedElementTransition with Navigation Architecture Component

この記事は?

  • RecyclerView(一覧画面) -> 詳細画面 の構成
  • SharedElementTransition の適用
  • Fragment -> Fragment 遷移をNavigation Architecture Componentで行う

output.gif

利用SDK

project/build.gradle
classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0-alpha02"
app/build.gradle
apply plugin: 'androidx.navigation.safeargs.kotlin'

// 省略

implementation "androidx.navigation:navigation-fragment-ktx:2.1.0-alpha02"

手順まとめ

一覧画面

  • postponeEnterTransition()を実行しておく
  • RecyclerViewの描画直前に、startPostponedEnterTransition()を実行
  • 共有したいViewに一意のTransitionNameを設定
    • RecyclerView内のアイテムなら、position等を利用して一意にする
  • 詳細画面にTransitionNameを渡す
  • 遷移処理時に、addSharedElementをする

詳細画面

  • sharedElementEnterTransitionを設定する
  • 前画面と共有するViewに渡されてきたTransitionNameを設定する

実装

Navigation Graph

xmlを記載したら、一度ビルドしましょう。
後ほど必要なクラスが生成されます。

nav_graph
<?xml version="1.0" encoding="utf-8"?>
<navigation
    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:id="@+id/nav_graph"
    app:startDestination="@id/home_fragment"
    >

    <!-- 省略 -->

   <!-- 一覧画面 -->
    <fragment
        android:id="@+id/todo_list_fragment"
        android:name="com.helmos.mysandbox.presentation.todo.TodoListFragment"
        android:label="TodoListFragment"
        tools:layout="@layout/fragment_todo_list"
        >
        <action
            android:id="@+id/start_detail"
            app:destination="@id/todo_detail_fragment"
            >
            <argument
                android:name="todo"
                app:argType="com.helmos.mysandbox.data.entity.Todo"
                />
            <argument
                android:name="transition_name"
                app:argType="string"
                />
        </action>
    </fragment>

    <!-- 詳細画面 -->
    <fragment
        android:id="@+id/todo_detail_fragment"
        android:name="com.helmos.mysandbox.presentation.todo.detail.TodoDetailFragment"
        android:label="TodoDetailFragment"
        tools:layout="@layout/fragment_todo_detail"
        >
        <argument
            android:name="todo"
            app:argType="com.helmos.mysandbox.data.entity.Todo"
            />
        <argument
            android:name="transition_name"
            app:argType="string"
            />
    </fragment>
</navigation>

一覧画面

RecyclerViewの使い方などは省略しています。

TodoListFragment
override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {

    // 省略

    // RecyclerViewのTransitionは遅延させる
    postponeEnterTransition()
    todoRecycler.viewTreeObserver.addOnPreDrawListener {
        startPostponedEnterTransition()
        true
    }
}

// onBindViewHolder 共有要素にTransitionNameを設定
view.transitionName = "todo_transition_$position"

// 一覧アイテムクリック時の処理
setOnItemClickListener { item, view ->
    val todo = (item as TodoItem).todo
    val sharedView = view.findViewById<View>(R.id.todo_icon)
    // 画面遷移
    findNavController().navigate(
            // 自動生成されるDirectionsクラス
            TodoListFragmentDirections.startDetail(todo, sharedView.transitionName),
            // addSharedElementを行ってくれるUtilクラス
            FragmentNavigatorExtras(
                    sharedView to sharedView.transitionName
            )
    )
}

詳細画面

TodoDetailFragment
// 自動生成される引数データクラス
private val args by navArgs<TodoDetailFragmentArgs>()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    sharedElementEnterTransition = TransitionInflater.from(context).inflateTransition(android.R.transition.move)
}

override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
): View? {

    // 省略

    // 共有要素に設定
    todoImage.transitionName = args.transitionName

}

迷った点/今後調べること

android.R.transition.moveを使わないとダメだった

自作のtransitionだと、
Activity->Activity時には動いていたが、Fragment->Fragmentでは動作しなかった
何か制約があるのかも?

移動させるだけであれば、android.R.transition.moveでOK

詳細から戻った時にアニメーションしなかった

RecyclerView描画直前にstartPostponedEnterTransition()を実行して解決
こちらもActivity->Activity時には設定していなくても動いていたので、Fragment間の遷移だと色々と勝手が違いそう??

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【kotlin】okhttp3でログを出力する

API通信まわりの実装をする際は、ログを見れたほうが作業が捗りますよね。
今回はokhttp3を使ってログを出力する方法をご紹介します。

  • macOS: 10.14.1
  • Android Studio: 3.4
  • kotlin: 1.3.21

手順

build.gradleへの追加

logging-interceptorを追加します。

app/build.gradle
def okhttp_version = '3.11.0'
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" // ログ出力に必要

OkHttpClientの設定

ランタイムなどの設定と一緒に、.addInterceptor...の箇所を追加します。

init {
    val okHttpClient = OkHttpClient.Builder()
        .connectTimeout(20, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        // ログを出力させる設定
        .addInterceptor(HttpLoggingInterceptor().apply {
            level = HttpLoggingInterceptor.Level.BODY
        })
        .build()
}

手順は以上です。

出力してみる

スクリーンショット 2019-05-06 15.42.44.png
こんな感じで、LogcatにD/OkHttp:のメッセージが吐き出されるようになれば成功です。
通信したURLや、GET/POST、ステータスコードや受け取ったレスポンスなど、色々表示してくれて便利です。

※もし手順通りに設定したにもかかわらずログが出力されない場合は、通信以前のところでつまずいている可能性があるので(例えばインターネット通信の許可をしていないとか)確認してみてください。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MonacaでAndroidのアップロード鍵で既存アプリが更新できなくなった時の対応

はじめに

最近、MonacaでAndroidアプリを更新したときにGoogle Play アプリ署名が有効にしたためにハマったことのメモを残しておきます。
Google Play アプリ署名の仕組みについてはこちらをご覧ください。

MonacaのAndroidキーストアのエクスポート

MonacaのクラウドIDEのメニューから、Androidキーストア設定を開いて、キーストアをエクスポートします。

image.png

アップロード鍵の作成とキーストアの更新

ダウンロードしたキーストアファイル keystore.private にアップロード鍵を更新します。

アップロード鍵は、Google Play アプリ署名を有効にするときに作成するか、後で [リリース管理] > [アプリの署名] にアクセスして作成できます。

アップロード鍵を作成する際は下記をご参照ください。

  1. Android Developers サイトの手順に沿ってアップロード鍵を作成します。鍵は安全な場所に保管してください。
  2. アップロード鍵の証明書を PEM 形式でエクスポートします。以下の下線付きの引数を置き換えてください。
    $ keytool -export -rfc -keystore keystore.private -alias jp.linedesign -file upload_cert.der

  3. リリース プロセス中、指示に沿って、証明書をアップロードして Google に登録します。

アップロード鍵を使用すると:

  • アップロード鍵はアプリ作成者の認証する目的でのみ Google に登録されます。
  • アップロードした APK は、ユーザーに送信される前に署名が削除されます。

MonacaのAndroidキーストアのインポート

最後にMonacaのクラウドIDEのメニューから、Androidキーストア設定を開いて、キーストアをインポートすると完了です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

外部決済代行業者にアプリ内課金をさせることができるか

スマホアプリ(iOS/Android)で有料会員のみに一部コンテンツへのアクセスを可能とさせる場合、通常はApple/GoogleにてI/Fが用意されているアプリ内課金の仕組みで実装するのが正攻法と考えられる。

しかし、例えばAmazonのPrime videoなどではアプリ内課金の仕組みは利用されておらず、外部ブラウザにて会員登録をさせる仕組みとなっている。これらの仕様はiOS/Androidそれぞれのディベロッパーポリシー等に違反しないのか、その解釈を調べてみた。

まず結論

下記条件であれば、アプリ内課金を利用しなくても(外部の決済代行業者を利用しても)良いと考えられる。

  • 有料会員の特典は、アプリ外でも利用できる(アプリ内のみのコンテンツではない)
  • 決済画面はアプリ内WebViewよりは、ブラウザアプリで表示させたほうが良い。

判断理由(iOS)

App Store Reviewガイドライン(https://developer.apple.com/jp/app-store/review/guidelines/#in-app-purchase)によると

3.1.5(a)Appの外部で使用する商品やサービス:ユーザーがAppの外部で使用する商品やサービスをAppで購入できるようにする場合、そうした商品の支払いにはApp内課金以外(Apple Payや クレジットカードなど)の方法を使用する必要があります。

とある。つまり、

  • アプリケーションの「内部」で使用する商品やサービス: App内課金での支払いで実装
  • アプリケーションの「外部」で使用する商品やサービス: App内課金以外の方法での支払い(クレジットカード支払いなど)で実装

と考えられる(参考:https://inside.pixiv.blog/danbo-tanaka/4749)。
有料会員の特典適応範囲がアプリ内のみであるとみなされた場合、リジェクト対象となる恐れがある。

判断理由(Android)

デベロッパーポリシー(https://play.google.com/intl/ja/about/developer-content-policy-print/ )では下記と定義されています。

デベロッパーは、Google Play からダウンロードされた別のカテゴリのアプリ内でプロダクトを提供する場合、支払い方法として Google Play アプリ内課金を使用しなければなりません。
ただし、以下の場合を除きます。
・物理的なプロダクトのみの支払い
・そのアプリ以外で消費できるデジタル コンテンツに対する支払い(他の音楽プレーヤーで再生できる曲など)

つまり、基本的な考えはiOSと同じと考えられます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

FirebaseでPUSH通知を行った時の覚書(Android)

やりたいこと

AndroidでFCM(Firebase Cloud Messaging)を使って、PUSH通知を受信できるようにする。
参画した案件の開発で純正FCMの実装を初めて行ったので、その覚書。

やったこと

  • Firebaseの導入
  • FCMの導入(PUSH通知を受信するロジックの実装)

Firebaseの導入

Firebase全般の概要 を見て、プロジェクトを作成

メモ1: プロジェクトを作る時の設定値
スクリーンショット 2019-05-03 22.18.53.png

メモ2: SHA1の取得方法
AndroidStudioを使う場合、コマンドを打たなくてもTaskがすでに用意されているので、
キャプチャにあるsigningReportを実行するとSHA1などを取得できます。
スクリーンショット 2019-05-05 0.58.09.png
スクリーンショット 2019-05-03 22.27.56.png

FCMの導入(PUSH通知を受信するロジックの実装)

ガイド を読むと、AndroidManifestの設定やServiceの実装についてわかります。
わからない部分やソースの全体像を確認したい場合は、サンプルが用意されています。
https://github.com/firebase/quickstart-android/tree/master/messaging

わからなかった、使わなかった部分

Google Play 開発者サービスのチェック
使わなかったですが、理解を深めようとした時にこちらの記事が参考なりました。
GoogleMapなどを未使用だったので、チェックは未実装にしましたが、やっておいた方がよかったのかも。

AndroidアプリでGoogle Play Serviceの使用可否を検証する

自動初期化を禁止する

ベストプラクティスを知りたい部分

/**
 * Called if InstanceID token is updated. This may occur if the security of
 * the previous token had been compromised. Note that this is called when the InstanceID token
 * is initially generated so this is where you would retrieve the token.
 */
override fun onNewToken(token: String?) {
    Log.d(TAG, "Refreshed token: $token")

    // If you want to send messages to this application instance or
    // manage this apps subscriptions on the server side, send the
    // Instance ID token to your app server.
    sendRegistrationToServer(token)
}

サンプルコードの sendRegistrationToServer で 該当するユーザーやグループにのみメッセージを送ったりするためにTokenをアプリケーションサーバに送るのですが、このタイミングで通信が失敗した場合はどこでリカバリするべきなのか?今回はアプリ起動時にリカバリ処理(インスタンスIDの再送信)を入れましたが、ベストプラクティスがあれば教えていただきたいです。。

PUSH通知を受信する

https://firebase.google.com/docs/cloud-messaging/android/receive
データの処理自体は前述のサンプルにもコードがのっているので、それほど難しくないように思えます。
メッセージの処理 にある表のパターンを読んで、
それぞれのパターンごとに挙動を理解することが大事だと思います。

今回できなかったこと

サンプルベースの開発だったため、トピック、デバイスグループを使用しませんでした。
実際は配信の目的やテストを考慮するとデバイスグループも使用するべきですし、
そもそもFirebaseコンソールを使って都度手動の送信を行うのではなく、
サーバ側のアプリケーションも準備しておいた方が良いと思いました。

まとめ

覚書として、あとでやった時に忘れている・困りそうな部分を中心にメモしましたが、
Firebase自体は内容も大きいのでやっぱり細かい部分を忘れていたり(実装内容というより何でそうしたか)、
コードや参考URLを丁寧にまとめようとすると時間がかかりすぎるので、こまめにまとめようと思いました。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidの開発環境構築

まえがき

AndroidStudioインストールからHello World!までやってみた。
思ってた以上に問題が起こらずにできたので、
あまりほかの人には参考にならないかもしれない。

構築環境

  • Windows Home 64bit
  • AndroidStudio 3.4

Androidの開発環境構築

AndroidStudioのインストール

https://developer.android.com/studio

DOWNLOAD ANDROID STUDIO のボタンでインストーラーをダウンロード
android.PNG

ダウンロードしたexeを起動してインストール開始
最初はとりあえずNEXTで進めていく
android2.PNG

NEXTで進めていく
android3.PNG

NEXTで進めていく
android4.PNG

NEXTで進めていく

(ショートカットはいらないかなと思って、チェックをつけてインストールしたら
どこにexeがあるのかわからなくなったので、チェックつけずにインストールしたほうがいいかもしれない)
android5.PNG

インストール終わるまで待機してから、NEXTで進める
android6.PNG

とりあえずのインストールが完了
そのまま起動してみる。
android7.PNG

初回起動

なんか出たけどよくわからないのでそのままOK
android8.PNG

「情報送ってねー」ってことだと思うので適当に選んで進める
android9.PNG

NEXTで進める
android10.PNG

よくわからないところはそのままNEXTで進める
android11.PNG

どちらか好きな色選べるみたいなので、
とりあえず黒い方を選んで進める
android12.PNG
android13.PNG

設定が終わったみたいなのでfinish
android14.PNG

何かのダウンロードが始まる
android15.PNG

終わったらfinishで次に進む
android16.PNG

Hello World!

Android Studio が立ち上がるので、

[Start a new Android Studio project] から

新しいプロジェクトを立ち上げる

android17.PNG

とりあえず[Empty Activity]を選択してNEXT
android18.PNG

LanguageがKotlinになっていることを確認して、
ほかは、お試しなのでそのままでFinish
android19.PNG

開発画面(らしきもの)が開く
下の領域を見るとなにかダウンロード?しているようなので、そのまま待機
android20.PNG

確認が出てくるので、許可を出す
android21.PNG

しばらく待つと、少しエラー?が出ているものの、完了したらしい
android22.PNG

右上のアイコンが並んでいる中から右向き三角(▶)をクリック

すると、使用するデバイスの選択画面が出る

実機持ってないので仮想デバイスの準備をする

[Create New Virtual Device]をクリック

android23.PNG

エミュレータの画面サイズ(機種)を選択
とりあえず、そのままPixel2で、作ってみる
android24.PNG

エミュレータのAndroidOSのバージョンの選択画面

せっかくなので一番新しいQを選ぶ

DownLoadをクリックしてつかう準備をする

android25.PNG

同意してNextで進む
android26.PNG

インストールが終わったらfinish
android27.PNG

再び、OSのバージョン選択画面で、
ダウンロードしたQを選択した状態で、NEXTで進める
android28.PNG

デバイス名等々の設定画面

とりあえずそのままFinis

android29.PNG

使用するデバイスの選択画面に戻って、
さっき作った仮想デバイスを選択してOK
android30.PNG

なんかないけど入れる?って聞かれるので、とりあえず入れとく
android31.PNG

インストール終わったらFinish
android32.PNG

ビルドは正常に終わったけど、

仮想デバイスが起動に失敗している、、、?
android33.PNG

アイコン並んでいるところから
端末とドロイド君のアイコンをクリック
android34.PNG

仮想デバイスの画面を開いて、

作成した仮想デバイスのActionの列にある右向き▶アイコンをクリックして

仮想デバイスを起動
android35.PNG

警告は出たものの起動した。
android36.PNG

仮想デバイスを起動したまま、
再び、開発環境からプログラムを実行

無事、Hello World! が表示された。
android37.PNG

あとがき

AndroidStudioインストールからHello World!まで、
なんとなくで進めて普通に動いた。

いままで何回か環境構築やったけど、大体一回は環境構築時点で挫折していたので、
すんなりいかないだろうから、備忘録かねて記録取ろう記事作成しましたが、
あまり意味のないものになったかもしれない。

今回はとりあえず動けばいいやと思い、いろいろ設定をすっ飛ばして構築したので
追々、設定も調べて追記(もしくは別記事)できたらと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アンインストールに失敗して「INSTALL_FAILED_UPDATE_INCOMPATIBLE」と出てきた時の復旧方法

エラー画面.png
コマンドプロンプトからアプリをインストールしようとして失敗している様子

症状

デバッグ中にアプリをリセットしたいと思い、GooglePlayの「アンインストール」ボタンを押してアンインストールを開始しました。
しかし、「アンインストール中...」のまま画面が変わらなくなり、全く終了する気配がしなかったので一度GooglePlayを強制終了したら、今度はインストールできないという事態にハマってしまいました...
設定のアプリ一覧にも表示されず、インストールもできず、本当に困りましたが、検索したらいろいろやり方が書いてあったのでまとめてみます。

失敗した復旧方法

正しく復旧できた方法は下の「復旧方法」に書いています

1.GooglePlayから入れなおしてみる


ちょうどGooglePlayにリリースした自分のアプリだったので、アプリのページを開いて「インストール」ボタンを押してみるも...
「他のユーザーが既にこのデバイスに互換性のないバージョンをインストールしているため、このアプリケーションをインストールできません」
と表示されてインストールできませんでした。

2.Apkファイルから直接インストールしてみる

ビルドしたApkファイルを端末に送って、開いて「インストール」ボタンを押してみるも...
「このアプリはインストールできません」
と表示されてまたもインストール失敗。

3.コマンドプロンプトでアプリをアンインストールする

エラー画面.png
USBデバッグを許可したスマホをPCに接続し、「adb uninstall rabbitp.sns.notifisor(アプリのID)」とコマンドプロンプトで入力して実行してみましたが、「Exception occurred while executing:」の後に何行もエラーが返ってきました。

「adb install "C:\androidstudio-pc\Notification Sorting\Playストア用\app_v1.7.apk"(アプリのパス)」と入力してインストールも試みましたが、「adb: failed to install C:\androidstudio-pc\Notification Sorting\Playストア用\app_v1.7.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package rabbitp.sns.notifisort signatures do not match previously installed version; ignoring!]」とエラーが返ってきて出来ませんでした。

復旧方法

成功.png
1.Android Studioでアプリのプロジェクトを開く

2.左のタブにある「ビルド・バリアント」(私は開発環境の言語を日本語にしています。名前はバージョンによって異なる可能性あり。)をクリックする。

3.タブが開いたら、「ビルド・バリアント」を「build」から「release」に変更する

4.ビルドする!

5.一度アプリをアンインストールしてもいいですか?的なダイアログが出たら、「OK」(ボタンに色がついている方)をクリックする。

6.見事アプリのインストール成功。

直ってよかったです。
初期化しないとダメかと思いました...

その後...

この後すぐにまた同じ状況になりましたが、今度はapkファイルを直接インストールしたらあっさり直りました。
状況によって復旧方法が異なるようです。
上に書いた復旧方法すべてを試してみることをお勧めします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アンインストールに失敗して「INSTALL_FAILED_UPDATE_INCOMPATIBLE」と出てきた時の復旧方法(アプリをインストール/アンインストールできなくなった時)

エラー画面.png
コマンドプロンプトからアプリをインストールしようとして失敗している様子

症状

デバッグ中にアプリをリセットしたいと思い、GooglePlayの「アンインストール」ボタンを押してアンインストールを開始しました。
しかし、「アンインストール中...」のまま画面が変わらなくなり、全く終了する気配がしなかったので一度GooglePlayを強制終了したら、今度はインストールできないという事態にハマってしまいました...
設定のアプリ一覧にも表示されず、インストールもできず、本当に困りましたが、検索したらいろいろやり方が書いてあったのでまとめてみます。

失敗した復旧方法

正しく復旧できた方法は下の「復旧方法」に書いています

1.GooglePlayから入れなおしてみる


ちょうどGooglePlayにリリースした自分のアプリだったので、アプリのページを開いて「インストール」ボタンを押してみるも...
「他のユーザーが既にこのデバイスに互換性のないバージョンをインストールしているため、このアプリケーションをインストールできません」
と表示されてインストールできませんでした。

2.Apkファイルから直接インストールしてみる

ビルドしたApkファイルを端末に送って、開いて「インストール」ボタンを押してみるも...
「このアプリはインストールできません」
と表示されてまたもインストール失敗。

3.コマンドプロンプトでアプリをアンインストールする

エラー画面.png
USBデバッグを許可したスマホをPCに接続し、「adb uninstall rabbitp.sns.notifisor(アプリのID)」とコマンドプロンプトで入力して実行してみましたが、「Exception occurred while executing:」の後に何行もエラーが返ってきました。

「adb install "C:\androidstudio-pc\Notification Sorting\Playストア用\app_v1.7.apk"(アプリのパス)」と入力してインストールも試みましたが、「adb: failed to install C:\androidstudio-pc\Notification Sorting\Playストア用\app_v1.7.apk: Failure [INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package rabbitp.sns.notifisort signatures do not match previously installed version; ignoring!]」とエラーが返ってきて出来ませんでした。

復旧方法

成功.png
1.Android Studioでアプリのプロジェクトを開く

2.左のタブにある「ビルド・バリアント」(私は開発環境の言語を日本語にしています。名前はバージョンによって異なる可能性あり。)をクリックする。

3.タブが開いたら、「ビルド・バリアント」を「build」から「release」に変更する

4.ビルドする!

5.一度アプリをアンインストールしてもいいですか?的なダイアログが出たら、「OK」(ボタンに色がついている方)をクリックする。

6.見事アプリのインストール成功。

直ってよかったです。
初期化しないとダメかと思いました...

その後...

この後すぐにまた同じ状況になりましたが、今度はapkファイルを直接インストールしたらあっさり直りました。
状況によって復旧方法が異なるようです。
上に書いた復旧方法すべてを試してみることをお勧めします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む