20210112のAndroidに関する記事は12件です。

非 SDK インターフェースの制限を確認する veridex ツールについて

最初に

Android のプロジェクトを targetSdkVersion 29 にアップデートした際に、
公式に記載の 非 SDK インターフェースの制限 に引っかかるかどうかを確認しました。

※これに引っかかると、特定環境での実行時に例外が発生することになります。

公開SDKインターフェイスとは?

Android フレームワークのパッケージ インデックスに記述されているインターフェースのことです。

今回の議題である「非 SDK インターフェース」とは インターフェイス化されている詳細を実装するもの です。

非SDK インターフェイスには、
利用するのは良くないブロックリスト、使っても問題ないグレイリストなどが定義されています。

用語 意味
ブロックリスト アプリがインターフェースのいずれかにアクセスしようとすると、システムによってエラーがスローされます。
グレイリスト いまは問題ないが、そのうちブロックリストになるかも
条件付きブロックリスト Android 9(API レベル 28)以降、アプリの対象 API レベルごとに制限される非 SDK インターフェース。特定のAPI レベルまではセーフで、それ以降はアウト

発生する例外

リフレクションや Dalvik によるフィールド・メソッド参照を行うと下記例外が発生します。
image.png
>>公式より引用

確認方法

今回は veridex ツールを使用したテスト で確認することにしました。
このツールを利用すると、サードパーティ製ライブラリのリンクも踏まえてチェックすることができます。

※環境構築は公式を参考にしました。私は WSL の Ubuntsu で実施しました。
https://developer.android.com/distribute/best-practices/develop/restrictions-non-sdk-interfaces?hl=ja#veridex-windows

コマンドは下記で実施します。

./appcompat.sh --dex-file=【apkファイル】

実施結果

個人アプリで試したところ、、
Android 8 まで OK なのが2個、Android 9 まで OK なのが5個という結果になりました。

#34: Reflection greylist-max-p Landroid/view/inputmethod/InputMethodManager;->mH use(s):
       Landroidx/activity/ImmLeaksCleaner;->initializeReflectiveFields()V
--
#36: Reflection greylist-max-p Landroid/widget/AutoCompleteTextView;->doAfterTextChanged use(s):
       Landroidx/appcompat/widget/SearchView$PreQAutoCompleteTextViewReflector;-><init>()V

#37: Reflection greylist-max-p Landroid/widget/AutoCompleteTextView;->doBeforeTextChanged use(s):
       Landroidx/appcompat/widget/SearchView$PreQAutoCompleteTextViewReflector;-><init>()V

#38: Reflection greylist-max-p Landroid/widget/AutoCompleteTextView;->ensureImeVisible use(s):
       Landroidx/appcompat/widget/SearchView$PreQAutoCompleteTextViewReflector;-><init>()V

#39: Reflection greylist-max-p Landroid/widget/TextView;->getHorizontallyScrolling use(s):
       Landroidx/appcompat/widget/AppCompatTextViewAutoSizeHelper$Impl;->isHorizontallyScrollable(Landroid/widget/TextView;)Z
--
#45: Reflection greylist-max-o Lcom/android/internal/view/menu/MenuBuilder;->removeItemAt use(s):
       Landroidx/core/widget/TextViewCompat$OreoCallback;->recomputeProcessTextMenuItems(Landroid/view/Menu;)V
--
#47: Reflection greylist-max-o Ljava/nio/file/Files;->copy use(s):
       Lorg/assertj/core/internal/bytebuddy/build/Plugin$Engine$Target$ForFolder$Dispatcher$CreationAction;->run()Lorg/assertj/core/internal/bytebuddy/build/Plugin$Engine$Target$ForFolder$Dispatcher;
--
51 hidden API(s) used: 6 linked against, 45 through reflection
        44 in greylist
        0 in blacklist
        2 in greylist-max-o
        5 in greylist-max-p
        0 in greylist-max-q

こちらの見方ですが、、例えば37番目の例で解説すると、

#37: Reflection greylist-max-p Landroid/widget/AutoCompleteTextView;->doBeforeTextChanged use(s):
   Landroidx/appcompat/widget/SearchView$PreQAutoCompleteTextViewReflector;-><init>()V

(解説)
・androidx の SearchView クラス内で AutoCompleteTextView.doBeforeTextChanged をリフレクションで呼び出している
・Anroid 9(P)まではブロックされないが Android10 以降はブロックされる

ということになります。実際にそのようにソースコードがなっているかを確認したところ、
image.png
たしかに SearchView 内のクラスで、リフレクションでコールされています。

▼まとめ
targetSdkVersion 29 のプロジェクトで SearchView を利用し、
そのクラスの forceSuggestionQuery() が呼び出されると AutoCompleteTextViewReflector.doBeforeTextChanged() が呼ばれ
image.png
Android10 以降で NoSuchMethodException が発生するってことですね。

ただその場合でも Exception で例外をキャッチをしているので、アプリそのものはクラッシュはしない実装になってます。

おまけ

