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);


Monaca + Vue.js + Firebase で匿名認証

スマホアプリ開発でiOS、Androidの両方を行ってみて思うのが、やっぱり2つ開発はシンドイと思います。
で、考えるのはハイブリッド開発でしょうか?
Web、Xamarin、React nativeなどいくつか選択肢がありますが、まずはMonacaというHTML5ベースのプラットフォームを試します。

今回は試しにFirebaseと連携し匿名認証を行います。
ユーザーをこちらから特定する必要はないですが、ユーザーの属性情報だけ欲しいというような使い方です。
試して思ったのですが、FirebaseではSNSアカウント、メールアドレスなどで認証するのも簡単そうです。

Monacaの開発環境はクラウド上ではなくローカルで行いました。
MonacaでVue.jsのテンプレートから作成しています。
また、Firebaseで匿名認証を許可する設定まで完了してる前提でプログラムのみ紹介。

src/public/index.html.ejs

まずはFirebaseの設定です。最初 www/index.html に書いたのですが、何かのタイミングでファイルが戻ってしまう?現象があったので、index.html.ejs に書きました。

<body>
  <div id="app"></div>
  <script src="https://www.gstatic.com/firebasejs/5.5.5/firebase.js"></script>
  <script>
    // Initialize Firebase
    var config = {
      apiKey: "{Firebaseから}",
      authDomain: "{Firebaseから}",
      databaseURL: "{Firebaseから}",
      projectId: "{Firebaseから}",
      storageBucket: "{Firebaseから}",
      messagingSenderId: "{Firebaseから}"
    };
    firebase.initializeApp(config);
  </script>
</body>

ここの設定は Firebase console の “ウェブアプリに Firebase を追加” から取得できます。

src/Home.vue

Monacaのテンプレートに入っているHome.vueを変更しています。
イメージは起動時に匿名認証を行っているか判定し、行っていなければダイアログでユーザー属性を入力させ、Firebaseで匿名認証させます。
認証時の属性情報は Firebase の Cloud Firestore に保存します。
あとで気づいたのですが Cloud Firestore は現在ベータ版で、Webで”Firebase database”で検索すると Realtime Database の情報も出てくるので混乱しました。

<template id="main">
  <v-ons-page>
    <div style="display: table; height: 100%; width: 100%;">
      <div style="display: table-cell;text-align: center;vertical-align: middle;">
        <p>
          Hi!
        </p>
        <p>
          <v-ons-button modifier="quiet" style="background-color: transparent;" @click="signOut">
            登録解除
          </v-ons-button>
        </p>
      </div>
    </div>

    <v-ons-modal style="background-color: #FFFFFF;" :visible="showSignInModal">
      <div style="text-align: center; color: #333333;">
        <p>
          あなたのことを教えてください。
        </p>
        <p>
          <v-ons-select class="signInSelect" v-model="gender">
            <option disabled="disabled" value="">性別</option>
            <option v-for="(item, index) in genders" :key="index" :value="item.value">
              {{ item.text }}
            </option>
          </v-ons-select>
        </p>
        <p>
          <v-ons-select class="signInSelect" v-model="generation">
            <option disabled="disabled" value="">年齢</option>
            <option v-for="(item, index) in generations" :key="index" :value="item.value">
              {{ item.text }}
            </option>
          </v-ons-select>
        </p>
        <v-ons-button modifier="outline" class="signInBtn" @click="signIn">次へ</v-ons-button>
      </div>
    </v-ons-modal>

    <v-ons-modal :visible="showLoading">
      <p style="text-align: center">
        Loading <v-ons-icon icon="fa-spinner" spin></v-ons-icon>
      </p>
    </v-ons-modal>
  </v-ons-page>
</template>

