Android で versionName, versionCode を取得

Android で build.gradle に設定した versionName, versionCode をプログラムで取得する。

        PackageManager pm = context.getPackageManager();
        String versionName = "";
        Integer versionCode = null;
        try {
            PackageInfo packageInfo = pm.getPackageInfo(context.getPackageName(), 0);
            versionName = packageInfo.versionName;
            versionCode = packageInfo.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            //
        }

Android の ActionBar に画像を表示

ActionBar にオリジナルのレイアウトを適用し画像を表示する。

action_bar.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="48dp">

    <ImageView
        android:id="@+id/logo_action_bar"
        android:layout_width="match_parent"
        android:layout_height="32dp"
        android:layout_alignParentStart="true"
        android:layout_centerVertical="true"
        android:layout_marginLeft="8dp"
        android:scaleType="fitStart"
        android:src="@drawable/logo_action_bar"/>

</RelativeLayout>

Activityで適用

public class XxxActivity extends AppCompatActivity {

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ・・・

        // 表示するlayoutファイルの取得
        LayoutInflater inflater = LayoutInflater.from(this);
        View actionBarView = inflater.inflate(R.layout.action_bar, null);

        // プログラムでオリジナル画像を設定するなら下記
        // ((ImageView)actionBarView.findViewById(R.id.logo_action_bar)).setImageDrawable(getDrawable(R.drawable.xxxx));

        ActionBar actionBar = getSupportActionBar();
        actionBar.setCustomView(actionBarView);
        actionBar.setDisplayShowCustomEnabled(true);


Android でバックグラウンド(Service)処理(Android8対応)

Androidでバックグラウンド処理をする場合Serviceを使いますが、その使用についてAndroid8から制限がかかりました。
一番の変更点は”バックグラウンドで処理するならユーザーに通知せよ”という点です。
その対応をしないとすぐにサービスが停止してしまうので、その辺りの対策を踏まえてServiceの使用方法を少し書いてみます。

私はAndroid5で実機テストしながら実装していたので、かなりハマってしまいました。

今回は下記のようなイメージです。

  • ActivityからServiceを起動
  • ActivityからServiceクラスにアクセス
  • Service起動時に通知したNotificationをタップしたらServiceを停止

Service起動はAndroid8以降では、起動から5秒以内にstartForegroundを呼ばないといけない。
Service停止はどこかでBindされていると停止できないので起動時のフラグを間違えないようにする。
などいくつかハマりポイントがありました。

実装例を下記します。

Serviceでは、起動(onStartCommand)で、startForegroundを呼ぶのと、ActivityからServiceを取得させるためにonBindを実装します。
Serviceの起動、停止はローカルにブロードキャストさせます。

public class XxxService extends Service {
    public static final String ACTION_START_XXX_SERVICE = "ACTION_START_XXX_SERVICE";
    public static final String ACTION_STOP_XXX_SERVICE = "ACTION_STOP_XXX_SERVICE";

    private static final String ACTION_CLICK_SERVICE_NOTIFY = "ACTION_CLICK_SERVICE_NOTIFY";
    private static final String CHANNEL_XXX_SERVICE = "CHANNEL_XXX_SERVICE";

    private LocalBroadcastManager broadcastManager;
    private BroadcastReceiver notificationReceiver;

    // Activityから接続するためのBinder
    public class ServiceLocalBinder extends Binder {
        public XxxService getService() {
            return XxxService.this;
        }
    }
    private final IBinder binder = new ServiceLocalBinder();

