diff --git a/app/build.gradle b/app/build.gradle index 101d6b96..ad8a4e93 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId "jp.juggler.subwaytooter" minSdkVersion 21 targetSdkVersion 25 - versionCode 8 - versionName "0.0.8" + versionCode 10 + versionName "0.1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2b9dab99..8916e6a1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ package="jp.juggler.subwaytooter"> + + + + + + + + + + task = new AsyncTask< Void, String, TootApiResult >() { diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.java b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.java index 81db2386..ea7ae1cd 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.java @@ -47,8 +47,7 @@ public class ActAppSetting extends AppCompatActivity implements CompoundButton.O } private void saveUIToData(){ - pref - .edit() + pref.edit() .putBoolean( Pref.KEY_BACK_TO_COLUMN_LIST, swBackToColumnList.isChecked() ) .putBoolean( Pref.KEY_DONT_CONFIRM_BEFORE_CLOSE_COLUMN, swDontConfirmBeforeCloseColumn.isChecked() ) .apply(); diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java index f7f46516..36c8f653 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java @@ -80,29 +80,16 @@ public class ActMain extends AppCompatActivity initUI(); - Uri uri = ActOAuthCallback.last_uri.get(); - if( uri != null ){ - updateAccessToken( uri ); - } + AlarmService.startCheck( this ); loadColumnList(); } - @Override protected void onNewIntent( Intent intent ){ - super.onNewIntent( intent ); - Uri uri = ActOAuthCallback.last_uri.get(); - if( uri != null ){ - updateAccessToken( uri ); - } - } - - @Override - protected void onDestroy(){ + @Override protected void onDestroy(){ super.onDestroy(); } - @Override - protected void onResume(){ + @Override protected void onResume(){ super.onResume(); HTMLDecoder.link_callback = link_click_listener; @@ -122,6 +109,7 @@ public class ActMain extends AppCompatActivity } if( bRemoved ){ pager_adapter.setOrder( pager, new_order ); + saveColumnList(); } } @@ -136,12 +124,16 @@ public class ActMain extends AppCompatActivity if( pager_adapter.getCount() == 0 ){ llEmpty.setVisibility( View.VISIBLE ); } + + Uri uri = ActOAuthCallback.last_uri.get(); + if( uri != null ){ + ActOAuthCallback.last_uri.set( null ); + updateAccessToken( uri ); + } } - @Override - protected void onPause(){ + @Override protected void onPause(){ HTMLDecoder.link_callback = null; - saveColumnList(); super.onPause(); } @@ -164,6 +156,7 @@ public class ActMain extends AppCompatActivity ArrayList< Integer > order = data.getIntegerArrayListExtra( ActColumnList.EXTRA_ORDER ); if( order != null && isOrderChanged( order ) ){ pager_adapter.setOrder( pager, order ); + saveColumnList(); } if( pager_adapter.column_list.isEmpty() ){ @@ -413,6 +406,25 @@ public class ActMain extends AppCompatActivity private void updateAccessToken( final Uri uri ){ + // 通知タップ + // subwaytooter://notification_click?db_id=(db_id) + String sv = uri.getQueryParameter( "db_id" ); + if( ! TextUtils.isEmpty( sv ) ){ + try{ + long db_id = Long.parseLong( sv, 10 ); + SavedAccount account = SavedAccount.loadAccount( log, db_id ); + if( account != null ){ + Column column = addColumn( account, Column.TYPE_NOTIFICATIONS ); + if( ! column.bInitialLoading ){ + column.reload(); + } + } + }catch( Throwable ex ){ + ex.printStackTrace(); + } + return; + } + final ProgressDialog progress = new ProgressDialog( ActMain.this ); final AsyncTask< Void, Void, TootApiResult > task = new AsyncTask< Void, Void, TootApiResult >() { @@ -536,7 +548,8 @@ public class ActMain extends AppCompatActivity this.row_id = SavedAccount.insert( host, user, result.object, result.token_info ); SavedAccount account = SavedAccount.loadAccount( log, row_id ); if( account != null ){ - ActMain.this.onAccountUpdated( account ); + AlarmService.startCheck( ActMain.this ); + onAccountUpdated( account ); } } } @@ -580,6 +593,7 @@ public class ActMain extends AppCompatActivity int page_showing = pager.getCurrentItem(); int page_delete = pager_adapter.column_list.indexOf( column ); pager_adapter.removeColumn( pager, column ); + saveColumnList(); if( pager_adapter.getCount() == 0 ){ llEmpty.setVisibility( View.VISIBLE ); }else if( page_showing > 0 && page_showing == page_delete ){ @@ -590,12 +604,12 @@ public class ActMain extends AppCompatActivity ////////////////////////////////////////////////////////////// // カラム追加系 - public void addColumn( SavedAccount ai, int type, Object... params ){ + public Column addColumn( SavedAccount ai, int type, Object... params ){ // 既に同じカラムがあればそこに移動する for( Column column : pager_adapter.column_list ){ if( column.isSameSpec( ai, type, params ) ){ pager.setCurrentItem( pager_adapter.column_list.indexOf( column ), true ); - return; + return column; } } // @@ -603,7 +617,9 @@ public class ActMain extends AppCompatActivity // Column col = new Column( ActMain.this, ai, type, params ); int idx = pager_adapter.addColumn( pager, col ); + saveColumnList(); pager.setCurrentItem( idx, true ); + return col; } private void onAccountUpdated( SavedAccount data ){ @@ -641,7 +657,7 @@ public class ActMain extends AppCompatActivity ////////////////////////////////////////////////////////////// - public interface GetAccountCallback { + interface GetAccountCallback { // return account information // if failed, account is null. void onGetAccount( TootAccount account ); @@ -1100,7 +1116,7 @@ public class ActMain extends AppCompatActivity //////////////////////////////////////// private void performAccountSetting(){ - AccountPicker.pick( this, true,new AccountPicker.AccountPickerCallback() { + AccountPicker.pick( this, true, new AccountPicker.AccountPickerCallback() { @Override public void onAccountPicked( SavedAccount ai ){ ActAccountSetting.open( ActMain.this, ai, REQUEST_CODE_ACCOUNT_SETTING ); @@ -1137,6 +1153,13 @@ public class ActMain extends AppCompatActivity startActivityForResult( intent, REQUEST_CODE_COLUMN_LIST ); } + private void dumpColumnList(){ + for( int i = 0, ie = pager_adapter.column_list.size() ; i < ie ; ++ i ){ + Column column = pager_adapter.column_list.get( i ); + log.d( "dumpColumnList [%s]%s %s", i, column.access_info.acct, column.getColumnName( true ) ); + } + } + static final String FILE_COLUMN_LIST = "column_list"; private void saveColumnList(){ @@ -1170,7 +1193,7 @@ public class ActMain extends AppCompatActivity try{ JSONObject src = array.optJSONObject( i ); Column col = new Column( ActMain.this, src ); - pager_adapter.addColumn( pager, col ); + pager_adapter.addColumn( pager, col, pager_adapter.getCount() ); }catch( Throwable ex ){ ex.printStackTrace(); } @@ -1191,7 +1214,7 @@ public class ActMain extends AppCompatActivity //////////////////////////////////////////////////////////////////////////// - public interface RelationChangedCallback { + interface RelationChangedCallback { // void onRelationChanged( TootRelationShip relationship ); void onRelationChanged(); } @@ -1226,8 +1249,8 @@ public class ActMain extends AppCompatActivity if( result.object != null ){ remote_who = TootAccount.parse( log, access_info, result.object ); - Utils.showToast( ActMain.this, false, bFollow ? R.string.follow_succeeded : R.string.unfollow_succeeded ); - }else if( bFollow && who.locked && result.response.code() == 422 ){ + Utils.showToast( ActMain.this, false, R.string.follow_succeeded ); + }else if( who.locked && result.response.code() == 422 ){ Utils.showToast( ActMain.this, false, R.string.cant_follow_locked_user ); }else{ Utils.showToast( ActMain.this, false, result.error ); @@ -1351,7 +1374,7 @@ public class ActMain extends AppCompatActivity // アカウントを選択してからユーザをフォローする void followFromAnotherAccount( final SavedAccount access_info, final TootAccount who, final RelationChangedCallback callback ){ - AccountPicker.pick( ActMain.this, false,new AccountPicker.AccountPickerCallback() { + AccountPicker.pick( ActMain.this, false, new AccountPicker.AccountPickerCallback() { @Override public void onAccountPicked( SavedAccount ai ){ String acct = who.acct; @@ -1502,9 +1525,8 @@ public class ActMain extends AppCompatActivity }.execute(); } - public interface ReportCompleteCallback { + interface ReportCompleteCallback { void onReportComplete( TootApiResult result ); - } private void callReport( final SavedAccount account, final TootAccount who, final TootStatus status @@ -1605,7 +1627,7 @@ public class ActMain extends AppCompatActivity if( ! tmp_list.isEmpty() ){ dialog.addAction( getString( R.string.favourite_from_another_account ), new Runnable() { @Override public void run(){ - AccountPicker.pick( ActMain.this, false,tmp_list, new AccountPicker.AccountPickerCallback() { + AccountPicker.pick( ActMain.this, false, tmp_list, new AccountPicker.AccountPickerCallback() { @Override public void onAccountPicked( SavedAccount ai ){ if( ai != null ) performFavourite( ai, status ); } @@ -1614,7 +1636,7 @@ public class ActMain extends AppCompatActivity } ); dialog.addAction( getString( R.string.boost_from_another_account ), new Runnable() { @Override public void run(){ - AccountPicker.pick( ActMain.this,false, tmp_list, new AccountPicker.AccountPickerCallback() { + AccountPicker.pick( ActMain.this, false, tmp_list, new AccountPicker.AccountPickerCallback() { @Override public void onAccountPicked( SavedAccount ai ){ if( ai != null ) performBoost( ai, status, false ); } diff --git a/app/src/main/java/jp/juggler/subwaytooter/AlarmReceiver.java b/app/src/main/java/jp/juggler/subwaytooter/AlarmReceiver.java new file mode 100644 index 00000000..64982532 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/AlarmReceiver.java @@ -0,0 +1,19 @@ +package jp.juggler.subwaytooter; + +import android.content.Context; +import android.content.Intent; +import android.support.v4.content.WakefulBroadcastReceiver; + + +public class AlarmReceiver extends WakefulBroadcastReceiver { + + static final String EXTRA_RECEIVED_INTENT = "received_intent"; + static final String ACTION_FROM_RECEIVER = "from_receiver"; + + @Override public void onReceive( Context context, Intent intent ){ + Intent serviceIntent = new Intent(context,AlarmService.class); + serviceIntent.setAction( ACTION_FROM_RECEIVER ); + serviceIntent.putExtra(EXTRA_RECEIVED_INTENT,intent); + startWakefulService(context,serviceIntent); + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java b/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java new file mode 100644 index 00000000..9d56a08b --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/AlarmService.java @@ -0,0 +1,493 @@ +package jp.juggler.subwaytooter; + +import android.app.AlarmManager; +import android.app.IntentService; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.PowerManager; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.support.v4.app.NotificationCompat; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.WakefulBroadcastReceiver; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.concurrent.ConcurrentLinkedQueue; + +import jp.juggler.subwaytooter.api.TootApiClient; +import jp.juggler.subwaytooter.api.TootApiResult; +import jp.juggler.subwaytooter.api.entity.TootNotification; +import jp.juggler.subwaytooter.table.NotificationTracking; +import jp.juggler.subwaytooter.table.SavedAccount; +import jp.juggler.subwaytooter.util.LogCategory; +import jp.juggler.subwaytooter.util.Utils; + +public class AlarmService extends IntentService { + + static final LogCategory log = new LogCategory( "AlarmService" ); + + // PendingIntent の request code + static final int PENDING_CODE_ALARM = 1; + static final String ACTION_NOTIFICATION_DELETE = "notification_delete"; + static final String ACTION_NOTIFICATION_CLICK = "notification_click"; + static final int NOTIFICATION_ID = 1; + static final long INTERVAL_MIN = 60000L * 5; + + // Notifiation のJSONObject を日時でソートするためにデータを追加する + static final String KEY_TIME = "<>time"; + private static final String ACTION_DATA_INJECTED = "data_injected"; + private static final String EXTRA_DB_ID = "db_id"; + + public AlarmService(){ + // name: Used to name the worker thread, important only for debugging. + super( "AlarmService" ); + } + + AlarmManager alarm_manager; + PowerManager power_manager; + NotificationManager notification_manager; + PowerManager.WakeLock wake_lock; + PendingIntent pi_next; + + @Override public void onCreate(){ + super.onCreate(); + log.d("ctor"); + + alarm_manager = (AlarmManager) getApplicationContext().getSystemService( ALARM_SERVICE ); + power_manager = (PowerManager) getApplicationContext().getSystemService( POWER_SERVICE ); + notification_manager = (NotificationManager) getApplicationContext().getSystemService( NOTIFICATION_SERVICE ); + + wake_lock = power_manager.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, AlarmService.class.getName() ); + wake_lock.setReferenceCounted( false ); + wake_lock.acquire(); + + // 次回レシーバーを起こすためのPendingIntent + Intent next_intent = new Intent( this, AlarmReceiver.class ); + pi_next = PendingIntent.getBroadcast( this, PENDING_CODE_ALARM, next_intent, PendingIntent.FLAG_UPDATE_CURRENT ); + + } + + @Override public void onDestroy(){ + log.d("dtor"); + wake_lock.release(); + + super.onDestroy(); + } + + // IntentService は onHandleIntent をワーカースレッドから呼び出す + // 同期処理を行って良い + @Override protected void onHandleIntent( @Nullable Intent intent ){ + + ArrayList< SavedAccount > account_list = SavedAccount.loadAccountList( log ); + + if( intent != null ){ + String action = intent.getAction(); + log.d("onHandleIntent action=%s",action); + + if( ACTION_DATA_INJECTED.equals( action ) ){ + processInjectedData(); + }else if( AlarmReceiver.ACTION_FROM_RECEIVER.equals( action ) ){ + WakefulBroadcastReceiver.completeWakefulIntent( intent ); + // + Intent received_intent = intent.getParcelableExtra( AlarmReceiver.EXTRA_RECEIVED_INTENT ); + if( received_intent != null ){ + + action = received_intent.getAction(); + log.d("received_intent.action=%s",action); + + if( Intent.ACTION_BOOT_COMPLETED.equals( action ) ){ + NotificationTracking.resetPostAll(); + }else if( ACTION_NOTIFICATION_DELETE.equals( action ) ){ + log.d( "Notification deleted!" ); + long db_id = received_intent.getLongExtra( EXTRA_DB_ID ,0L); + NotificationTracking.updateRead( db_id ); + return; + }else if( ACTION_NOTIFICATION_CLICK.equals( action ) ){ + log.d( "Notification clicked!" ); + long db_id = received_intent.getLongExtra( EXTRA_DB_ID ,0L); + NotificationTracking.updateRead( db_id ); + notification_manager.cancel( Long.toString(db_id),NOTIFICATION_ID ); + // + intent = new Intent( this, ActOAuthCallback.class ); + intent.setData( Uri.parse( "subwaytooter://notification_click?db_id="+ db_id ) ); + intent.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK ); + startActivity( intent ); + return; + + } + } + } + } + + TootApiClient client = new TootApiClient( this, new TootApiClient.Callback() { + @Override public boolean isApiCancelled(){ + return false; + } + + @Override public void publishApiProgress( String s ){ + + } + } ); + + boolean bAlarmRequired = false; + if( account_list != null ){ + for( SavedAccount account : account_list ){ + try{ + if( account.notification_mention + || account.notification_boost + || account.notification_favourite + || account.notification_follow + ){ + bAlarmRequired = true; + + ArrayList< Data > data_list = new ArrayList<>(); + + checkAccount( client, data_list, account ); + + showNotification( account.db_id, data_list ); + + } + }catch( Throwable ex ){ + ex.printStackTrace(); + } + } + } + + + + alarm_manager.cancel( pi_next ); + if( bAlarmRequired ){ + long now = SystemClock.elapsedRealtime(); + alarm_manager.setWindow( + AlarmManager.ELAPSED_REALTIME_WAKEUP + , now + INTERVAL_MIN + , 60000L * 10 + , pi_next + ); + log.d("alarm set!"); + }else{ + log.d("alarm is no longer required."); + } + } + + + + private static class Data { + SavedAccount access_info; + TootNotification notification; + } + + private static final String PATH_NOTIFICATIONS = "/api/v1/notifications"; + + private void checkAccount( TootApiClient client, ArrayList< Data > data_list, SavedAccount account ){ + NotificationTracking nr = NotificationTracking.load( account.db_id ); + + // まずキャッシュされたデータを処理する + HashSet< Long > duplicate_check = new HashSet<>(); + ArrayList< JSONObject > dst_array = new ArrayList<>(); + if( nr.last_data != null ){ + try{ + JSONArray array = new JSONArray( nr.last_data ); + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + update_sub( src, nr, account, dst_array, data_list, duplicate_check ); + } + }catch( JSONException ex ){ + ex.printStackTrace(); + } + } + + // 前回の更新から一定時刻が経過したら新しいデータを注ぎ足す + long now = System.currentTimeMillis(); + if( now - nr.last_load >= INTERVAL_MIN ){ + nr.last_load = now; + + client.setAccount( account ); + + for( int nTry = 0 ; nTry < 4 ; ++ nTry ){ + TootApiResult result = client.request( PATH_NOTIFICATIONS ); + if( result == null ){ + log.d( "cancelled." ); + break; + }else if( result.array != null ){ + try{ + JSONArray array = result.array; + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + update_sub( src, nr, account, dst_array, data_list, duplicate_check ); + } + }catch( JSONException ex ){ + ex.printStackTrace(); + } + break; + }else{ + log.d( "error. %s", result.error ); + } + } + } + + Collections.sort( dst_array, new Comparator< JSONObject >() { + @Override public int compare( JSONObject a, JSONObject b ){ + long la = a.optLong( KEY_TIME, 0 ); + long lb = b.optLong( KEY_TIME, 0 ); + // 新しい順 + if( la < lb ) return + 1; + if( la > lb ) return - 1; + return 0; + } + } ); + + JSONArray d = new JSONArray(); + for( int i = 0 ; i < 10 ; ++ i ){ + if( i >= dst_array.size() ) break; + d.put( dst_array.get( i ) ); + } + nr.last_data = d.toString(); + nr.save(); + } + + void update_sub( + JSONObject src + , NotificationTracking nr + , SavedAccount account + , ArrayList< JSONObject > dst_array + , ArrayList< Data > data_list + , HashSet< Long > duplicate_check + ) throws JSONException{ + + long id = src.optLong( "id" ); + + if( duplicate_check.contains( id ) ) return; + duplicate_check.add( id ); + + String type = Utils.optStringX( src, "type" ); + + if( id <= nr.nid_read ){ + return; + }else if( id > nr.nid_show ){ + // 種別チェックより先に「表示済み」idの更新を行う + nr.nid_show = id; + } + + if( ( ! account.notification_mention && TootNotification.TYPE_MENTION.equals( type ) ) + || ( ! account.notification_boost && TootNotification.TYPE_REBLOG.equals( type ) ) + || ( ! account.notification_favourite && TootNotification.TYPE_FAVOURITE.equals( type ) ) + || ( ! account.notification_follow && TootNotification.TYPE_FOLLOW.equals( type ) ) + ){ + return; + } + + // + Data data = new Data(); + data.access_info = account; + data.notification = TootNotification.parse( log, account, src ); + if( data.notification != null ){ + data_list.add( data ); + // + src.put( KEY_TIME, data.notification.time_created_at ); + dst_array.add( src ); + } + } + + public String getNotificationLine( String type, CharSequence display_name ){ + if( TootNotification.TYPE_FAVOURITE.equals( type ) ){ + return "- "+getString( R.string.display_name_favourited_by, display_name ); + } + if( TootNotification.TYPE_REBLOG.equals( type ) ){ + return "- "+getString( R.string.display_name_boosted_by, display_name ); + } + if( TootNotification.TYPE_MENTION.equals( type ) ){ + return "- "+getString( R.string.display_name_replied_by, display_name ); + } + if( TootNotification.TYPE_FOLLOW.equals( type ) ){ + return "- "+getString( R.string.display_name_followed_by, display_name ); + } + return "- "+"?"; + } + + private void showNotification( long account_db_id,ArrayList< Data > data_list ){ + String notification_tag = Long.toString( account_db_id ); + if( data_list.isEmpty() ){ + notification_manager.cancel( notification_tag,NOTIFICATION_ID ); + return; + } + + Collections.sort( data_list, new Comparator< Data >() { + @Override public int compare( Data a, Data b ){ + long la = a.notification.time_created_at; + long lb = b.notification.time_created_at; + // 新しい順 + if( la < lb ) return + 1; + if( la > lb ) return - 1; + return 0; + } + } ); + + Data item = data_list.get( 0 ); + NotificationTracking nt = NotificationTracking.load( account_db_id ); + if( item.notification.time_created_at == nt.post_time + && item.notification.id == nt.post_id + ){ + // 先頭にあるデータが同じなら、通知を更新しない + // このマーカーは端末再起動時にリセットされるので、再起動後は通知が出るはず + return; + } + nt.updatePost( item.notification.id, item.notification.time_created_at ); + + // 通知タップ + Intent intent_click = new Intent( this, AlarmReceiver.class ); + intent_click.putExtra(EXTRA_DB_ID,account_db_id); + intent_click.setAction( ACTION_NOTIFICATION_CLICK ); + + Intent intent_delete = new Intent( this, AlarmReceiver.class ); + intent_click.putExtra(EXTRA_DB_ID,account_db_id); + intent_delete.setAction( ACTION_NOTIFICATION_DELETE ); + + PendingIntent pi_click = PendingIntent.getBroadcast( this, (256+(int)account_db_id), intent_click, PendingIntent.FLAG_UPDATE_CURRENT ); + + // 通知を消去した時のPendingIntent + PendingIntent pi_delete = PendingIntent.getBroadcast( this, (Integer.MAX_VALUE-(int)account_db_id), intent_delete, PendingIntent.FLAG_UPDATE_CURRENT ); + + NotificationCompat.Builder builder = new NotificationCompat.Builder( this ) + .setContentIntent( pi_click ) + .setDeleteIntent( pi_delete ) + .setAutoCancel( false ) + .setSmallIcon( R.drawable.ic_notification ) + .setColor( ContextCompat.getColor( this, R.color.colorAccent ) ) + .setDefaults( NotificationCompat.DEFAULT_ALL ) + .setWhen( item.notification.time_created_at ); + + String a = getNotificationLine( item.notification.type, item.notification.account.display_name ); + String acct = item.access_info.acct +" "+getString( R.string.app_name ); + if( data_list.size() == 1 ){ + builder.setContentTitle( a ); + builder.setContentText( acct ); + }else{ + String header = getString( R.string.notification_count, data_list.size() ); + builder.setContentTitle( header ) + .setContentText( a ); + + NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle() + .setBigContentTitle( header ) + .setSummaryText( acct ); + for( int i = 0 ; i < 5 ; ++ i ){ + if( i >= data_list.size() ) break; + item = data_list.get( i ); + a = getNotificationLine( item.notification.type, item.notification.account.display_name ); + style.addLine( a ); + } + builder.setStyle( style ); + } + + notification_manager.notify( notification_tag,NOTIFICATION_ID, builder.build() ); + } + + //////////////////////////////////////////////////////////////////////////// + // Activity との連携 + + public static void startCheck(Context context){ + Intent intent = new Intent(context,AlarmReceiver.class); + context.sendBroadcast( intent ); + } + + private static class InjectData { + long account_db_id; + TootNotification.List list = new TootNotification.List(); + } + + static final ConcurrentLinkedQueue< InjectData > inject_queue = new ConcurrentLinkedQueue<>(); + + public static void injectData( Context context, long account_db_id, TootNotification.List src ){ + InjectData data = new InjectData(); + data.account_db_id = account_db_id; + data.list.addAll( src ); + inject_queue.add( data ); + + Intent intent = new Intent( context, AlarmService.class ); + intent.setAction( ACTION_DATA_INJECTED ); + context.startService( intent ); + } + + private void processInjectedData(){ + while( inject_queue.size() > 0 ){ + + InjectData data = inject_queue.poll(); + + SavedAccount account = SavedAccount.loadAccount( log, data.account_db_id ); + if( account == null ) continue; + + NotificationTracking nr = NotificationTracking.load( data.account_db_id ); + + HashSet< Long > duplicate_check = new HashSet<>(); + + ArrayList< JSONObject > dst_array = new ArrayList<>(); + if( nr.last_data != null ){ + // まずキャッシュされたデータを処理する + try{ + JSONArray array = new JSONArray( nr.last_data ); + for( int i = array.length() - 1 ; i >= 0 ; -- i ){ + JSONObject src = array.optJSONObject( i ); + dst_array.add( src ); + duplicate_check.add( src.optLong( "id" ) ); + } + }catch( JSONException ex ){ + ex.printStackTrace(); + } + } + for( TootNotification item : data.list ){ + try{ + if( duplicate_check.contains( item.id ) ) continue; + duplicate_check.add( item.id ); + + String type = item.type; + + if( ( ! account.notification_mention && TootNotification.TYPE_MENTION.equals( type ) ) + || ( ! account.notification_boost && TootNotification.TYPE_REBLOG.equals( type ) ) + || ( ! account.notification_favourite && TootNotification.TYPE_FAVOURITE.equals( type ) ) + || ( ! account.notification_follow && TootNotification.TYPE_FOLLOW.equals( type ) ) + ){ + continue; + } + + // + JSONObject src = item.json; + src.put( KEY_TIME, item.time_created_at ); + dst_array.add( src ); + }catch( JSONException ex ){ + ex.printStackTrace(); + } + } + + // 新しい順にソート + Collections.sort( dst_array, new Comparator< JSONObject >() { + @Override public int compare( JSONObject a, JSONObject b ){ + long la = a.optLong( KEY_TIME, 0 ); + long lb = b.optLong( KEY_TIME, 0 ); + // 新しい順 + if( la < lb ) return + 1; + if( la > lb ) return - 1; + return 0; + } + } ); + + // 最新10件を保存 + JSONArray d = new JSONArray(); + for( int i = 0 ; i < 10 ; ++ i ){ + if( i >= dst_array.size() ) break; + d.put( dst_array.get( i ) ); + } + nr.last_data = d.toString(); + nr.save(); + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/App1.java b/app/src/main/java/jp/juggler/subwaytooter/App1.java index 24d25078..6670db5f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/App1.java +++ b/app/src/main/java/jp/juggler/subwaytooter/App1.java @@ -19,6 +19,7 @@ import jp.juggler.subwaytooter.table.ClientInfo; import jp.juggler.subwaytooter.table.ContentWarning; import jp.juggler.subwaytooter.table.LogData; import jp.juggler.subwaytooter.table.MediaShown; +import jp.juggler.subwaytooter.table.NotificationTracking; import jp.juggler.subwaytooter.table.SavedAccount; import okhttp3.OkHttpClient; import uk.co.chrisjenx.calligraphy.CalligraphyConfig; @@ -26,38 +27,11 @@ import uk.co.chrisjenx.calligraphy.TypefaceUtils; public class App1 extends Application { - @Override - public void onCreate(){ - super.onCreate(); - - CalligraphyConfig.initDefault(new CalligraphyConfig.Builder() - .setFontAttrId(R.attr.fontPath) - .build() - ); - - if( typeface_emoji == null ){ - typeface_emoji = TypefaceUtils.load(getAssets(), "emojione_android.ttf"); - } - - if( db_open_helper == null ){ - db_open_helper = new DBOpenHelper( getApplicationContext() ); - } - - if( image_loader == null ){ - image_loader = new MyImageLoader( - Volley.newRequestQueue( getApplicationContext() ) - , new BitmapCache() - ); - } - } - - @Override - public void onTerminate(){ - super.onTerminate(); - } static final String DB_NAME = "app_db"; - static final int DB_VERSION = 1; + static final int DB_VERSION = 2; + // 2017/4/25 v10 1=>2 SavedAccount に通知設定を追加 + // 2017/4/25 v10 1=>2 NotificationTracking テーブルを追加 static DBOpenHelper db_open_helper; @@ -65,9 +39,10 @@ public class App1 extends Application { return db_open_helper.getWritableDatabase(); } - static class DBOpenHelper extends SQLiteOpenHelper { + + private static class DBOpenHelper extends SQLiteOpenHelper { - public DBOpenHelper( Context context ){ + DBOpenHelper( Context context ){ super( context, DB_NAME, null, DB_VERSION ); } @@ -79,6 +54,7 @@ public class App1 extends Application { ClientInfo.onDBCreate( db ); MediaShown.onDBCreate(db); ContentWarning.onDBCreate(db); + NotificationTracking.onDBCreate(db); } @Override @@ -89,6 +65,7 @@ public class App1 extends Application { ClientInfo.onDBUpgrade( db, oldVersion, newVersion ); MediaShown.onDBUpgrade( db, oldVersion, newVersion ); ContentWarning.onDBUpgrade( db, oldVersion, newVersion ); + NotificationTracking.onDBUpgrade( db, oldVersion, newVersion ); } } @@ -98,7 +75,7 @@ public class App1 extends Application { return image_loader; } - public static class MyImageLoader extends ImageLoader { + private static class MyImageLoader extends ImageLoader { /** * Constructs a new ImageLoader. @@ -122,7 +99,7 @@ public class App1 extends Application { } } - public static class BitmapCache implements ImageLoader.ImageCache { + private static class BitmapCache implements ImageLoader.ImageCache { private LruCache mCache; @@ -154,4 +131,38 @@ public class App1 extends Application { // public static final RelationshipMap relationship_map = new RelationshipMap(); + @Override + public void onCreate(){ + super.onCreate(); + + CalligraphyConfig.initDefault(new CalligraphyConfig.Builder() + .setFontAttrId(R.attr.fontPath) + .build() + ); + + if( typeface_emoji == null ){ + typeface_emoji = TypefaceUtils.load(getAssets(), "emojione_android.ttf"); + } + + if( db_open_helper == null ){ + db_open_helper = new DBOpenHelper( getApplicationContext() ); + + if( BuildConfig.DEBUG){ +// SQLiteDatabase db = db_open_helper.getWritableDatabase(); +// db_open_helper.onCreate( db ); + } + } + + if( image_loader == null ){ + image_loader = new MyImageLoader( + Volley.newRequestQueue( getApplicationContext() ) + , new BitmapCache() + ); + } + } + + @Override + public void onTerminate(){ + super.onTerminate(); + } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.java b/app/src/main/java/jp/juggler/subwaytooter/Column.java index a391beee..311170b4 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/Column.java +++ b/app/src/main/java/jp/juggler/subwaytooter/Column.java @@ -1,5 +1,6 @@ package jp.juggler.subwaytooter; +import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; import android.support.annotation.NonNull; @@ -72,9 +73,9 @@ class Column { static final String KEY_COLUMN_NAME = "column_name"; static final String KEY_OLD_INDEX = "old_index"; - private final ActMain activity; + private final @NonNull ActMain activity; - final SavedAccount access_info; + final @NonNull SavedAccount access_info; final int type; static final int TYPE_HOME = 1; @@ -82,7 +83,7 @@ class Column { static final int TYPE_FEDERATE = 3; static final int TYPE_PROFILE = 4; static final int TYPE_FAVOURITES = 5; - static final int TYPE_REPORTS = 6; + private static final int TYPE_REPORTS = 6; static final int TYPE_NOTIFICATIONS = 7; static final int TYPE_CONVERSATION = 8; static final int TYPE_HASHTAG = 9; @@ -106,7 +107,7 @@ class Column { int scroll_pos; int scroll_y; - Column( ActMain activity, @NonNull SavedAccount access_info, int type, Object... params ){ + Column( @NonNull ActMain activity, @NonNull SavedAccount access_info, int type, Object... params ){ this.activity = activity; this.access_info = access_info; this.type = type; @@ -155,10 +156,13 @@ class Column { item.put( KEY_OLD_INDEX, old_index ); } - Column( ActMain activity, JSONObject src ){ + Column( @NonNull ActMain activity, JSONObject src ){ this.activity = activity; - this.access_info = SavedAccount.loadAccount( log, src.optLong( KEY_ACCOUNT_ROW_ID ) ); - if( access_info == null ) throw new RuntimeException( "missing account" ); + + SavedAccount ac = SavedAccount.loadAccount( log, src.optLong( KEY_ACCOUNT_ROW_ID ) ); + if( ac == null ) throw new RuntimeException( "missing account" ); + this.access_info = ac; + this.type = src.optInt( KEY_TYPE ); switch( type ){ case TYPE_CONVERSATION: @@ -452,8 +456,14 @@ class Column { TootApiResult parseNotifications( TootApiResult result ){ if( result != null ){ saveRange( result, true, true ); - list_tmp = new ArrayList<>(); - list_tmp.addAll( TootNotification.parseList( log, access_info, result.array ) ); + TootNotification.List src= TootNotification.parseList( log, access_info, result.array ); + if( src != null){ + list_tmp = new ArrayList<>(); + list_tmp.addAll( src ); + // + AlarmService.injectData( activity,access_info.db_id, src ); + } + } return result; } @@ -594,6 +604,8 @@ class Column { if( list_tmp != null ){ list_data.clear(); list_data.addAll( list_tmp ); + + } } @@ -700,8 +712,14 @@ class Column { TootApiResult parseNotifications( TootApiResult result ){ if( result != null ){ saveRange( result, bBottom, ! bBottom ); - list_tmp = new ArrayList<>(); - list_tmp.addAll( TootNotification.parseList( log, access_info, result.array ) ); + + TootNotification.List src = TootNotification.parseList( log, access_info, result.array ); + if( src != null ){ + list_tmp = new ArrayList<>(); + list_tmp.addAll( src ); + // + AlarmService.injectData( activity,access_info.db_id, src ); + } } return result; } diff --git a/app/src/main/java/jp/juggler/subwaytooter/Pref.java b/app/src/main/java/jp/juggler/subwaytooter/Pref.java index 9123bd56..e6aac280 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/Pref.java +++ b/app/src/main/java/jp/juggler/subwaytooter/Pref.java @@ -6,7 +6,8 @@ import android.preference.PreferenceManager; public class Pref { - public static SharedPreferences pref(Context context){ + + public static SharedPreferences pref( Context context){ return PreferenceManager.getDefaultSharedPreferences( context ); } diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java index f6402e51..c2be4e53 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootNotification.java @@ -33,10 +33,13 @@ public class TootNotification extends TootId { public long time_created_at; + public JSONObject json; + public static TootNotification parse( LogCategory log, LinkClickContext accopunt, JSONObject src ){ if( src == null ) return null; try{ TootNotification dst = new TootNotification(); + dst.json = src; dst.id = src.optLong( "id" ); dst.type = Utils.optStringX( src, "type" ); dst.created_at = Utils.optStringX( src, "created_at" ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.java b/app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.java new file mode 100644 index 00000000..51a6eb31 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.java @@ -0,0 +1,154 @@ +package jp.juggler.subwaytooter.table; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import jp.juggler.subwaytooter.App1; +import jp.juggler.subwaytooter.util.LogCategory; + +public class NotificationTracking { + + private static final LogCategory log = new LogCategory( "NotificationTracking" ); + + private static final String table = "noti_trac"; + + // アカウントDBの行ID。 サーバ側のIDではない + private static final String COL_ACCOUNT_DB_ID = "a"; + + // サーバから通知を取得した時刻 + private static final String COL_LAST_LOAD = "ll"; + + // サーバから最後に読んだデータ。既読は排除されてるかも + private static final String COL_LAST_DATA = "ld"; + + // 通知ID。ここまで既読 + private static final String COL_NID_READ = "nr"; + + // 通知ID。もっとも最近取得したもの + private static final String COL_NID_SHOW = "ns"; + + // 最後に表示した通知のID + private static final String COL_POST_ID = "pi"; + // 最後に表示した通知の作成時刻 + private static final String COL_POST_TIME = "pt"; + + public static void onDBCreate( SQLiteDatabase db ){ + + db.execSQL( + "create table if not exists " + table + + "(_id INTEGER PRIMARY KEY" + + ",a integer not null" + + ",ll integer default 0" + + ",ld text" + + ",nr integer default 0" + + ",ns integer default 0" + + ",pi integer default 0" + + ",pt integer default 0" + + ")" + ); + db.execSQL( + "create unique index if not exists " + table + "_a on " + table + "(a)" + ); + } + + public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ + if( oldVersion < 2 && newVersion >= 2 ){ + onDBCreate( db ); + } + } + + private long account_db_id; + public long last_load; + public long nid_read; + public long nid_show; + + public long post_id; + public long post_time; + + public String last_data; + + private static final String WHERE_AID = COL_ACCOUNT_DB_ID + "=?"; + + public static NotificationTracking load( long account_db_id ){ + NotificationTracking dst = new NotificationTracking(); + dst.account_db_id = account_db_id; + try{ + Cursor cursor = App1.getDB().query( table, null,WHERE_AID, new String[]{ Long.toString( account_db_id ) }, null, null, null ); + try{ + if( cursor.moveToFirst() ){ + dst.last_load = cursor.getLong( cursor.getColumnIndex( COL_LAST_LOAD ) ); + dst.nid_read = cursor.getLong( cursor.getColumnIndex( COL_NID_READ ) ); + dst.nid_show = cursor.getLong( cursor.getColumnIndex( COL_NID_SHOW ) ); + + dst.post_id = cursor.getLong( cursor.getColumnIndex( COL_POST_ID ) ); + dst.post_time = cursor.getLong( cursor.getColumnIndex( COL_POST_TIME ) ); + + int idx_last_data = cursor.getColumnIndex( COL_LAST_DATA ); + dst.last_data = cursor.isNull( idx_last_data ) ? null : cursor.getString( idx_last_data ); + } + }finally{ + cursor.close(); + } + }catch( Throwable ex ){ + log.e( ex, "load failed." ); + } + return dst; + } + + public void save(){ + try{ + ContentValues cv = new ContentValues(); + cv.put( COL_ACCOUNT_DB_ID, account_db_id ); + cv.put( COL_LAST_LOAD, last_load ); + cv.put( COL_NID_READ, nid_read ); + cv.put( COL_NID_SHOW, nid_show ); + cv.put( COL_LAST_DATA, last_data ); + App1.getDB().replace( table, null, cv ); + }catch( Throwable ex ){ + log.e( ex, "save failed." ); + } + } + public void updatePost(long post_id,long post_time){ + this.post_id = post_id; + this.post_time = post_time; + try{ + ContentValues cv = new ContentValues(); + cv.put( COL_POST_ID, post_id ); + cv.put( COL_POST_TIME, post_time ); + App1.getDB().update( table, cv,WHERE_AID, new String[]{ Long.toString( account_db_id ) } ); + }catch( Throwable ex ){ + log.e( ex, "save failed." ); + } + } + + public static void updateRead(long account_db_id){ + try{ + String[] where_args = new String[]{ Long.toString( account_db_id ) }; + Cursor cursor = App1.getDB().query( table, new String[]{ COL_NID_SHOW }, WHERE_AID, where_args, null, null, null ); + try{ + if( cursor.moveToFirst() ){ + long nid = cursor.getLong( cursor.getColumnIndex( COL_NID_SHOW ) ); + ContentValues cv = new ContentValues(); + cv.put( COL_NID_READ, nid ); + App1.getDB().update( table, cv, WHERE_AID,where_args ); + } + }finally{ + cursor.close(); + } + }catch( Throwable ex ){ + log.e( ex, "load failed." ); + } + } + + public static void resetPostAll(){ + try{ + ContentValues cv = new ContentValues(); + cv.put( COL_POST_ID, 0 ); + cv.put( COL_POST_TIME, 0 ); + App1.getDB().update( table, cv,null,null); + }catch( Throwable ex ){ + log.e( ex, "save failed." ); + } + } +} diff --git a/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.java b/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.java index 6ce92441..56e07974 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.java +++ b/app/src/main/java/jp/juggler/subwaytooter/table/SavedAccount.java @@ -29,6 +29,10 @@ public class SavedAccount extends TootAccount implements LinkClickContext{ private static final String COL_VISIBILITY = "visibility"; private static final String COL_CONFIRM_BOOST = "confirm_boost"; private static final String COL_DONT_HIDE_NSFW = "dont_hide_nsfw"; + private static final String COL_NOTIFICATION_MENTION = "notification_mention"; + private static final String COL_NOTIFICATION_BOOST = "notification_boost"; + private static final String COL_NOTIFICATION_FAVOURITE = "notification_favourite"; + private static final String COL_NOTIFICATION_FOLLOW = "notification_follow"; public static final long INVALID_ID = -1L; @@ -40,6 +44,10 @@ public class SavedAccount extends TootAccount implements LinkClickContext{ public String visibility; public boolean confirm_boost; public boolean dont_hide_nsfw; + public boolean notification_mention; + public boolean notification_boost; + public boolean notification_favourite; + public boolean notification_follow; public static void onDBCreate( SQLiteDatabase db ){ db.execSQL( @@ -52,6 +60,11 @@ public class SavedAccount extends TootAccount implements LinkClickContext{ + ",visibility text" + ",confirm_boost integer default 1" + ",dont_hide_nsfw integer default 0" + // 以下はDBスキーマ2で追加 + + ",notification_mention integer default 1" + + ",notification_boost integer default 1" + + ",notification_favourite integer default 1" + + ",notification_follow integer default 1" + ")" ); db.execSQL("create index if not exists " + table + "_user on " + table + "(u)" ); @@ -59,11 +72,31 @@ public class SavedAccount extends TootAccount implements LinkClickContext{ } public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){ - + if( oldVersion < 2 && newVersion >= 2){ + try{ + db.execSQL( "alter table "+table+" add column notification_mention integer default 1" ); + }catch(Throwable ex){ + ex.printStackTrace( ); + } + try{ + db.execSQL( "alter table "+table+" add column notification_boost integer default 1" ); + }catch(Throwable ex){ + ex.printStackTrace( ); + } + try{ + db.execSQL( "alter table "+table+" add column notification_favourite integer default 1" ); + }catch(Throwable ex){ + ex.printStackTrace( ); + } + try{ + db.execSQL( "alter table "+table+" add column notification_follow integer default 1" ); + }catch(Throwable ex){ + ex.printStackTrace( ); + } + } } private SavedAccount(){ - } private static SavedAccount parse( Cursor cursor ) throws JSONException{ @@ -81,6 +114,11 @@ public class SavedAccount extends TootAccount implements LinkClickContext{ dst.confirm_boost = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_CONFIRM_BOOST ) ) ); dst.dont_hide_nsfw = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_DONT_HIDE_NSFW ) ) ); + dst.notification_mention = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_NOTIFICATION_MENTION ) ) ); + dst.notification_boost = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_NOTIFICATION_BOOST ) ) ); + dst.notification_favourite = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_NOTIFICATION_FAVOURITE ) ) ); + dst.notification_follow = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_NOTIFICATION_FOLLOW ) ) ); + dst.token_info = new JSONObject( cursor.getString( cursor.getColumnIndex( COL_TOKEN ) ) ); } return dst; @@ -125,6 +163,12 @@ public class SavedAccount extends TootAccount implements LinkClickContext{ cv.put( COL_VISIBILITY, visibility ); cv.put( COL_CONFIRM_BOOST, confirm_boost? 1:0 ); cv.put( COL_DONT_HIDE_NSFW, dont_hide_nsfw ? 1: 0 ); + + cv.put( COL_NOTIFICATION_MENTION, notification_mention ? 1: 0 ); + cv.put( COL_NOTIFICATION_BOOST, notification_boost ? 1: 0 ); + cv.put( COL_NOTIFICATION_FAVOURITE, notification_favourite ? 1: 0 ); + cv.put( COL_NOTIFICATION_FOLLOW, notification_follow ? 1: 0 ); + App1.getDB().update( table, cv, COL_ID + "=?", new String[]{ Long.toString(db_id) } ); } } @@ -138,6 +182,10 @@ public class SavedAccount extends TootAccount implements LinkClickContext{ this.confirm_boost = b.confirm_boost; this.dont_hide_nsfw = b.dont_hide_nsfw; this.token_info = b.token_info; + this.notification_mention = b.notification_follow; + this.notification_boost = b.notification_boost; + this.notification_favourite = b.notification_favourite; + this.notification_follow = b.notification_follow; } } } diff --git a/app/src/main/res/drawable-hdpi/ic_notification.png b/app/src/main/res/drawable-hdpi/ic_notification.png new file mode 100644 index 00000000..9b441787 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notification.png b/app/src/main/res/drawable-mdpi/ic_notification.png new file mode 100644 index 00000000..f0df3a01 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notification.png b/app/src/main/res/drawable-xhdpi/ic_notification.png new file mode 100644 index 00000000..feeca4c4 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notification.png b/app/src/main/res/drawable-xxhdpi/ic_notification.png new file mode 100644 index 00000000..0f81d8c4 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notification.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notification.png b/app/src/main/res/drawable-xxxhdpi/ic_notification.png new file mode 100644 index 00000000..e846b2b2 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notification.png differ diff --git a/app/src/main/res/layout/act_account_setting.xml b/app/src/main/res/layout/act_account_setting.xml index 503a4cf8..66d6876c 100644 --- a/app/src/main/res/layout/act_account_setting.xml +++ b/app/src/main/res/layout/act_account_setting.xml @@ -3,6 +3,7 @@ xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" + xmlns:tools="http://schemas.android.com/tools" android:fillViewport="true" android:scrollbarStyle="outsideOverlay" > @@ -61,7 +62,7 @@ android:id="@+id/btnOpenBrowser" style="@style/setting_horizontal_stretch" android:ellipsize="start" - android:text="@string/update_access_token" + tools:text="open http://mastodon.juggler.jp/" android:textAllCaps="false" /> @@ -118,8 +119,12 @@ + @@ -135,12 +140,56 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index 098c28ad..36c421b5 100644 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png index ec0e8bf8..fc9884c2 100644 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png index d5accf2f..ee121158 100644 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index 3699cd1a..6ec54534 100644 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index d6af91eb..e801b4dc 100644 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 22b5422f..f6b7b7d7 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -145,4 +145,8 @@ バージョン %1$s OSSライセンス https://%1$s/ を開く + 返信 + %1$d件の通知 + ブースト + お気に入り diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31a20bec..4eb45212 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -144,4 +144,8 @@ Please support this app! version %1$s open https://%1$s/ + boost + favourite + mention + %1$d notifications diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 911a41f3..ff05acdd 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -82,7 +82,10 @@ wrap_content 1 - + diff --git a/ic_notification-817.png b/ic_notification-817.png new file mode 100644 index 00000000..976cdd69 Binary files /dev/null and b/ic_notification-817.png differ diff --git a/resizeLauncherIcon.pl b/resizeLauncherIcon.pl index ca1d2ed2..79349d79 100644 --- a/resizeLauncherIcon.pl +++ b/resizeLauncherIcon.pl @@ -49,4 +49,4 @@ sub resize_scales{ my $res_dir = "app/src/main/res"; resize_scales( "ic_launcher-1024.png",$res_dir,"mipmap","ic_launcher",0,48); #resize_scales( "ic_app_logo-512.png",$res_dir,"drawable","ic_app_logo",0,32); -#resize_scales( "ic_service-512.png",$res_dir,"drawable","ic_service",0,24); +resize_scales( "ic_notification-817.png",$res_dir,"drawable","ic_notification",0,24);