diff --git a/app/build.gradle b/app/build.gradle index 210cf4d1..2afe007b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId "jp.juggler.subwaytooter" minSdkVersion 21 targetSdkVersion 26 - versionCode 169 - versionName "1.6.9" + versionCode 170 + versionName "1.7.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java index 8b0a971d..f3b439aa 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java @@ -73,7 +73,7 @@ import jp.juggler.subwaytooter.api.entity.TootStatus; import jp.juggler.subwaytooter.api.entity.TootStatusLike; import jp.juggler.subwaytooter.api_msp.entity.MSPToot; import jp.juggler.subwaytooter.dialog.AccountPicker; -import jp.juggler.subwaytooter.dialog.DlgAccessToken; +import jp.juggler.subwaytooter.dialog.DlgTextInput; import jp.juggler.subwaytooter.dialog.DlgConfirm; import jp.juggler.subwaytooter.dialog.LoginForm; import jp.juggler.subwaytooter.dialog.ReportForm; @@ -1147,7 +1147,8 @@ public class ActMain extends AppCompatActivity if( bPseudoAccount ){ return api_client.checkInstance(); }else{ - return api_client.authorize1( Pref.pref( ActMain.this ).getString( Pref.KEY_CLIENT_NAME, "" ) ); + String client_name = Pref.pref( ActMain.this ).getString( Pref.KEY_CLIENT_NAME, "" ); + return api_client.authorize1( client_name ); } } @@ -1177,11 +1178,15 @@ public class ActMain extends AppCompatActivity if( bInputAccessToken ){ // アクセストークンの手動入力 - DlgAccessToken.show( ActMain.this, new DlgAccessToken.Callback() { + DlgTextInput.show( ActMain.this, getString( R.string.access_token ), null, new DlgTextInput.Callback() { @Override - public void startCheck( Dialog dialog_token, String access_token ){ + public void onOK( Dialog dialog_token, String access_token ){ checkAccessToken( dialog, dialog_token, instance, access_token, null ); } + + @Override public void onEmptyError(){ + Utils.showToast( ActMain.this, true, R.string.token_not_specified ); + } } ); }else{ // OAuth認証が必要 @@ -1453,8 +1458,9 @@ public class ActMain extends AppCompatActivity } this.host = client.instance; + String client_name = Pref.pref( ActMain.this ).getString( Pref.KEY_CLIENT_NAME, "" ); - TootApiResult result = client.authorize2( code ); + TootApiResult result = client.authorize2( client_name,code ); if( result != null && result.object != null ){ // taは使い捨てなので、生成に使うLinkClickContextはダミーで問題ない LinkClickContext lcc = new LinkClickContext() { @@ -1672,10 +1678,14 @@ public class ActMain extends AppCompatActivity final SavedAccount sa = SavedAccount.loadAccount( this, log, db_id ); if( sa == null ) return; - DlgAccessToken.show( this, new DlgAccessToken.Callback() { - @Override public void startCheck( Dialog dialog_token, String access_token ){ + DlgTextInput.show( this, getString( R.string.access_token ), null, new DlgTextInput.Callback() { + @Override public void onOK( Dialog dialog_token, String access_token ){ checkAccessToken( null, dialog_token, sa.host, access_token, sa ); } + + @Override public void onEmptyError(){ + Utils.showToast( ActMain.this, true, R.string.token_not_specified ); + } } ); } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.java b/app/src/main/java/jp/juggler/subwaytooter/ActPost.java index b6a239b8..1198100a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.java @@ -3,6 +3,7 @@ package jp.juggler.subwaytooter; import android.Manifest; import android.annotation.SuppressLint; import android.app.Activity; +import android.app.Dialog; import android.app.ProgressDialog; import android.content.ClipData; import android.content.ContentValues; @@ -57,12 +58,14 @@ import java.util.Locale; import jp.juggler.subwaytooter.api.TootApiClient; import jp.juggler.subwaytooter.api.TootApiResult; +import jp.juggler.subwaytooter.api.entity.TootAccount; import jp.juggler.subwaytooter.api.entity.TootAttachment; import jp.juggler.subwaytooter.api.entity.TootMention; import jp.juggler.subwaytooter.api.entity.TootResults; import jp.juggler.subwaytooter.api.entity.TootStatus; import jp.juggler.subwaytooter.dialog.AccountPicker; import jp.juggler.subwaytooter.dialog.DlgDraftPicker; +import jp.juggler.subwaytooter.dialog.DlgTextInput; import jp.juggler.subwaytooter.table.AcctColor; import jp.juggler.subwaytooter.table.PostDraft; import jp.juggler.subwaytooter.table.SavedAccount; @@ -147,16 +150,16 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, break; case R.id.ivMedia1: - performAttachmentDelete( 0 ); + performAttachmentClick( 0 ); break; case R.id.ivMedia2: - performAttachmentDelete( 1 ); + performAttachmentClick( 1 ); break; case R.id.ivMedia3: - performAttachmentDelete( 2 ); + performAttachmentClick( 2 ); break; case R.id.ivMedia4: - performAttachmentDelete( 3 ); + performAttachmentClick( 3 ); break; case R.id.btnPost: @@ -952,8 +955,31 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, } // 添付した画像をタップ - void performAttachmentDelete( int idx ){ + void performAttachmentClick( int idx ){ final PostAttachment pa = attachment_list.get( idx ); + + new AlertDialog.Builder( this ) + .setTitle( R.string.media_attachment ) + .setItems( new CharSequence[]{ + getString( R.string.set_description ), + getString( R.string.delete ) + }, new DialogInterface.OnClickListener() { + @Override public void onClick( DialogInterface dialogInterface, int i ){ + switch( i ){ + case 0: + editAttachmentDescription( pa ); + break; + case 1: + deleteAttachment( pa ); + break; + } + } + } ) + .setNegativeButton( R.string.cancel, null ) + .show(); + } + + void deleteAttachment( @NonNull final PostAttachment pa ){ new AlertDialog.Builder( this ) .setTitle( R.string.confirm_delete_attachment ) .setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() { @@ -971,6 +997,107 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, } + void editAttachmentDescription( @NonNull final PostAttachment pa ){ + if( pa.attachment == null ){ + Utils.showToast( this, true, R.string.attachment_description_cant_edit_while_uploading ); + } + DlgTextInput.show( this, getString( R.string.attachment_description ), pa.attachment.description, new DlgTextInput.Callback() { + @Override public void onOK( Dialog dialog, String text ){ + setAttachmentDescription( pa, dialog, text ); + } + + @Override public void onEmptyError(){ + Utils.showToast( ActPost.this, true, R.string.description_empty ); + } + } ); + } + + private void setAttachmentDescription( final PostAttachment pa, final Dialog dialog, final String text ){ + //noinspection deprecation + final ProgressDialog progress = new ProgressDialog( ActPost.this ); + + final AsyncTask< Void, Void, TootApiResult > task = new AsyncTask< Void, Void, TootApiResult >() { + + final SavedAccount target_account = account; + + @Override protected TootApiResult doInBackground( Void... params ){ + TootApiClient client = new TootApiClient( ActPost.this, new TootApiClient.Callback() { + @Override public boolean isApiCancelled(){ + return isCancelled(); + } + + @Override public void publishApiProgress( String s ){ + progress.setMessage( s ); + } + } ); + client.setAccount( target_account ); + + JSONObject json = new JSONObject(); + try{ + json.put( "description", text ); + }catch( JSONException ex ){ + log.trace( ex ); + log.e( ex, "description encoding failed." ); + } + + String body_string = json.toString(); + RequestBody request_body = RequestBody.create( + TootApiClient.MEDIA_TYPE_JSON + , body_string + ); + final Request.Builder request_builder = new Request.Builder().put( request_body ); + + TootApiResult result = client.request( "/api/v1/media/" + pa.attachment.id, request_builder ); + if( result != null && result.object != null ){ + this.attachment = TootAttachment.parse( result.object ); + } + return result; + } + + TootAttachment attachment; + + @Override protected void onCancelled( TootApiResult result ){ + super.onCancelled( result ); + } + + @Override protected void onPostExecute( TootApiResult result ){ + + try{ + progress.dismiss(); + }catch( Throwable ignored ){ + + } + + if( result == null ){ + // cancelled. + }else if( attachment != null ){ + pa.attachment = attachment; + showMediaAttachment(); + + try{ + dialog.dismiss(); + }catch( Throwable ignored ){ + + } + + }else{ + Utils.showToast( ActPost.this, true, result.error ); + } + } + }; + + progress.setIndeterminate( true ); + progress.setCancelable( true ); + progress.setOnCancelListener( new DialogInterface.OnCancelListener() { + @Override public void onCancel( DialogInterface dialog ){ + task.cancel( true ); + } + } ); + progress.show(); + + task.executeOnExecutor( App1.task_executor ); + } + void openAttachment(){ int permissionCheck = ContextCompat.checkSelfPermission( this, Manifest.permission.WRITE_EXTERNAL_STORAGE ); if( permissionCheck != PackageManager.PERMISSION_GRANTED ){ @@ -1291,16 +1418,18 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener, Utils.showToast( ActPost.this, false, R.string.attachment_uploaded ); - // 投稿欄に追記する - String sv = etContent.getText().toString(); - int l = sv.length(); - if( l > 0 ){ - char c = sv.charAt( l - 1 ); - if( c > 0x20 ) sv = sv + " "; + // 投稿欄の末尾に追記する + int selStart = etContent.getSelectionStart(); + int selEnd = etContent.getSelectionEnd(); + Editable e = etContent.getEditableText(); + int len = e.length(); + char last_char = ( len <= 0 ? ' ' : e.charAt( len - 1 ) ); + if( ! EmojiDecoder.isWhitespaceBeforeEmoji( last_char ) ){ + e.append( " " + pa.attachment.text_url ); + }else{ + e.append( pa.attachment.text_url ); } - sv = sv + pa.attachment.text_url + " "; - etContent.setText( sv ); - etContent.setSelection( sv.length() ); + etContent.setSelection( selStart, selEnd ); showMediaAttachment(); } diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.java b/app/src/main/java/jp/juggler/subwaytooter/App1.java index f1fcb8d6..136f0781 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/App1.java +++ b/app/src/main/java/jp/juggler/subwaytooter/App1.java @@ -71,7 +71,7 @@ public class App1 extends Application { public static final String FILE_PROVIDER_AUTHORITY = "jp.juggler.subwaytooter.FileProvider"; static final String DB_NAME = "app_db"; - static final int DB_VERSION = 18; + static final int DB_VERSION = 19; // 2017/4/25 v10 1=>2 SavedAccount に通知設定を追加 // 2017/4/25 v10 1=>2 NotificationTracking テーブルを追加 @@ -89,6 +89,7 @@ public class App1 extends Application { // 2017/7/22 v99 15=>16 SavedAccountに項目追加 // 2017/7/22 v106 16=>17 AcctColor に項目追加 // 2017/9/23 v161 17=>18 SavedAccountに項目追加 + // 2017/9/23 v161 18=>19 ClientInfoテーブルを置き換える private static DBOpenHelper db_open_helper; diff --git a/app/src/main/java/jp/juggler/subwaytooter/AppDataExporter.java b/app/src/main/java/jp/juggler/subwaytooter/AppDataExporter.java index 21c34a55..280cf458 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/AppDataExporter.java +++ b/app/src/main/java/jp/juggler/subwaytooter/AppDataExporter.java @@ -23,6 +23,7 @@ import java.util.Locale; import java.util.Map; import jp.juggler.subwaytooter.table.AcctColor; +import jp.juggler.subwaytooter.table.ClientInfo; import jp.juggler.subwaytooter.table.MutedApp; import jp.juggler.subwaytooter.table.MutedWord; import jp.juggler.subwaytooter.table.SavedAccount; @@ -438,6 +439,7 @@ public class AppDataExporter { private static final String KEY_ACCT_COLOR = "acct_color"; private static final String KEY_MUTED_APP = "muted_app"; private static final String KEY_MUTED_WORD = "muted_word"; + private static final String KEY_CLIENT_INFO = "client_info2"; static void encodeAppData( Context context, JsonWriter writer ) throws IOException, JSONException{ @@ -455,6 +457,7 @@ public class AppDataExporter { writeFromTable( writer, KEY_ACCT_COLOR, AcctColor.table ); writeFromTable( writer, KEY_MUTED_APP, MutedApp.table ); writeFromTable( writer, KEY_MUTED_WORD, MutedWord.table ); + writeFromTable( writer, KEY_CLIENT_INFO, ClientInfo.table ); ////////////////////////////////////// { @@ -494,6 +497,9 @@ public class AppDataExporter { }else if( KEY_MUTED_WORD.equals( name ) ){ importTable( reader, MutedWord.table, null ); + + }else if( KEY_CLIENT_INFO.equals( name ) ){ + importTable( reader, ClientInfo.table, null ); }else if( KEY_COLUMN.equals( name ) ){ result = readColumn( app_state, reader, account_id_map ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java index 272299d4..121fabe9 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ColumnViewHolder.java @@ -679,6 +679,8 @@ class ColumnViewHolder break; case R.id.btnColumnReload: + App1.custom_emoji_cache.clearErrorCache(); + if( column.column_type == Column.TYPE_SEARCH || column.column_type == Column.TYPE_SEARCH_PORTAL ){ Utils.hideKeyboard( activity, etSearch ); etSearch.setText( column.search_query ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java b/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java index 327f1cb5..45c490fe 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ItemViewHolder.java @@ -684,8 +684,31 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener { url = ta.url; } } - // 正方形じゃないせいか、うまく動かない activity.density * 4f ); + // 正方形じゃないせいか、うまく動かない // activity.density * 4f ); iv.setImageUrl( activity.pref, 0f, access_info.supplyBaseUrl( url ), access_info.supplyBaseUrl( url ) ); + + if(!TextUtils.isEmpty( ta.description )){ + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT ); + lp.topMargin = (int) ( 0.5f + llExtra.getResources().getDisplayMetrics().density * 3f ); + MyTextView tv = new MyTextView( activity ); + tv.setLayoutParams( lp ); + // + tv.setMovementMethod( MyLinkMovementMethod.getInstance() ); + if( ! Float.isNaN( activity.timeline_font_size_sp ) ){ + tv.setTextSize( activity.timeline_font_size_sp ); + } + int c = column.content_color != 0 ? column.content_color : content_color_default; + tv.setTextColor( c ); + + // + String desc = activity.getString( R.string.media_description,idx+1,ta.description ); + tv.setText( new DecodeOptions() + .setCustomEmojiMap(status.custom_emojis ) + .setProfileEmojis( status.profile_emojis ) + .decodeEmoji( activity, desc ) + ); + llExtra.addView( tv ); + } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.java b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.java index de261980..979153f6 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiClient.java @@ -7,6 +7,7 @@ import android.support.annotation.Nullable; import android.text.TextUtils; import org.json.JSONArray; +import org.json.JSONException; import org.json.JSONObject; import jp.juggler.subwaytooter.App1; @@ -48,7 +49,7 @@ public class TootApiClient { void onCallCreated( Call call ); } - CurrentCallCallback call_callback; + private CurrentCallCallback call_callback; public void setCurrentCallCallback( CurrentCallCallback call_callback ){ this.call_callback = call_callback; @@ -91,12 +92,15 @@ public class TootApiClient { return result; } + private static final String DEFAULT_CLIENT_NAME = "SubwayTooter"; + private static final String KEY_CLIENT_CREDENTIAL = "SubwayTooterClientCredential"; + private static final String KEY_AUTH_VERSION = "SubwayTooterAuthVersion"; private static final int AUTH_VERSION = 1; private static final String REDIRECT_URL = "subwaytooter://oauth"; - public static final String MIMUMEDON = "mimumedon.com"; - public static final String MIMUMEDON_ERROR = "mimumedon.comには対応しません"; + private static final String MIMUMEDON = "mimumedon.com"; + private static final String MIMUMEDON_ERROR = "mimumedon.comには対応しません"; private @Nullable TootApiResult request_sub( @NonNull String path, @NonNull Request.Builder request_builder ){ @@ -255,24 +259,163 @@ public class TootApiClient { } } + // クライアントアプリの登録を確認するためのトークンを生成する + // oAuth2 Client Credentials の取得 + // https://github.com/doorkeeper-gem/doorkeeper/wiki/Client-Credentials-flow + // このトークンはAPIを呼び出すたびに新しく生成される… + private @Nullable String getClientCredential( JSONObject client_info ){ + try{ + Request request = new Request.Builder() + .url( "https://" + instance + "/oauth/token" ) + .post( RequestBody.create( MEDIA_TYPE_FORM_URL_ENCODED + , "grant_type=client_credentials" + + "&client_id=" + Uri.encode( client_info.optString( "client_id" ) ) + + "&client_secret=" + Uri.encode( client_info.optString( "client_secret" ) ) + ) ) + .build(); + Call call = ok_http_client.newCall( request ); + if( call_callback != null ) call_callback.onCallCreated( call ); + Response response = call.execute(); + if( callback.isApiCancelled() ){ + return null; + } + + if( ! response.isSuccessful() ){ + log.e( "getClientCredential: " + Utils.formatResponse( response, instance ) ); + return null; + } + + //noinspection ConstantConditions + String body = response.body().string(); + if( TextUtils.isEmpty( body ) || body.startsWith( "<" ) ){ + log.e( "getClientCredential: " + context.getString( R.string.response_not_json ) + ": " + body ); + return null; + } + + JSONObject json = new JSONObject( body ); + String error = Utils.optStringX( json, "error" ); + if( ! TextUtils.isEmpty( error ) ){ + log.e( "getClientCredential: API returns error: %s", error ); + return null; + } + + String client_credential = Utils.optStringX( json, "access_token" ); + if( TextUtils.isEmpty( client_credential ) ){ + log.e( "getClientCredential: API returns empty client_credential." ); + return null; + } + return client_credential; + + }catch( Throwable ex ){ + log.trace( ex ); + log.e( "getClientCredential: " + instance + ": " + Utils.formatError( ex, context.getResources(), R.string.network_error ) ); + return null; + } + } + + private boolean verifyClientCredential( @NonNull String client_credential ){ + try{ + Request request = new Request.Builder() + .url( "https://" + instance + "/api/v1/apps/verify_credentials" ) + .header( "Authorization", "Bearer " + client_credential ) + .build(); + + Call call = ok_http_client.newCall( request ); + if( call_callback != null ) call_callback.onCallCreated( call ); + Response response = call.execute(); + if( callback.isApiCancelled() ){ + return false; + } + + if( ! response.isSuccessful() ){ + log.e( "verifyClientCredential: " + Utils.formatResponse( response, instance ) ); + return false; + } + + //noinspection ConstantConditions + String body = response.body().string(); + if( TextUtils.isEmpty( body ) || body.startsWith( "<" ) ){ + log.e( "verifyClientCredential: " + context.getString( R.string.response_not_json ) + ": " + body ); + return false; + } + + JSONObject json = new JSONObject( body ); + String error = Utils.optStringX( json, "error" ); + if( ! TextUtils.isEmpty( error ) ){ + log.e( "verifyClientCredential: API returns error: %s", error ); + return false; + } + + // {"name":"SubwayTooter","website":null} + + return true; + + }catch( Throwable ex ){ + log.trace( ex ); + log.e( "verifyClientCredential: " + instance + ": " + Utils.formatError( ex, context.getResources(), R.string.network_error ) ); + return false; + } + } + + private TootApiResult prepareBrowserUrl( @NonNull JSONObject client_info ){ + // 認証ページURLを作る + final String browser_url = "https://" + instance + "/oauth/authorize" + + "?client_id=" + Uri.encode( Utils.optStringX( client_info, "client_id" ) ) + // この段階では要らない + "&client_secret=" + Uri.encode( Utils.optStringX( client_info, "client_secret" ) ) + + "&response_type=code" + + "&redirect_uri=" + Uri.encode( REDIRECT_URL ) + + "&scope=read write follow" + + "&scopes=read write follow" + + "&state=" + ( account != null ? "db:" + account.db_id : "host:" + instance ) + + "&grant_type=authorization_code" + // + "&username=" + Uri.encode( user_mail ) + // + "&password=" + Uri.encode( password ) + + "&approval_prompt=force" + // +"&access_type=offline" + ; + // APIリクエストは失敗?する + // URLをエラーとして返す + return new TootApiResult( browser_url ); + + } + public @Nullable TootApiResult authorize1( String client_name ){ if( MIMUMEDON.equalsIgnoreCase( instance ) ) return new TootApiResult( MIMUMEDON_ERROR ); - JSONObject client_info; + if( TextUtils.isEmpty( client_name ) ){ + client_name = DEFAULT_CLIENT_NAME; + } - // サーバ側がクライアント情報を消した場合、今の認証フローではアプリがそれを知ることができない - // 毎回クライアント情報を作ることでしか対策できない - - callback.publishApiProgress( context.getString( R.string.register_app_to_server, instance ) ); + // クライアントIDがアプリ上に保存されているか? + JSONObject client_info = ClientInfo.load( instance, client_name ); + if( client_info != null ){ + // client_credential をまだ取得していないなら取得する + String client_credential = Utils.optStringX( client_info, KEY_CLIENT_CREDENTIAL ); + if( TextUtils.isEmpty( client_credential ) ){ + client_credential = getClientCredential( client_info ); + if( ! TextUtils.isEmpty( client_credential ) ){ + try{ + client_info.put( KEY_CLIENT_CREDENTIAL, client_credential ); + ClientInfo.save( instance, client_name, client_info.toString() ); + }catch( JSONException ignored ){ + } + } + } + // client_credential があるならcredentialがまだ使えるか確認する + if( ! TextUtils.isEmpty( client_credential ) ){ + boolean isClientOk = verifyClientCredential( client_credential ); + if( isClientOk ){ + return prepareBrowserUrl( client_info ); + } + } + } // OAuth2 クライアント登録 - + callback.publishApiProgress( context.getString( R.string.register_app_to_server, instance ) ); Response response; + try{ - if( TextUtils.isEmpty( client_name ) ){ - client_name = "SubwayTooter"; - } Request request = new Request.Builder() .url( "https://" + instance + "/api/v1/apps" ) @@ -296,6 +439,7 @@ public class TootApiClient { if( ! response.isSuccessful() ){ return new TootApiResult( response, Utils.formatResponse( response, instance ) ); } + try{ //noinspection ConstantConditions String json = response.body().string(); @@ -311,38 +455,25 @@ public class TootApiClient { client_info.put( KEY_AUTH_VERSION, AUTH_VERSION ); // authorize2 で使う - ClientInfo.save( instance, client_info.toString() ); + ClientInfo.save( instance, client_name, client_info.toString() ); }catch( Throwable ex ){ log.trace( ex ); return new TootApiResult( Utils.formatError( ex, "API data error" ) ); } - // 認証ページURLを作る - final String browser_url = "https://" + instance + "/oauth/authorize" - + "?client_id=" + Uri.encode( Utils.optStringX( client_info, "client_id" ) ) - // この段階では要らない + "&client_secret=" + Uri.encode( Utils.optStringX( client_info, "client_secret" ) ) - + "&response_type=code" - + "&redirect_uri=" + Uri.encode( REDIRECT_URL ) - + "&scope=read write follow" - + "&scopes=read write follow" - + "&state=" + ( account != null ? "db:" + account.db_id : "host:" + instance ) - + "&grant_type=authorization_code" - // + "&username=" + Uri.encode( user_mail ) - // + "&password=" + Uri.encode( password ) - + "&approval_prompt=force" - // +"&access_type=offline" - ; - // APIリクエストは失敗?する - // URLをエラーとして返す - return new TootApiResult( browser_url ); + return prepareBrowserUrl( client_info ); } - public @Nullable TootApiResult authorize2( String code ){ + public @Nullable TootApiResult authorize2( String client_name, String code ){ if( MIMUMEDON.equalsIgnoreCase( instance ) ) return new TootApiResult( MIMUMEDON_ERROR ); - JSONObject client_info = ClientInfo.load( instance ); + if( TextUtils.isEmpty( client_name ) ){ + client_name = DEFAULT_CLIENT_NAME; + } + + JSONObject client_info = ClientInfo.load( instance, client_name ); if( client_info == null ){ return new TootApiResult( "missing client id" ); } @@ -455,11 +586,6 @@ public class TootApiClient { if( MIMUMEDON.equalsIgnoreCase( instance ) ) return new TootApiResult( MIMUMEDON_ERROR ); - JSONObject client_info = ClientInfo.load( instance ); - if( client_info == null ){ - return new TootApiResult( "missing client id" ); - } - JSONObject token_info; Response response; try{ diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAttachment.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAttachment.java index 9ceccddc..14fa8e51 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAttachment.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootAttachment.java @@ -41,6 +41,9 @@ public class TootAttachment { // Shorter URL for the image, for insertion into text (only present on local images) @Nullable public String text_url; + // ALT text (Mastodon 2.0.0 or later) + @Nullable public String description; + public JSONObject json; @Nullable @@ -55,6 +58,7 @@ public class TootAttachment { dst.remote_url = Utils.optStringX( src, "remote_url" ); dst.preview_url = Utils.optStringX( src, "preview_url" ); dst.text_url = Utils.optStringX( src, "text_url" ); + dst.description = Utils.optStringX( src, "description" ); return dst; }catch( Throwable ex ){ log.trace( ex ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgAccessToken.java b/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgTextInput.java similarity index 50% rename from app/src/main/java/jp/juggler/subwaytooter/dialog/DlgAccessToken.java rename to app/src/main/java/jp/juggler/subwaytooter/dialog/DlgTextInput.java index e192bac7..f33740cc 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgAccessToken.java +++ b/app/src/main/java/jp/juggler/subwaytooter/dialog/DlgTextInput.java @@ -4,6 +4,7 @@ import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.view.KeyEvent; import android.view.View; @@ -13,44 +14,57 @@ import android.widget.EditText; import android.widget.TextView; import jp.juggler.subwaytooter.R; -import jp.juggler.subwaytooter.util.Utils; -public class DlgAccessToken { +public class DlgTextInput { public interface Callback { - void startCheck( Dialog dialog, String access_token ); + void onOK( Dialog dialog, String text ); + + void onEmptyError(); } - public static void show( @NonNull final Activity activity ,@NonNull final Callback callback ){ - @SuppressLint("InflateParams") final View view = activity.getLayoutInflater().inflate( R.layout.dlg_access_token, null, false ); - final EditText etToken = (EditText) view.findViewById( R.id.etToken ); + public static void show( + @NonNull final Activity activity + , @NonNull CharSequence caption + , @Nullable CharSequence initial_text + , @NonNull final Callback callback + ){ + @SuppressLint("InflateParams") final View view = activity.getLayoutInflater().inflate( R.layout.dlg_text_input, null, false ); + final EditText etInput = view.findViewById( R.id.etInput ); final View btnOk = view.findViewById( R.id.btnOk ); + final TextView tvCaption = view.findViewById( R.id.tvCaption ); - etToken.setOnEditorActionListener( new TextView.OnEditorActionListener() { - @Override public boolean onEditorAction( TextView v, int actionId, KeyEvent event ){ - if( actionId == EditorInfo.IME_ACTION_DONE ){ - btnOk.performClick(); - return true; - } - return false; + tvCaption.setText( caption ); + if( !TextUtils.isEmpty( initial_text) ){ + etInput.setText( initial_text ); + etInput.setSelection( initial_text.length() ); + } + + etInput.setOnEditorActionListener( new TextView.OnEditorActionListener() { + @Override public boolean onEditorAction( TextView v, int actionId, KeyEvent event ){ + if( actionId == EditorInfo.IME_ACTION_DONE ){ + btnOk.performClick(); + return true; } - } ); - + return false; + } + } ); + final Dialog dialog = new Dialog( activity ); dialog.setContentView( view ); btnOk.setOnClickListener( new View.OnClickListener() { @Override public void onClick( View v ){ - final String token = etToken.getText().toString().trim(); + final String token = etInput.getText().toString().trim(); if( TextUtils.isEmpty( token ) ){ - Utils.showToast( activity, true, R.string.token_not_specified ); - return; + callback.onEmptyError(); + }else{ + callback.onOK( dialog, token ); } - callback.startCheck( dialog, token ); } } ); - + view.findViewById( R.id.btnCancel ).setOnClickListener( new View.OnClickListener() { @Override public void onClick( View v ){ diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.java b/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.java index 429ad3ea..ecc686a6 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.java +++ b/app/src/main/java/jp/juggler/subwaytooter/table/ClientInfo.java @@ -3,6 +3,7 @@ package jp.juggler.subwaytooter.table; import android.content.ContentValues; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; +import android.support.annotation.NonNull; import org.json.JSONObject; @@ -10,34 +11,36 @@ import jp.juggler.subwaytooter.App1; import jp.juggler.subwaytooter.util.LogCategory; public class ClientInfo { - static final LogCategory log = new LogCategory( "ClientInfo" ); + private static final LogCategory log = new LogCategory( "ClientInfo" ); - static final String table = "client_info"; - static final String COL_HOST = "h"; - static final String COL_RESULT = "r"; + public static final String table = "client_info2"; + private static final String COL_HOST = "h"; + private static final String COL_CLIENT_NAME = "cn"; + private static final String COL_RESULT = "r"; public static void onDBCreate( SQLiteDatabase db ){ db.execSQL( "create table if not exists " + table + "(_id INTEGER PRIMARY KEY" + ",h text not null" + + ",cn text not null" + ",r text not null" + ")" ); db.execSQL( - "create unique index if not exists " + table + "_host on " + table - + "(h" - + ")" + "create unique index if not exists " + table + "_host_client_name on " + table + "(h,cn)" ); } public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ - + if( oldVersion <= 18 && newVersion >= 19 ){ + onDBCreate( db ); + } } - public static JSONObject load( String instance ){ + public static JSONObject load( @NonNull String instance, @NonNull String client_name ){ try{ - Cursor cursor = App1.getDB().query( table, null, "h=?", new String[]{ instance }, null, null, null ); + Cursor cursor = App1.getDB().query( table, null, "h=? and cn=?", new String[]{ instance, client_name }, null, null, null ); try{ if( cursor.moveToFirst() ){ return new JSONObject( cursor.getString( cursor.getColumnIndex( COL_RESULT ) ) ); @@ -51,10 +54,11 @@ public class ClientInfo { return null; } - public static void save( String host, String json ){ + public static void save( @NonNull String instance, @NonNull String client_name, @NonNull String json ){ try{ ContentValues cv = new ContentValues(); - cv.put( COL_HOST, host ); + cv.put( COL_HOST, instance ); + cv.put( COL_CLIENT_NAME, client_name ); cv.put( COL_RESULT, json ); App1.getDB().replace( table, null, cv ); }catch( Throwable ex ){ diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.java b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.java index e9a363ac..164e9692 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/CustomEmojiCache.java @@ -39,6 +39,11 @@ public class CustomEmojiCache { final ConcurrentHashMap< String, Long > cache_error = new ConcurrentHashMap<>(); + // カラムのリロードボタンを押したタイミングでエラーキャッシュをクリアする + public void clearErrorCache(){ + cache_error.clear(); + } + //////////////////////////////// // 成功キャッシュ diff --git a/app/src/main/res/layout/dlg_access_token.xml b/app/src/main/res/layout/dlg_text_input.xml similarity index 89% rename from app/src/main/res/layout/dlg_access_token.xml rename to app/src/main/res/layout/dlg_text_input.xml index 4dbada43..b254fc06 100644 --- a/app/src/main/res/layout/dlg_access_token.xml +++ b/app/src/main/res/layout/dlg_text_input.xml @@ -12,12 +12,12 @@ android:layout_marginEnd="12dp" android:layout_marginStart="12dp" android:layout_marginTop="12dp" - android:labelFor="@+id/etToken" - android:text="@string/access_token" + android:id="@+id/tvCaption" + android:labelFor="@+id/etInput" /> Emoji Not blocked. Not muted. + (attachment %1$d) %2$s + set description(Mastodon 2.0.0 or later) + Attachment description can\'t be edit while uploading. + attachment description + description is not specified. + sending attachment description… diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index f27603fd..899e4dfb 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -807,4 +807,10 @@ 絵文字 ブロックできません ミュートできません + (添付 %1$d) %2$s + 説明文を設定(マストドン2.0.0以降) + 添付メディアのアップロード中は説明文を設定できません + 添付メディアの説明文 + 説明文が指定されてません + 添付メディアの説明文の送信中… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0907e96d..68e68cb5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -511,5 +511,11 @@ Emoji Not blocked. Not muted. + (attachment %1$d) %2$s + set description(Mastodon 2.0.0 or later) + Attachment description can\'t be edit while uploading. + attachment description + description is not specified. + sending attachment description…