Android Studio のデバッガから SearchView を開いたところ、
API29 以降は doBeforeTextChanged() をリフレクションで呼び出されないように対策されていました。
image.png
この辺りは androix のビルドバージョンによって実装が異なっているのだと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】背景色を透過する方法(備忘録)

プログラミング勉強日記

2021年1月12日
Android Studioで開発をしているときに、背景の色を透過することにてこずったのでその方法を備忘録として記録する。

背景色を透過する方法

CSSではtransparentやrgbaで指定する。Androidでのレイアウトでの背景色透過の方法をまとめる。

1. 8桁の色指定で設定する方法

 8桁での色指定では上位2桁が不透明度で、最初の6桁がRGBの指定である。上位2桁の不透明度は、00(透明)~FF(不透明)の相対値で指定する。
 

android:background="#00000000"

2. Viewのalpha属性で指定する方法

 alpha属性に少数で指定する。Alpha値は0.00(透明)~1.00(不透明)の範囲で相対的に設定できる。

android:alpha="0.25"
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidで、タイトル付きの日付選択スピナーを作成する

意外に記事がなかったのでメモ

以下のようにダイアログをカスタムし、

package com.example.buttonsandbox;

import android.app.DatePickerDialog;
import android.content.Context;
import android.widget.DatePicker;

class MyDatePickerDialog extends DatePickerDialog {
    public CharSequence title;

    public MyDatePickerDialog(Context context, int style, String title, OnDateSetListener callBack, int year, int monthOfYear, int dayOfMonth) {
        super(context, style, callBack, year, monthOfYear, dayOfMonth);
        this.title = title;
        setTitle(title);
    }

    @Override
    public void onDateChanged(DatePicker view, int year, int month, int day) {
        super.onDateChanged(view, year, month, day);
        setTitle(title);
    }
}

スピナーのスタイルを与える。

// styles.xml
// SpinnerDatePickerStyle
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="SpinnerDatePickerStyle" parent="android:Theme.Material.Dialog">
        <item name="android:datePickerStyle">@style/SpinnerDatePicker</item>
    </style>

    <style name="SpinnerDatePicker" parent="android:Widget.Material.DatePicker">
        <item name="android:datePickerMode">spinner</item>
    </style>
</resources>

そして呼び出す。

new MyDatePickerDialog(this, R.style.SpinnerDatePickerStyle, "Set birthday", dateSelectedListener, 1990, 0, 1).show();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android アプリに定期購入を実装する

概要

Android アプリに Google Play 課金システム定期購入 を実装するときのポイントを簡単にまとめておきます。

設定

Google Cloud Platform - Pub/Sub

リアルタイム デベロッパー通知を構成する
基本的にはこちらの内容です。

定期購入されたときの通知を受け取るトピックを作成していきます。

トピック作成

  • GCP の Pub/Sub から「トピックを作成」

  • トピック ID を入力して「トピックを作成」

サブスクリプション作成

  • 作成したトピックに「サブスクリプションを作成」

  • サブスクリプション ID を入力して CREATE

トピックに権限付与

  • 作成したトピックを選択して「メンバーを追加」

  • メンバー追加して保存

新しいメンバーに google-play-developer-notifications@system.gserviceaccount.com を入力、
ロールに Pub/Sub パブリッシャー を選択

以上で Google Cloud Platform - Pub/Sub の設定は終了です。

Google Play Console

基本的に アプリでリアルタイム デベロッパー通知を有効にするアイテムを作成して構成する の内容になります。

収益化のセットアップ

  • 先ほど作成したトピックのトピック名を入力

入力したら 「テスト通知を送信」 してエラーが表示されないことを確認してください。
もしエラーが表示されたらトピック名が間違えてるか権限付与されていない可能性があります。

定期購入を作成

以上で Google Play Console の設定は終了です。

実装

ここからは具体的な実装方法になります。

  • アプリから定期購入処理を呼び出す処理

と定期購入処理が完了したあとに Google から通知される

  • リアルタイムデベロッパー通知を受け取る処理

の 2 つを実装する必要があります。

アプリから定期購入処理を呼び出す処理

主に Google Play Billing Library をアプリに統合する の内容になります。

BillingClient の初期化など

private lateinit var billingClient: BillingClient
private var autosubscriptionSkuDetails: SkuDetails? = null

