2017/9/18

Guice


Guice 是 google 推出的一個輕量級依賴注入框架,解決Java中的 Dependency Injection 依賴注入問題,這個功能就像是 Spring 的 DI IoC。但因為 Spring 的框架 scope 龐大,如果只是想要一個單純的 DI library,那麼 Guice 是一個很好的選擇。


JSR 330 DI


在 Spring 誕生後,Google 也提供了另一個 DI 的實作 Guice,後來在 2009 年,定義了 JSR 330 DI 的規範,隨後 Spring 與 Guice 也都支援了 JSR 330。


javax.inject.* 提供了依賴注入的定義類別,但沒有限制依賴配置方式,
依賴配置方式取決於注入器的實作,injector 可以有多種配置設定的方式,可以基於XML、annotation、DSL(Domain-specific language),甚至是Java代碼,在 injector 實作的部分,可以採用反射、代碼生成技術等等,不受限制。




@Inject


可在constructor、field、method上使用,也可以在static 的非 final 的field、method上使用。使用該註解標註的constructor、field、method訪問修飾符 (private、package- private、protected、public 中任意一種) 不受限制。Injector在進行注入時,要按照constructors、fiedls、methods的順序進行。


對被標註 @Inject 的constructor的要求:


  • 在滿足上述說明的情況下,可以有其他的依賴作為方法的參數,別的要求倒沒有什麼。

對被標註 @Inject 的field的要求:


  • 不能是final

對被標註 @Inject 的method的要求:


  • 方法不能是abstract
  • 可以有其他的依賴作為該方法的參數

  • 當一個方法標註了 @Inject 並覆寫了其他標註了 @Inject 的方法時,對於每一個實例的每一次注入請求,該方法只會被注入一次。

  • 當一個方法沒有標註 @Inject 並覆寫了其他標註了 @Inject 的方法時,該方法不會被注入。




@Qualifier


用於標記限定器 annotation,用來指定採用哪個 class


假設 class A 有兩個 subclass A1,A2。B 依賴了A,那麼DI容器在為 B 的實例注入 A 時到底該注入 A1 或 A2 呢?


class B {
    @Inject
    A a;
}

解決方式是在 A1 及 A2 分別寫上 Qualifier 標記


@A_1
public class A1{
}

@A_2
public class A2{
}

在 class B 中指定 A1 或是 A2


class B{
    @Inject
    @A_1
     A a;
}

也可以用 @Named 進行標記


@Named("A1")
Public class A1{
}

@Named("A2")
Public class A2{
}

class B{
    @Inject
    @Named("A1")
    A a;
}



@Scope @Singleton


@Scope 用在 class 上,用來告訴 injector,為該 class 建立多少個 instances。


@Singleton 就是指產生一個 instance。


Guice example in scala sbt project


在 build.sbt 中加上 guice library


libraryDependencies += "com.google.inject" % "guice" % "4.1.0"

定義兩個 Service 介面,分別有 UserServiceImpl 及 LogServiceImpl 實作。


trait UserService {
  def process(): Unit
}

class UserServiceImpl extends UserService {
  override def process(): Unit = {
    System.out.println("UserServiceImpl in process")
  }
}

trait LogService {
  def log(msg: String): Unit
}

class LogServiceImpl extends LogService {

  override def log(msg: String): Unit = {
    System.out.println("log message:" + msg)
  }
}

定義 Application 介面,在 MyApp 實作的 constructor 中,引用了 UserService 及 LogService,將來由 guice 動態指定 UserServiceImpl 及 LogServiceImpl 實作。


import javax.inject.Inject

trait Application {
  def work(): Unit
}

class MyApp @Inject()(val userService: UserService, val logService: LogService) extends Application {
  override def work(): Unit ={
    userService.process()
    logService.log("MyApp is working")
  }
}

Guice 的 Module 定義,必須要 extends AbstractModule,在 configure 中,設定 class 實作的 Denpendency 關係。


import com.google.inject.AbstractModule

class AppModule extends AbstractModule {
  override protected def configure(): Unit = {
    bind(classOf[UserService]).to(classOf[UserServiceImpl])
    bind(classOf[LogService]).to(classOf[LogServiceImpl])

    bind(classOf[Application]).to(classOf[MyApp])
  }
}

scala 主程式,以 Guice.createInjector(new AppModule) 產生 injector,藉由 inject 取得 Application 的 instance,然後就能呼叫 work。


