20200430のAndroidに関する記事は8件です。

[Kotlin]Firebase Remote Configの実装

 はじめに

  • 公式はこちら
  • メンテナンスモードや強制アップデートの実装に使える

画面

Firebase consoleでRemoteConfigの設定

スクリーンショット 2020-05-01 11.09.34.png

FirabseConsoleのRemoteConfigで設定する値は2つの為、それぞれ表示する用のTextViewを2つ用意
スクショは実装結果です

File.jpg

actvity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/main_first_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="first text" />

    <TextView
        android:id="@+id/main_second_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="second text" />

</LinearLayout>

実装

build.gradle(app)
dependencies {
   〜

    // Remote config
    implementation 'com.google.firebase:firebase-config:19.1.4'
    // Remote Config (Kotlin)
    implementation 'com.google.firebase:firebase-config-ktx:19.1.4'

Activity

MainActivity.kt
class MainActivity : AppCompatActivity() {

    private lateinit var remoteConfig: FirebaseRemoteConfig

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // firebase-config-ktx により簡潔に書ける
        remoteConfig = Firebase.remoteConfig

        // debug modeの有効化
        val configSettings = remoteConfigSettings {
            minimumFetchIntervalInSeconds = 3600 // fetch間隔 。頻繁に取得できないので、debug時は短く設定する
        }
        remoteConfig.setConfigSettingsAsync(configSettings)

        // デフォルト値設定
        remoteConfig.setDefaultsAsync(R.xml.remote_config_defaults)

        // apply default
        main_first_text.text = remoteConfig[LOADING_PHRASE_CONFIG_KEY].asString()
        main_second_text.text = remoteConfig[LOADING_PHRASE_CONFIG_KEY].asString()

        fetchWelcome()
    }

    private fun fetchWelcome() {
        remoteConfig.fetchAndActivate()
            .addOnCompleteListener(this) { task ->
                if (task.isSuccessful) {
                    val updated = task.result
                    Timber.d("Config params updated: $updated")
                    Toast.makeText(this, "Fetch and activate succeeded", Toast.LENGTH_SHORT).show()
                } else {
                    Toast.makeText(this, "Fetch failed", Toast.LENGTH_SHORT).show()
                }
                displayWelcomeMessage()
            }
    }

    private fun displayWelcomeMessage() {
        // 失敗してたらxmlより反映。成功してたら firebase console にて設定した値
        main_first_text.text = remoteConfig.getBoolean(WELCOME_MESSAGE_CAPS_KEY).toString()
        main_second_text.text = remoteConfig.getString(WELCOME_MESSAGE_KEY)
    }

    companion object {
        // Remote Config keys
        private const val LOADING_PHRASE_CONFIG_KEY = "loading_phrase"
        private const val WELCOME_MESSAGE_CAPS_KEY = "welcome_message_caps"
        private const val WELCOME_MESSAGE_KEY = "welcome_message"
    }
}

 所感

ソースコードはこちら

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

AndroidでPush通知を表示する

image.png

Firebase Cloud Messaging(FCM)を使って AndroidでPush通知を実装したときのメモです。(2020/4/29時点)

Firebase Cloud Messagingとは

Firebase Cloud Messaging(FCM)は、メッセージを無料で確実に配信するためのクロスプラットフォーム メッセージング ソリューションです。以前は Google Cloud Messaging API が使われていたようですが、現在はFCMに移行しています。
image.png

FCMで端末にメッセージを送信すると、スリープ画面でも通知メッセージを表示できます。
image.png

Push通知の実装なおおまかな流れ

  1. Androidアプリの作成 - Android Studio
  2. Firebaseプロジェクトの作成 - Firebaseコンソール
  3. Androidアプリでサービスを実装 - Android Studio
  4. テスト通知 - Firebaseコンソール

それでは、順番にやっていきます。

Androidアプリの作成

最初にAndroid Studioでスケルトンアプリを作成します。

  1. Android StudioでEmpty Activityプロジェクトを作成
    ここで指定したPackage nameは後ほどFirebaseプロジェクトに設定します。
    image.png

  2. Firebaseのデバッグ用証明書キーの取得
    Firebaseプロジェクトの作成時にSHA-1が必要になります。画面右のGradleタブを引き出して、app > Tasks > singningReportを右クリックして「Run」をクリックします。
    image.png

    Buildペインにキーストア情報が表示されます。ここで表示されたSHA-1は後ほどFirebaseプロジェクトに設定します。
    image.png