private fun billingSetup() {
    // 購入処理のコールバック
    val purchaseUpdateListener = PurchasesUpdatedListener { billingResult, purchases ->
        Log.d("BILLING_TEST_TAG", "billingResult.responseCode: ${billingResult.responseCode}")
    }

    // BillingClient の初期化
    billingClient = BillingClient.newBuilder(this)
        .setListener(purchaseUpdateListener)
        .enablePendingPurchases()
        .build()

    // セットアップ処理のコールバック
    val billingClientStateListener: BillingClientStateListener = object : BillingClientStateListener {
        override fun onBillingSetupFinished(billingResult: BillingResult) {
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                // 商品情報取得
                val skuList = listOf(
                    // 「定期購入を作成」で入力した定期購入のアイテム ID
                    "subscription.product.id"
                )
                val params = SkuDetailsParams.newBuilder().setSkusList(skuList).setType(BillingClient.SkuType.SUBS).build()

                billingClient.querySkuDetailsAsync(params) { querySkuBillingResult, skuDetailsResult ->
                    if (querySkuBillingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                        skuDetailsResult ?: return@querySkuDetailsAsync
                        for (skuDetails: SkuDetails in skuDetailsResult) {
                            if (skuDetails.sku == "subscription.product.id") {
                                autosubscriptionSkuDetails = skuDetails
                                return@querySkuDetailsAsync
                            }
                        }
                    } else {
                        Log.d(
                            BILLING_TEST_TAG,
                            "querySkuDetailsAsync error. responseCode:${querySkuBillingResult.responseCode}"
                        )
                    }
                }
            } else {
                Log.d(BILLING_TEST_TAG, "Setup error. responseCode:${billingResult.responseCode}")
            }
        }

        override fun onBillingServiceDisconnected() {
            Log.d(
                "BILLING_TEST_TAG",
                "Try to restart the connection on the next request to Google Play by calling the startConnection() method."
            )
        }
    }

    // 接続開始
    billingClient.startConnection(billingClientStateListener)
}

購入処理の呼び出し

val skuDetails: SkuDetails? = autosubscriptionSkuDetails
if (skuDetails != null) {
    val billingFlowParams = BillingFlowParams.newBuilder()
        .setSkuDetails(skuDetails)
        .build()

    // 購入フローを開始
    billingClient.launchBillingFlow(this, billingFlowParams)
}

ここまで実装すれば以下のような購入画面が表示できると思います。

ちなみにテスト購入できるようにするためにいくつか設定が必要なので Google Play Billing Library 統合をテストする を参照してください。

以上でアプリから定期購入処理を呼び出す処理は終了です。

リアルタイムデベロッパー通知を受け取る処理

定期購入を販売する
こちらに記載されているライフサイクルを処理します。

定期購入されると 「収益化のセットアップ」 で設定したトピックに対してメッセージが送信されてくるので
それをトリガーに GoogleCloudPlatform の Cloud Functions で処理します。

functions/src/googlePlayRtdn/index.ts
import { Message } from 'firebase-functions/lib/providers/pubsub';
import { EventContext } from 'firebase-functions';

import * as JsonPrune from 'json-prune';
import * as Functions from 'firebase-functions';

class GooglePlayRtdnEndpoint {
    public static topicName = 'test';

    public async run(resolve: any, reject: any, message: Message, context?: EventContext) {
        const data = message.json;
        if (!data.subscriptionNotification && !data.oneTimeProductNotification && !data.testNotification) {
            return reject({ data, error: new Error(`PubSub message was invalid JSON: ${JsonPrune(data)}`) });
        }

        console.log(JsonPrune(data));
        return resolve();
    }
}

const googlePlayRtdnEndpoint = new GooglePlayRtdnEndpoint();

export const googlePlayRtdn = Functions.region('asia-northeast1')
    .pubsub.topic(GooglePlayRtdnEndpoint.topicName)
    .onPublish((message: Message, context?: EventContext) => {
        return new Promise((resolve, reject) => googlePlayRtdnEndpoint.run(resolve, reject, message, context)).catch(error => {
            console.error(error);
        });
    });

こちらのソースコードを GoogleCloudPlatform にデプロイします。

GoogleCloudPlatform の Cloud Functions に googlePlayRtdn という関数が追加されていることを確認してください。

以上でリアルタイムデベロッパー通知を受け取る処理は終了です。

動作確認

アプリからテスト購入を実行します。
GoogleCloudPlatform の Cloud Functions googlePlayRtdn の「ログ」に

{
    "version": "1.0",
    "packageName": "******",
    "eventTimeMillis": "1610083333446",
    "subscriptionNotification": {
        "version": "1.0",
        "notificationType": 4,
        "purchaseToken": "************************************",
        "subscriptionId": "subscription.product.id"
    }
}

こんな感じの json が出力されれば成功です。

あとは notificationType によってどういう処理を実行させるかを判断します。
notificationType の種類などは リアルタイム デベロッパー通知のリファレンス ガイド を参照してください。

有料会員かどうか、は Firebase の Custom Claims を使用するのが良いと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ConstraintLayout でViewを移動する方法

ConstraintLayout でViewを移動するにはConstraintSetを使用し、制約を編集する必要があります。
このサンプルではUPボタンを押せば上へDOWNボタンを押せば下へTextViewが移動します。

コード

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/root"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:text="Hello World!"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnUp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="up"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/btnDwon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="down"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/btnUp" />

</android.support.constraint.ConstraintLayout>
package com.example.test;