    // サービスの開始
    public static void start(Context context) {
        ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
        for (ActivityManager.RunningServiceInfo serviceInfo : manager.getRunningServices(Integer.MAX_VALUE)) {
            if (XxxService.class.getName().equals(serviceInfo.service.getClassName())) {
                // すでに実行中なら起動しない
                return;
            }
        }

        Intent intent = new Intent(context, XxxService.class);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            context.startForegroundService(intent);
        } else {
            // Android8より前の対応
            context.startService(intent);
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        broadcastManager = LocalBroadcastManager.getInstance(getApplicationContext());

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // Android8以降ではNotificationを表示するのにchannnelが必要
            NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
            NotificationChannel channel = new NotificationChannel(
                    CHANNEL_XXX_SERVICE,    // チャンネルID
                    "Xxxサービス",           // チャンネル名
                    NotificationManager.IMPORTANCE_DEFAULT  // 重要度
            );
            channel.setLockscreenVisibility(Notification.VISIBILITY_PUBLIC);    // ロック画面での表示
            manager.createNotificationChannel(channel);
        }
        notificationReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                // 通知をタップされたらService終了
                if (TextUtils.equals(intent.getAction(), ACTION_CLICK_SERVICE_NOTIFY)) {
                    XxxService.this.stopSelf();
                }
            }
        };
        // Notificationタップを検知するReceiver登録
        IntentFilter notificationFilter = new IntentFilter();
        notificationFilter.addAction(ACTION_CLICK_SERVICE_NOTIFY);
        registerReceiver(notificationReceiver, notificationFilter);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        super.onStartCommand(intent, flags, startId);

        // サービス開始をブロードキャスト
        final Intent startIntent = new Intent();
        startIntent.setAction(ACTION_START_XXX_SERVICE);
        broadcastManager.sendBroadcast(startIntent);

        // Notificationを定義
        Notification.Builder builder = new Notification.Builder(this)
                .setContentTitle("XxxService")
                .setContentText("タップして終了します。")
                .setSmallIcon(R.drawable.icon)
                .setContentIntent(
                        // 通知タップ時のPendingIntent
                        PendingIntent.getBroadcast(
                                getApplicationContext(),
                                0 ,
                                new Intent(ACTION_CLICK_SERVICE_NOTIFY), 0)
                );
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            builder.setChannelId(CHANNEL_XXX_SERVICE);
        }
        Notification notification = builder.build();
        startForeground(1, notification);

        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        unregisterReceiver(notificationReceiver);

        // サービス停止をブロードキャスト
        final Intent intent = new Intent();
        intent.setAction(ACTION_STOP_XXX_SERVICE);
        broadcastManager.sendBroadcast(intent);

        stopForeground(true);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

Activityでは、ブロードキャストされたServiceの起動、停止に合わせてbind、unbindを行います。

public class XxxActivity extends AppCompatActivity {

    // サービスをBindしたか?
    private boolean isBound;
    // 取得したService
    private XxxService xxxService;
    // ブロードキャストマネージャ
    private LocalBroadcastManager broadcastReceiverManager;
    // レシーバ
    private BroadcastReceiver serviceStatusReceiver;

    private ServiceConnection xxxServiceConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className, IBinder service) {
            // IBinderからServiceを取得
            xxxService = ((XxxService.ServiceLocalBinder)service).getService();
            // XxxServiceを取得した時の何かの処理
            // to do something. 
        }

        public void onServiceDisconnected(ComponentName className) {
            // サービスとの切断
            xxxService = null;
        }
    };

    private void doBindService() {
        if (!isBound) {
            //サービスと接続
            bindService(new Intent(this, XxxService.class), xxxServiceConnection, 0); // ここの0を BIND_AUTO_CREATE にしているとServiceが終了できない。
            isBound = true;
        }
    }

    private void doUnbindService() {
        if (isBound) {
            // コネクションの解除
            unbindService(xxxServiceConnection);
            isBound = false;
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // LocalBroadcastManagerのインスタンスを取得
        broadcastReceiverManager = LocalBroadcastManager.getInstance(getApplicationContext());
        // レシーバをインスタンス化
        serviceStatusReceiver =  new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                String action = intent.getAction();
                switch (action) {
                    case XxxService.ACTION_START_XXX_SERVICE:
                        // サービスをバインド
                        doBindService();
                        break;
                    case XxxService.ACTION_STOP_XXX_SERVICE:
                        doUnbindService();
                        break;
                }
            }
        };
        final IntentFilter filter = new IntentFilter();
        filter.addAction(XxxService.ACTION_START_XXX_SERVICE);
        filter.addAction(XxxService.ACTION_STOP_XXX_SERVICE);
        broadcastReceiverManager.registerReceiver(serviceStatusReceiver, filter);

        // サービス起動
        XxxService.start(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        broadcastReceiverManager.unregisterReceiver(serviceStatusReceiver);
        doUnbindService();
    }

    private xxx() {
        if (xxxService != null) {
            // xxxServiceの何かの処理を実行
            // to do something.
        }
    }

少し長くなりますがバックグラウンド処理が必要なら、仕様上仕方ないというところでしょうか。
ただ、バックグランド処理を通知するということは、ユーザーにとってはバックグラウンドで何が行われているか分かるので良いことかもしれません。
今回はBLEデバイスとの連携のためにServiceが必要で、そのためにNotificationを通知したら、そこからユーザーが停止出来ないとUX的に微妙と思ったのでServiceの停止まで実装してみました。

AndroidですべてのActivityを終了する

ログアウト処理などですべてのActivityを終了する。