<script>
export default {
  data() {
    return {
      showLoading: false,
      showSignInModal: false,
      gender: "",
      genders: [
        { text: "男性", value: "MALE" },
        { text: "女性", value: "FEMALE" },
        { text: "その他", value: "OTHER" }
      ],
      generation: "",
      generations: [
        { text: "10歳未満", value: "_10" },
        { text: "10-19歳", value: "10_19" },
        { text: "20-29歳", value: "20_29" },
        { text: "30-39歳", value: "30_39" },
        { text: "40-49歳", value: "40_49" },
        { text: "50-59歳", value: "50_59" },
        { text: "60-69歳", value: "60_69" },
        { text: "70-79歳", value: "70_79" },
        { text: "80歳以上", value: "80_" }
      ]
    };
  },
  methods: {
    signIn(event) {
      this.showLoading = true;
      // 匿名ユーザサインイン
      firebase
        .auth()
        .signInAnonymously()
        .catch(function(error) {
          alert(error.message);
        });
    },
    signOut(event) {
      firebase.auth().signOut();
    }
  created() {
    var self = this;
    firebase.auth().onAuthStateChanged(function(user) {
      if (user) {
        const firestore = firebase.firestore();
        firestore.settings({ timestampsInSnapshots: true });
        firestore
          .collection("users")
          .doc(user.uid)
          .set({
            gender: self.gender,
            generation: self.generation
          })
          .then(function() {
            self.showLoading = false;
            self.showSignInModal = false;
          })
          .catch(function(error) {
            console.error("Error writing document: ", error);
            alert("登録に失敗しました");
          });
      } else {
        self.showSignInModal = true;
      }
    });
  }
};
</script>

こんな感じでユーザー情報がFirebase の Authentication と Database(Cloud Firestore) に登録されます。

user.uid をキーにアプリデータをFirebaseのDatabaseに保存すれば、アンケートみたいなものは簡単に作れそうです。
もちろん要件によってはiOS、Androidを別々に開発すべきですが、要件を満たせるならハイブリッドで開発するのは十分にアリだと思います。
ただ、バックグラウンド処理やBLE周りとかどうなんでしょう?

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 で WebView を使用している時に Rejecting re-init on previously-failed class java.lang.Class: java.lang.NoClassDefFoundError: Failed resolution of: Landroid/webkit/TracingController;

Android 8 でWebView周りで下記の例外が発生。
Rejecting re-init on previously-failed class java.lang.Class: java.lang.NoClassDefFoundError: Failed resolution of: Landroid/webkit/TracingController;

ログに出力されているが、アプリ自体は落ちない。
今の所は無視するしかないのかな?
コチラに似たよう事象が。

Android で java.lang.ClassNotFoundException: Didn’t find class “android.arch.lifecycle.ProcessLifecycleOwnerInitializer”

Androidで難読化していて実行時に、
java.lang.ClassNotFoundException: Didn’t find class “android.arch.lifecycle.ProcessLifecycleOwnerInitializer”
が発生した。

proguard-rules.proに下記追記で解決した。

-keep class android.arch.lifecycle.** {*;}

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でSNS認証を行う際に開発者サイトにキーハッシュを登録する

AndroidでGoogle、Facebook認証を行う際に、それぞれの開発者サイトにキーハッシュを登録する必要がある。
それぞれの確認方法は下記の通り。

Google

開発者向けコンソール

キーファイルを指定して下記のコマンドで生成

keytool -exportcert -alias {エイリアス} -keystore {キーファイル} -list -v

Facebook

開発者向けコンソール

キーファイルを指定して下記のコマンドで生成

keytool -exportcert -alias {エイリアス} -keystore {キーファイル} | openssl sha1 -binary | openssl base64

Androidの実機検証でインストール時に “Failed to finalize session: INSTALL_FAILED_INVALID_APK” エラー

Android Studio から実機にアプリをインストールしようとして下記のようなエラーが発生。

直前までは動作していたが、”signatures are inconsistent”とあり、
debug用の署名ファイルを共用するために変更していたので、それが原因と思われる。
ただ、”OK”を選択しても、アプリはアンインストールされたが引き続きエラーが発生する状況でした。

解決方法は、プロジェクトのクリーンと、リビルドを行ったところ解決しました。

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
    }