import android.support.constraint.ConstraintLayout;
import android.support.constraint.ConstraintSet;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private TextView mTextView;
    private Button mBtnUp;
    private Button mBtnDown;

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

        mTextView = findViewById(R.id.textView);
        mBtnUp = findViewById(R.id.btnUp);
        mBtnDown = findViewById(R.id.btnDwon);

        mBtnUp.setOnClickListener(this);
        mBtnDown.setOnClickListener(this);
    }

    @Override
    public void onClick(View view) {
        int newMergin;
        // 現在のLayoutParamを取得
        ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) mTextView.getLayoutParams();
        if (view.equals(mBtnUp)) {
            newMergin = lp.topMargin - 32; // Topマージンを-32する
        } else if (view.equals(mBtnDown)) {
            newMergin = lp.topMargin + 32; // Topマージンを+32する
        } else {
            return;
        }

        // ConstraintLayoutを取得
        ConstraintLayout cl = findViewById(R.id.root);
        ConstraintSet constraintSet = new ConstraintSet();
        // 既存のConstraintLayoutの設定をクローンする
        constraintSet.clone(cl);
        // 制約を設定する
        constraintSet.connect(R.id.textView, ConstraintSet.TOP,
                ConstraintSet.PARENT_ID, ConstraintSet.TOP, newMergin);
        // 設定を反映する
        constraintSet.applyTo(cl);
    }
}

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android Studioインストール後にコマンドラインでビルドしようとするとJDKのインストールを求められる問題の対処法

macでAndroid Studioの環境構築をした際に若干詰まった部分をメモしておきます。

問題

macにAndroid Studioをインストールしたあと、Android Studio外で以下のようなコマンドによりビルドしようとした際に、以下のエラーメッセージとスクショのようなダイアログが表示され、JDKのインストールを求められました。

ビルドコマンドとエラーメッセージ
$ ./gradlew assembleDebug
No Java runtime present, requesting install.

JDKをインストールを求めるダイアログ
スクリーンショット 2021-01-12 15.23.01.png

環境

  • macOS Catalina 10.15.17
    • Intel CPU
  • Android Studio 4.1.1

解決策

Android StudioにはすでにJDKを内包しているため、パスを通してやれば良いです。

具体的なパスは、Android Studioのメニュー > File > Project Structureの画面で、左ペインのSDK Locationを選択し、JDK Locationを見ることで確認できます。
JDKパス確認画面.png

ちなみに、ここの項目のヒントに、「外部プロセスで利用したい場合はJAVA_HOMEにこのパスを追加する」旨が記載されていました。
JDKパスヒント.png

確認したパスを~/.zprofileファイルに以下のように書き込んだのちターミナルを再起動すると、ビルドコマンドが利用できるようになります。

.zprofile
export JAVA_HOME="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home"

参照

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidのバックグラウンドでGmail(Android Studio Javamail)送信する方法

AndroidのバックグラウンドでGmail送信する方法

AndroidでバックグラウンドでGmailを送信する機能を利用してみたいと思い、調べてみましたので備忘録としてまとめます。

利用する機能

Android Studio Javamail

実装手順

STEP1 マニフェストでインターネットを許可する

インターネットアクセスのパーミッションを追加します。

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

  //追加
  <uses-permission android:name="android.permission.INTERNET"/>

  <application>
  //・・・割愛
  </application>

</manifest>

STEP2 bundle.gradleに追記

以下2行をbundle.gradleのdependenciesに追記します

build.gradle
dependencies {
    //・・・割愛
    implementation 'com.sun.mail:android-mail:1.6.5'
    implementation 'com.sun.mail:android-activation:1.6.5'
}

STEP3 非同期処理(AsyncTask)のメソッドを作成

非同期処理については別でまとめてありますので、そちらをご参照ください。

Androidの非同期処理「AsyncTask」の基本の基
※該当コードだけ抜粋してきました

class asyncTask extends android.os.AsyncTask{
    //※この引数はObject... ですが通常通り、型・個数を指定することもできます
    @Override
    protected Object doInBackground(Object... obj){
      //ここに処理を記入


   }

STEP4 非同期処理のメソッド内でメール送信関連の情報をjava propatyファイルに登録

java propatyファイルにメール送信に関する情報を登録します。
具体的には「グーグルアカウント名」「パスワード」「送信メールのタイトル」「送信メールの内容」です。

このコードでは引数objからこれらの情報を取得し、その値をjava propatyファイルにセットする形式となっております。

※java propatyファイル

キーと値が対になったデータを保存しているファイル

class asyncTask extends android.os.AsyncTask{


      protected String account;
      protected String password;
      protected String title;
      protected String text;

      @Override
      protected Object doInBackground(Object... obj){
            account=(String)obj[0];
            password=(String)obj[1];
            title=(String)obj[2];
            text=(String)obj[3];

            java.util.Properties properties = new java.util.Properties();
            properties.put("mail.smtp.host", "smtp.gmail.com");
            properties.put("mail.smtp.auth", "true");
            properties.put("mail.smtp.port", "465");
            properties.put("mail.smtp.socketFactory.post", "465");
            properties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");
     }

   }

STEP5 非同期処理のメソッド内でメール送信関連の処理を記載

STEP4で入力したjava propatyファイルを用いてメール送信する処理を記載します

GmailのSMTPを利用します

※SMTP:電子メールを送信するために使用するアプリケーション層 のプロトコル
class asyncTask extends android.os.AsyncTask{