Intent intent = new Intent(getApplicationContext(), LoginActivity.class);
startActivity(intent);
finishAffinity();

他にも下記のようなサンプルを見つけたが、思い通りに動作しない???

Intent intent = new Intent(getApplicationContext(), LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);

Android で android.view.WindowLeaked エラー

Androidで ログイン画面 -> TOP画面 のような遷移をするときに、ログイン画面で認証処理の間に ProgressDialog を表示し認証後にTOP画面を表示したら、
android.view.WindowLeaked: Activity xx.TopActivity has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView
というエラーが発生した。

原因は、画面遷移時にログイン画面を finish() で終わらせていたが、ProgressDialog を消していないために発生していた。

エラー発生バージョン

Intent intent = new Intent(this, TopActivity.class);
startActivity(intent);
finish();

修正版

if (progressDialog != null && progressDialog.isShowing()) {
    progressDialog.dismiss();
    progressDialog = null;
}
Intent intent = new Intent(this, TopActivity.class);
startActivity(intent);
finish();

AndroidアプリでGoogle認証

AndroidアプリからGoogle認証を使う。
ユーザーがボタンを押したらGoogleで認証を行い、Googleからメールアドレスなどを取得する。

OAuth クライアント ID を作成

Google APIs コンソール にアクセスしOAuth クライアント ID を作成する。

実装

build.gradle(モジュール)

dependencies {
    ・・・
    implementation 'com.google.android.gms:play-services-auth:16.0.1'
}

layout.xml

公式から、com.google.android.gms.common.SignInButtonも提供されているが、デザインを合わせたいので独自のボタンを使用する。

    <com.beardedhen.androidbootstrap.BootstrapButton
        android:id="@+id/login_google"
        app:bootstrapText="Google でログイン"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="24dp"
        android:layout_marginRight="24dp"
        android:layout_marginTop="24dp"
        app:bootstrapBrand="primary"
        app:bootstrapSize="lg"
        app:buttonMode="regular"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/xxxx"
        app:showOutline="false" />

Activity

public class XxxxActivity extends AppCompatActivity implements GoogleApiClient.OnConnectionFailedListener {
    ・・・
    static final int RC_SIGN_IN_GOOGLE = 999;
    BootstrapButton btnLoginGoogle;
    GoogleApiClient googleApiClient;
    ・・・
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ・・・
        GoogleSignInOptions gso = new GoogleSignInOptions
                .Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
                .requestEmail()
                .build();
        googleApiClient = new GoogleApiClient.Builder(this)
                .enableAutoManage(this , this)
                .addApi(Auth.GOOGLE_SIGN_IN_API, gso)
                .build();
        btnLoginGoogle = findViewById(R.id.login_google);
        btnLoginGoogle.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent signInIntent = Auth.GoogleSignInApi.getSignInIntent(googleApiClient);
                startActivityForResult(signInIntent, RC_SIGN_IN_GOOGLE);
            }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == RC_SIGN_IN_GOOGLE) {
            GoogleSignInResult result = Auth.GoogleSignInApi.getSignInResultFromIntent(data);
            if (result.isSuccess()) {
                GoogleSignInAccount googleSignInAccount = result.getSignInAccount();
                doSomesthing(
                        googleSignInAccount.getId(),
                        googleSignInAccount.getEmail(),
                        googleSignInAccount.getDisplayName());
            }
        }
    }

    @Override
    public void onConnectionFailed(@NonNull ConnectionResult connectionResult) {
        // to do
    }

AndroidアプリでFacebook認証

AndroidアプリからFacebook認証を使う。
ユーザーがボタンを押したらFacebookで認証を行い、Facebookからメールアドレスなどを取得する。
基本は公式サイトの通りでOK。
Facebook 公式

実装箇所は下記の通り。
その他、Facebookの開発者ページでアプリ作成、プラットフォーム追加、パッケージ名の登録など必要です。

build.gradle(プロジェクト)