Firebaseプロジェクトの作成

  1. Firebaseのコンソールから新規にプロジェクトを作成
    Firebaseコンソール
    https://console.firebase.google.com/
    image.png

  2. プロジェクト名を指定
    image.png

  3. ひとまずアナリティクスは無効に
    image.png

  4. 作成完了
    image.png

  5. Androidアプリを追加
    左ペインにあるCloud Messagingをクリックします。
    image.png

  6. Androidのアイコンをクリック
    image.png

  7. Android Studioで作成したアプリのパッケージ名を設定
    image.png

  8. 設定ファイルをダウンロード
    image.png

    ダウンロードした設定ファイルgoogle-services.jsonをAndroid Studioに追加します。
    Finderで設定ファイルをコピー(command+C)し、プロジェクトのappフォルダーにフォーカスを当てた状態でペースト(command+V)します。
    image.png

  9. AndroidアプリにFirebaseを追加
    あとで設定するので次に進みます。
    image.png

  10. Firebaseの設定完了
    image.png

Androidアプリの設定

  1. プロジェクトレベルのbuild.gradleを修正
    dependenciesに classpath 'com.google.gms:google-services:4.3.3'を追加します。

    build.gradle(Project)
    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    
    buildscript {
    
        repositories {
            google()
            jcenter()
    
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:3.6.3'
    
            // Add this line
            classpath 'com.google.gms:google-services:4.3.3'
    
            // NOTE: Do not place your application dependencies here; they belong
            // in the individual module build.gradle files
        }
    }
    
    allprojects {
        repositories {
            google()
            jcenter()
    
        }
    }
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    
  2. アプリレベルのbuild.gradleを修正
    dependenciesに implementation 'com.google.firebase:firebase-messaging:20.0.0' を追加します。
    必要なライブラリは使用可能なライブラリの一覧(https://firebase.google.com/docs/android/setup#available-libraries) の「Cloud Messaging」の項目を確認します。

    デバイス登録トークンを取得するために、getToken()を呼び出す場合は、末尾に apply plugin: 'com.google.gms.google-services'を追加しておきます。

    build.gradle(app)
    apply plugin: 'com.android.application'
    
    android {
        compileSdkVersion 29
        buildToolsVersion "29.0.1"
    
        defaultConfig {
            applicationId "jp.yuppe.push1"
            minSdkVersion 24
            targetSdkVersion 29
            versionCode 1
            versionName "1.0"
    
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        }
    
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
    
    }
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
    
        implementation 'androidx.appcompat:appcompat:1.1.0'
        implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    
        // Add this line
        implementation 'com.google.firebase:firebase-messaging:20.0.0'
    
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'androidx.test.ext:junit:1.1.1'
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    }
    
    // Add this line
    apply plugin: 'com.google.gms.google-services'
    

    修正したら「Sync now」をクリックします。

  3. FirebaseMessagingServiceを継承したMyFirebaseMessagingServiceクラスを追加
    Firebaseのガイド(https://firebase.google.com/docs/cloud-messaging/android/receive) を参考にサービスクラスを追加します。

    MyFirebaseMessagingService.java
    package jp.yuppe.push1;
    
    import android.app.NotificationChannel;
    import android.app.NotificationManager;
    import android.app.PendingIntent;
    import android.content.Context;
    import android.content.Intent;
    import android.media.RingtoneManager;
    import android.net.Uri;
    import android.os.Build;
    import android.util.Log;
    import android.widget.Toast;
    
    import androidx.core.app.NotificationCompat;
    
    import com.google.firebase.messaging.FirebaseMessagingService;
    import com.google.firebase.messaging.RemoteMessage;
    
    public class MyFirebaseMessagingService extends FirebaseMessagingService {
        private static final String TAG = MyFirebaseMessagingService.class.getSimpleName();
    
        @Override
        public void onNewToken(String token) {
            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);
    
        }
    
        @Override
        public void onMessageReceived(RemoteMessage remoteMessage) {
            // ...
    
            // TODO(developer): Handle FCM messages here.
            // Not getting messages here? See why this may be: https://goo.gl/39bRNJ
            Log.d(TAG, "From: " + remoteMessage.getFrom());
    
            // Check if message contains a data payload.
            if (remoteMessage.getData().size() > 0) {
                Log.d(TAG, "Message data payload: " + remoteMessage.getData());
    
                if (/* Check if data needs to be processed by long running job */ true) {
                    // For long-running tasks (10 seconds or more) use WorkManager.
                    //scheduleJob();
                } else {
                    // Handle message within 10 seconds
                    //handleNow();
                }
    
            }
    
            // Check if message contains a notification payload.
            String messageBody = "";
            if (remoteMessage.getNotification() != null) {
                messageBody = remoteMessage.getNotification().getBody();
                Log.d(TAG, "Message Notification Body: " + messageBody);
            }
    
            // Also if you intend on generating your own notifications as a result of a received FCM
            // message, here is where that should be initiated. See sendNotification method below.
    
            sendNotification(messageBody);
        }
    
        /**
        * Create and show a simple notification containing the received FCM message.
        *
        * @param messageBody FCM message body received.
        */
        private void sendNotification(String messageBody) {
            Intent intent = new Intent(this, MainActivity.class);
            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
                    PendingIntent.FLAG_ONE_SHOT);
    
            //String channelId = getString(R.string.default_notification_channel_id);
            String channelId = "001";
            Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
            NotificationCompat.Builder notificationBuilder =
                    new NotificationCompat.Builder(this, channelId)
                            //.setSmallIcon(R.drawable.ic_stat_ic_notification)
                            .setSmallIcon(R.drawable.ic_launcher_foreground)
                            .setContentTitle("Push1 App")
                            .setContentText(messageBody)
                            .setAutoCancel(true)
                            .setSound(defaultSoundUri)
                            .setContentIntent(pendingIntent);
    
            NotificationManager notificationManager =
                    (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
    
            // Since android Oreo notification channel is needed.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                NotificationChannel channel = new NotificationChannel(channelId,
                        "Channel human readable title",
                        NotificationManager.IMPORTANCE_DEFAULT);
                notificationManager.createNotificationChannel(channel);
            }
    
            notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
        }
    }
    
  4. AndroidManifest.xmlにサービスを登録

    AndroidManifest.xml
    <?xml version="1.0" encoding="utf-8"?>
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="jp.yuppe.push1">
    
        <application
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
            <activity android:name=".MainActivity">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
    
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    
            <!-- add ここから -->
            <service
                android:name=".MyFirebaseMessagingService"
                android:exported="false">
                <intent-filter>
                    <action android:name="com.google.firebase.MESSAGING_EVENT" />
                </intent-filter>
            </service>
            <!-- add ここまで -->
    
        </application>
    
    </manifest>
    
  5. これで完成
    Androidアプリを実行するとPush通知が受け取れるようになります。