import com.google.inject.Guice

object Main {
  def main(args: Array[String]) {
    println("Main")

    val injector = Guice.createInjector(new AppModule)

    val myApp = injector.getInstance(classOf[Application])

    myApp.work()
  }
}

執行結果


Main
UserServiceImpl in process
log message:MyApp is working



如果要限制 MyApp 為 Singleton,可以在 MyApp 上加上 @Singleton


import javax.inject.{Inject, Singleton}

trait Application {
  def work(): Unit
}

@Singleton
class MyApp @Inject()(val userService: UserService, val logService: LogService) extends Application {
  override def work(): Unit ={
    userService.process()
    logService.log("MyApp is working")
  }
}

也可在 Module 中設定 class dependency 的地方,加上 .in(classOf[Singleton]) 的限制


bind(classOf[Application]).to(classOf[MyApp]).in(classOf[Singleton])

或是寫成 asEagerSingleton,在程式啟動時,就馬上產生 MyApp


bind(classOf[Application]).to(classOf[MyApp]).asEagerSingleton

Spring vs Guice


關於選擇Spring還是Google-Guice的一些想法


SpringComparison


以往的 Spring 是使用 xml 的方式定義 java bean,一般會認為 Guice 處理速度比 Spring 快,但可能只在啟動的時候有差異,因為 spring 需要讀取 xml 設定檔,而 guice 完全都是用程式碼處理的。


Guice 是由 Google 的 AdWords 專案誕生的,他不像是 Spring 整合了許多不同的 Java EE Framework,只是單純且專注在處理 Dependency Injection 的問題。在官方 Spring Comparison 文件中提到一個例子,他是由 Spring 轉換到 Guice,發現大約有 3/4 的程式碼是不需要的,用 Guice 寫的 module 程式碼短,且容易閱讀。


Guice 不支援以設定檔的方式設定 DI,完全是以 annotations 及 generics 程式碼的方式處理,因此可以達成動態 DI 的功能。


References


Guice簡明教程


Guice 快速入門


Guice Getting Started


Google Guice的動機


Java 依賴注入標準(JSR-330)簡介


JSR330 DI

2017/9/11

scala play from 2.5 to 2.6


scala play framework 專案如果要由 2.5 升級到 2.6,必須調整一些設定項目,另外 action composition 部分的程式也有更新的寫法,Lightbend activator 在 2017/5/24 已經退役,現在都要直接使用 sbt 編譯及封裝專案。


build.sbt


scala play 2.6 相關的 library 版本都要更新,scala 也要由 2.11 版改為 2.12,以下是 build.sbt


name := """project"""
organization := "tw.com.maxkit"

version := "0.1.0"

lazy val root = (project in file(".")).enablePlugins(PlayScala, JavaServerAppPackaging)

scalaVersion := "2.12.2"

scalacOptions ++= Seq("-encoding", "UTF-8")

libraryDependencies += guice

// Adds additional packages into Twirl
//TwirlKeys.templateImports += "tw.com.maxkit.controllers._"

// Adds additional packages into conf/routes
// play.sbt.routes.RoutesKeys.routesImport += "tw.com.maxkit.binders._"

libraryDependencies ++= Seq(
  ws,
  filters,
  "com.typesafe.play" %% "play-slick" % "3.0.0",
  "com.typesafe.play" %% "play-slick-evolutions" % "3.0.0",

  // slick
  "com.typesafe.slick" %% "slick" % "3.2.1",
  "org.slf4j" % "slf4j-nop" % "1.6.4",
  "com.typesafe.slick" %% "slick-hikaricp" % "3.2.1",

  // 讓 slick 支援 Timestamp 轉換 slick-joda-mapper https://github.com/tototoshi/slick-joda-mapper
  "com.github.tototoshi" %% "slick-joda-mapper" % "2.3.0",
  "joda-time" % "joda-time" % "2.7",
  "org.joda" % "joda-convert" % "1.7",

  // akka remoteing
  "com.typesafe.akka" % "akka-remote_2.12" % "2.5.3",

  // smtp email plugin
  // https://github.com/playframework/play-mailer
  "com.typesafe.play" %% "play-mailer" % "6.0.0",

  // mariadb java client
  //"mysql" % "mysql-connector-java" % "5.1.36",
  "org.mariadb.jdbc" % "mariadb-java-client" % "2.0.3",

  // 使用 FileUtils
  "commons-io" % "commons-io" % "2.5",

  // 使用 Base64
  "commons-codec" % "commons-codec" % "1.10",

  // object pool
  "commons-pool" % "commons-pool" % "1.6",

  // CLI parser library scopt https://github.com/scopt/scopt
  "com.github.scopt" % "scopt_2.12" % "3.6.0",

  // redis for Play https://github.com/KarelCemus/play-redis
  play.sbt.PlayImport.cacheApi,
  // include play-redis library
  "com.github.karelcemus" %% "play-redis" % "1.5.1",

  // Test Framework
  "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.0" % Test,
  specs2 % Test
)

