- 添付メディアに説明文を指定できます。指定した説明文はTL中に表示されます。
- アカウント追加とアクセストークン更新の際にクライアント登録を再利用する
- カラムのリロードボタンを押したらカスタム絵文字のエラーキャッシュをクリアする
- 添付メディアのアップロード完了時に投稿欄にURLを付与する位置をテキストの末尾に変更
This commit is contained in:
tateisu 2017-10-19 07:01:20 +09:00
parent 8baa66dacb
commit f9f0352d82
16 changed files with 440 additions and 98 deletions

View File

@ -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"
}

View File

@ -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 );
}
} );
}

View File

@ -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();
}

View File

@ -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;

View File

@ -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 );

View File

@ -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 );

View File

@ -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 );
}
}
}

View File

@ -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{

View File

@ -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 );

View File

@ -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 ){

View File

@ -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 ){

View File

@ -39,6 +39,11 @@ public class CustomEmojiCache {
final ConcurrentHashMap< String, Long > cache_error = new ConcurrentHashMap<>();
// カラムのリロードボタンを押したタイミングでエラーキャッシュをクリアする
public void clearErrorCache(){
cache_error.clear();
}
////////////////////////////////
// 成功キャッシュ

View File

@ -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"
/>
<EditText
android:id="@+id/etToken"
android:id="@+id/etInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"

View File

@ -520,6 +520,12 @@
<string name="emoji">Emoji</string>
<string name="not_blocked">Not blocked.</string>
<string name="not_muted">Not muted.</string>
<string name="media_description">(attachment %1$d) %2$s</string>
<string name="set_description">set description(Mastodon 2.0.0 or later)</string>
<string name="attachment_description_cant_edit_while_uploading">Attachment description can\'t be edit while uploading.</string>
<string name="attachment_description">attachment description</string>
<string name="description_empty">description is not specified.</string>
<string name="attachment_description_sending">sending attachment description…</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>-->

View File

@ -807,4 +807,10 @@
<string name="emoji">絵文字</string>
<string name="not_blocked">ブロックできません</string>
<string name="not_muted">ミュートできません</string>
<string name="media_description">(添付 %1$d) %2$s</string>
<string name="set_description">説明文を設定(マストドン2.0.0以降)</string>
<string name="attachment_description_cant_edit_while_uploading">添付メディアのアップロード中は説明文を設定できません</string>
<string name="attachment_description">添付メディアの説明文</string>
<string name="description_empty">説明文が指定されてません</string>
<string name="attachment_description_sending">添付メディアの説明文の送信中…</string>
</resources>

View File

@ -511,5 +511,11 @@
<string name="emoji">Emoji</string>
<string name="not_blocked">Not blocked.</string>
<string name="not_muted">Not muted.</string>
<string name="media_description">(attachment %1$d) %2$s</string>
<string name="set_description">set description(Mastodon 2.0.0 or later)</string>
<string name="attachment_description_cant_edit_while_uploading">Attachment description can\'t be edit while uploading.</string>
<string name="attachment_description">attachment description</string>
<string name="description_empty">description is not specified.</string>
<string name="attachment_description_sending">sending attachment description…</string>
</resources>