      protected String account;
      protected String password;
      protected String title;
      protected String text;

      @Override
      protected Object doInBackground(Object... obj){
            account=(String)obj[0];
            password=(String)obj[1];
            title=(String)obj[2];
            text=(String)obj[3];

            java.util.Properties properties = new java.util.Properties();
            properties.put("mail.smtp.host", "test.gmail.com");
            properties.put("mail.smtp.auth", "true");
            properties.put("mail.smtp.port", "465");
            properties.put("mail.smtp.socketFactory.post", "465");
            properties.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory");


           //ここから
           final javax.mail.Message msg = new javax.mail.internet.MimeMessage(javax.mail.Session.getDefaultInstance(properties, new javax.mail.Authenticator(){

                @Override
                protected javax.mail.PasswordAuthentication getPasswordAuthentication() {
                    return new javax.mail.PasswordAuthentication(account,password);
                }
            }));

            try {
                msg.setFrom(new javax.mail.internet.InternetAddress(account + "@gmail.com"));
                //自分自身にメールを送信
                msg.setRecipients(javax.mail.Message.RecipientType.TO, javax.mail.internet.InternetAddress.parse(account + "@gmail.com"));
                msg.setSubject(title);
                msg.setText(text);

                javax.mail.Transport.send(msg);

            } catch (Exception e) {
                return (Object)e.toString();
            }

            return (Object)"送信が完了しました";

            //ここまで

     }

        //メール送信自体とは関係なし
     //非同期処理が完了後の処理を記載するメソッド。
     //送信後、何かしらの処理を行いたければこちらを利用※詳細は非同期処理でまとめた記事をご覧ください
        @Override
        protected void onPostExecute(Object obj) {
            //画面にメッセージを表示する
            Toast.makeText(MainActivity.this,(String)obj,Toast.LENGTH_LONG).show();
        }

   }

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自動入力サービス実装メモ

上司から急に「Androidでこれできない?」って言われた人(a.k.a自分)用メモ

自動入力サービスってなに

https://developer.android.com/guide/topics/text/autofill-services?hl=ja
フォームに入力する手間を省くアプリ
多分このメモよりも上記のリファレンスを読んだほうが良い

自動入力サービスというだけあって
サービスを実装して、自前で自動入力の処理を実装する必要がありそう

サービスを実装するには

AndroidManifest.xmlの中に以下の属性を定義する

  • android:name
    • サービスを実装するアプリのクラス名。 AutofillServiceを継承していること。
  • android:permission
    • BIND_AUTOFILL_SERVICEパーミッションを宣言。 ユーザが端末の設定で作成した自動入力サービスを有効にできる。
  • <intent-filter>
    • <action>android.service.autofill.AutofillServiceを指定する。
  • <meta-data>
    • (オプション)実装した自動入力サービスの設定をするActivityを指定できる。
AndroidManifest.xml
<service
        android:name=".UserAutofillService"
        android:label="デモ用自動入力サービス"
        android:enabled="true"
        android:exported="true"
        android:permission="android.permission.BIND_AUTOFILL_SERVICE">
    <meta-data
            android:name="android.autofill"
            android:resource="@xml/user_service"/>

    <intent-filter>
        <action android:name="android.service.autofill.AutofillService"/>
    </intent-filter>
</service>

meta-data要素について

ここには自動入力サービスを設定するためのアクティビティを設定できる。
ここでアクティビティを設定しておくと
設定から自動入力サービスを選択した際に、右側に歯車のマークが出て
タップすると該当のアクティビティが開く様になる。

  • android:settingsActivity
    • 自動入力サービスの設定に使用したいアクティビティ
xml/user_service.xml
<autofill-service
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:settingsActivity="com.example.android.SettingsActivity" />

作ったサービスを使う

ユーザが設定する場合は設定 > システム > 言語と入力 > 詳細設定 > 自動入力サービスから設定可能
(Xperia XZ2@Android10 で確認)

アプリから設定させる場合は、ACTION_REQUEST_SET_AUTOFILL_SERVICEインテントを使うと
自動入力設定を変更するリクエストを画面に表示することができる。
(ただし、設定画面を開くだけなのでユーザの操作は必須っぽい)

// 念の為端末に自動入力サービスがあるか確認
getSystemService(AutofillManager::class.java) ?: return
val intent = Intent(Settings.ACTION_REQUEST_SET_AUTOFILL_SERVICE)
// 設定したい自動入力サービスのパッケージ名
intent.data = Uri.parse("package:com.example.myautofillservice")
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT)

パッケージ名が一致する自動入力サービスを、ユーザーが選択した場合はRESULT_OK値が返る

自動入力サービスの実装

ユーザが自動入力するまでの流れ