resolvers ++= Seq(
  "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/",
  "Typesafe Maven Repository" at "http://repo.typesafe.com/typesafe/maven-releases/",
  "Typesafe ivy" at "http://dl.bintray.com/typesafe/ivy-releases",
  "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"
)

fork in run := true

// production settings
maintainer := "service <service@maxkit.com.tw>"
packageSummary := "project"
packageDescription := """"""

project/plugins.sbt


resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.2")

project/build.properties


sbt.version=0.13.15

Action Composition


Actions Composition 2.6 官方文件


如果要在 controller 裡面每一個 method 都進行的登入的驗證,可以用 ActionComposition 的方式實作,但 2.6 版的 ActionComposition 已經調整為以下的做法。


首先獨立實作一個 AuthAction.scala


package controllers.admin

import java.sql.Timestamp
import java.util.Date
import javax.inject.Inject

import model.Usr
import play.api.{Environment, Logger, Mode}
import play.api.cache.redis.CacheApi
import play.api.mvc._
import utils.ServerConstants

import scala.concurrent.{ExecutionContext, Future}

class AuthRequest[A](val usr: Option[Usr], request: Request[A]) extends WrappedRequest[A](request)

class AuthAction @Inject()(cache: CacheApi,
                           env: Environment,
                           val parser: BodyParsers.Default)
                          (implicit val executionContext: ExecutionContext)
  extends ActionBuilder[AuthRequest, AnyContent] with ActionTransformer[Request, AuthRequest] {
  val cacheTimeout = ServerConstants.cacheTimeout

  def transform[A](request: Request[A]) = Future.successful {
    //    val now: Timestamp = new Timestamp(new Date().getTime)
    //    val usr: Usr = new Usr(0, "", "", "", now, "", now, "")
    //
    //    new AuthRequest(Some(usr), request)

    (request.session.get("key").flatMap { key =>
      cache.get[Usr](key)
    } map { usr =>

      // 需要再設定一次 cache,否則會發生 cache timeout
      cache.set(request.session.get("key").get, usr, cacheTimeout)
      new AuthRequest(Some(usr), request)

    }).orElse {

      env.mode match {
        case Mode.Dev | Mode.Test => {
          Logger.info("Mode.Dev don't check admin login status")

          val now: Timestamp = new Timestamp(new Date().getTime)
          val usr: Usr = new Usr(0, "", "devusr", "", now, "", now, "")
          Some( new AuthRequest(Some(usr), request) )
        }
        case Mode.Prod => {
          Some( new AuthRequest(None, request) )
        }
      }

    }.get
  }
}

在 controller 中,要用 injection 的方式將 AuthAction 引用進來。


@Singleton
class MyController @Inject()(
                               authAction: AuthAction,
                               actorSystem: ActorSystem, env: Environment,
                               implicit val executionContext: ExecutionContext,
                               cc: ControllerComponents) extends AbstractController(cc) {
    def listCdrs = authAction.async { request: Request[AnyContent] =>

        val body: AnyContent = request.body
        val formdata: Option[Map[String, Seq[String]]] = body.asFormUrlEncoded
        ........
    }
}

移除 Play.current


在 scala play 2.5 就已經不能用 Play.current,這裡記錄怎麼利用 injector 直接產生 instance。


在一般的 scala class 直接使用 database 的 model,已經不能用以下這種寫法,Play.current、DatabaseConfigProvider.get 都已經是 deprecated method。


val dbConfig = DatabaseConfigProvider.get[JdbcProfile](Play.current)

首先建立一個新的 GlobalContext


package modules

import play.api.inject.Injector
import javax.inject.{Inject,Singleton}

@Singleton
class GlobalContext @Inject()(playInjector: Injector) {
  GlobalContext.injectorRef = playInjector
}

object GlobalContext {
  private var injectorRef: Injector = _

