Androidのアプリアイコンとタイトルを動的に変更する

Android には [activity-alias](https://developer.android.com/guide/topics/manifest/activity-alias-element) というアクティビティに別名をつける仕組みがあります。この機能をうまく使うことでアプリのアイコンやタイトルを動的に変更してみたので、そのメモ書きです。これは iOS 10.3 から追加された [User-Selectable App Icons](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/app-icon/) に近いことを実現できそうです。

他にも以下の機能でランチャーにアプリのショートカットを作成するということでアプリアイコンとタイトルを変更したように見せる方法もありますが、この記事では触れません。

- Android 7.1 以前: `com.android.launcher.action.INSTALL_SHORTCUT` インテント
- Android 8 以降: [android.content.pm.ShortcutManager](https://developer.android.com/reference/android/content/pm/ShortcutManager) の [requestPinShortcut](https://developer.android.com/reference/android/content/pm/ShortcutManager.html#requestPinShortcut(android.content.pm.ShortcutInfo,%20android.content.IntentSender)) メソッド

以降のコードや実行結果は下記の環境のものとなります。

- Android Studio : 3.3.2
- Kotlin : 1.3.21
- Target SDK : 28
 
## アプリアイコンとタイトルを変更する手順

### マニフェストファイルにメインアクティビティのエイリアスを作成する

AndroidManifest.xml に次のように記述を変更します。

- メインアクティビティから `<intent-filter>` 削除
- デフォルトで利用するアプリアイコンとタイトルを指定した `<activity-alias>` を追加
    - `android:icon` と `android:label` がそれぞれアプリアイコンとタイトル
    - `android:name` はエイリアス名なので、実在しないクラスでOK
    - `android:targetActivity` が実体となるアクティビティ
    - `android:enabled` はデフォルト使用するものなので `true` としておく
    - `<intent-filter>` はこのエイリアスの方で指定する
- デフォルトの代替として利用するアプリアイコンとタイトルを指定した `<activity-alias>` を追加
    - `android:icon` と `android:label` に代替となるアプリアイコンとタイトルを指定する
    - `android:enabled` は `false` としておく

このように、指定するアイコンやタイトルは静的なものですので、ダウンロードしたアイコンを指定するというようなことはおそらくできないと思われます。（この点も、iOS の User-Selectable App Icon と似ていますね）

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.appiconchanger">

    <application
            android:allowBackup="true"
            android:label="@string/app_name"
            android:icon="@mipmap/ic_launcher"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">

        <!-- メインアクティビティ -->
        <activity
                android:name=".MainActivity"
                android:label="@string/app_name"
                android:theme="@style/AppTheme.NoActionBar">
            <!-- intent-filter は記述しません -->
        </activity>

        <!-- メインアクティビティのエイリアス１ (デフォルトアイコン/タイトル) -->
        <activity-alias android:label="@string/app_name"
                        android:icon="@mipmap/ic_launcher"
                        android:name=".MainActivity_default"
                        android:enabled="true"
                        android:targetActivity=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity-alias>

        <!-- メインアクティビティのエイリアス２ (代替アイコン/タイトル) -->
        <activity-alias android:label="@string/app_name_beta"
                        android:icon="@mipmap/ic_launcher_beta"
                        android:name=".MainActivity_beta"
                        android:enabled="false"
                        android:targetActivity=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity-alias>

    </application>

</manifest>
```

記述に不整合があると Android Studio でのビルド時に Manifest の Merge エラーが出ます。その際は、AndroidManifest.xml の Merged Manifest タブを表示することでエラー内容を確認することができますので、それを頼りにエラーを解消しましょう。（下図はメインアクティビティ本体の方に `<intent-filter>` が記述されていた時のエラー）

<img width="992" alt="スクリーンショット 2019-03-12 13.57.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/16508/36f7388e-cacd-7994-ddcc-c3dc997a7a41.png">

### 有効なエイリアスを切り替えるコードを書く

下記のコードは、MainActivity に配置した FAB を押すたびにデフォルトエイリアスと代替エイリアスを切り替える例です。

- エイリアスは `ComponentName` のインスタンスにより指定します
- [android.content.pm.PackageManager](https://developer.android.com/reference/android/content/pm/PackageManager.html) の [getComponentEnabledSetting](https://developer.android.com/reference/android/content/pm/PackageManager.html#getComponentEnabledSetting(android.content.ComponentName)) メソッドでエイリアスの有効/無効状態を判断できます
- [android.content.pm.PackageManager](https://developer.android.com/reference/android/content/pm/PackageManager.html) の [setComponentEnabledSetting](https://developer.android.com/reference/android/content/pm/PackageManager.html#setComponentEnabledSetting(android.content.ComponentName,%20int,%20int)) メソッドでエイリアスの有効/無効状態を変更できます
- 下記のコードでは、デフォルトエイリアスが有効な場合は、代替エイリアスを有効にし、デフォルトエイリアスを無効にする（逆も同様）というようにしています

```kotlin:MainActivity.kt
package com.example.appiconchanger

import android.content.ComponentName
import android.content.pm.PackageManager
import android.os.Bundle
import com.google.android.material.snackbar.Snackbar
import androidx.appcompat.app.AppCompatActivity;

import kotlinx.android.synthetic.main.activity_main.*

class MainActivity : AppCompatActivity() {


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

        // ボタンを押すたびにエイリアスを切り替える
        fab.setOnClickListener { view ->
            // デフォルト ComponentName
            val defaultComponentName = ComponentName(packageName, packageName + ".MainActivity_default")
            
            // 代替 ComponentName
            val substituteComponentName = ComponentName(packageName, packageName + ".MainActivity_substitute")

            // エイリアスを切り替える
            val state = packageManager.getComponentEnabledSetting(substituteComponentName)
            if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
                packageManager.setComponentEnabledSetting(
                    substituteComponentName,
                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                    PackageManager.DONT_KILL_APP)
                packageManager.setComponentEnabledSetting(defaultComponentName,
                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                    PackageManager.DONT_KILL_APP)
                Snackbar.make(view, "Switch to substitute alias.", Snackbar.LENGTH_LONG).show()

            } else {
                packageManager.setComponentEnabledSetting(
                    defaultComponentName,
                    PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
                    PackageManager.DONT_KILL_APP)
                packageManager.setComponentEnabledSetting(
                    substituteComponentName,
                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                    PackageManager.DONT_KILL_APP)
                Snackbar.make(view, "Switch to default alias.", Snackbar.LENGTH_LONG).show()
            }
        }
    }

}
```

## 検証

### 実行結果

下図のようにアプリのアイコンとタイトルを変更することに成功しました。左画像がデフォルトエイリアス、右画像が代替エイリアスです。

<img width="320" alt="default.png" src="https://qiita-image-store.s3.amazonaws.com/0/16508/7642c16e-a902-e036-e984-21f174b1b25b.png"> <img width="320" alt="substitute.png" src="https://qiita-image-store.s3.amazonaws.com/0/16508/bb3f4bd6-271d-8b79-68c3-281106f8a0a9.png">


### いくつかのデバイスで試してみた

ランチャーにアプリのショートカットアイコンを作成する方法の場合、デバイスや Android のバージョンによって動作しないケースがあったため、今回のエイリアスによる方法を手元にあるデバイスで試してみた結果が下記の表です。

| デバイス | Android ver. | 結果 | 備考 |
| --- | :-: | :-: | --- |
| Google Nexus 5X | 8.1 | ◯ | |
| Galaxy S6 edge (SCV31) | 7.0 | ◯ | |
| Google Nexus 5 | 6.0.1 | △ | 変更後にランチャーがクラッシュ？して再起動された。変更はされている。 |
| Google Nexus 5 | 5.1.1 | ◯ |  |

Google Nexus 5 (6.0.1) のみランチャーがクラッシュしてしまう問題がありましたので、他のデバイスでも何らかの問題が起きる可能性がありそうです。