https://developer.android.com/reference/android/service/autofill/AutofillService?hl=ja#BasicUsage
上記を読んだ感じ、大体以下のような流れで自動入力が走る

  1. ユーザが編集可能なビューにフォーカスする。
  2. ビューがAutofillManager#notifyViewEntered(android.view.View)を呼び出す。
  3. 全てのビューを表すViewStructureが作成される。サービスはこのクラスから表示されているビューにアクセスする。
  4. Androidシステムが自動入力サービスにバインドし、onConnect()を呼び出す。
  5. サービスがonFillRequest(android.service.autofill.FillRequest, android.os.CancellationSignal, android.service.autofill.FillCallback)コールバックでViewStructureを受け取る。
    • ViewStructureFillRequestから取得できる
  6. サービスがFillCallback#onSuccess(FillResponse)を使用して応答する
  7. AndroidシステムがonDisconnected()を呼び出してバインドを解除する
  8. Androidシステムがサービスで作成したオプションを含む自動入力UIを表示する
  9. ユーザがオプションを選択する
  10. ビューに自動入力される

開発者が主に実装する箇所

開発者としてはAutofillServiceクラスを継承したサービスを作成し
onFillRequestメソッドを目的に合わせて実装することで自動入力ができる

ビュー解析

ビュー構造を解析して自動入力するビューを探す。
以下の実装例ではビューに設定されているautofillHintsを確認して
fieldsにヒントとIdを登録している。

fun getAutofillableFields(structure: AssistStructure): Map<String, AutofillId> {
    val fields: MutableMap<String, AutofillId> = mutableMapOf()
    val nodes = structure.windowNodeCount
    for (i in 0 until nodes) {
        val node = structure.getWindowNodeAt(i).rootViewNode
        addAutofillableFields(fields, node)
    }
    return fields
}

private fun addAutofillableFields(
    fields: MutableMap<String, AutofillId>, node: ViewNode
) {
    val hints = node.autofillHints
    if (hints == null) {
        // 子ノードに対しても同様に再帰で調べる
        val childrenSize = node.childCount
        for (i in 0 until childrenSize) {
            addAutofillableFields(fields, node.getChildAt(i))
        }
        return
    }

    // とりあえず最初のヒントだけ確認する
    val hint = hints[0].toLowerCase(Locale.getDefault())
    val id = node.autofillId
    if (id == null) {
        Log.d(TAG, "addAutofillableFields: autofillId == null")
    } else {
        if (!fields.containsKey(hint)) {
            Log.v(TAG, "$id にヒントを設定 '$hint' ")
            fields[hint] = id
        }
    }

    val childrenSize = node.childCount
    for (i in 0 until childrenSize) {
        addAutofillableFields(fields, node.getChildAt(i))
    }
}

自動入力データの取得

自動入力するビューに対応するユーザのデータを探す。
実際はユーザIDやパスワード等を、ビジネスロジックに合わせて取得する処理になると思うので
その時その時でいい感じに実装する。

Datasetを作成する

実際にユーザに選んでもらうデータを作成する。
以下の実装例は、AutofillHintusernamepasswordのどちらかが
設定されている場合に自動入力させたい場合の処理を記載している。

val packageName = applicationContext.packageName
val response = FillResponse.Builder()
val dataset = Dataset.Builder()
// AutofillHintとIdでペアとしている
for ((hint, id) in fields) {
    when {
        hint.contains("username") -> {
            val userName = pref.userName
            val presentation = RemoteViews(packageName, android.R.layout.simple_list_item_1)
            // ユーザへ表示するテキスト(例:ユーザ名、パスワード)
            presentation.setTextViewText(android.R.id.text1, userName)
            // 自動入力したいビューのid, 自動入力する値(例:username、passw0rd), ユーザに表示するビュー
            dataset.setValue(id, AutofillValue.forText(userName), presentation)
        }
        hint.contains("password") -> {
            val userName = pref.passWord
            val presentation = RemoteViews(packageName, android.R.layout.simple_list_item_1)
            presentation.setTextViewText(android.R.id.text1, "password for $userName")
            dataset.setValue(id, AutofillValue.forText(userName), presentation)
        }
        else -> {
            Log.d(TAG, "onFillRequest: hint:$hint id:$id")
        }
    }
}
response.addDataset(dataset.build())

結果の返却

作ったDatasetをresponseに詰めてcallback.onSuccess(response.build())を呼ぶ。
仮に自動入力できない場合でもonSuccessメソッドはnullで良いので呼び出す必要がある。

その他

サービスに適宜バインドして、入力を作り終わったらバインド解除をしている。
なので、常駐サービスの様に常に起動しているわけではない。(状態を持たせたりするのは難しいんじゃないかな?)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Android]keystoreのパスワードが間違っていると出た時

Android開発中にテストしようとDeployGate用にbundletoolを使ってaabから署名付きapksを作ろうとしたときにエラーが出たので、その時の解決策を載せておきます。

問題

bundletool build-apks --bundle=release.aab --output=release.apks \
--ks=/Users/hoge/.android/release-key.keystore \
--ks-pass=pass:パスワード \
--ks-key-alias=エイリアス \
--key-pass=pass:パスワード

[Error] Keystore was tampered with, or password was incorrect