  def injector: Injector = injectorRef
}

在自訂的 Guice Module 中,產生 GlobalContext


bind(classOf[GlobalContext]).asEagerSingleton()

然後就能直接使用 injector 產生 database Model


 val npRepo = GlobalContext.injector.instanceOf[NpRepo]

ref: How to access Play Framework 2.4 guice Injector in application?


Lightbend activator 在 2017/5/24 終止


因為 LIGHTBEND ACTIVATOR TEMPLATES 發布,未來已經不會再用 activator 進行 project template 的管理,要求大家改用 giter8 templates


以往用 activator 產生新的 project 的指令,都要用 sbt 取代。


根據 giter8 template 產生新的 project


sbt new playframework/play-scala-seed.g8

在過程中,要填寫 project name, organization 等資料


This template generates a Play Scala project

name [play-scala-seed]: projectname
organization [com.example]: tw.com.maxkit
scalatestplusplay_version [3.1.1]:
play_version [2.6.2]:

在 poject 中,以往使用 activator 的指令,都要改成 sbt


編譯


sbt compile

啟動 server


sbt run

封裝整個 project


sbt clean update compile stage dist

giter8


Giter8 是一個基於發佈在 Github 或任何 git 上的template來生成文件或目錄的命令行工具,它是以 Scala 實作並由 sbt launcher 運行。


除了一個官方的 giter8 project templates 集散地 之外,我們可以自己建立自己的 project template 並以 git 形式存放及分享在 git server 中。


References


Giter8 gitbook


Giter8


使用Scalatra創建Scala WEB工程


action-composition 2.5

2017/9/4

monorepos


Monorepo 是一種管理企業代碼的方式,在這種方式下會摒棄原先一個 module 一個 git repo 的方式,而是把所有的 modules 都放在一個 repo 內來管理。單體倉庫 monorepos 是一個包含了多個獨立 project 的代碼倉庫,一個代碼 repository 包含一個單體倉庫。


目前有 Babel, React, Angular, Ember, Meteor 等等專案都使用了這個專案管理方式。


multirepos


當一個軟體專案隨著功能跟開發人員的擴增,漸漸地會發生共用程式碼的問題,一個大型的專案會拆分出多個小型 repos,每個 repo 代表了一個單獨的離散想法。


但也因為多個平行專案的發展,會發生一些問題:


  1. 架構孤島: 因為在一個整合專案中,拆分了功能並分配給不同的開發團隊處理及發展,每個團隊在不同的精進道路上,使用了不同的 library,好處是工程師可以根據自由選擇適當的 library,缺點是越來越多的 library,表示開發人員必須花更多時間學習不同的架構。

  2. 依賴地獄(Dependency Hell): 某個程式的修正,會影響到多少專案,在專案整合建構時,會需要很多時間找出整合的問題並提出修正。

  3. 建構耗時: 傳統的循序建構方式,會需要數十分鐘的時間,因應 monorepo 提供的新 build tool,可以平行建構加速建構的過程,也有更快的 incremental build 模式。


build tool


在 monorepo 中會搭配使用一個適當的 build tool,原因當然是為了在這樣的環境下,縮短建構的時間。



沒有萬靈丹


在一個專案建置初期,會因為開發維護的工程師人數不多,傾向於建置一個單一的專案進行開發,隨著應用本身的演進,公司的業務成長,會慢慢增加這個專案開發的參與人員數量,甚至會成長到有一個或多個開發 team。


在多人分工開發的環境下,就會面臨切割專案,抽取共用的函式庫的過程,一般的直覺,就是讓不同的專案模組有各自獨立負責的人員/團隊,也就是有各自獨立的版本演進,最後再整合在一起。


但如果分工是用垂直方式分工,以功能的方式分工,就有可能會造成程式碼的衝突,或是撰寫出來的程式運作的邏輯不同,或是使用了不同的輔助函式庫的問題。


這情況又有 monorepos 這樣的解決方案,想要解決這種問題。


應該說不管什麼樣的方案,都會有相對應的優點及缺點,那就要看每一個使用的情境來決定,該用什麼方式處理,讓優點多一點,缺點少一點,也沒有必要換來換去,畢竟轉換作業方式,加上適應的過程,也需要花上不少時間。


《三國演義》的第一句話就說:「話說天下大勢,分久必合,合久必分。」整體的局勢就是在分分合合中,不斷地來來往往反覆進行。


References


