AndroidでGitHub見るとき、ブラウザで見てますか?
私はChromeとOctoDroidを併用してます。どちらも良いところがあります。
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
<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(ついっとぺーん)(正確には、有料版のTwitPanePlus(ついっとぺーん ぷらす))
+ MateCha
ですが、
どちらも https://github.com 上のリンクを開くときにOctoDroidを選ぶと、
呼び出し元のアプリのタスク上にOctoDroidが起動します。
これで何が困るかというと、
途中まで読んで、あるいは、(後で読むことにして)読まずに、
OctoDroidで目的のレポジトリ等を開いたままTwitterクライアントへ戻ることができません。
【重要】Lollipop以降のandroid:launchMode="standard"
の挙動について
以下の記事には、外部アプリのActivityがlaunchModeがstandardの場合に
Lollipop以降は挙動が異なるように書かれています
【翻訳】AndroidのActivityのlaunchMode(standard, singleTop, singleTask, singleInstance)を理解する
しかし、手元の端末(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を作りたいので、半透明にします。
<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>
<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を使って常時起動設定を解除できるようにする
@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();
}
});
}
完成
同様に
フックするアプリ側にintent-filterの設定が必要なので、汎用的なアプリは作れませんが、
別タスクで開きたいアプリのintent-filter(に相当する情報)が分かれば、
同じようなアプリが作れるはずです。