テストメッセージを送信

Firebaseコンソール(https://console.firebase.google.com/) でテスト用の通知メッセージを作成します。
image.png

メッセージを送信するとAndroidアプリにPush通知が表示されます。
image.png

アプリがバックグランドにまわっている、アプリがKillされている、端末がスリープ状態のいづれも通知を受け取ることができます。アプリがフォアグラウンド以外の場合はFMCによって自動的に通知が処理されます。

デバイス登録トークンを取得する

通知対象を単一のデバイスを対象にする場合、またはデバイス グループを作成する場合は、FirebaseMessagingService を拡張して onNewToken をオーバーライドすることで、このトークンにアクセスする必要があります。

MainActivity.java
public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        FirebaseInstanceId.getInstance().getInstanceId()
                .addOnCompleteListener(new OnCompleteListener<InstanceIdResult>() {
                    @Override
                    public void onComplete(@NonNull Task<InstanceIdResult> task) {
                        if (!task.isSuccessful()) {
                            Log.w(TAG, "getInstanceId failed", task.getException());
                            return;
                        }

                        // Get new Instance ID token
                        String token = task.getResult().getToken();

                        // Log and toast
                        String msg = "token:" + token;
                        Log.d(TAG, msg);
                        Toast.makeText(MainActivity.this, msg, Toast.LENGTH_SHORT).show();
                    }
                });
    }
}

例外が発生したら

この例外が出る場合は、プロジェクトの設定が不十分な場合に発生します。

Default FirebaseApp is not initialized in this process <パッケージ名>. Make sure to call FirebaseApp.initializeApp(Context) first.

アプリレベルのbuild.gradleに、 apply plugin: 'com.google.gms.google-services' が追加されているか確認してください。これは公式のチュートリアルに書かれていますので、参考にしてください。

curlでメッセージを投げる

POSTでリクエストを投げるときに、「サーバーキー」が必要になります。Firebaseコンソールのプロジェクト認証情報で確認しておきます。
image.png

2つタイプのメッセージを送信できます。
- 通知メッセージ: 「表示メッセージ」とみなされることもあります。FCM SDK によって自動的に処理されます。
- データ メッセージ: クライアント アプリによって処理されます。

参考: https://firebase.google.com/docs/cloud-messaging/concept-options

通知メッセージ

curl \
-X POST \
--header "Authorization: key=<your_server_key>" \
--Header "Content-Type: application/json" \
https://fcm.googleapis.com/fcm/send \
-d "{ \
  \"to\":\"<device_token>\",
  \"notification\":{ \
    \"title\" : \"FCM Message\" \
    \"body\" : \"Hello FCM\" \
  }, \
  \"priority\":10  \
}"

データメッセージ

curl \
-X POST \
--header "Authorization: key=<your_server_key>" \
--Header "Content-Type: application/json" \
https://fcm.googleapis.com/fcm/send \
-d "{ \
  \"to\":\"<device_token>\",
  \"data\":{ \
    \"key1\" : \"val1\", \
    \"key2\" : \"val2\", \
    \"key3\" : \"val3\" \
  }, \
  \"priority\":10  \
}"

公式ドキュメント

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

startActivityForResult / requestPermissions が deprecated になる話

はじめに

影響範囲が大きそうなのでまとめておきます。
※本記事は Medium にも投稿済みです。 Qiita の方が見る人が多そうなのでこちらにも投稿しました。

androidx.fragment:fragment-ktx:1.3.0-alpha04 から、以下のメソッドが deprecated になります。

  • startActivityForResult
  • onActivityResult
  • requestPermissions
  • onRequestPermissionsResult

