外部からURLを受け取った際の挙動を改善

This commit is contained in:
tateisu 2017-12-21 01:03:05 +09:00
parent ea5438913c
commit a57489f98d
8 changed files with 163 additions and 153 deletions

View File

@ -71,8 +71,9 @@
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:host="oauth"
android:scheme="subwaytooter"
android:host="*"
android:pathPattern=".*"
/>
</intent-filter>
@ -83,9 +84,9 @@
<category android:name="android.intent.category.BROWSABLE"/>
<data
android:scheme="https"
android:host="*"
android:pathPrefix="/@"
android:scheme="https"
/>
</intent-filter>

View File

@ -7,12 +7,16 @@ import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.customtabs.CustomTabsIntent;
@ -60,6 +64,7 @@ import java.io.Reader;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -1247,73 +1252,107 @@ public class ActMain extends AppCompatActivity
// ActOAuthCallbackで受け取ったUriを処理する
private void handleIntentUri( @NonNull final Uri uri ){
// プロフURL
if( "https".equals( uri.getScheme() ) ){
if( uri.getPath().startsWith( "/@" ) ){
Matcher m = reStatusPage.matcher( uri.toString() );
if( m.find() ){
// ステータスをアプリ内で開く
try{
// https://mastodon.juggler.jp/@SubwayTooter/(status_id)
final String host = m.group( 1 );
final long status_id = Long.parseLong( m.group( 3 ), 10 );
openStatusOtherInstance( getDefaultInsertPosition(), null, uri.toString(), status_id, host, status_id );
//
// ArrayList< SavedAccount > account_list_same_host = new ArrayList<>();
//
// for( SavedAccount a : SavedAccount.loadAccountList( log ) ){
// if( host.equalsIgnoreCase( a.host ) ){
// account_list_same_host.add( a );
// }
// }
//
// // ソートする
// Collections.sort( account_list_same_host, new Comparator< SavedAccount >() {
// @Override public int compare( SavedAccount a, SavedAccount b ){
// return String.CASE_INSENSITIVE_ORDER.compare( AcctColor.getNickname( a.acct ), AcctColor.getNickname( b.acct ) );
// }
// } );
//
// if( account_list_same_host.isEmpty() ){
// account_list_same_host.add( addPseudoAccount( host ) );
// }
//
// AccountPicker.pick( this, true, true
// , getString( R.string.open_status_from )
// , account_list_same_host
// , new AccountPicker.AccountPickerCallback() {
// @Override
// public void onAccountPicked( @NonNull final SavedAccount ai ){
// openStatus( getDefaultInsertPosition(), ai, status_id );
// }
// } );
}catch( Throwable ex ){
Utils.showToast( this, ex, "can't parse status id." );
}
return;
}
m = reUserPage.matcher( uri.toString() );
if( m.find() ){
// ユーザページをアプリ内で開く
// https://mastodon.juggler.jp/@SubwayTooter
final String host = m.group( 1 );
final String user = Uri.decode( m.group( 2 ) );
openProfileByHostUser( getDefaultInsertPosition(), null, uri.toString(), host, user );
}
return;
if( "subwaytooter".equals( uri.getScheme() ) ){
try{
handleOAuth2CallbackUri( uri );
}catch( Throwable ex ){
log.trace( ex );
}
// https なら oAuth用の導線は通さない
return;
}
final String url = uri.toString();
Matcher m = reStatusPage.matcher( url );
if( m.find() ){
try{
// https://mastodon.juggler.jp/@SubwayTooter/(status_id)
final String host = m.group( 1 );
final long status_id = Long.parseLong( m.group( 3 ), 10 );
// ステータスをアプリ内で開く
openStatusOtherInstance( getDefaultInsertPosition(), null, uri.toString(), status_id, host, status_id );
}catch( Throwable ex ){
Utils.showToast( this, ex, "can't parse status id." );
}
return;
}
m = reUserPage.matcher( url );
if( m.find() ){
// https://mastodon.juggler.jp/@SubwayTooter
final String host = m.group( 1 );
final String user = Uri.decode( m.group( 2 ) );
// ユーザページをアプリ内で開く
openProfileByHostUser( getDefaultInsertPosition(), null, uri.toString(), host, user );
return;
}
// このアプリでは処理できないURLだった
// 外部ブラウザを開きなおそうとすると無限ループの恐れがある
// アプリケーションチューザーを表示する
String error_message = getString( R.string.cant_handle_uri_of, url );
try{
int query_flag;
if( Build.VERSION.SDK_INT >= 23 ){
// Android 6.0以降
// MATCH_DEFAULT_ONLY だと標準の設定に指定されたアプリがあるとソレしか出てこない
// MATCH_ALL を指定すると 以前と同じ挙動になる
query_flag = PackageManager.MATCH_ALL;
}else{
// Android 5.xまでは MATCH_DEFAULT_ONLY でマッチするすべてのアプリを取得できる
query_flag = PackageManager.MATCH_DEFAULT_ONLY;
}
// queryIntentActivities に渡すURLは実在しないホストのものにする
Intent intent = new Intent( Intent.ACTION_VIEW ,Uri.parse( "https://dummy.subwaytooter.club/" ));
intent.setFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
List< ResolveInfo > resolveInfoList = getPackageManager().queryIntentActivities( intent, query_flag );
if( resolveInfoList.isEmpty() ){
throw new RuntimeException( "resolveInfoList is empty." );
}
// このアプリ以外の選択肢を集める
String my_name = getPackageName();
ArrayList< Intent > choice_list = new ArrayList<>();
for( ResolveInfo ri : resolveInfoList ){
// 選択肢からこのアプリを除外
if( my_name.equals( ri.activityInfo.packageName ) ) continue;
// 選択肢のIntentは目的のUriで作成する
Intent choice = new Intent( Intent.ACTION_VIEW ,uri);
intent.setFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
choice.setPackage( ri.activityInfo.packageName );
choice.setClassName( ri.activityInfo.packageName, ri.activityInfo.name );
choice_list.add( choice );
}
if( choice_list.isEmpty() ){
throw new RuntimeException( "choice_list is empty." );
}
// 指定した選択肢でチューザーを作成して開く
Intent chooser = Intent.createChooser( choice_list.remove( 0 ), error_message );
chooser.putExtra( Intent.EXTRA_INITIAL_INTENTS, choice_list.toArray( new Intent[ choice_list.size() ] ) );
startActivity( chooser );
return;
}catch( Throwable ex ){
log.trace( ex );
}
new AlertDialog.Builder( this )
.setCancelable( true )
.setMessage( error_message )
.setPositiveButton( R.string.close, null )
.show();
}
private void handleOAuth2CallbackUri( @NonNull final Uri uri ){
// 通知タップ
// subwaytooter://notification_click?db_id=(db_id)
// subwaytooter://notification_click/?db_id=(db_id)
String sv = uri.getQueryParameter( "db_id" );
if( ! TextUtils.isEmpty( sv ) ){
try{
@ -1336,7 +1375,7 @@ public class ActMain extends AppCompatActivity
}
// OAuth2 認証コールバック
// subwaytooter://oauth/?...
new TootApiTask( ActMain.this, true ) {
TootAccount ta;
@ -1418,7 +1457,7 @@ public class ActMain extends AppCompatActivity
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( result.error != null ){
Utils.showToast( ActMain.this, true, result.error );
@ -1427,12 +1466,12 @@ public class ActMain extends AppCompatActivity
}else if( result.object == null ){
Utils.showToast( ActMain.this, true, "can't parse json response." );
}else if( ta == null ){
// 自分のユーザネームを取れなかった
// 普通はエラーメッセージが設定されてるはずだが
Utils.showToast( ActMain.this, true, "can't verify user credential." );
}else if( sa != null ){
// アクセストークン更新時
@ -1734,69 +1773,28 @@ public class ActMain extends AppCompatActivity
}
static final Pattern reUrlHashTag = Pattern.compile( "\\Ahttps://([^/]+)/tags/([^?#]+)(?:\\z|\\?)" );
static final Pattern reUserPage = Pattern.compile( "\\Ahttps://([^/]+)/@([^?#/]+)(?:\\z|\\?)" );
static final Pattern reStatusPage = Pattern.compile( "\\Ahttps://([^/]+)/@([^?#/]+)/(\\d+)(?:\\z|\\?)" );
static final Pattern reUrlHashTag = Pattern.compile( "\\Ahttps://([^/]+)/tags/([^?#]+)(?:\\z|[?#])" );
static final Pattern reUserPage = Pattern.compile( "\\Ahttps://([^/]+)/@([^?#/@]+)(?:\\z|[?#])" );
static final Pattern reStatusPage = Pattern.compile( "\\Ahttps://([^/]+)/@([^?#/@]+)/(\\d+)(?:\\z|[?#])" );
public void openChromeTab( final int pos, @Nullable final SavedAccount access_info, final String url, boolean noIntercept ){
try{
log.d( "openChromeTab url=%s", url );
if( ! noIntercept && access_info != null && access_info.isNA() ){
// トゥート検索カラムではaccess_infoは何にも紐ついていない
// ハッシュタグをアプリ内で開く
Matcher m = reUrlHashTag.matcher( url );
if( m.find() ){
// https://mastodon.juggler.jp/tags/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%B0
String host = m.group( 1 );
String tag = Uri.decode( m.group( 2 ) );
openHashTagOtherInstance( pos, access_info, url, host, tag );
return;
}
// ステータスページをアプリから開く
m = reStatusPage.matcher( url );
if( m.find() ){
try{
// https://mastodon.juggler.jp/@SubwayTooter/(status_id)
final String host = m.group( 1 );
final long status_id = Long.parseLong( m.group( 3 ), 10 );
openStatusOtherInstance( pos, access_info, url, status_id, host, status_id );
return;
}catch( Throwable ex ){
Utils.showToast( this, ex, "can't parse status id." );
}
return;
}
// ユーザページをアプリ内で開く
m = reUserPage.matcher( url );
if( m.find() ){
// https://mastodon.juggler.jp/@SubwayTooter
final String host = m.group( 1 );
final String user = Uri.decode( m.group( 2 ) );
openProfileByHostUser( pos, access_info, url, host, user );
return;
}
}
if( ! noIntercept && access_info != null ){
// ハッシュタグをアプリ内で開く
Matcher m = reUrlHashTag.matcher( url );
if( m.find() ){
// https://mastodon.juggler.jp/tags/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%B0
String host = m.group( 1 );
String tag = Uri.decode( m.group( 2 ) );
if( host.equalsIgnoreCase( access_info.host ) ){
openHashTag( pos, access_info, tag );
return;
String tag_without_sharp = Uri.decode( m.group( 2 ) );
if( access_info.isNA() || ! host.equalsIgnoreCase( access_info.host ) ){
openHashTagOtherInstance( pos, access_info, url, host, tag_without_sharp );
}else{
openHashTagOtherInstance( pos, access_info, url, host, tag );
return;
openHashTag( pos, access_info, tag_without_sharp );
}
return;
}
// ステータスページをアプリから開く
@ -1806,12 +1804,10 @@ public class ActMain extends AppCompatActivity
// https://mastodon.juggler.jp/@SubwayTooter/(status_id)
final String host = m.group( 1 );
final long status_id = Long.parseLong( m.group( 3 ), 10 );
if( host.equalsIgnoreCase( access_info.host ) ){
openStatusLocal( pos, access_info, status_id );
return;
}else{
if( access_info.isNA() || ! host.equalsIgnoreCase( access_info.host ) ){
openStatusOtherInstance( pos, access_info, url, status_id, host, status_id );
return;
}else{
openStatusLocal( pos, access_info, status_id );
}
}catch( Throwable ex ){
Utils.showToast( this, ex, "can't parse status id." );
@ -1825,10 +1821,9 @@ public class ActMain extends AppCompatActivity
// https://mastodon.juggler.jp/@SubwayTooter
final String host = m.group( 1 );
final String user = Uri.decode( m.group( 2 ) );
openProfileByHostUser( pos, access_info, url, host, user );
return;
}
}
@ -1863,19 +1858,29 @@ public class ActMain extends AppCompatActivity
//////////////////////////////////////////////////////////////////////////////////////////////////////
public void openHashTag( int pos, SavedAccount access_info, String tag ){
while( tag.startsWith( "#" ) ) tag = tag.substring( 1 );
addColumn( pos, access_info, Column.TYPE_HASHTAG, tag );
public void openHashTag( int pos, @NonNull SavedAccount access_info, @NonNull String tag_without_sharp ){
addColumn( pos, access_info, Column.TYPE_HASHTAG, tag_without_sharp );
}
// 他インスタンスのハッシュタグの表示
private void openHashTagOtherInstance( final int pos, final SavedAccount access_info, final String url, final String host, String tag ){
while( tag.startsWith( "#" ) ) tag = tag.substring( 1 );
openHashTagOtherInstance_sub( pos, access_info, url, host, tag );
private void openHashTagOtherInstance(
int pos
, @NonNull SavedAccount access_info
, @NonNull String url
, @NonNull String host
, @NonNull String tag_without_sharp
){
openHashTagOtherInstance_sub( pos, access_info, url, host, tag_without_sharp );
}
// 他インスタンスのハッシュタグの表示
private void openHashTagOtherInstance_sub( final int pos, final SavedAccount access_info, final String url, final String host, final String tag ){
private void openHashTagOtherInstance_sub(
final int pos
, @NonNull final SavedAccount access_info
, @NonNull final String url
, @NonNull final String host
, @NonNull final String tag_without_sharp
){
ActionsDialog dialog = new ActionsDialog();
@ -1912,7 +1917,7 @@ public class ActMain extends AppCompatActivity
@Override public void run(){
SavedAccount sa = addPseudoAccount( host );
if( sa != null ){
openHashTag( pos, sa, tag );
openHashTag( pos, sa, tag_without_sharp );
}
}
} );
@ -1924,7 +1929,7 @@ public class ActMain extends AppCompatActivity
dialog.addAction( AcctColor.getStringWithNickname( ActMain.this, R.string.open_in_account, a.acct ), new Runnable() {
@Override public void run(){
openHashTag( pos, _a, tag );
openHashTag( pos, _a, tag_without_sharp );
}
} );
}
@ -1933,7 +1938,7 @@ public class ActMain extends AppCompatActivity
final SavedAccount _a = a;
dialog.addAction( AcctColor.getStringWithNickname( ActMain.this, R.string.open_in_account, a.acct ), new Runnable() {
@Override public void run(){
openHashTag( pos, _a, tag );
openHashTag( pos, _a, tag_without_sharp );
}
} );
}
@ -1942,12 +1947,12 @@ public class ActMain extends AppCompatActivity
final SavedAccount _a = a;
dialog.addAction( AcctColor.getStringWithNickname( ActMain.this, R.string.open_in_account, a.acct ), new Runnable() {
@Override public void run(){
openHashTag( pos, _a, tag );
openHashTag( pos, _a, tag_without_sharp );
}
} );
}
dialog.show( this, "#" + tag );
dialog.show( this, "#" + tag_without_sharp );
}
final MyClickableSpan.LinkClickCallback link_click_listener = new MyClickableSpan.LinkClickCallback() {
@ -1983,17 +1988,18 @@ public class ActMain extends AppCompatActivity
if( m.find() ){
// https://mastodon.juggler.jp/tags/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E3%82%BF%E3%82%B0
final String host = m.group( 1 );
final String tag = span.text.startsWith( "#" ) ? span.text : "#" + Uri.decode( m.group( 2 ) );
final String tag_with_sharp = span.text.startsWith( "#" ) ? span.text : "#" + Uri.decode( m.group( 2 ) );
final String tag_without_sharp = tag_with_sharp.substring( 1 );
ActionsDialog d = new ActionsDialog()
.addAction( getString( R.string.open_hashtag_column ), new Runnable() {
@Override public void run(){
openHashTagOtherInstance( pos, (SavedAccount) span.lcc, span.url, host, tag );
openHashTagOtherInstance( pos, (SavedAccount) span.lcc, span.url, host, tag_without_sharp );
}
} )
.addAction( getString( R.string.quote_hashtag_of, tag ), new Runnable() {
.addAction( getString( R.string.quote_hashtag_of, tag_with_sharp ), new Runnable() {
@Override public void run(){
openPost( tag + " " );
openPost( tag_with_sharp + " " );
}
} );
@ -2028,7 +2034,7 @@ public class ActMain extends AppCompatActivity
} );
}
d.show( ActMain.this, tag );
d.show( ActMain.this, tag_with_sharp );
return;
}
@ -2605,7 +2611,7 @@ public class ActMain extends AppCompatActivity
// Tootsearch ではステータスのアクセス元ホストは分からない
// ステータスの投稿元ホストでのIDも分からない
openStatusOtherInstance( pos, access_info, status.url
, -1L
, - 1L
, null, - 1L
);
}else if( status instanceof TootStatus ){
@ -4539,11 +4545,11 @@ public class ActMain extends AppCompatActivity
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
if( access_info.isMe( local_who )){
return new TootApiResult( getString(R.string.it_is_you ));
if( access_info.isMe( local_who ) ){
return new TootApiResult( getString( R.string.it_is_you ) );
}
TootApiResult result;
if( bFollow ){
@ -4685,7 +4691,7 @@ public class ActMain extends AppCompatActivity
new TootApiTask( this, access_info, true ) {
@Override protected TootApiResult doInBackground( Void... params ){
return client.request(
"/api/v1/lists/" + list_id + "/accounts?account_ids[]=" + local_who.id
, new Request.Builder().delete()
@ -4714,7 +4720,7 @@ public class ActMain extends AppCompatActivity
Utils.showToast( ActMain.this, false, result.error );
}
}finally{
if( callback != null ) callback.onListMemberUpdated( false ,bSuccess );
if( callback != null ) callback.onListMemberUpdated( false, bSuccess );
}
}
}.executeOnExecutor( App1.task_executor );

View File

@ -828,7 +828,7 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
case R.id.btnSearchTag:
if( search_tag != null ){
activity.openHashTag( activity.nextPosition( column ), access_info, search_tag );
activity.openHashTag( activity.nextPosition( column ), access_info, search_tag.substring( 1 ) );
}else if( gap != null ){
column.startGap( gap );
}else if( domain_block != null ){

View File

@ -1251,7 +1251,7 @@ public class PollingWorker {
// 通知タップ時のPendingIntent
Intent intent_click = new Intent( context, ActCallback.class );
intent_click.setAction( ActCallback.ACTION_NOTIFICATION_CLICK );
intent_click.setData( Uri.parse( "subwaytooter://notification_click?db_id=" + account.db_id ) );
intent_click.setData( Uri.parse( "subwaytooter://notification_click/?db_id=" + account.db_id ) );
intent_click.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
PendingIntent pi_click = PendingIntent.getActivity( context, ( 256 + (int) account.db_id ), intent_click, PendingIntent.FLAG_UPDATE_CURRENT );

View File

@ -97,7 +97,7 @@ public class TootApiClient {
private static final String KEY_AUTH_VERSION = "SubwayTooterAuthVersion";
private static final int AUTH_VERSION = 1;
private static final String REDIRECT_URL = "subwaytooter://oauth";
private static final String REDIRECT_URL = "subwaytooter://oauth/";
private static final String MIMUMEDON = "mimumedon.com";
private static final String MIMUMEDON_ERROR = "mimumedon.comには対応しません";

View File

@ -566,7 +566,8 @@
<string name="toot_search_ts">Toot search(TS)</string>
<string name="toot_search_ts_of">Toot search(TS) \"%1$s\"</string>
<string name="tootsearch">tootsearch(JP)</string>
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
<string name="cant_handle_uri_of">Subway Tooter can\'t handle URI \"%1$s\". Please select other app.</string>
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
<!--<string name="abc_action_bar_home_description_format">%1$s, %2$s</string>-->
<!--<string name="abc_action_bar_home_subtitle_description_format">%1$s, %2$s, %3$s</string>-->
<!--<string name="abc_action_bar_up_description">Revenir en haut de la page</string>-->

View File

@ -854,4 +854,5 @@
<string name="toot_search_ts">トゥート検索(TS)</string>
<string name="toot_search_ts_of">トゥート検索(TS) \"%1$s\"</string>
<string name="tootsearch">tootsearch(JP)</string>
<string name="cant_handle_uri_of">Subway Tooter は URI \"%1$s\"を扱えません。別のアプリを選んでください。</string>
</resources>

View File

@ -558,4 +558,5 @@
<string name="your_lists">Your lists</string>
<string name="media_attachment_max_byte_size">Media attachment maximum bytes size (unit:Mega bytes. default is 8)</string>
<string name="tootsearch">tootsearch(JP)</string>
<string name="cant_handle_uri_of">Subway Tooter can\'t handle URI \"%1$s\". Please select other app.</string>
</resources>