絶対パスワードもエイリアスもあっているのになあと思ったのですが、一応分けて実行してみました。
↓↓↓

解決策

エイリアスを入力、パスワードは聞かれるまで入力せず実行

bundletool build-apks --bundle=release.aab --output=release.apks --ks=/Users/hoge/.android/release-key.keystore --ks-key-alias=エイリアス
パスワードが聞かれる
pass:                        ←ここにパスワード入力

これで解決

おそらく最初のやり方で「\」の前後でいらないスペースとか何かが含まれてしまっていたのでしょう。

パスワードあってるだろおおおおおおお!!!!ってなっても解決しない時は入力を分けて実行してみてください!
困っている誰かに役立てたら幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】Android アプリでスクショや画面録画などを禁止する

背景

アプリによっては、セキュリティやコンテンツ的にスクショや画面録画を禁止したいときがあると思います。
そのとき Android の機能である WindowManager.LayoutParams.FLAG_SECURE を Activity の OnCreate() で呼ぶ必要があります。

しかし、Unity だとどうするのか分からなかったので調べてまとめた記事になります。

FLAG_SECURE を設定するとどうなるか

  • スクショを無効化
  • 画面録画(スクリーンレコーディング)をすると、録画した画面が真っ黒になる
  • タスク画面・タスク一覧で、アプリ画面を真っ白にできる

動作確認環境

  • Unity 2018.4.22f1

※ Unity2018.2以前のバージョンを使っている場合は、以下の手順とは異なるかと思いますので、ご注意ください。

手順

Unity + Android で実現するためには、

  1. UnityPlayerActivity を拡張して新しい Activity を定義する
  2. 新しい Activity が使用されるように AndroidManifest.xml を修正する
  3. 新しい Activity の OnCreate()FLAG_SECURE を設定する

という手順が必要です。

1. UnityPlayerActivity を拡張する

自分が作業中の Unityプロジェクトの Assets/Plugins/Android 配下に新しい Activity を作成します。
例として、 OverrideExample クラスを作成します。

ちなみに、 package name 以外のファイル内容は、Unity公式からそのままコピペしています。
UnityPlayerActivity Java コードの拡張 - Unity マニュアル
package name は、自分が設定した名前に置き換えてください。

OverrideExample.java
package com.DefaultCompany.ExtendUnityPlayerActivity;
import com.unity3d.player.UnityPlayerActivity;
import android.os.Bundle;
import android.util.Log;

public class OverrideExample extends UnityPlayerActivity {

  protected void onCreate(Bundle savedInstanceState) {

    // UnityPlayerActivity.onCreate() を呼び出す
    super.onCreate(savedInstanceState);

    // logcat にデバッグメッセージをプリントする
    Log.d("OverrideActivity", "onCreate called!");
  }

  public void onBackPressed()
  {
    // UnityPlayerActivity.onBackPressed() を呼び出す代わりに、Back ボタンイベントを無視する
    // super.onBackPressed();
  }
}

2. AndroidManifest.xml を修正する

Activity と同様に Assets/Plugins/AndroidAndroidManifest.xml を配置します。
android:name の部分は、1. で作成したクラス名に合わせてください。

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.company.product">
  <application android:icon="@drawable/app_icon" android:label="@string/app_name">
    <activity android:name=".OverrideExample"
             android:label="@string/app_name"
             android:configChanges="fontScale|keyboard|keyboardHidden|locale|mnc|mcc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|uiMode|touchscreen">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
  </application>
</manifest>

この状態で、Androidビルドして問題なくアプリが使用できるか確認してください。
特に、 package name を間違えるとアプリがクラッシュしたりするので、注意してください。

3. FLAG_SECURE を設定する

onCreate()FLAG_SECURE を設定します。

OverrideExample.java
package com.DefaultCompany.ExtendUnityPlayerActivity;
import com.unity3d.player.UnityPlayerActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.WindowManager;

public class OverrideExample extends UnityPlayerActivity {

  protected void onCreate(Bundle savedInstanceState) {

    // UnityPlayerActivity.onCreate() を呼び出す
    super.onCreate(savedInstanceState);

    // logcat にデバッグメッセージをプリントする
    Log.d("OverrideActivity", "onCreate called!");

    //FLAG_SECUREの設定
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
  }

  public void onBackPressed()
  {
    // UnityPlayerActivity.onBackPressed() を呼び出す代わりに、Back ボタンイベントを無視する
    // super.onBackPressed();
  }
}

これで作業は完了です。
Androidビルドして、問題なければスクショや画面録画ができなくなっていると思います。

参考文献

UnityPlayerActivity Java コードの拡張 - Unity マニュアル
Androidアプリでキャプチャーをされたくないときにする方法

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidの非同期処理「AsyncTask」の基本の基

Androidの非同期処理「AsyncTask」について

AsyncTaskをなんとなく利用しておりましたので、改めて基礎をまとめてみました。

※主に自身の毎日の復習・学習の機会創出、アウトプットによる知識の定着を目的としております。
暖かい目で見ていただけますと幸いです。