公式ソース

代わりに 新しく追加された Activity Result APIs を使えとのアナウンスがあります。

Getting a result from an activity

While the underlying startActivityForResult() and onActivityResult() APIs are available on the Activity class on all API levels, it is strongly recommended to use the Activity Result APIs introduced in AndroidX Activity 1.2.0-alpha02 and Fragment 1.3.0-alpha02.

どうやら、ActivityResultContract の 「registerForActivityResult」 を使えば良いようです。
そこで、registerForActivityResult の使い方をまとめました。

リポジトリ

以下にサンプルリポジトリがあります。
https://github.com/nanaten/Activity-Contract-Example

使い方

build.gradle

build.gradleに以下を追加します。

build.gradle
implementation "androidx.activity:activity-ktx:1.2.0-alpha04"
implementation "androidx.fragment:fragment-ktx:1.3.0-alpha03"

シンプルな使い方

シンプルにActivityの結果だけを受け取りたい場合は以下のように書けます。

MainActivity
// 普段どおりIntentを生成する
val intent = Intent(this, SecondActivity::class.java)

// registerForActivityResult を定義する
val launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
    // 呼び出し先のActivityを閉じた時に呼び出されるコールバック
    if(result.resultCode == Activity.RESULT_OK) {
        // RESULT_OK時の処理
        val intent = result.data
        intent?.getStringExtra("result_key")
    }
}

// Activityを起動
launcher.launch(intent)

registerForActivityResultActivityResultContracts.StartActivityForResult() を引数として渡すことによって、 ActivityResult 型の結果をコールバックで受け取ることが出来ます。

ActivityResult には resultCode ( onActivityResult の resultCode と同様)と、 data (Intent) というプロパティがあるので、そちらから結果を処理します。

呼び出し先のActivityは以前と変わりません。

SecondActivity
intent.putExtra("result_key", "result")
// 結果をセット
setResult(Actiivty.RESULT_OK, intent)
finish()

カスタムの ActivityResultContracts

ActivityResultContracts を継承した class を定義することで、カスタムの ActivityResultContracts を利用することもできます。

ActivityResultContracts 継承クラスの作成

今回はobjectで定義しました。

ResultContracts
object ResultContracts: ActivityResultContract<Int, String?>() {
    // Activity起動時の処理
    override fun createIntent(context: Context, input: Int): Intent {
        val intent = Intent(context, SecondActivity::class.java)
        intent.putExtra("select_id", input)
        return intent
    }

    // 結果受け取り時の処理
    override fun parseResult(resultCode: Int, intent: Intent?): String? {
        if(resultCode == Activity.RESULT_OK) {
            return intent?.getStringExtra("result_key")
        }
        return null
    }
}

class に ActivityResultContract<{Inputの型}, {Outputの型}> の形で継承させます。

作成したクラスには、Activityを起動する時に呼ばれる createIntent() とActivityから結果を受け取る時に呼ばれる parseResult() をoverrideする必要があります。

createIntent() には registerForActivityResult.launch() が呼び出された時の処理を、 parseResult() には Activityから結果を受け取った時の処理を書きます。

あとは、作成した class を registerForActivityResult に渡してあげます。

val launcher = registerForActivityResult(ResultContracts) { result ->
    // 受け取った結果を処理する
    text_view.text = result 
}

launcher.launch(0) // InputにInt型を定義したのでInt型の引数を渡す

どんな時に使う?

上記ではInputとOutputで定義した型をそのまま渡しているため恩恵はありませんが、Activityから受け取った値を加工してから渡したい場合などに便利です。

object ResultContracts: ActivityResultContract<Int, User?>() { // Outputの型をUser型に修正
    // Activity起動時の処理
    ...

    // 結果受け取り時の処理
    override fun parseResult(resultCode: Int, intent: Intent?): User? {
        if(resultCode == Activity.RESULT_OK) {
            // Activityから文字列を受け取る
            val text = intent?.getStringExtra("result_key")
            // 受け取った値を独自クラスに詰め替えて返す
            val user = User(name = text)
            return user
        }
        return null
    }
}
val launcher = registerForActivityResult(ResultContracts) { result: User? ->
    // OutputをUser?型で定義したのでUser?型が返ってくる
    text_view.text = result?.name
}

launcher.launch(0) // Inputは変えていないのでInt型のまま

requestPermissions

requestPermissions も ActivityResultContracts を利用して代替可能です。
ActivityResultContracts.RequestPermission という関数が用意されているので、 registerForActivityResult に引数として渡してあげます。

val requestPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { result ->
    if(result) {
        // リクエスト許可時の処理
        Toast.makeText(this, "Permission Accepted.", Toast.LENGTH_SHORT).show()
    }
    else {
        // リクエスト拒否時の処理
        Toast.makeText(this, "Permission Denied.", Toast.LENGTH_SHORT).show()
    }
}

