Firebase Cloud Messagingの組み込み

This commit is contained in:
tateisu 2017-05-24 18:20:56 +09:00
parent 0d9b7c1750
commit e99872fe4d
16 changed files with 371 additions and 33 deletions

View File

@ -6,6 +6,7 @@
<w>emojione</w>
<w>enty</w>
<w>favourited</w>
<w>firebase</w>
<w>hashtag</w>
<w>hashtags</w>
<w>idempotency</w>

View File

@ -37,7 +37,7 @@
<ConfirmationsSetting value="0" id="Add" />
<ConfirmationsSetting value="0" id="Remove" />
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" assert-keyword="true" jdk-15="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/build/classes" />
</component>
<component name="ProjectType">

View File

@ -9,8 +9,8 @@ android {
applicationId "jp.juggler.subwaytooter"
minSdkVersion 21
targetSdkVersion 25
versionCode 67
versionName "0.6.7"
versionCode 68
versionName "0.6.8"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
@ -55,6 +55,9 @@ dependencies {
compile 'com.android.support:customtabs:25.3.1'
compile 'com.android.support:support-v4:25.3.1'
compile 'com.google.firebase:firebase-core:10.0.1'
compile 'com.google.firebase:firebase-messaging:10.0.1'
// compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
@ -74,3 +77,5 @@ dependencies {
// annotationProcessor 'com.github.bumptech.glide:compiler:3.8.0'
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0'
}
apply plugin: 'com.google.gms.google-services'

42
app/google-services.json Normal file
View File

@ -0,0 +1,42 @@
{
"project_info": {
"project_number": "433682361381",
"firebase_url": "https://subway-tooter.firebaseio.com",
"project_id": "subway-tooter",
"storage_bucket": "subway-tooter.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:433682361381:android:7ae4daf10abb936c",
"android_client_info": {
"package_name": "jp.juggler.subwaytooter"
}
},
"oauth_client": [
{
"client_id": "433682361381-33pqgervqgcehbm9hup1g6qu967vs427.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyDVF995DFPjoY3ynGjGiI-5KDs8_BemE98"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}

View File

@ -171,6 +171,32 @@
<!--android:name="com.bumptech.glide.integration.okhttp3.OkHttpGlideModule"-->
<!--android:value="GlideModule" />-->
<service
android:name=".MyFirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
</intent-filter>
</service>
<service
android:name=".MyFirebaseInstanceIDService">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT"/>
</intent-filter>
</service>
<!-- Set custom default icon. This is used when no icon is set for incoming notification messages.
See README(https://goo.gl/l4GJaQ) for more. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_notification" />
<!-- Set color used with incoming notification messages. This is used when no color is set for the incoming
notification message. See README(https://goo.gl/6BKBk7) for more. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/Light_colorAccent" />
</application>
</manifest>

View File

@ -56,7 +56,7 @@ public class ActAccountSetting extends AppCompatActivity
}
@Override protected void onStop(){
AlarmService.startCheck( this );
AlarmService.startCheck( this ,false);
super.onStop();
}

View File

@ -128,7 +128,7 @@ public class ActMain extends AppCompatActivity
}
}
AlarmService.startCheck( this );
AlarmService.startCheck( this ,false);
}
@Override protected void onDestroy(){
@ -1192,7 +1192,7 @@ public class ActMain extends AppCompatActivity
account.saveSetting();
}
Utils.showToast( ActMain.this, false, R.string.account_confirmed );
AlarmService.startCheck( ActMain.this );
AlarmService.startCheck( ActMain.this ,false);
long count = SavedAccount.getCount();
if( count > 1 ){
addColumn( getDefaultInsertPosition(), account, Column.TYPE_HOME );

View File

@ -15,6 +15,7 @@ import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;
import android.support.v4.content.ContextCompat;
import android.support.v4.content.WakefulBroadcastReceiver;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONException;
@ -25,6 +26,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
@ -39,6 +41,10 @@ import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
import jp.juggler.subwaytooter.util.WordTrieTree;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class AlarmService extends IntentService {
@ -61,6 +67,10 @@ public class AlarmService extends IntentService {
public static final AtomicBoolean mBusyAppDataImportBefore = new AtomicBoolean( false );
public static final AtomicBoolean mBusyAppDataImportAfter = new AtomicBoolean( false );
public static final String ACTION_APP_DATA_IMPORT_AFTER = "app_data_import_after";
public static final String ACTION_DEVICE_TOKEN = "device_token";
private static final String ACTION_RESET_LAST_LOAD = "reset_last_load";
static final String APP_SERVER = "https://mastodon-msg.juggler.jp";
public AlarmService(){
// name: Used to name the worker thread, important only for debugging.
@ -92,6 +102,7 @@ public class AlarmService extends IntentService {
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(){
@ -101,6 +112,8 @@ public class AlarmService extends IntentService {
super.onDestroy();
}
String install_id;
// IntentService onHandleIntent をワーカースレッドから呼び出す
// 同期処理を行って良い
@Override protected void onHandleIntent( @Nullable Intent intent ){
@ -109,6 +122,10 @@ public class AlarmService extends IntentService {
// データベースへアクセスできるようにする
App1.prepareDB( this.getApplicationContext() );
install_id = getInstallId();
if( intent != null ){
String action = intent.getAction();
log.d( "onHandleIntent action=%s", action );
@ -138,10 +155,19 @@ public class AlarmService extends IntentService {
if( intent != null ){
String action = intent.getAction();
if( ACTION_DATA_DELETED.equals( action ) ){
if( ACTION_DEVICE_TOKEN.equals( action ) ){
// デバイストークンが更新された
// TODO 過去に中継サーバに登録したものは登録解除して登録しなおす必要がある
}else if( ACTION_RESET_LAST_LOAD.equals( action ) ){
NotificationTracking.resetLastLoad();
}else if( ACTION_DATA_DELETED.equals( action ) ){
deleteCacheData( intent.getLongExtra( EXTRA_DB_ID, - 1L ) );
}else if( ACTION_DATA_INJECTED.equals( action ) ){
processInjectedData();
}else if( AlarmReceiver.ACTION_FROM_RECEIVER.equals( action ) ){
WakefulBroadcastReceiver.completeWakefulIntent( intent );
//
@ -192,26 +218,36 @@ public class AlarmService extends IntentService {
@Override public void run(){
try{
if( account.notification_mention
|| account.notification_boost
|| account.notification_favourite
|| account.notification_follow
if( account.isPseudo() ) return;
if( ! account.notification_mention
&& ! account.notification_boost
&& ! account.notification_favourite
&& ! account.notification_follow
){
bAlarmRequired.set( true );
TootApiClient client = new TootApiClient( AlarmService.this, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return false;
}
@Override public void publishApiProgress( String s ){
}
} );
ArrayList< Data > data_list = new ArrayList<>();
checkAccount( client, data_list, account, muted_app, muted_word );
showNotification( account.db_id, data_list );
unregisterDeviceToken( account );
return;
}
if( registerDeviceToken( account ) ){
return;
}
bAlarmRequired.set( true );
TootApiClient client = new TootApiClient( AlarmService.this, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return false;
}
@Override public void publishApiProgress( String s ){
}
} );
ArrayList< Data > data_list = new ArrayList<>();
checkAccount( client, data_list, account, muted_app, muted_word );
showNotification( account.db_id, data_list );
}catch( Throwable ex ){
ex.printStackTrace();
}
@ -244,6 +280,118 @@ public class AlarmService extends IntentService {
}
}
String getInstallId(){
String sv = pref.getString(Pref.KEY_INSTALL_ID,null);
if( ! TextUtils.isEmpty( sv ) ) return sv;
try{
String device_token = pref.getString( Pref.KEY_DEVICE_TOKEN, null );
if( TextUtils.isEmpty( device_token ) ) return null;
Request request = new Request.Builder()
.url( APP_SERVER + "/counter" )
.build();
Call call = App1.ok_http_client.newCall( request );
Response response = call.execute();
if( ! response.isSuccessful() ){
log.e("getInstallId: get /counter failed. %s",response);
return null;
}
//noinspection ConstantConditions
sv = Utils.digestSHA256( device_token + UUID.randomUUID() + response.body().string() );
pref.edit().putString(Pref.KEY_INSTALL_ID, sv).apply();
return sv;
}catch( Throwable ex ){
ex.printStackTrace();
return null;
}
}
private void unregisterDeviceToken( @NonNull SavedAccount account ){
try{
// ネットワーク的な事情でインストールIDを取得できなかったのなら何もしない
if( TextUtils.isEmpty( install_id ) ) return;
String device_token = pref.getString( Pref.KEY_DEVICE_TOKEN, null );
if( TextUtils.isEmpty( device_token ) ) return;
String tag = account.notification_tag;
if( TextUtils.isEmpty( tag ) ) return;
String post_data = "instance_url=" + Uri.encode( "https://" + account.host )
+ "&app_id=" + Uri.encode( getPackageName() )
+ "&tag=" + tag;
Request request = new Request.Builder()
.url( APP_SERVER + "/unregister" )
.post( RequestBody.create( TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED, post_data ) )
.build();
Call call = App1.ok_http_client.newCall( request );
Response response = call.execute();
log.e( "unregisterDeviceToken:%s", response );
}catch( Throwable ex ){
ex.printStackTrace();
}
}
// 定期的な通知更新が不要なら真を返す
private boolean registerDeviceToken( @NonNull SavedAccount account ){
try{
// ネットワーク的な事情でインストールIDを取得できなかったのなら何もしない
if( TextUtils.isEmpty( install_id ) ) return false;
String device_token = pref.getString( Pref.KEY_DEVICE_TOKEN, null );
if( TextUtils.isEmpty( device_token ) ) return false;
String tag = account.notification_tag;
if( TextUtils.isEmpty( tag ) ){
tag = account.notification_tag = Utils.digestSHA256( install_id + account.db_id + account.acct );
account.saveNotificationTag();
}
// サーバ情報APIを使う
String post_data = "instance_url=" +
Uri.encode( "https://" + account.host ) +
"&app_id=" +
Uri.encode( getPackageName() ) +
"&tag=" +
tag +
"&access_token=" +
Utils.optStringX( account.token_info, "access_token" ) +
"&device_token=" +
device_token;
Request request = new Request.Builder()
.url( APP_SERVER + "/register" )
.post( RequestBody.create( TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED, post_data ) )
.build();
Call call = App1.ok_http_client.newCall( request );
Response response = call.execute();
log.e( "registerDeviceToken:%s", response );
// TODO 登録結果をDBに記録する
// 登録してから一定時間は再登録しない
}catch( Throwable ex ){
ex.printStackTrace();
}
return false;
}
private static class Data {
SavedAccount access_info;
TootNotification notification;
@ -492,9 +640,11 @@ public class AlarmService extends IntentService {
////////////////////////////////////////////////////////////////////////////
// Activity との連携
public static void startCheck( @NonNull Context context ){
Intent intent = new Intent( context, AlarmReceiver.class );
context.sendBroadcast( intent );
public static void startCheck( @NonNull Context context, boolean bResetLastLoad ){
Intent intent = new Intent( context, AlarmService.class );
if( bResetLastLoad ) intent.setAction( ACTION_RESET_LAST_LOAD );
context.startService( intent );
}
private static class InjectData {

View File

@ -50,7 +50,7 @@ public class App1 extends Application {
static final LogCategory log = new LogCategory( "App1" );
static final String DB_NAME = "app_db";
static final int DB_VERSION = 12;
static final int DB_VERSION = 13;
// 2017/4/25 v10 1=>2 SavedAccount に通知設定を追加
// 2017/4/25 v10 1=>2 NotificationTracking テーブルを追加
// 2017/4/29 v20 2=>5 MediaShown,ContentWarningのインデクスが間違っていたので貼り直す
@ -61,11 +61,11 @@ public class App1 extends Application {
// 2017/5/04 v33 9=>10 SavedAccountに項目追加
// 2017/5/08 v41 10=>11 MutedWord テーブルの追加
// 2017/5/17 v59 11=>12 PostDraft テーブルの追加
// 2017/5/23 v68 12=>13 SavedAccountに項目追加
private static DBOpenHelper db_open_helper;
public static SQLiteDatabase getDB(){
return db_open_helper.getWritableDatabase();
}

View File

@ -314,6 +314,13 @@ public class AppDataExporter {
reader.skipValue();
e.remove( k );
break;
// just ignore
case Pref.KEY_DEVICE_TOKEN:
case Pref.KEY_INSTALL_ID:
reader.skipValue();
e.remove( k );
break;
}
}
reader.endObject();

View File

@ -0,0 +1,32 @@
package jp.juggler.subwaytooter;
import android.content.Intent;
import com.google.firebase.iid.FirebaseInstanceId;
import com.google.firebase.iid.FirebaseInstanceIdService;
import jp.juggler.subwaytooter.util.LogCategory;
public class MyFirebaseInstanceIDService extends FirebaseInstanceIdService {
static final LogCategory log = new LogCategory("MyFirebaseInstanceIDService");
// Called when the system determines that the tokens need to be refreshed.
@Override public void onTokenRefresh(){
super.onTokenRefresh();
try{
String token = FirebaseInstanceId.getInstance().getToken();
log.d("onTokenRefresh: instance_token=%s",token);
Pref.pref(this).edit().putString(Pref.KEY_DEVICE_TOKEN,token).apply();
Intent intent = new Intent(this,AlarmService.class);
intent.setAction( AlarmService.ACTION_DEVICE_TOKEN );
startService( intent );
}catch(Throwable ex){
ex.printStackTrace();
}
}
}

View File

@ -0,0 +1,24 @@
package jp.juggler.subwaytooter;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
import java.util.Map;
import jp.juggler.subwaytooter.util.LogCategory;
public class MyFirebaseMessagingService extends FirebaseMessagingService {
static final LogCategory log = new LogCategory("MyFirebaseMessagingService");
@Override public void onMessageReceived( RemoteMessage remoteMessage ){
super.onMessageReceived( remoteMessage );
Map< String, String > data = remoteMessage.getData();
if( data != null ){
for( Map.Entry< String, String > entry : data.entrySet() ){
log.d( "onMessageReceived: %s=%s", entry.getKey(), entry.getValue() );
}
}
AlarmService.startCheck( getApplicationContext() ,true);
}
}

View File

@ -47,8 +47,10 @@ public class Pref {
static final String KEY_MEDIA_THUMB_HEIGHT = "MediaThumbHeight";
static final String KEY_TIMELINE_FONT = "timeline_font";
static final String KEY_DONT_CROP_MEDIA_THUMBNAIL = "DontCropMediaThumb";
static final String KEY_DEVICE_TOKEN = "device_token";
static final String KEY_INSTALL_ID = "install_id";
// 項目を追加したらAppDataExporter#importPref のswitch文も更新すること
}

View File

@ -33,6 +33,7 @@ public class NotificationTracking {
// 最後に表示した通知のID
private static final String COL_POST_ID = "pi";
// 最後に表示した通知の作成時刻
private static final String COL_POST_TIME = "pt";
@ -187,7 +188,19 @@ public class NotificationTracking {
App1.getDB().update( table, cv,null,null);
}catch( Throwable ex ){
log.e( ex, "save failed." );
log.e( ex, "resetPostAll failed." );
}
}
public static void resetLastLoad(){
try{
ContentValues cv = new ContentValues();
cv.put( COL_LAST_LOAD, 0 );
App1.getDB().update( table, cv,null,null);
}catch( Throwable ex ){
log.e( ex, "resetLastLoad failed." );
}
}
}

View File

@ -25,7 +25,7 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
public static final String table = "access_info";
public static final String COL_ID = BaseColumns._ID;
private static final String COL_ID = BaseColumns._ID;
private static final String COL_HOST = "h";
private static final String COL_USER = "u";
private static final String COL_ACCOUNT = "a";
@ -44,6 +44,9 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
private static final String COL_CONFIRM_FOLLOW_LOCKED = "confirm_follow_locked";
private static final String COL_CONFIRM_UNFOLLOW = "confirm_unfollow";
private static final String COL_CONFIRM_POST = "confirm_post";
// スキーマ13から
private static final String COL_NOTIFICATION_TAG = "notification_server";
public static final long INVALID_ID = - 1L;
@ -65,6 +68,8 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
public boolean confirm_unfollow;
public boolean confirm_post;
public String notification_tag;
// アプリデータのインポート時に呼ばれる
public static void onDBDelete( SQLiteDatabase db ){
try{
@ -97,6 +102,11 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
+ "," + COL_CONFIRM_FOLLOW_LOCKED + " integer default 1"
+ "," + COL_CONFIRM_UNFOLLOW + " integer default 1"
+ "," + COL_CONFIRM_POST + " integer default 1"
// 以下はDBスキーマ13で更新
+ "," + COL_NOTIFICATION_TAG + " text default ''"
+ ")"
);
db.execSQL( "create index if not exists " + table + "_user on " + table + "(u)" );
@ -148,6 +158,13 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
ex.printStackTrace();
}
}
if( oldVersion < 13 && newVersion >= 13 ){
try{
db.execSQL( "alter table " + table + " add column " + COL_NOTIFICATION_TAG + " text default ''" );
}catch( Throwable ex ){
ex.printStackTrace();
}
}
}
private SavedAccount(){
@ -178,6 +195,9 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
dst.confirm_unfollow = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_CONFIRM_UNFOLLOW ) ) );
dst.confirm_post = ( 0 != cursor.getInt( cursor.getColumnIndex( COL_CONFIRM_POST ) ) );
int idx_notification_tag = cursor.getColumnIndex( COL_NOTIFICATION_TAG );
dst.notification_tag = cursor.isNull(idx_notification_tag) ? null : cursor.getString( idx_notification_tag );
dst.token_info = new JSONObject( cursor.getString( cursor.getColumnIndex( COL_TOKEN ) ) );
}
return dst;
@ -237,6 +257,18 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
cv.put( COL_CONFIRM_UNFOLLOW, confirm_unfollow ? 1 : 0 );
cv.put( COL_CONFIRM_POST, confirm_post ? 1 : 0 );
if( notification_tag != null ) cv.put( COL_NOTIFICATION_TAG, notification_tag );
App1.getDB().update( table, cv, COL_ID + "=?", new String[]{ Long.toString( db_id ) } );
}
public void saveNotificationTag(){
if( db_id == INVALID_ID )
throw new RuntimeException( "SavedAccount.saveSetting missing db_id" );
ContentValues cv = new ContentValues();
cv.put( COL_NOTIFICATION_TAG, notification_tag );
App1.getDB().update( table, cv, COL_ID + "=?", new String[]{ Long.toString( db_id ) } );
}
@ -254,6 +286,7 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
this.notification_boost = b.notification_boost;
this.notification_favourite = b.notification_favourite;
this.notification_follow = b.notification_follow;
this.notification_tag = b.notification_tag;
}
public static @Nullable SavedAccount loadAccount( @NonNull LogCategory log, long db_id ){
@ -370,4 +403,5 @@ public class SavedAccount extends TootAccount implements LinkClickContext {
return 0L;
}
}

View File

@ -8,6 +8,8 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:2.3.2'
classpath 'com.google.gms:google-services:3.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}