單體代碼倉庫:Uber的Android代碼倉庫演化史


Monorepos in Git


monorepo 新浪潮 | introduce lerna


多包存儲庫管理工具 Lerna


語意化版本 2.0.0

2017/8/28

Firebase Cloud Messaging FCM


GCM 已經被 Firebase Cloud Messaging FCM 取代,以下記錄測試 FCM 的過程。


註冊 Firebase


firebase 的頁面登入 google 帳號,登入後點擊右上方的 "Console" 即可進入控制台,第一件事是建立Firebase 專案,進入主控台後,請按下「CREATE NEW PROJECT」建立一個新專案,專案名稱填 alarm,國家/地區 填台灣。



接下來建立一個 android 應用程式,套件名稱的部分,要跟 Android APP 一樣,我們填 tw.com.maxkit.alarm



建立 Android project


用 Android Studio 建立一個新的 project: Alarm,package name 為 tw.com.maxkit.alarm。


將剛剛新增的 firebase android 應用程式裡面下載的 "google-services.json" 這個檔案,放到 Alarm/app 這個目錄裡面。


修改以下的設定檔


  • Alarm/build.gradle

增加一行 classpath 'com.google.gms:google-services:3.1.0'


buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.2'

        classpath 'com.google.gms:google-services:3.1.0'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

  • Alarm/app/build.gradle

增加這三個部分


compile 'com.google.firebase:firebase-messaging:10.2.6'
compile 'com.firebase:firebase-jobdispatcher:0.5.2'

apply plugin: 'com.google.gms.google-services'

完整的內容


apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "25.0.3"
    defaultConfig {
        applicationId "tw.com.maxkit.alarm"
        minSdkVersion 14
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'

    compile 'com.google.firebase:firebase-messaging:10.2.6'
    compile 'com.firebase:firebase-jobdispatcher:0.5.2'
}

apply plugin: 'com.google.gms.google-services'

  • Alarm/app/src/main/AndroidManifest.xml

增加兩個 meta-data,以及 MyFirebaseMessagingService 這個 service


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="tw.com.maxkit.alarm">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <!-- 收到通知時, 到狀態列要顯示的 icon -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_icon"
            android:resource="@drawable/ic_stat_announcement" />
        <!-- 收到通知的背景色 -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_color"
            android:resource="@color/black" />

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".MyFirebaseMessagingService">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
            </intent-filter>
        </service>
    </application>

</manifest>

這裡用到了自訂的 color 跟 icon



Alarm/app/src/main/res/values/colors.xml


要增加 black


<color name="black">#00000000</color>

  • MainActivity.java

處理 notification 的部分,由系統列點擊 notification 會進入 MainActivity,在這裡取得該 notification 的資訊。


package tw.com.maxkit.alarm;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import com.google.firebase.messaging.FirebaseMessaging;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //這一行是要註冊 alarms 這個 topic,如果不需要,就把這行刪除
        FirebaseMessaging.getInstance().subscribeToTopic("alarms");

        Log.d(TAG, "onCreate ..");

        Intent intent = getIntent();
        String msg = intent.getStringExtra("msg");

        if (msg!=null)
            Log.d(TAG, "msg:"+msg);

        if (getIntent().getExtras() != null) {
            for (String key : getIntent().getExtras().keySet()) {
                Object value = getIntent().getExtras().get(key);
                Log.d(TAG, "Key: " + key + " Value: " + value);
            }
        }
    }

}

  • MyFirebaseMessagingService.java

處理訊息通知的 code,主要是 onMessageReceived


package tw.com.maxkit.alarm;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

public class MyFirebaseMessagingService extends FirebaseMessagingService{

    private static final String TAG = "MyFirebaseMsgService";

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        Log.d(TAG, "onMessageReceived:"+remoteMessage+", from:"+remoteMessage.getFrom()+", data="+remoteMessage.getData());

        if (remoteMessage.getData().size() > 0) {
            Log.d(TAG, "Message data notifytitle: "+ remoteMessage.getData().get("notifytitle"));
            Log.d(TAG, "Message data notifybody: "+ remoteMessage.getData().get("notifybody"));
            Log.d(TAG, "Message data payload: " + remoteMessage.getData());
        }

        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification title: "+ remoteMessage.getData().get("title"));
            Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
        }

        //Display notification in notification bar
        sendNotification(remoteMessage.getData().get("notifytitle"), remoteMessage.getData().get("notifybody"));
    }

    private void sendNotification(String notifytitle, String notifybody) {
        Intent intent = new Intent(this, MainActivity.class);

        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
                PendingIntent.FLAG_ONE_SHOT);

        Uri defaultSoundUri=
                RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.drawable.ic_stat_access_alarms)
                .setContentTitle( notifytitle )
                .setContentText( notifybody )
                //.setAutoCancel(true)
                .setSound(defaultSoundUri)
                .setContentIntent(pendingIntent);

        NotificationManager notificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
    }
}

