diff --git a/.idea/dictionaries/tateisu.xml b/.idea/dictionaries/tateisu.xml index 4749007f..83b1d704 100644 --- a/.idea/dictionaries/tateisu.xml +++ b/.idea/dictionaries/tateisu.xml @@ -10,6 +10,7 @@ enty favourited firebase + foregrounder gifv hashtag hashtags diff --git a/.idea/misc.xml b/.idea/misc.xml index fbb68289..5d199810 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -37,7 +37,7 @@ - + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 11b52bc2..61122a88 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + + + = 26 ){ + context.startForegroundService( intent ); + }else{ + context.startService(intent); + } } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/PollingForegrounder.java b/app/src/main/java/jp/juggler/subwaytooter/PollingForegrounder.java new file mode 100644 index 00000000..316fa292 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/PollingForegrounder.java @@ -0,0 +1,107 @@ +package jp.juggler.subwaytooter; + +import android.app.IntentService; +import android.app.Notification; +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; +import android.support.v4.content.ContextCompat; +import android.text.TextUtils; + +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.NotificationHelper; + +public class PollingForegrounder extends IntentService { + + static final LogCategory log = new LogCategory( "PollingForegrounder" ); + + static final int NOTIFICATION_ID_FOREGROUNDER = 2; + + public PollingForegrounder(){ + super( "PollingForegrounder" ); + } + + @Nullable @Override public IBinder onBind( Intent intent ){ + return null; + } + + @Override public void onCreate(){ + log.d( "onCreate" ); + super.onCreate(); + + // メインスレッド上でPollingWorkerを初期化しておく + PollingWorker.getInstance( getApplicationContext() ); + + startForeground( NOTIFICATION_ID_FOREGROUNDER, createNotification( getApplicationContext(), "" ) ); + } + + @Override public void onDestroy(){ + log.d( "onDestroy" ); + + stopForeground( true ); + super.onDestroy(); + } + + private Notification createNotification( Context context, String text ){ + // 通知タップ時のPendingIntent + Intent intent_click = new Intent( context, ActMain.class ); + intent_click.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK ); + PendingIntent pi_click = PendingIntent.getActivity( context, 2, intent_click, PendingIntent.FLAG_UPDATE_CURRENT ); + + NotificationCompat.Builder builder; + if( Build.VERSION.SDK_INT >= 26 ){ + // Android 8 から、通知のスタイルはユーザが管理することになった + // NotificationChannel を端末に登録しておけば、チャネルごとに管理画面が作られる + NotificationChannel channel = NotificationHelper.createNotificationChannel( + context + , "PollingForegrounder" // id + , "real-time message notifier" // The user-visible name of the channel. + , null // The user-visible description of the channel. + , NotificationManager.IMPORTANCE_LOW + ); + builder = new NotificationCompat.Builder( context, channel.getId() ); + }else{ + builder = new NotificationCompat.Builder( context, "not_used" ); + } + + builder + .setContentIntent( pi_click ) + .setAutoCancel( false ) + .setOngoing( true ) + .setSmallIcon( R.drawable.ic_notification ) // ここは常に白テーマのアイコンを使う + .setColor( ContextCompat.getColor( context, R.color.Light_colorAccent ) ) // ここは常に白テーマの色を使う + .setWhen( System.currentTimeMillis() ) + .setContentTitle( context.getString( R.string.loading_notification_title ) ) + .setContentText( text ) + ; + + // Android 7.0 ではグループを指定しないと勝手に通知が束ねられてしまう。 + // 束ねられた通知をタップしても pi_click が実行されないので困るため、 + // アカウント別にグループキーを設定する + builder.setGroup( context.getPackageName() + ":PollingForegrounder" ); + + return builder.build(); + } + + String last_status = null; + + @Override protected void onHandleIntent( @Nullable Intent intent ){ + if( intent == null ) return; + String tag = intent.getStringExtra( PollingWorker.EXTRA_TAG ); + final Context context = getApplicationContext(); + PollingWorker.handleFCMMessage( this, tag, new PollingWorker.JobStatusCallback() { + @Override public void onStatus( final String sv ){ + if( TextUtils.isEmpty( sv ) || sv.equals( last_status ) ) return; + last_status = sv; + log.d( "onStatus %s",sv ); + startForeground( NOTIFICATION_ID_FOREGROUNDER, createNotification( context, sv ) ); + } + } ); + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.java b/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.java index 1025e895..df3c0490 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.java +++ b/app/src/main/java/jp/juggler/subwaytooter/PollingWorker.java @@ -11,6 +11,8 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; import android.net.Uri; import android.net.wifi.WifiManager; import android.os.Build; @@ -38,6 +40,7 @@ import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; +import java.util.TreeSet; import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; @@ -68,6 +71,7 @@ public class PollingWorker { static final LogCategory log = new LogCategory( "PollingWorker" ); static final int NOTIFICATION_ID = 1; + static final int NOTIFICATION_ID_ERROR = 3; // Notification のJSONObject を日時でソートするためにデータを追加する static final String KEY_TIME = "<>time"; @@ -125,6 +129,7 @@ public class PollingWorker { final Context context; final Handler handler; final SharedPreferences pref; + final ConnectivityManager connectivityManager; final NotificationManager notification_manager; final JobScheduler scheduler; final PowerManager power_manager; @@ -136,6 +141,7 @@ public class PollingWorker { log.d( "ctor" ); this.context = c.getApplicationContext(); + this.connectivityManager = (ConnectivityManager) context.getSystemService( Context.CONNECTIVITY_SERVICE ); this.notification_manager = (NotificationManager) context.getSystemService( Context.NOTIFICATION_SERVICE ); this.scheduler = (JobScheduler) context.getSystemService( Context.JOB_SCHEDULER_SERVICE ); @@ -209,6 +215,7 @@ public class PollingWorker { public void run(){ log.e( "worker thread start." ); + job_status.set( "worker thread start." ); while( ! bThreadCancelled.get() ){ JobItem item = null; try{ @@ -221,15 +228,19 @@ public class PollingWorker { break; } } + if( item == null ){ + job_status.set( "no job to run." ); waitEx( 86400000L ); continue; } + job_status.set( "start job " + item.jobId ); acquirePowerLock(); try{ item.refWorker.set( Worker.this ); item.run(); }finally{ + job_status.set( "end job " + item.jobId ); item.refWorker.set( null ); releasePowerLock(); } @@ -237,6 +248,7 @@ public class PollingWorker { log.trace( ex ); } } + job_status.set( "worker thread end." ); log.e( "worker thread end." ); } } @@ -263,7 +275,7 @@ public class PollingWorker { // JobService#onStartJob から呼ばれる public boolean onStartJob( @NonNull JobService jobService, @NonNull JobParameters params ){ JobItem item = new JobItem( jobService, params ); - addJob( item ); + addJob( item, true ); return true; // return True if your context needs to process the work (on a separate thread). // return False if there's no more work to be done for this job. @@ -272,9 +284,7 @@ public class PollingWorker { // FCMメッセージイベントから呼ばれる private boolean hasJob( int jobId ){ synchronized( job_list ){ - Iterator< JobItem > it = job_list.iterator(); - while( it.hasNext() ){ - JobItem itemOld = it.next(); + for( JobItem itemOld : job_list ){ if( itemOld.jobId == jobId ) return true; } } @@ -282,23 +292,25 @@ public class PollingWorker { } // FCMメッセージイベントから呼ばれる - private void addJob( int jobId ){ - addJob( new JobItem( jobId ) ); + private void addJob( int jobId, boolean bRemoveOld ){ + addJob( new JobItem( jobId ), bRemoveOld ); } - private void addJob( @NonNull JobItem item ){ + private void addJob( @NonNull JobItem item, boolean bRemoveOld ){ int jobId = item.jobId; // 同じジョブ番号がジョブリストにあるか? synchronized( job_list ){ - Iterator< JobItem > it = job_list.iterator(); - while( it.hasNext() ){ - JobItem itemOld = it.next(); - if( itemOld.jobId == jobId ){ - log.w( "onStartJob: jobId=%s, old job cancelled." ); - // 同じジョブをすぐに始めるのだからrescheduleはfalse - itemOld.cancel( false ); - it.remove(); + if( bRemoveOld ){ + Iterator< JobItem > it = job_list.iterator(); + while( it.hasNext() ){ + JobItem itemOld = it.next(); + if( itemOld.jobId == jobId ){ + log.w( "addJob: jobId=%s, old job cancelled.", jobId ); + // 同じジョブをすぐに始めるのだからrescheduleはfalse + itemOld.cancel( false ); + it.remove(); + } } } log.d( "addJob: jobId=%s, add to list.", jobId ); @@ -397,11 +409,25 @@ public class PollingWorker { public void run(){ + job_status.set( "job start." ); try{ log.d( "(JobItem.run jobId=%s", jobId ); - if( isJobCancelled() ) throw new JobCancelledException(); + job_status.set( "check network status.." ); + + long net_wait_start = SystemClock.elapsedRealtime(); + while( ! checkNetwork() ){ + if( isJobCancelled() ) throw new JobCancelledException(); + long now = SystemClock.elapsedRealtime(); + long delta = now - net_wait_start; + if( delta >= 10000L ){ + log.d( "network state timeout." ); + break; + } + waitWorkerThread( 333L ); + } + muted_app = MutedApp.getNameSet(); muted_word = MutedWord.getNameSet(); @@ -418,6 +444,7 @@ public class PollingWorker { // タスクがなかった場合でも定期実行ジョブからの実行ならポーリングを行う new TaskRunner().runTask( JobItem.this, TASK_POLLING, null ); } + job_status.set( "make next schedule." ); if( ! isJobCancelled() && bPollingComplete ){ // ポーリングが完了したのならポーリングが必要かどうかに合わせてジョブのスケジュールを変更する @@ -447,6 +474,8 @@ public class PollingWorker { }catch( Throwable ex ){ log.trace( ex ); log.e( ex, "job execution failed." ); + }finally{ + job_status.set( "job finished." ); } // ジョブ終了報告 if( ! isJobCancelled() ){ @@ -474,6 +503,23 @@ public class PollingWorker { log.d( ")JobItem.run jobId=%s, cancel=%s", jobId, isJobCancelled() ); } + private boolean checkNetwork(){ + NetworkInfo ni = connectivityManager.getActiveNetworkInfo(); + if( ni == null ){ + log.d( "checkNetwork: getActiveNetworkInfo() returns null." ); + return false; + }else{ + NetworkInfo.State state = ni.getState(); + NetworkInfo.DetailedState detail = ni.getDetailedState(); + log.d( "checkNetwork: state=%s,detail=%s", state, detail ); + if( state != NetworkInfo.State.CONNECTED ){ + log.d( "checkNetwork: not connected." ); + return false; + }else{ + return true; + } + } + } } ////////////////////////////////////////////////////////////////////// @@ -490,13 +536,17 @@ public class PollingWorker { JobItem job; int taskId; + final ArrayList< String > error_instance = new ArrayList<>(); + public void runTask( JobItem job, int taskId, JSONObject taskData ){ try{ log.e( "(runTask: taskId=%s", taskId ); + job_status.set( "start task " + taskId ); + this.job = job; this.taskId = taskId; - long process_db_id = -1L; + long process_db_id = - 1L; if( taskId == TASK_APP_DATA_IMPORT_BEFORE ){ scheduler.cancelAll(); @@ -569,6 +619,8 @@ public class PollingWorker { loadCustomStreamListenerSetting(); + job_status.set( "make install id" ); + // インストールIDを生成する // インストールID生成時にSavedAccountテーブルを操作することがあるので // アカウントリストの取得より先に行う @@ -576,16 +628,19 @@ public class PollingWorker { job.install_id = getInstallId(); } + job_status.set( "create account thread" ); + LinkedList< AccountThread > thread_list = new LinkedList<>(); for( SavedAccount _a : SavedAccount.loadAccountList( context, log ) ){ if( _a.isPseudo() ) continue; - if( process_db_id != -1L && _a.db_id != process_db_id ) continue; + if( process_db_id != - 1L && _a.db_id != process_db_id ) continue; AccountThread t = new AccountThread( _a ); thread_list.add( t ); t.start(); } for( ; ; ){ + TreeSet< String > set = new TreeSet<>(); Iterator< AccountThread > it = thread_list.iterator(); while( it.hasNext() ){ AccountThread t = it.next(); @@ -593,15 +648,28 @@ public class PollingWorker { it.remove(); continue; } + set.add( t.account.host ); if( job.isJobCancelled() ){ t.cancel(); } } - if( thread_list.isEmpty() ) break; - + int remain = thread_list.size(); + if( remain <= 0 ) break; + // + StringBuilder sb = new StringBuilder(); + for( String s : set ){ + if( sb.length() > 0 ) sb.append( ", " ); + sb.append( s ); + } + job_status.set( "waiting " + sb.toString() ); + // job.waitWorkerThread( job.isJobCancelled() ? 50L : 1000L ); } + synchronized( error_instance ){ + createErrorNotification( error_instance ); + } + if( ! job.isJobCancelled() ) job.bPollingComplete = true; }catch( Throwable ex ){ @@ -609,9 +677,67 @@ public class PollingWorker { log.e( ex, "task execution failed." ); }finally{ log.e( ")runTask: taskId=%s", taskId ); + job_status.set( "end task " + taskId ); } } + private void createErrorNotification( ArrayList< String > error_instance ){ + if( error_instance.isEmpty() ){ + return; + } + + // 通知タップ時のPendingIntent + Intent intent_click = new Intent( context, ActCallback.class ); + intent_click.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK ); + PendingIntent pi_click = PendingIntent.getActivity( context, 3, intent_click, PendingIntent.FLAG_UPDATE_CURRENT ); + + NotificationCompat.Builder builder; + if( Build.VERSION.SDK_INT >= 26 ){ + // Android 8 から、通知のスタイルはユーザが管理することになった + // NotificationChannel を端末に登録しておけば、チャネルごとに管理画面が作られる + NotificationChannel channel = NotificationHelper.createNotificationChannel( + context + , "ErrorNotification" + , "Error" + , null + , NotificationManager.IMPORTANCE_LOW + ); + + builder = new NotificationCompat.Builder( context, channel.getId() ); + }else{ + builder = new NotificationCompat.Builder( context, "not_used" ); + } + + builder + .setContentIntent( pi_click ) + .setAutoCancel( true ) + .setSmallIcon( R.drawable.ic_notification ) // ここは常に白テーマのアイコンを使う + .setColor( ContextCompat.getColor( context, R.color.Light_colorAccent ) ) // ここは常に白テーマの色を使う + .setWhen( System.currentTimeMillis() ) + .setGroup( context.getPackageName() + ":" + "Error" ) + ; + + { + String header = context.getString( R.string.error_notification_title ); + String summary = context.getString( R.string.error_notification_summary ); + + builder + .setContentTitle( header ) + .setContentText( summary + ": " + error_instance.get( 0 ) ) + ; + + NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle() + .setBigContentTitle( header ) + .setSummaryText( summary ); + for( int i = 0 ; i < 5 ; ++ i ){ + if( i >= error_instance.size() ) break; + style.addLine( error_instance.get( i ) ); + } + builder.setStyle( style ); + } + notification_manager.notify( NOTIFICATION_ID_ERROR, builder.build() ); + } + void loadCustomStreamListenerSetting(){ mCustomStreamListenerSetting = null; mCustomStreamListenerSecret = null; @@ -950,6 +1076,22 @@ public class PollingWorker { break; }else{ log.d( "error. %s", result.error ); + + String sv = result.error; + if( sv.contains( "Timeout" ) ){ + synchronized( error_instance ){ + boolean bFound = false; + for( String x : error_instance ){ + if( x.equals( sv ) ){ + bFound = true; + break; + } + } + if( ! bFound ){ + error_instance.add( sv ); + } + } + } } } } @@ -1356,43 +1498,6 @@ public class PollingWorker { // FCMメッセージの処理 // - public static void handleFCMMessage( @NonNull Context context, long time_start, @Nullable String tag ){ - // FirebaseMessagingService#onMessageReceived はバックグラウンドスレッドから実行されるので、少しなら待機してもよい - // https://firebase.google.com/docs/cloud-messaging/android/receive - // 10秒を超えるとプロセスごと殺されるかもしれない - - // タスクを追加 - JSONObject data = new JSONObject(); - try{ - if( tag != null ) data.putOpt( EXTRA_TAG, tag ); - data.put( EXTRA_TASK_ID, TASK_FCM_MESSAGE ); - }catch( JSONException ignored ){ - } - task_list.addLast( context, true, data ); - - - // JobScheduler 経由ではないがジョブを追加して実行開始 - PollingWorker pw = getInstance( context ); - pw.addJob( JOB_FCM ); - - for( ; ; ){ - long now = SystemClock.elapsedRealtime(); - if( ! pw.hasJob( JOB_FCM ) ){ - log.d( "handleFCMMessage: JOB_FCM completed. time=%.2f",(now-time_start)/1000f ); - break; - } - if( now - time_start >= ( 1000L * 300 ) ){ - log.d( "handleFCMMessage: JOB_FCM timeout. exit onMessageReceived..." ); - break; - } - try{ - Thread.sleep( 50L ); - }catch( InterruptedException ex ){ - break; - } - } - } - //////////////////////////////////////////////////////////////////////////// // タスクの追加 @@ -1478,4 +1583,55 @@ public class PollingWorker { public static void queuePackageReplaced( Context context ){ addTask( context, true, TASK_PACKAGE_REPLACED, null ); } + + public interface JobStatusCallback { + void onStatus( String sv ); + } + + static final AtomicReference< String > job_status = new AtomicReference<>( null ); + + public static void handleFCMMessage( Context context, String tag, JobStatusCallback callback ){ + log.d( "handleFCMMessage: start. tag=%s", tag ); + long time_start = SystemClock.elapsedRealtime(); + + callback.onStatus( "=>" ); + + // タスクを追加 + JSONObject data = new JSONObject(); + try{ + if( tag != null ) data.putOpt( EXTRA_TAG, tag ); + data.put( EXTRA_TASK_ID, TASK_FCM_MESSAGE ); + }catch( JSONException ignored ){ + } + task_list.addLast( context, true, data ); + + callback.onStatus( "==>" ); + + // 疑似ジョブを開始 + PollingWorker pw = getInstance( context ); + pw.addJob( JOB_FCM, false ); + + // 疑似ジョブが終了するまで待機する + for( ; ; ){ + // ジョブが完了した? + long now = SystemClock.elapsedRealtime(); + if( ! pw.hasJob( JOB_FCM ) ){ + log.d( "handleFCMMessage: JOB_FCM completed. time=%.2f", ( now - time_start ) / 1000f ); + break; + } + // ジョブの状況を通知する + String sv = job_status.get(); + if( sv == null ) sv = "(null)"; + callback.onStatus( sv ); + + // 少し待機 + try{ + Thread.sleep( 50L ); + }catch( InterruptedException ex ){ + log.e( ex, "handleFCMMessage: blocking is interrupted." ); + break; + } + } + } + } diff --git a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java index 0e51deb0..6f17e708 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java +++ b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java @@ -162,7 +162,7 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener { , ActMain.NOT_CROSS_ACCOUNT , ! status.reblogged , false - , bSimpleList ? activity.boost_complete_callback : null + , !bSimpleList ? null : status.reblogged ? activity.boost_complete_callback : activity.unboost_complete_callback ); } break; @@ -176,7 +176,7 @@ class StatusButtons implements View.OnClickListener, View.OnLongClickListener { , status , ActMain.NOT_CROSS_ACCOUNT , ! status.favourited - , bSimpleList ? activity.favourite_complete_callback : null + , !bSimpleList ? null : status.favourited ? activity.unfavourite_complete_callback : activity.favourite_complete_callback ); } break; diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/NotificationHelper.java b/app/src/main/java/jp/juggler/subwaytooter/util/NotificationHelper.java index b99d0ed1..e5228257 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/NotificationHelper.java +++ b/app/src/main/java/jp/juggler/subwaytooter/util/NotificationHelper.java @@ -5,6 +5,7 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import jp.juggler.subwaytooter.R; import jp.juggler.subwaytooter.table.SavedAccount; @@ -15,19 +16,24 @@ public class NotificationHelper { @TargetApi(26) public static NotificationChannel createNotificationChannel( @NonNull Context context, @NonNull SavedAccount account ){ - + return createNotificationChannel(context + ,account.acct + ,account.acct + ,context.getString( R.string.notification_channel_description, account.acct ) + ,NotificationManager.IMPORTANCE_DEFAULT // : NotificationManager.IMPORTANCE_LOW; + ); + } + + @TargetApi(26) + public static NotificationChannel createNotificationChannel( + @NonNull Context context + , @NonNull String channel_id // id + , @NonNull String name // The user-visible name of the channel. + , @Nullable String description // The user-visible description of the channel. + , int importance + ){ NotificationManager notification_manager = (NotificationManager) context.getSystemService( Context.NOTIFICATION_SERVICE ); - // The id of the channel. - String channel_id = account.acct; - - // The user-visible name of the channel. - CharSequence name = context.getString( R.string.notification_for, account.acct ); - - // The user-visible description of the channel. - String description = context.getString( R.string.notification_channel_description, account.acct ); - // - int importance = NotificationManager.IMPORTANCE_DEFAULT; // : NotificationManager.IMPORTANCE_LOW; // NotificationChannel channel = null; try{ @@ -39,7 +45,8 @@ public class NotificationHelper { channel = new NotificationChannel( channel_id, name, importance ); } channel.setName( name ); - channel.setDescription( description ); + channel.setImportance( importance ); + if( description != null ) channel.setDescription( description ); notification_manager.createNotificationChannel( channel ); return channel; } diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 799f88b2..03ea692f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -51,6 +51,7 @@ Partager Partager à partir d\'un autre compte Partagé avec succès + boost removed Partagé par … Couleur d\'arrière plan des onglets Couleur des onglets @@ -132,6 +133,7 @@ Favori Placer dans les favoris à partir d\'un autre compte Mis en favori avec succès + Mis en unfavori avec succès Mis en favori par … Favoris Chronologie fédérée @@ -468,6 +470,9 @@ \"%1$s\" is too long (%2$d/%3$d characters).\nIt is not acceptable in the standard instance, but it may be acceptable in some instances.\nAre you sure? Avatar icon size (unit:dp. default:48. app restart required) Short acct for local user (app restart required) + Loading notification… + Server Timeout + If it\'s dead instance, please remove account on that server. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 0cf3835b..d36df853 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -369,8 +369,10 @@ アプリ: %1$s ブーストできました + ブースト解除しました お気に入りにしました + お気に入り解除しました スワイプで解除。解除した後はカラムをリロードすると再表示されます @@ -755,5 +757,7 @@ \"%1$s\"が長すぎます(%2$d/%3$d文字).\n標準的なインスタンスではエラーとなりますが、いくつかのインスタンスでは許容されるかも。\nよろしいですか? アバターアイコンサイズ(単位:dp. デフォルト:48. アプリ再起動が必要) ローカルユーザのAcct表記を短くする(アプリ再起動が必要) - + 通知の取得中… + サーバータイムアウト + もし死んだインスタンスなら、そのサーバ上のアカウントを除去してください diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bdcc474a..b2e03fef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -190,7 +190,9 @@ mute app \"%1$s\" App was muted. favourite succeeded + favourite removed boost succeeded + boost removed notification option sound vibration @@ -462,5 +464,8 @@ \"%1$s\" is too long (%2$d/%3$d characters).\nIt is not acceptable in the standard instance, but it may be acceptable in some instances.\nAre you sure? Avatar icon size (unit:dp. default:48. app restart required) Short acct for local user (app restart required) + Loading notification… + Server Timeout + If it\'s dead instance, please remove account on that server.