起動元のタスク上で起動するアプリ（例えばOctoDroid）を別タスクで開く

AndroidでGitHub見るとき、ブラウザで見てますか？
私はChromeと[OctoDroid](https://play.google.com/store/apps/details?id=com.gh4a&hl=ja)を併用してます。どちらも良いところがあります。

# OctoDroidの仕様と挙動

OctoDroidで「https://github.com を開く暗黙的インテント」を拾っているのはBrowseFilterというActivityです。
https://github.com/slapperwan/gh4a/blob/26f763df001a22c69d3ac0a9c66f87f053f65c82/app/src/main/AndroidManifest.xml#L126
https://github.com/slapperwan/gh4a/blob/26f763df001a22c69d3ac0a9c66f87f053f65c82/app/src/main/java/com/gh4a/resolver/BrowseFilter.java

```app/src/main/AndroidManifest.xml
<activity android:name=".resolver.BrowseFilter"
            android:excludeFromRecents="true"
            android:taskAffinity=""
            android:theme="@style/TransparentLightTheme">
            <intent-filter>
                <action
                    android:name="android.intent.action.VIEW" />
                <category
                    android:name="android.intent.category.DEFAULT" />
                <category
                    android:name="android.intent.category.BROWSABLE" />
                <data
                    android:host="github.com"
                    android:scheme="https" />
                <data
                    android:host="gist.github.com"
                    android:scheme="https" />
                <data
                    android:host="github.com"
                    android:scheme="http" />
				<data
                    android:host="gist.github.com"
                    android:scheme="http" />
            </intent-filter>
        </activity>
```

このActivityはlaunchModeが何も指定されていませんので、デフォルトのstandardです。
そして、

> 通常、アクティビティは startActivity() を呼び出したタスク内で起動されます
https://developer.android.com/guide/topics/manifest/activity-element.html?hl=ja#lmode

そのため、以下のような状況で**なければ**、呼び出したアプリのタスク上で起動します。

+ 呼び出すアプリで作成した暗黙的インテントにFLAG_ACTIVITY_NEW_TASK を設定する
+ 呼び出すアプリのActivityのlaunchModeがSingleInstanceになっている

（空文字が設定されたtaskAffinityがどのように効いてくるかについては、私は把握していません）

# ChromeからOctoDroidを開く場合

（少なくとも最近の）Chromeは別タスクで外部アプリを開くため、
Chromeから開いた場合は、OctoDroidも常に呼び出し元のアプリ（つまりChrome）とは別タスク上で起動します。

# 公式TwitterクライアントからOctoDroidを開く場合

公式Twitterクライアントは、短縮URLを開くインテントを投げるため、ブラウザ経由となります。
端末内のブラウザがChromeだけであれば、Chromeから開く場合と同様です。
他のブラウザは確認していませんが、
端末にプリインストールされているメーカーの独自ブラウザの中には、
素直にブラウザのタスク上にActivity起動するものがあります。
（というか、そういうものしか私は見たことありません）

# 非公式TwitterクライアントからOctoDroidを開く場合

確認したのは
+ [TwitPane(ついっとぺーん)](https://play.google.com/store/apps/details?id=com.twitpane&hl=ja)（正確には、有料版の[TwitPanePlus(ついっとぺーん ぷらす)](https://play.google.com/store/apps/details?id=com.twitpane.premium&hl=ja)）
+ [MateCha](https://play.google.com/store/apps/details?id=net.wakamesoba98.matecha&hl=ja)
ですが、
どちらも https://github.com 上のリンクを開くときにOctoDroidを選ぶと、
呼び出し元のアプリのタスク上にOctoDroidが起動します。

これで何が困るかというと、
途中まで読んで、あるいは、（後で読むことにして）読まずに、
OctoDroidで目的のレポジトリ等を開いたままTwitterクライアントへ戻ることができません。

# 【重要】Lollipop以降の`android:launchMode="standard"`の挙動について

以下の記事には、外部アプリのActivityがlaunchModeがstandardの場合に
Lollipop以降は挙動が異なるように書かれています
[【翻訳】AndroidのActivityのlaunchMode（standard, singleTop, singleTask, singleInstance）を理解する](https://shoma2da.net/2016/10/30/understand-android-activity-launchmode_ja/)
しかし、手元の端末（FREETEL/SAMURAI REI/Android 6.0）ではこのような挙動にはなっていません。
（呼び出し先ではなく）呼び出し元のアプリのtargetSdkVersionやlaunchModeによるのか、あるいは、
この記事で例に挙がっている（呼び出し元の）Galleryアプリ自体がOSアップデートに伴い変化したのか、
いずれにせよ詳細は未確認で不明です。
（この端末がおかしいだけだったら悲しい。。。）

# どうあるべきか

外部アプリの起動の方法および起動される方法について、そもそもどうあるべきか、というのは難しいところです。
仮にAndroidの思想に沿った理想があるとして、
それに従うかどうかがアプリに委ねられているのもAndroidです。

# どうにかする

そして、自分でどうにかできてしまうのもAndroidですね。
というわけで、インテントの起動をフックして別タスクで起動するアプリを作りました。
https://github.com/guignol/IntentHook

github.comをこのアプリで常時開くようにしておいて、
OctoDroidかChrome（あるいは、その他GitHubクライアントアプリ）を選びます。

アプリ選択のUIについては、
暗黙的インテントで起動するアプリを選ぶAndroidデフォルトのUIに、
そんなには似せていませんが、起動元アプリに半透明で重なる程度の雰囲気は出してます。

## AndroidManifest

+ OctoDroidと同じintent-filterを設定します。
こうすることで、（OctoDroidを常時起動する設定になっていなければ）
OctoDroidと並んでこのアプリが選択できるようになります。

+ タスク一覧に出ても意味ないので`android:excludeFromRecents="true"`にします。

+ このアプリは中継するだけなのでlaunchModeはsingleInstanceにします。

+ 呼び出し元のアプリの上に、通常の暗黙インテント起動と似たようなUIを作りたいので、半透明にします。

```app/src/main/AndroidManifest.xml
        <activity
            android:name=".IntentHookActivity"
            android:excludeFromRecents="true"
            android:launchMode="singleInstance"
            android:theme="@style/TransparentTheme">
            <!-- https://github.com/slapperwan/gh4a/blob/master/app/src/main/AndroidManifest.xml -->
            <intent-filter>
                <action android:name="android.intent.action.VIEW"/>

                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>

                <data
                    android:host="github.com"
                    android:scheme="https"/>
                <data
                    android:host="gist.github.com"
                    android:scheme="https"/>
                <data
                    android:host="github.com"
                    android:scheme="http"/>
                <data
                    android:host="gist.github.com"
                    android:scheme="http"/>
            </intent-filter>
        </activity>
```

```app/src/main/res/values/styles.xml
    <drawable name="translucent_background">#55000000</drawable>
    <style name="TransparentTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowBackground">@drawable/translucent_background</item>
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowTranslucentNavigation">true</item>
    </style>
```

## Activity

+ 受け取ったインテントの`Uri`を転送先のインテントにそのまま設定する

+ このアプリは`android:launchMode="singleInstance"`なので必ず別タスクで開かれるのでフラグは不要

+ `packageManager.queryIntentActivities(Intent intent, int flags)`で対象のインテントを開けるアプリの一覧を得る

+ ResolveInfoのloadLabelやloadIconでアプリ名やアプリアイコンを表示する

+ ListViewで選んだアプリのpackageNameを暗黙的インテントに設定して、起動するアプリを指定する

+ packageManager.clearPackagePreferredActivitiesを使って常時起動設定を解除できるようにする

```app/src/main/java/com/github/guignol/intenthook/IntentHookActivity.java
@Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.intetn_hook);

        final Uri data = getIntent().getData();
        final Intent forward = new Intent();
        forward.setData(data);
        forward.setAction(Intent.ACTION_VIEW);
        forward.addCategory(Intent.CATEGORY_DEFAULT);
        forward.addCategory(Intent.CATEGORY_BROWSABLE);

        final String thisAppPackageName = getPackageName();
        final PackageManager packageManager = getPackageManager();
        final List<ResolveInfo> resolveInfos = packageManager.queryIntentActivities(forward, PackageManager.MATCH_DEFAULT_ONLY);

        final List<AppInfo> appsList = new ArrayList<>();
        for (final ResolveInfo resolveInfo : resolveInfos) {
            final String packageName = resolveInfo.activityInfo.packageName;
            // 自分を除外する
            if (thisAppPackageName.equals(packageName)) {
                continue;
            }
            appsList.add(new AppInfo() {
                @Override
                public CharSequence loadLabel(PackageManager pm) {
                    return resolveInfo.loadLabel(pm);
                }

                @Override
                public Drawable loadIcon(PackageManager pm) {
                    return resolveInfo.loadIcon(pm);
                }

                @Override
                public String packageName() {
                    return packageName;
                }
            });
        }
        appsList.add(new AppInfo() {
            @Override
            public CharSequence loadLabel(PackageManager pm) {
                return "このアプリで常にフックする設定を解除する";
            }

            @Override
            public Drawable loadIcon(PackageManager pm) {
                return null;
            }

            @Override
            public String packageName() {
                return thisAppPackageName;
            }
        });

        final ListView targetList = (ListView) findViewById(R.id.target_list);
        final AppIconAdapter targetAdapter = new AppIconAdapter(this, appsList);
        targetList.setAdapter(targetAdapter);
        targetList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
                final AppInfo itemClicked = targetAdapter.getItem(position);
                if (thisAppPackageName.equals(itemClicked.packageName())) {
                    // このアプリで常に開く設定を初期化する
                    packageManager.clearPackagePreferredActivities(thisAppPackageName);
                } else {
                    // 選んだアプリにインテントを転送する
                    forward.setPackage(itemClicked.packageName());
                    startActivity(forward);
                }
                finish();
            }
        });
    }
```

## 完成

![Screenshot_20171105-031842.png](https://qiita-image-store.s3.amazonaws.com/0/202327/247c7da9-45bc-b22f-47b8-6dfd83a94f85.png)

# 同様に

フックするアプリ側にintent-filterの設定が必要なので、汎用的なアプリは作れませんが、
別タスクで開きたいアプリのintent-filter（に相当する情報）が分かれば、
同じようなアプリが作れるはずです。