mirror of
https://github.com/tateisu/SubwayTooter
synced 2025-01-29 18:19:22 +01:00
v1.7.0
- 添付メディアに説明文を指定できます。指定した説明文はTL中に表示されます。 - アカウント追加とアクセストークン更新の際にクライアント登録を再利用する - カラムのリロードボタンを押したらカスタム絵文字のエラーキャッシュをクリアする - 添付メディアのアップロード完了時に投稿欄にURLを付与する位置をテキストの末尾に変更
This commit is contained in:
parent
8baa66dacb
commit
f9f0352d82
@ -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"
|
||||
}
|
||||
|
||||
|
@ -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 );
|
||||
}
|
||||
} );
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 );
|
||||
|
@ -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 );
|
||||
|
@ -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 );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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{
|
||||
|
@ -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 );
|
||||
|
@ -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 ){
|
@ -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 ){
|
||||
|
@ -39,6 +39,11 @@ public class CustomEmojiCache {
|
||||
|
||||
final ConcurrentHashMap< String, Long > cache_error = new ConcurrentHashMap<>();
|
||||
|
||||
// カラムのリロードボタンを押したタイミングでエラーキャッシュをクリアする
|
||||
public void clearErrorCache(){
|
||||
cache_error.clear();
|
||||
}
|
||||
|
||||
////////////////////////////////
|
||||
// 成功キャッシュ
|
||||
|
||||
|
@ -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"
|
@ -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>-->
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user