用 Firebase 網頁測試


在左側點擊 Notifications 的部分,填寫以下的訊息,就會以 notification 的方式,收到訊息。意思就是說,當 APP 在背景時,會直接在系統列裡面收到 notification。




用 curl 測試


根據 Firebase Cloud Messaging HTTP Protocol 的說明,可以直接用 http 的方式發送訊息。


在專案設定 -> Cloud Messaging 的地方,可以找到 Authorization Key 伺服器金鑰



以下是 curl 的測試,"Authorization: key=" 後面要換成剛剛取得的 伺服器金鑰。


data 的部分可以參考 Firebase Cloud Messaging HTTP Protocol 的說明,以下我們設定了 to alarms 這個 topic,另外只傳送了 data 區塊,在 android APP 不管是前景或背景,都會在 onMessageReceived 收到訊息,我們可在那邊發送 local notification 把資料顯示在系統列上。


curl -X POST -H "Content-Type: application/json" \
    -H "Authorization: key=yourkey" \
    https://fcm.googleapis.com/fcm/send \
    -d '{"to":"/topics/alarms", "priority":"high", "data":{"notifytitle":"測試title", "notifybody":"測試body", "訊息 key": "訊息 Topic!", "key2": "value2"}}'

也可以加上 notification 的區塊,這部分就會跟在 Firebase 頁面測試的結果一樣,Android APP 會直接收到 notification。


curl -X POST -H "Content-Type: application/json" \
    -H "Authorization: key= yourkey" \
    https://fcm.googleapis.com/fcm/send \
    -d '{"to":"/topics/alarms", "priority":"high", "notification":{"title" : "title","icon" : "new","body" : "訊息 body"}, "data":{"訊息 key": "訊息 Topic!", "key2": "value2"}}'

References


Firebase 心得(Cloud Messaging)


在Android中使用2016新版Firebase加快開發過程(一)


Firebase雲端訊息-發送測試通知至Android APP中


Firebase Cloud Messaging (FCM) 入門到進階應用(1) --- 開發環境建立


【Android Studio】從 GCM 移植到 FCM


Android: 利用Firebase實作使用者間的產品分享機制


Send Messages to Multiple Devices


Firebase push Notification using curl command — Devoid Backend




for ios


Push Notification教學:如何使用Firebase在iOS實現推播功能


[教學] 實作Google Firebase的Notification 使用Objective-C

2017/8/21

記錄用 CentOS 7 安裝 Redmine 3.3.3


因機器搬遷,需要把舊的 server 的資料,移到新機器上。


用 docker 測試


docker run -d -p 10022:22 -p 10080:80 centosssh /usr/sbin/sshd -D

如果遇到 Failed to get D-Bus connection: Operation not permitted 的問題,就改用這個方式啟動。因為需要 ssh 及 web 的 port,啟動時先對應好。


ref [原创] 解决 CentOS7 容器 Failed to get D-Bus connection: Operation not permitted


docker run -d -p 10022:22 -p 10080:80 -e "container=docker" --privileged=true -v /sys/fs/cgroup:/sys/fs/cgroup --name centos7test centosssh /usr/sbin/init

docker exec -it centos7test /bin/bash

安裝Apache、MariaDB、PHP


yum install -y httpd php mariadb mariadb-server mariadb-devel systemd which wget

設定MariaDB的DB為utf8


vi /etc/my.cnf.d/server.cnf


[mysqld]
character-set-server=utf8

vi /etc/my.cnf.d/client.cnf


[client]
default-character-set=utf8

啟動 MariaDB


systemctl start mariadb
mysql_secure_installation

建立 redmine 資料庫及帳號


mysql -u root -p

CREATE DATABASE redmine CHARACTER SET utf8;
CREATE USER 'redmine'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON redmine.* TO 'redmine'@'localhost';
flush privileges;
exit;

修改 MariaDB 密碼