そもそも非同期処理とは

同期処理: あるタスクを順番に実行すること。
非同期処理:あるタスクが実行をしている際に、他のタスクが別の処理を実行すること。

なぜ非同期処理させる必要があるのか

仮に、もし非同期処理をしなかった場合を考えてみます。
AndroidやSwift、webアプリなどででユーザーが操作している際、例えば何か情報(動画一覧・検索結果一覧など)を取得する際、同期処理だと取得の処理が終わるまではUIなどの他の処理が一切できず画面は止まったままになります。
そうなると、ユーザーからしたらアプリが止まった(バグの)ように感じてしまいます。

非同期処理で実装するとUI・画面を良い感じに見せながら、並行して情報取得(動画一覧・検索結果一覧など)の処理を実施することができます。そうすることによって、ユーザーには今情報を取得中であることを提示でき、誤解を与えずにすみます

よく見ると、YoutubeやSNSなどもロード中は画面上に動くローディング画面が表示される場面がよく見られます。

AsyncTaskとは

Androidの非同期処理にはHandlerやThreadHandlerクラスを使うことができますが、これらのHelper ClassとしてAsyncTaskがあります。

AsyncTaskは他の方法より簡単に非同期を扱うことができますます。

AsyncTaskの主なメソッド

onPreExecute()

doInBackgroundメソッドの実行前にメインスレッドで実行されます。
非同期処理前に何か処理を行いたい時などに利用。

doInBackground()

メインスレッドとは別のスレッドで実行されます。
非同期で処理したい内容を記述します。
※唯一実装が必須

onProgressUpdate()

メインスレッドで実行されます。
途中経過をメインスレッドに返します。
非同期処理の進行状況をプログレスバーで 表示したい時などに使うことができます。

onPostExecute()

doInBackgroundメソッドの実行後にメインスレッドで実行されます。
結果をメインスレッドに返します。
doInBackgroundメソッドの戻り値をこのメソッドの引数として受け取り、その結果を画面に反映させることができます。

※実行順番
1.onPreExecute()
2.doInBackground()
3.onProgressUpdate() ※doInBackground()で、publishProgress()が呼ばれた場合に処理。
4.onPostExecute()

利用シーン

STEP1:AsyncTaskを継承するクラスを作成します

class asyncTask extends android.os.AsyncTask{
    @Override
    protected Object doInBackground(Object... obj){

   }

STEP2: 各メソッド内容を入力(doInBackgroundは必須)

class asyncTask extends android.os.AsyncTask{
    //※この引数はObject... ですが通常通り、型・個数を指定することもできます
    @Override
    protected Object doInBackground(Object... obj){
      //ここに処理を記入
      System.out.plintln("非同期処理の内容をここに記載します")
      //引数を取得することもできます
      String message = (String)obj[0];

   }

STEP3: メインスレッドで、STEP1,2で作成したクラスをインスタンス化し、executeメソッドで呼び出す

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // タスクの生成
        mAsyncTask = new asyncTask(textView);
        //タスクの実行
        mAsyncTask.execute("ここにasyncTaskに渡したい引数");
    }
}

利用時の注意点

doInBackgroundで直接メインスレッドに対しての処理を行うと例外が発生します。

まとめ

・AsyncTaskは非同期処理に利用できる
・AsyncTaskは他の方法より簡易に非同期処理を利用できる
・AsyncTaskを利用の際は、doInBackgroundは必須

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlin Android Extensions から View Binding に置き換える

今更ですが、Kotlin Android Extensions が非推奨になりましたね。
公式としては、View Binding を使うことが推奨されています。

./gradlew buildコマンドを実行すると、以下のような警告文が出力されます。

Warning: The 'kotlin-android-extensions' Gradle plugin is deprecated. Please use this migration guide (https://goo.gle/kotlin-android-extensions-deprecation) to start working with View Binding (https://developer.android.com/topic/libraries/view-binding) and the 'kotlin-parcelize' plugin.

修正方法

例えばapp/build.gradleが以下のように記載されていたとします。

app/build.gradle
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-android-extensions'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

android {
    ...
    androidExtensions {
        experimental = true
    }
    buildFeatures {
        dataBinding = true
    }
}
...

まず、androidExtensionsおよびid 'kotlin-android-extensions'を削除し、buildFeaturesviewBinding = trueを追記します。

app/build.gradle
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

android {
    ...
    buildFeatures {
        dataBinding = true
        viewBinding = true
    }
}
...

そして、ソースコード内で Kotlin Android Extensions を使用している箇所を View Binding に置き換えます。

// Reference to "name" TextView using synthetic properties.
name.text = viewModel.nameString

// Reference to "name" TextView using the binding class instance.
binding.name.text = viewModel.nameString

Parcelable を使用している場合

ソースコード内で@Parcelizeアノテーションを使用している場合は、pluginsid 'kotlin-parcelize'を追記します。

app/build.gradle
plugins {
    id 'com.android.application'
    id 'kotlin-android'
    id 'kotlin-parcelize'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}
...

参考 URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む