// アクセスしたい権限を引数に渡す
requestPermission.launch(Manifest.permission.ACCESS_FINE_LOCATION)

コールバックの引数(result)はBoolean型です。権限が許可された場合 true が、拒否された場合は false が渡されてきます。

AndroidManifest.xmlに要求する権限を記載する必要があるのはいつも通りです。

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

注意点

すたぜろさんのこちらの記事の注意点は registerForActivityResult の場合にも当てはまると思われます。 prepareCall -> registerForActivityResult に置き換えて読んでください。

(このあたりは自分で検証出来てません。すみません)

おわりに

Activityへの値の受け渡しは長い間 startActivityForResult が使われてきましたが、ここにきて大きな変更が加わりました。

これから新規作成するアプリは、将来的に registerForActivityResult へ置き換えることを念頭に入れて作る必要があると思われます。

本記事を書くにあたり、以下の記事を参考にさせて頂きました。

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

ExoPlayerで、暗号化されたHLSを任意のkeyで復号・再生するには

※この記事はDRMが施されたHLSの復号についてではなく、AES等で暗号化されたHLSを復号する方法について記載しています。

HLSの暗号化の仕様

HLSストリームの暗号化に関する情報はm3u8ファイルの#EXT-X-KEYに記載されています。
例えば、以下のファイルでは暗号化の方法・暗号化キーのurl・初期化ベクトルの値がわかります。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:8
#EXT-X-MEDIA-SEQUENCE:1653
#EXT-X-DISCONTINUITY-SEQUENCE:36
#EXT-X-KEY:METHOD=AES-128,URI="https://hogehoge",IV=0xdad51a9c61cc4344c03a566980ac21e7
#EXTINF:8.333333,
/ts/CD8xZLiy68pVp3/h264/480/a1qLvavaVwX.ts
#EXTINF:8.333334,
/ts/CD8xZLiy68pVp3/h264/480/LAAK1hXHVLa.ts
#EXTINF:8.333333,
/ts/CD8xZLiy68pVp3/h264/480/KXzsNPf8KYx.ts
#EXTINF:8.333333,
/ts/CD8xZLiy68pVp3/h264/480/JmHJgZGPZQA.ts

このキーファイルのURIにロカールUriを記載してExoPlayerで再生することはできませんが、キーファイルをData URI schemeに変換して記載すればExoPlayerで再生することができます(参考)。
つまり、HLSを暗号化したうえで、暗号化キーのUrlではなくキーそのものをm3u8ファイルにベタ書きしてストリームを配信・再生することが可能だということです。

パーサをカスタマイズする

HLSに限らず、ExoPlayerではDataSourceを自由にinjectすることが可能です。
ここではデフォルトで使用されているHlsPlaylistParserの代わりに自前でカスタマイズしたHLSMediaParserを使用してExoPlayerを初期化します。

class CustomHlsPlaylistParserFactory() : HlsPlaylistParserFactory  {

    override fun createPlaylistParser(): ParsingLoadable.Parser<HlsPlaylist> =
        CustomHlsPlaylistParser()

    override fun createPlaylistParser(masterPlaylist: HlsMasterPlaylist): ParsingLoadable.Parser<HlsPlaylist> =
        CustomHlsPlaylistParser(masterPlaylist)
}

class CustomHlsPlaylistParser() : ParsingLoadable.Parser<HlsPlaylist> {


    @Throws(IOException::class)
    override parse(uri: Uri, inputStream: InputStream): HlsPlaylist {
     // パーサを実装
     // HlsPlaylistParser#parseMediaPlaylist`のfullSegmentEncryptionKeyUri`を任意のData Uri schemeに書き換えます
    }


    private fun toDataUriScheme(file: File): String {
        val base64str = Base64.encodeToString(file.readBytes(), Base64.NO_WRAP)//NO_PADDINGは使用できません
        return "data:text/plain;base64,$base64str"
    }
}

fun initPlayer(remoteUrl: String): HlsMediaSource {
    val userAgent = WebView(getApplication()).settings.userAgentString
    val hppf = CustomHlsPlaylistParserWrapper(getApplication(), this)
    val dsf = DefaultDataSourceFactory(getApplication(), userAgent)//DefaultDataSourceFactory
    val mediaSource =  HlsMediaSource.Factory(dsf)
        .setPlaylistParserFactory(hppf)
        .createMediaSource(remoteUrl.toUri())
    player.prepare(mediaSource)
    player.playWhenReady = true
}

なお、HLSの仕様書によればAES-128ではキーのパディングを省略できないようです。

An encryption method of AES-128 signals that Media Segments are completely encrypted using the Advanced Encryption Standard (AES) [AES_128] with a 128-bit key, Cipher Block Chaining (CBC), and Public-Key Cryptography Standards #7 (PKCS7) padding [RFC5652].

また、DataSourceFactoryにはDefaultHttpDataSourceFactoryではなくDefaultDataSourceFactoryを用います(DRMを使用する場合はDataSchemeDataSource.Factory)。もしDefaultHttpDataSourceFactoryを使用すると、キーのURIがUrlとして扱われてしまい、MalformedURLExceptionがスローされます。