mysqladmin -u root password 'password'

安裝 rvm


安裝RVM (Ruby管理工具)、Ruby、Rubygem


# Install Key
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
#Install RVM
\curl -sSL https://get.rvm.io | bash -s stable

測試過 ruby 2.4.0 有些問題,所以安裝 ruby 限制在 2.2.4 版。


source /etc/profile.d/rvm.sh
rvm requirements
rvm install ruby 2.2.4
#rvm install rubygem
#gem install rails --no-rdoc --no-ri

不使用 yum 安裝 ruby (現在是 2.0 版)


#yum list ruby 
yum install -y gcc libxml2-devel

# 包含ruby/gem/libyaml
#yum install -y ruby ruby-devel

gem install bundler

gem install rake --no-document
gem i nokogiri --no-document -v='1.6.8'
gem i mime-types --no-document

# ruby 2 與 rails 5 不相容
gem install rails --no-document -v='4.2.7'

gem install rbpdf --no-document
gem install rbpdf-font --no-document

安裝passenger


yum install -y libcurl-devel httpd-devel apr-devel apr-util-devel

gem install passenger
passenger-install-apache2-module

安裝完成後,會出現module passenger的設定檔文字


vi /etc/httpd/conf.d/passenger.conf

LoadModule passenger_module /usr/local/rvm/gems/ruby-2.2.4/gems/passenger-5.1.4/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
     PassengerRoot /usr/local/rvm/gems/ruby-2.2.4/gems/passenger-5.1.4
     PassengerDefaultRuby /usr/local/rvm/gems/ruby-2.2.4/wrappers/ruby
</IfModule>

設定 redmine httpd virtual host


vi /etc/httpd/conf.d/redmine.conf

RailsEnv production
RailsBaseURI /redmine

<Directory /home/redmine/redmine-3.3.3/public>
  Options FollowSymlinks
  AllowOverride none
  Require all granted
</Directory>

restart httpd


systemctl restart httpd

安裝 redmine


cd /home

mkdir redmine
cd redmine

wget http://www.redmine.org/releases/redmine-3.3.3.tar.gz

tar -xf redmine-3.3.3.tar.gz

ln -s /home/redmine/redmine-3.3.3/public /var/www/html/redmine

chown -R apache:apache redmine-3.3.3

設定 mysql


cd redmine-3.3.3/config
cp database.yml.example database.yml

# 設定資料庫使用者名稱、密碼。

vi database.yml

把
production:
  adapter: mysql2
  database: redmine
  host: localhost
  username: root
  password: ""
  encoding: utf8

改為

production:
  adapter: mysql2
  database: redmine
  host: localhost
  username: redmine
  password: "dbpassword"
  encoding: utf8

cd ..

bundle install --without development test rmagick

### 這個是生成redmine的什麼token,不生成的話瀏覽器會連接不上的,每次生成,以前的cookie內容就會失效
bundle exec rake generate_secret_token

如果有舊的 DB 要先 restore redmine db


mysql -uroot -p redmine < redmine_noplugins.sql

升級 DB schema


RAILS_ENV=production bundle exec rake db:migrate

有舊DB 就不需要 loaddefaultdata


# 生成數據庫對象

RAILS_ENV=production REDMINE_LANG=zh bundle exec rake redmine:load_default_data

可用 webrick 內建 web server 測試,也可以跳過不做


bundle exec rails server webrick -e production
wget http://localhost:3000
  > 測試安裝

為以後apache服務器對應(redmine/public目錄)做準備


cd public

cp htaccess.fcgi.example htaccess.fcgi
cp dispatch.fcgi.example dispatch.fcgi

設定 email


cd /home/redmine/redmine-3.3.3/config
cp configuration.yml.example configuration.yml
vi configuration.yml

修改 configuration.yml 前面的 email_delivery,要注意不能修改縮排的格式。


  email_delivery:
    delivery_method: :smtp
    smtp_settings:
      enable_starttls_auto: true
      address: "smtp.gmail.com"
      port: 587
      domain: "maxkit.com.tw"
      authentication: :login
      user_name: "maxkit@maxkit.com.tw"
      password: "youremailpassword"

references


Installing Redmine


CentOS 7 安裝 Redmine


Centos 7安裝 redmine 3.X


最小化安装centos7.3 redmine3.3.3 passenger




Upgrading redmine


Redmine 2.6.3 to 3.0.1 upgrade


redmine_bak to git repository