buildscript {
    ・・・
    repositories {
        ・・・
        jcenter()
    }

build.gradle(モジュール)

dependencies {
    ・・・
    implementation 'com.facebook.android:facebook-login:[4,5)'
}

strings.xml

    <string name="facebook_app_id">9999999999999</string>
    <string name="fb_login_protocol_scheme">fb9999999999999</string>

AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    ・・・
    <uses-permission android:name="android.permission.INTERNET" />
    ・・・
    <application>
        ・・・
        <!-- Facebook 認証 -->
        <meta-data android:name="com.facebook.sdk.ApplicationId" android:value="@string/facebook_app_id"/>
        <activity android:name="com.facebook.FacebookActivity"
            android:configChanges="keyboard|keyboardHidden|screenLayout|screenSize|orientation"
            android:label="@string/appName" />
        <activity android:name="com.facebook.CustomTabActivity" android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="@string/fb_login_protocol_scheme" />
            </intent-filter>
        </activity>
        ・・・

layout.xml

公式サイトでは、com.facebook.login.widget.LoginButtonを使用しているが、デザインを合わせたいので独自のボタンを使用する。

    <com.beardedhen.androidbootstrap.BootstrapButton
        android:id="@+id/login_facebook"
        app:bootstrapText="Facebook でログインする"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="24dp"
        android:layout_marginRight="24dp"
        android:layout_marginTop="24dp"
        app:bootstrapBrand="primary"
        app:bootstrapSize="lg"
        app:buttonMode="regular"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@id/xxxx"
        app:showOutline="false" />

Activity

public class XxxxActivity extends AppCompatActivity {
    ・・・
    BootstrapButton btnLoginFacebook;
    CallbackManager callbackManager;
    ・・・
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ・・・
        btnLoginFacebook = findViewById(R.id.login_facebook);
        btnLoginFacebook.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                LoginManager.getInstance().logInWithReadPermissions(this, Arrays.asList("public_profile", "email"));
            }
        });
        callbackManager = CallbackManager.Factory.create();
        LoginManager.getInstance().registerCallback(callbackManager,
                new FacebookCallback<LoginResult>() {
                    @Override
                    public void onSuccess(LoginResult loginResult) {
                        GraphRequest request = GraphRequest.newMeRequest(
                                loginResult.getAccessToken(),
                                new GraphRequest.GraphJSONObjectCallback() {
                                    @Override
                                    public void onCompleted(JSONObject object, GraphResponse response) {
                                        if (response.getError() != null || !object.has("email")) {
                                            // error
                                            return;
                                        }
                                        try {
                                            doSomesthing(
                                                    object.getString("id"),
                                                    object.getString("email"),
                                                    object.getString("name"));
                                        } catch (JSONException e) {
                                            // to do
                                        }
                                    }
                                });
                        Bundle parameters = new Bundle();
                        parameters.putString("fields", "id, name, email, gender");
                        request.setParameters(parameters);
                        request.executeAsync();
                    }

                    @Override
                    public void onCancel() {
                        // to do
                    }

                    @Override
                    public void onError(FacebookException exception) {
                        // to do
                    }
        });
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        callbackManager.onActivityResult(requestCode, resultCode, data);
        super.onActivityResult(requestCode, resultCode, data);
    }

Android でリソースのテキスト取得でリソースIDではなく、キー(文字列)指定で取得

Android で strings.xml に定義したテキストを取得するなら、通常以下のような感じになります。

getString(R.string.msg_xxxx);

 
この msg_xxxx を文字列として指定して動的に取得したいときは以下のような感じです。

    public static String getByName(Context context, String key) {
        int strId = context.getResources().getIdentifier(key, "string", context.getPackageName());
        if (strId <= 0) {
            return "";
        }
        return res.getString(strId);
    }

Activity で呼ぶなら以下のようになります。

    Xxxx.getByName(this, "msg_xxxx");

Spring Boot で定期的にバッチ処理を実行する

定期的に一時ファイルを削除するとか、データを更新するとか、バッチ処理を実行する。

スケジューリングを有効にする。

@SpringBootApplication
@EnableScheduling
public class Application {

実際に実行したい処理は下記のような感じ。

@Component
public class XxxxTasks {

    @Scheduled(cron = "0 * * * * *")
    public void xxxxTask() {

これは毎分0秒に実行する例です。
このように cron で書く以外にも、
前の処理から5秒遅延させる
@Scheduled(fixedDelay=5000)
とか、5秒間隔で実行させるとか、
@Scheduled(fixedRate=5000)
初回の待機時間を設定するとか、
@Scheduled(initialDelay=1000, fixedRate=5000)
できるようです。

スケジューリングのデフォルトスレッドプールサイズが1のため、複数のタスクを同時実行させる場合は、スレッドプールのサイズを変更する必要があるようです。

Spring Boot でCORSを制御

Spring Boot でAPIなどを作っていて、他サイトからアクセスさせると、
Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
が発生する。

これを回避させるためにCORSを制御する。

@CrossOrigin(origins = "*", methods = {RequestMethod.GET, RequestMethod.POST, RequestMethod.PUT, RequestMethod.DELETE})
public class XxxxxxController {

@CrossOrigin アノテーションで許可したいorigin、メソッドなどを設定する。