これで任意のキーに書き換えることができました。

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

FragmentでActionBarのTitleを設定する方法

FragmentでActionBarのTitleを設定する方法

すぐ忘れるのでメモ。

Navigationでもっとかっこよく出来るはずだが。。

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        (activity as AppCompatActivity).supportActionBar?.title = "タイトル"
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android - AsyncTaskの直列処理と並列処理の順番は保証されるのか??

Android開発において、AsyncTaskで直列処理と並列処理について、ほぼ同時に動いた時はどのような動きになるのか、実際に順番は保証されるのかわからなかったので、軽くコードを組んでみて確認してみましたので、メモとして残しておきたいと思います。

【参考URL】
developer - AsyncTask
Android - AsyncTaskの直列処理?並列処理?
AsyncTaskの非同期処理はどのように実現しているのか

【試作1】タスク1:直列処理、タスク2:並列処理の場合

task1:execute()
task2:executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)

// 1つ目の非同期タスク
AsyncTask task1 = new AsyncTask() {

    @Override
    protected Object doInBackground(Object[] objects) {
        Log.v("task1", "start!!!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onPostExecute(Object o) {
        super.onPostExecute(o);
        Log.v("task1", "done!!!");
    }
}
.execute(); 

// 2つ目の非同期タスク
AsyncTask task2 = new AsyncTask() {

    @Override
    protected Object doInBackground(Object[] objects) {
        Log.v("task2", "start!!!");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onPostExecute(Object o) {
        super.onPostExecute(o);
        Log.v("task2", "done!!!");
    }
}
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

実行結果

順番通りになっているかを確認するため、数回実行してみました。

【1回目】
5970-5995/com.example.testcode V/task2: start!!!
5970-5994/com.example.testcode V/task1: start!!!

5970-5970/com.example.testcode V/task2: done!!!
5970-5970/com.example.testcode V/task1: done!!!


【2回目】
7138-7157/com.example.testcode V/task2: start!!!
7138-7156/com.example.testcode V/task1: start!!!

7138-7138/com.example.testcode V/task2: done!!!
7138-7138/com.example.testcode V/task1: done!!!

【3回目】
7383-7403/com.example.testcode V/task1: start!!!
7383-7404/com.example.testcode V/task2: start!!!

7383-7383/com.example.testcode V/task2: done!!!
7383-7383/com.example.testcode V/task1: done!!!

2回目のスタートが逆になっていますね。。。

【試作2】タスク1:並列処理、タスク2:直列処理の場合

task1:executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR)
task2:execute()
※ソースコードは試作1とほぼ変わりません。

// 1つ目の非同期タスク
AsyncTask task1 = new AsyncTask() {

    @Override
    protected Object doInBackground(Object[] objects) {
        Log.v("task1", "start!!!");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onPostExecute(Object o) {
        super.onPostExecute(o);
        Log.v("task1", "done!!!");
    }
}
.execute(); 

// 2つ目の非同期タスク
AsyncTask task2 = new AsyncTask() {

    @Override
    protected Object doInBackground(Object[] objects) {
        Log.v("task2", "start!!!");
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onPostExecute(Object o) {
        super.onPostExecute(o);
        Log.v("task2", "done!!!");
    }
}
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);

実行結果

こちらも順番通りになっているかを確認するため、数回実行してみました。

【1回目】
7267-7273/com.example.testcode V/task2: start!!!
7267-7272/com.example.testcode V/task1: start!!!

7267-7267/com.example.testcode V/task2: done!!!
7267-7267/com.example.testcode V/task1: done!!!

【2回目】
7643-7662/com.example.testcode V/task1: start!!!
7643-7663/com.example.testcode V/task2: start!!!

7643-7643/com.example.testcode V/task2: done!!!
7643-7643/com.example.testcode V/task1: done!!!

【3回目】
7871-7891/com.example.testcode V/task1: start!!!
7871-7892/com.example.testcode V/task2: start!!!

7871-7871/com.example.testcode V/task2: done!!!
7871-7871/com.example.testcode V/task1: done!!!

こちらの実行結果も試作1と似たような結果となっています。

まとめ

直列処理と並列処理をほぼ同時に実行した場合の処理の順番は保証されないようです。
確実に順番を保証したい場合は、両方とも直列処理で行ったほうが良いですね。

最後に

それ違うぞ!っという点もあるかと思いますが、参考程度になれば幸いです。

以上です。

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

【Android】Dagger2 ~基本編

はじめに

前回の記事(DI(依存性の注入)とは依存性を注入するということである、、?)では
DIとは何かについて記述しました。

今回はDagger2を導入してみたので記事にまとめます。
言語はKotlinです!

参考にしたチュートリアルはこちらです。

準備

まず、build.gradleにてDagger2を導入します。
参照:Dagger README.md

build.gradle(app)
apply plugin: 'kotlin-kapt' // Kotlinでアノテーションを使用するために必要

dependencies {
    ..

    // 2020/4/26現在、
    // バージョンによって、使用できるアノテーションの種類が異なる.
    // また、2.25 ではコードが自動生成されない問題があることを確認済み.
    def dagger_version = 2.27 

    implementation "com.google.dagger:dagger:$dagger_version"
    annotationProcessor "com.google.dagger:dagger-compiler:$dagger_version"

    // Kotlinで開発する場合は必要.
    // KotlinでもDaggerに関するアノテーションを使用できるようにするためのもの.
    kapt "com.google.dagger:dagger-compiler:$dagger_version" 
}

Daggerの概要

Daggerとは、オブジェクト間の依存関係を管理するコードを自動生成してくれるツールです。

Daggerがコードを自動生成するために必要なこと:

  1. 提供するオブジェクトを把握する
  2. 注入先のオブジェクトを把握する
  3. 注入を実行する

1. 提供するオブジェクトを把握する

まずは、どのオブジェクトが何に依存しているのかを開発者が把握する必要がある。

どのオブジェクトを注入するのか

自分が依存関係を把握できたら、次はアノテーションを用いて「Dagger」にそれを教えてあげる

e.g.

@Module
class DatabaseModule {
    @Provides
    fun providePlayCallDao(): PlayCallDao {
        return App.database.playCallDao()
    }
}

@Provides

@Provides(提供)したいオブジェクトのインスタンス化メソッドにつけることで、何を@ProvideしたいかをDaggerに知らせる。

@Module

@Provideするためのメソッドを、@Moduleクラスにまとめる。

他の@Moduleも含めることができる。

e.g.

@Module
class DatabaseModule

@Module(includes = [DatabaseModule::class])
class RepositoryModule

ちょこっとまとめ

提供するオブジェクトをDaggerに知らせることができた!

-> これで、Daggerはコードを自動生成することができる。そして、@Provideするオブジェクトの生成方法を覚えてくれる。

2. 注入先のオブジェクトを把握する

Daggerは、1.の過程で何を注入するのかを把握することができた。
しかし、@Provideされるオブジェクトをどこで@Injectするのかは把握できていない。

-> どこで使うためのものなのかを知らせてあげよう!

注入したいタイミングで@Injectすれば良いが、後述の Field Injection を用いる場合は@Componentを作成する必要があります。
ここでは、その@Componentについて説明します。

@Component

@Componentを定義し、以下の役割を担わせる。
これは後述の Field Injection で使用します。

役割:

  • @Provideするオブジェクトを解放する
  • 目標のクラスに依存性を注入する

含まれるもの:

  • 使用する@Moduleの宣言
  • inject用メソッドのインターフェース
    このメソッドの引数に injectする対象のクラス(@Injectを記述するクラス)の型を設定します。
    ここで定義しておけば、あとは Dagger が処理内容を自動生成してくれます。
    inject()の使用方法は次節に持ち越します。

e.g.

@Component(modules = [DatabaseModule::class])
interface AppComponent {
    // Daoの注入
    fun inject(repository: PlayCallListRepository)
}

3. 注入を実行する

@Inject を用いて実装します。

@Inject

@Injectしたいオブジェクトを宣言している箇所につけます。
例は後述します。

Daggerが提供するDIの種類

Daggerが提供するDIの種類は基本的に以下の2つがあります。
*数字は優先度

  1. Constructor Injection
  2. Field Injection

優先度の理由と各種詳細を以下に記述します。

1. Constructor Injection

e.g.

class PlayCallRepository @Inject constructor(
    private val dao: PlayCallDao
) {
    // このメソッドの実装は Dagger には関係ありません
    suspend fun loadAllPlayCall(): List<PlayCallEntity> {
        return dao.loadAllPlayCall()
    }
}

1番良さげな理由:

  • オブジェクト生成時に依存関係をまとめて設定できるから
    (ここでは注入するものがPlayCalldaoのみですが)
  • 実装が手軽だから
    @Componentを作成しなくても良い

なので、可能であれば Contructor Injection を適用するのが良いと思います。

ただし、ActivityFragmentなどの Android環境独自の Lifecycleを持つオブジェクトに対しては使えません。
-> Field Injectionを使用する

2. Field Injection

e.g.

PlayCallListFragment.kt
class PlayCallListFragment : Fragment() {
    @Inject
    lateinit var viewModel: PlayCallListViewModel
    ..
}
PlayCallRepository.kt
class PlayCallRepository {
    @Inject
    lateinit var dao: PlayCallDao
    ..

@Injectアノテーションをつけるだけだと、Daggerは依存性を注入してくれません
-> @Component を作成し、inject用メソッドを用意する必要がある。

e.g.

AppComponent.kt
@Component(modules = [AppModule::class])
interface AppComponent {
  fun inject(repository: PlayCallListRepository)
}

ここで定義したinject(repository:)を使用し対象オブジェクト(ここでは PlayCallListRepository)に依存性を注入します。

このメソッドを呼んだ後に、@Injectを使用することができます。
* 逆にいうと、inject(repository:)より前に@Injectを通ってしまうとコンパイルエラーが発生します。

e.g. PlayCallRepository にて

PlayCallRepository.kt
class PlayCallRepository {
    @Inject
    lateinit var dao: PlayCallDao
    init {
        DaggerAppComponent.create().inject(this)
    }
}

e.g. PlayCallListFragment にて

PlayCallListFragment.kt
class PlayCallListFragment : Fragment() {
    @Inject
    lateinit var viewModel: PlayCallListViewModel

    override fun onAttach(context: Context) {
        DaggerPlayCallListComponent.create().inject(this)
    }
}

1つ目の例で少し解説します。

  1. DaggerAppComponent.create()DaggerAppComponentを生成します
  2. AppComponent内で定義したinject(repository:)メソッドを呼ぶことで@Injectアノテーションが付いているプロパティに依存性を注入することができます

補足:
DaggerAppComponentはDaggerが自動生成したクラスです。
このクラス内で、AppComponentで定義したインターフェースの処理を実装しています。
正確に言うと、DaggerAppComponent内から別クラスのメソッドを呼ぶ処理を実装しています。
Daggerが自動生成したDaggerAppComponentクラスを見てみましょう!

DaggerAppComponent.java
public final class DaggerAppComponent implements AppComponent {
  ..
  @Override
  public void inject(PlayCallRepository repository) {
    // ここで、inject(repository:) を実装している
    injectPlayCallRepository(repository);  // メソッドA
  }

  // メソッドA
  private PlayCallRepository injectPlayCallRepository(PlayCallRepository instance) {
  // 次に添付のクラス内の injectDao() を呼ぶ
  PlayCallRepository_MembersInjector.injectDao(instance, DatabaseModule_ProvidePlayCallDaoFactory.providePlayCallDao(databaseModule));
    return instance;
  }
  ..
}
PlayCallRepository_MembersInjector.java
public final class PlayCallRepository_MembersInjector implements MembersInjector<PlayCallRepository> {
  ..
  @InjectedFieldSignature("io.github.itakahiro.architecturefootball.repository.PlayCallRepository.dao")
  public static void injectDao(PlayCallRepository instance, PlayCallDao dao) {
    // ここで、PlayCallRepository に PlayCallDao を注入している!!
    instance.dao = dao;
  }
}

まとめ

Dagger の基礎的な部分をまとめてみました。
Dagger では、他にも様々な機能・アノテーションが用意されています。
それらを使いこなすのは難しく感じていますが、少しずつ理解していこうと思います。

サンプルコード

https://github.com/iTakahiro/ArchitectureFootball

参考資料

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

ViewModelProviders is deprecated になっているので、ViewModelProviderを使う

ViewModelProviders が非推奨になっている

Android Developer 公式: ViewModelに書いてある
ViewModelProviders.of(this).get(UserModel.class)
をつかうと deprecated(非推奨) と表示されてしまう。
公式のViewModelProvidersの説明によるともうメンテナンスがされないらしい。

今後は ViewModelProvider を使わないといけないらしいので今回はその使い方を紹介する。

用意するファイル

Activity

プロジェクト開始時に通常通り生成されるもの
解説は省く

ViewModel

MVVM アーキテクチャで View と Model をつなぐもの。メリットとしては下記の2つが考えられる。
・画面回転時に View に持たせていたデータが消えるのを防ぐ
・疎結合にしてくれるので Activity が太るのを防ぐ

Factory

ViewModel を Activity で呼ぶ際に使うクラス。
Factory クラスでインスタンスの作り方を管理して、Activity でクラスを呼び出す際にで具体的な処理を行っている。

使い方

以下のコードの通りに使うことができます。
ViewModelに設定した文字が Activity から呼び出されるだけのコードです。

MainActivity
package com.example.viewmodel_fragment

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider

class MainActivity : AppCompatActivity() {

    lateinit var factory: MainViewModelFactory
    lateinit var viewModel: MainViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        factory =  MainViewModelFactory()
        viewModel = ViewModelProvider(this, factory).get(MainViewModel::class.java)

        viewModel.loadData()

        viewModel.user.observe(this, Observer {
            println(it)
        })

    }
}

MainViewModel
package com.example.viewmodel_fragment

import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainViewModel : ViewModel(){

    val user: MutableLiveData<String> = MutableLiveData()

    fun loadData(){
        user.postValue("higu")
    }
}

MainViewModelFactory
package com.example.viewmodel_fragment

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider

class MainViewModelFactory : ViewModelProvider.NewInstanceFactory(){
        override fun <T : ViewModel?> create(modelClass: Class<T>):T {
            return MainViewModel() as T
        }
}

Factory に関して補足をすると T というのはジェネリクスといい、メソッドやクラスに型を
つけることができる機能です。

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