From a8c56f509fe3a289dac01836db1a16f46ff6c29c Mon Sep 17 00:00:00 2001 From: tateisu Date: Tue, 25 Apr 2017 15:54:05 +0900 Subject: [PATCH] =?UTF-8?q?v0.1.0=20=E3=83=97=E3=83=83=E3=82=B7=E3=83=A5?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=E3=80=82=E3=82=BF=E3=83=96=E3=81=AE=E4=B8=A6?= =?UTF-8?q?=E3=81=B3=E9=A0=86=E3=81=8C=E5=A4=89=E3=82=8F=E3=82=8B=E3=83=90?= =?UTF-8?q?=E3=82=B0=E3=81=AE=E4=BF=AE=E6=AD=A3=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 10 + .../subwaytooter/ActAccountSetting.java | 55 +- .../juggler/subwaytooter/ActAppSetting.java | 3 +- .../java/jp/juggler/subwaytooter/ActMain.java | 88 ++-- .../juggler/subwaytooter/AlarmReceiver.java | 19 + .../jp/juggler/subwaytooter/AlarmService.java | 493 ++++++++++++++++++ .../java/jp/juggler/subwaytooter/App1.java | 79 +-- .../java/jp/juggler/subwaytooter/Column.java | 40 +- .../java/jp/juggler/subwaytooter/Pref.java | 3 +- .../api/entity/TootNotification.java | 3 + .../table/NotificationTracking.java | 154 ++++++ .../subwaytooter/table/SavedAccount.java | 52 +- .../res/drawable-hdpi/ic_notification.png | Bin 0 -> 642 bytes .../res/drawable-mdpi/ic_notification.png | Bin 0 -> 484 bytes .../res/drawable-xhdpi/ic_notification.png | Bin 0 -> 792 bytes .../res/drawable-xxhdpi/ic_notification.png | Bin 0 -> 1128 bytes .../res/drawable-xxxhdpi/ic_notification.png | Bin 0 -> 1514 bytes .../main/res/layout/act_account_setting.xml | 59 ++- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 5799 -> 5799 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 3702 -> 3702 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 8079 -> 8079 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 13653 -> 13653 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 19917 -> 19917 bytes app/src/main/res/values-ja/strings.xml | 4 + app/src/main/res/values/strings.xml | 4 + app/src/main/res/values/styles.xml | 5 +- ic_notification-817.png | Bin 0 -> 13707 bytes resizeLauncherIcon.pl | 2 +- 29 files changed, 974 insertions(+), 103 deletions(-) create mode 100644 app/src/main/java/jp/juggler/subwaytooter/AlarmReceiver.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/AlarmService.java create mode 100644 app/src/main/java/jp/juggler/subwaytooter/table/NotificationTracking.java create mode 100644 app/src/main/res/drawable-hdpi/ic_notification.png create mode 100644 app/src/main/res/drawable-mdpi/ic_notification.png create mode 100644 app/src/main/res/drawable-xhdpi/ic_notification.png create mode 100644 app/src/main/res/drawable-xxhdpi/ic_notification.png create mode 100644 app/src/main/res/drawable-xxxhdpi/ic_notification.png create mode 100644 ic_notification-817.png 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 0000000000000000000000000000000000000000..9b44178758edb4bf1840833a40ee26229afa2387 GIT binary patch literal 642 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k0wldT1B8JTOS+@4BLl<6e(pbstUx|flDE4H z!~gdFGy8!&_7YEDSN4Z2lI#-9*BO{i7#JA0dAc};cpQH_#Xfs-ph)|C^R-t+jIZqP zzE-v(!R5$7N3NuU3Kk_BzDS#tiR$d)tYbRJ|3F1!*ADAlGyIN*@oeSjO6rNo+8Qcc zEYCi9i|4E#)0%?ky*yC;?!E2v@^gDHEInPn(24W=t>@=bm>!jO>s3^_2R*Vpy=vd* z9kq&F(a*k`$tO1$x=E_FuK6@8a$U&gzDdcZzYZzC-y~O>rSxD5>;JQV<6FKtC0!G6 zc58lIHsPlH>RJ1BW@SzB%IVTCw3zlk`%X&B%(AaMmhzitiK^@TJ}d0|_OIKSA1BQ> zO0Qlqt9U-|R+Tfq?n?U|6I(U0{7#BT#TlJ>A3M3@#Q1j#1uLw09~-{)!KBLNO2#_I z{+doRp62jup1$K)sGp&4*W@>8eamZdW$HIB*54|-TlrnlG4Z%5v%D|132fgHdtLX_ z498!d0ckV0?@8%vU&8p~Bx}(D#$y$33mGHxXr_iYO?37R{3-uoX$tVfodTszrij#>W2 ze!nfxTymHfPPYyDqiB2E^2SncpMMO|JhrVyv-g|@MyhIwYeY#(Vo9o1a#1RfVlXl= zG}kpS(KRv+F)+6>GO;o+);6%TGBB{%neBn1AvZrIGp!Q02G1v#zW_Bbc)I$ztaD0e F0sxeX4Y>dS literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..f0df3a01800ac772d4a52ab847c82232a32f3bc4 GIT binary patch literal 484 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM0wlfaz7_*1mUKs7M+SzC{oH>NS%G|}ByV>Y zhX3vTXZ8bm>?NMQuIvw4B-tgHuQM>4FfcGmc)B=-Se$-4X}_LxqD*E1`dLS|MAkQ!?|R*QdF`|Xd=pc<600t^&I14f)~z9&fY4=wQtq0ce6#ckJ(ixdg@x|%s=hty>#Kr-mJa)eGw%m zn)Z6WSh?zW+L1L!9Cp3p?0lIp-=^|g(=9Fk84O89Q<}EFPv(^=IW}dX=IiawagsZ~ z%(kDpZMWRV^skKRH`zZquTaeZ2AgV$YeY#(Vo9o1a#1RfVlXl=G}kpS(KRv+F)+6> tGO;o+);6%TGBB{%neBn1AvZrIGp!Q02G1v#zW_Bbc)I$ztaD0e0swL1y(9nt literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..feeca4c4fc9997868043aafba6edf6248681b0be GIT binary patch literal 792 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA0wn)(8}a}tmUKs7M+SzC{oH>NS%G|}ByV>Y zhX3vTXZ8bm>?NMQuIvw4B-tgHuQM>40Ofr=T^vIq4!@n|?GX|vb8Nr$;$GiiWv*b) zE>>P$o~Vo+r?UiES@*u!*xjM9s8_%-!BIrnwR?eY?yW1!rgkslxV?p=x^dF9Z#Q~c zlCSinG*2t8({(#md8hLAs^8If5C5J2-unCQ?|1%BlmP-Uc^BTgn6#5mZdZCwsf&sb z`f1uTIi+Lj`{5G2A#q?11h{)5zbw_px#Lb(g$tRGU%qeEy`eLnv+=BBFuJ*A< zc5ONwnNWW*-C@pJkE$N&0}|SrO79=0y0VpLCW-Cn`6n_V|6ye7!=BF!4Vl{}gx6`` z?=Xn@DD7e7RNU7p_=v$!sia_~7c)n(%FgYAb-P+J9++{maVS|CghorWaa=rWtM)>8 zxkPG%@D)YnEXlPVCQePB?+niP%sT6MXVJ{7OSW-53cUHK_{*c^?JR#;GSb+OTbxOo zJ%52<+`ehK2A<4LZ=P|>XKHT{7f|1#g+v9TZx8Yqk0`73I^a>zA#yd2OFOdHvgK zj$cx)v}SJVKQ`6y)v2BDuYI(s+;>jGG;4Qlr0=m6(ZN<)^HpB1x|cn>=~jbw;+>tw z_cfON`uSS^)s^Q$=Oo=a!p?o3z3`j0;Mh|B^LJ$D zT&ww=cRqQ{3SFe7$j;vWe{TJ1!J3A0hv|EPNlmrHHKHUXu_Vv z=o*=Z7?@ibnOGSZYa3Wv85mgX%=SRhkei>9nO2EggXfdWUw|4IJYD@<);T3K0RWml BNfiJ9 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0f81d8c489d6f738a3be24e7f2099105f834356f GIT binary patch literal 1128 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY0wn)GsXhawSkfJR9T^xl_H+M9WCik>lDyqr z82-2SpV<%Ov6p!Iy0SlHkz|)(zRtjG!oa{h$J50zB;(%O8NL}Yp%QKPjel-F<|%EG zwPs=Ws*sYzrCV1-+|^LhTk<1VX=;OzKH93Fmua3|U!W)cOmXs@?CVi?SMY6J8JQ7xS?X$~s?N&-S*x{IEt{EmwJ$WeeOW zq-gKzkT(T478V?LtKfSj?z#3>eqL*$ais8JL2F~LIlA4~`=<&`d4AZwsL21tw2r?s zug+N}{{QRWuJ&`qYS}5_ev5jV8shl=d|xf!I;myHwEV11Gxb%E9#;G&m?XQLAz41z zbehS*Fc(S7*%FLQao*N`*X}G;S-&A`r;4!QqnmSuA4jG&L{DwOVZN1Q0!Wy#L+X4z!*%IS|)W+YD2Y`by1tz_jRmW%QW zXP8uHxUG4fexTy8luu)_==BRHXKmtb&;j?DtxY{`H)32LW}UX$ zqH}lVb@o}p-wPxjvutnGab;R6%ig``4bx`V!$v39*PPOO_o#i(Zq93IvWAh&(G~~% zZRWlYYII|G>BuYg(tTgp`dwF+mTg_-%4@WA%9SO@OjZ@9r5GymzG&SuF@B@(!5}^J zooc49l5YAPNipmb<2`;qCRXoT$GoQG^01J7O54P4Fnj0lE510V^zdFtT+qT@A0|$E zo9zEnHPa=SH7{o6-l>*f7=kUcXFo3w>b~x|%c-J7=lp!mZx{eE+jM^WMg1ODA7RNnLF#eEn%?$lr`bQl3Ey z#ak}r$lXz+g$0!z+MUSGB}7q9i4;B-JXpC>2OC7#SFv>l&Eo8kvR|m|Gc{SQ!{= p8(3Ny7+CDg1{Dhk4Y~O#nQ4`{HF!R``~|3i!PC{xWt~$(697`m@0tJr literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e846b2b24b83a6bf5ab5d5e0a8941fdad6e16d51 GIT binary patch literal 1514 zcmZ{kYd8}M0EQ=*b!1Lv)=R10$8pIAqYj zIVyJ880OQKLx_c1Ia>h$&4tIl62%YqkzhxAG~j?neTi_1LJ#|j5&`kyngCMW+xfyI0Btcu1$qRsq>n)NvCq<;q}_y;)yx8Cg` zJ604{WZ^{1b(aZWnuzbJ+~!W&3c9?|h3rR&Q2wwQIMG%?b#xRDgi?%i!bp@yIvem~ z`|9C|1*J9^mGaP?(9}SixsOavL`qRb(7 z2FoS4OZG!H33)C82pw1b#MB|f$yhxqEOV@q+S?#jjdoT=ha1;W!K7Z^Fy97H&#OywGRJHP2~iaG-YP`DvQY zg8N2_VZ%_Y-)Qw?%)+v^^Fp13Q%MBAXWo%5fmMJ6o}QNHHSqU7GL9EF_QkG@KMyiW z5bhm4vANhpy?>H5TtXBEjS{D>COy>KnU!|$q^~uWT+HtldEKCKmgu;&71MrxP(ryY zWVz8>?|7}ykS4h7)kSV4Pl{M5%Ck-Cxwq;gr{R=1GumXR7pW#~fGPl1}SEB`2Sqe&WZ#(qVn7ay>&jF zu}~t!P~2#6%kF}RvS-&i26#R`m%BqP2dNnQ==D3D6-{UGyUsbeXw2WOs85gUT&rR> zAi$0|b8T0Trj?h{aUIMXb&(16=iHl}3@7TNa9TyPNYS-eyWMX(XH)s4++-b&cv^C_ z?f%;URin`7o!ms=)HtwF{wMt8rk%yNBgWrI526%U*iSiRp9Up=;rKw|3AW7d&2Xx8-0WWo)~sbglEfV{O`rLy+E5|l!4;R zR{zMN)>k#Y6G^-(3J5A5P_mUasVF;#8i{mU;{K*AliSu#AKNO5GgQ59|D@Oh=PC`Z z)aX%5G)!1G%H`Pba@?^EewV#~`awzNcVl&vT7k<4?b`ix8`fzAgS+)6-~kBqGGM)& z`MGo$857Z|I=ZFw;?#(#VLeeQRj9&pudJe{vdM7ZBlNnTl@gFD#-RYf M+Pa|cS_h^71LcCT?*IS* literal 0 HcmV?d00001 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 098c28adfd9f484ce63fca25660378e5c5e9dc8b..36c421b54de1d81af20295ffe210a792022191f1 100644 GIT binary patch delta 29 kcmZ3kyIgleq?n+Ym5Hg9p^3JErImq!>glCMle5HZ0fM0jmH+?% delta 20 bcmZ3kyIgleq}b#xFglCMlP&pd0g7M delta 20 bcmew+^G#-h2H)gJK3glCMlO;@T0h90v1^@s6 delta 20 bcmcbbbv0`PyXj;LQ(l&Irrp_-B}{DrR_6yO diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index d6af91ebb392907cccd581d25e17c82e0ade9dcf..e801b4dcdf7dae45c2a2e374a5fdf84eaeeb9303 100644 GIT binary patch delta 31 ncmX>*oAK;y#trqpf@W4GrdEa~+6I*oAK;y#trqpf~HocW>&^#+6Iバージョン %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 0000000000000000000000000000000000000000..976cdd69849706858359c94b2321ce91f5cb7675 GIT binary patch literal 13707 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