- 投稿日:2021-01-12T21:47:23+09:00
非 SDK インターフェースの制限を確認する veridex ツールについて
最初に
Android のプロジェクトを targetSdkVersion 29 にアップデートした際に、
公式に記載の 非 SDK インターフェースの制限 に引っかかるかどうかを確認しました。※これに引っかかると、特定環境での実行時に例外が発生することになります。
公開SDKインターフェイスとは?
Android フレームワークのパッケージ インデックスに記述されているインターフェースのことです。
今回の議題である「非 SDK インターフェース」とは インターフェイス化されている詳細を実装するもの です。
非SDK インターフェイスには、
利用するのは良くないブロックリスト、使っても問題ないグレイリストなどが定義されています。
用語 意味 ブロックリスト アプリがインターフェースのいずれかにアクセスしようとすると、システムによってエラーがスローされます。 グレイリスト いまは問題ないが、そのうちブロックリストになるかも 条件付きブロックリスト Android 9(API レベル 28)以降、アプリの対象 API レベルごとに制限される非 SDK インターフェース。特定のAPI レベルまではセーフで、それ以降はアウト 発生する例外
リフレクションや Dalvik によるフィールド・メソッド参照を行うと下記例外が発生します。
>>公式より引用
確認方法
今回は 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 以降はブロックされるということになります。実際にそのようにソースコードがなっているかを確認したところ、
たしかに SearchView 内のクラスで、リフレクションでコールされています。▼まとめ
targetSdkVersion 29 のプロジェクトで SearchView を利用し、
そのクラスの forceSuggestionQuery() が呼び出されると AutoCompleteTextViewReflector.doBeforeTextChanged() が呼ばれ
Android10 以降で NoSuchMethodException が発生するってことですね。ただその場合でも Exception で例外をキャッチをしているので、アプリそのものはクラッシュはしない実装になってます。
おまけ
Android Studio のデバッガから SearchView を開いたところ、
API29 以降は doBeforeTextChanged() をリフレクションで呼び出されないように対策されていました。
この辺りは androix のビルドバージョンによって実装が異なっているのだと思います。
- 投稿日:2021-01-12T21:15:47+09:00
【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"
- 投稿日:2021-01-12T20:50:07+09:00
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();
- 投稿日:2021-01-12T20:48:55+09:00
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
基本的に アプリでリアルタイム デベロッパー通知を有効にする と アイテムを作成して構成する の内容になります。
収益化のセットアップ
- 先ほど作成したトピックのトピック名を入力
入力したら 「テスト通知を送信」 してエラーが表示されないことを確認してください。
もしエラーが表示されたらトピック名が間違えてるか権限付与されていない可能性があります。定期購入を作成
- アイテム ID などを入力して「保存」(各入力項目は 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.tsimport { 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 FunctionsgooglePlayRtdn
の「ログ」に{ "version": "1.0", "packageName": "******", "eventTimeMillis": "1610083333446", "subscriptionNotification": { "version": "1.0", "notificationType": 4, "purchaseToken": "************************************", "subscriptionId": "subscription.product.id" } }こんな感じの json が出力されれば成功です。
あとは
notificationType
によってどういう処理を実行させるかを判断します。
notificationType
の種類などは リアルタイム デベロッパー通知のリファレンス ガイド を参照してください。有料会員かどうか、は Firebase の Custom Claims を使用するのが良いと思います。
- 投稿日:2021-01-12T17:14:32+09:00
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); } }参考
- 投稿日:2021-01-12T15:53:46+09:00
Android Studioインストール後にコマンドラインでビルドしようとするとJDKのインストールを求められる問題の対処法
macでAndroid Studioの環境構築をした際に若干詰まった部分をメモしておきます。
問題
macにAndroid Studioをインストールしたあと、Android Studio外で以下のようなコマンドによりビルドしようとした際に、以下のエラーメッセージとスクショのようなダイアログが表示され、JDKのインストールを求められました。
ビルドコマンドとエラーメッセージ$ ./gradlew assembleDebug No Java runtime present, requesting install.
環境
- macOS Catalina 10.15.17
- Intel CPU
- Android Studio 4.1.1
解決策
Android StudioにはすでにJDKを内包しているため、パスを通してやれば良いです。
具体的なパスは、Android Studioのメニュー > File > Project Structureの画面で、左ペインのSDK Locationを選択し、JDK Locationを見ることで確認できます。
ちなみに、ここの項目のヒントに、「外部プロセスで利用したい場合は
JAVA_HOME
にこのパスを追加する」旨が記載されていました。
確認したパスを
~/.zprofile
ファイルに以下のように書き込んだのちターミナルを再起動すると、ビルドコマンドが利用できるようになります。.zprofileexport JAVA_HOME="/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home"参照
- 投稿日:2021-01-12T12:03:00+09:00
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.gradledependencies { //・・・割愛 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(); } }
- 投稿日:2021-01-12T11:53:07+09:00
自動入力サービス実装メモ
上司から急に「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
上記を読んだ感じ、大体以下のような流れで自動入力が走る
- ユーザが編集可能なビューにフォーカスする。
- ビューが
AutofillManager#notifyViewEntered(android.view.View)
を呼び出す。- 全てのビューを表す
ViewStructure
が作成される。サービスはこのクラスから表示されているビューにアクセスする。- Androidシステムが自動入力サービスにバインドし、
onConnect()
を呼び出す。- サービスが
onFillRequest(android.service.autofill.FillRequest, android.os.CancellationSignal, android.service.autofill.FillCallback)
コールバックでViewStructure
を受け取る。
ViewStructure
はFillRequest
から取得できる- サービスが
FillCallback#onSuccess(FillResponse)
を使用して応答する- Androidシステムが
onDisconnected()
を呼び出してバインドを解除する- Androidシステムがサービスで作成したオプションを含む自動入力UIを表示する
- ユーザがオプションを選択する
- ビューに自動入力される
開発者が主に実装する箇所
開発者としては
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
を作成する実際にユーザに選んでもらうデータを作成する。
以下の実装例は、AutofillHint
にusername
とpassword
のどちらかが
設定されている場合に自動入力させたい場合の処理を記載している。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で良いので呼び出す必要がある。その他
サービスに適宜バインドして、入力を作り終わったらバインド解除をしている。
なので、常駐サービスの様に常に起動しているわけではない。(状態を持たせたりするのは難しいんじゃないかな?)
- 投稿日:2021-01-12T11:27:14+09:00
[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: ←ここにパスワード入力これで解決
おそらく最初のやり方で「\」の前後でいらないスペースとか何かが含まれてしまっていたのでしょう。
パスワードあってるだろおおおおおおお!!!!ってなっても解決しない時は入力を分けて実行してみてください!
困っている誰かに役立てたら幸いです。
- 投稿日:2021-01-12T10:06:55+09:00
【Unity】Android アプリでスクショや画面録画などを禁止する
背景
アプリによっては、セキュリティやコンテンツ的にスクショや画面録画を禁止したいときがあると思います。
そのとき Android の機能であるWindowManager.LayoutParams.FLAG_SECURE
を Activity のOnCreate()
で呼ぶ必要があります。しかし、Unity だとどうするのか分からなかったので調べてまとめた記事になります。
FLAG_SECURE を設定するとどうなるか
- スクショを無効化
- 画面録画(スクリーンレコーディング)をすると、録画した画面が真っ黒になる
- タスク画面・タスク一覧で、アプリ画面を真っ白にできる
動作確認環境
- Unity 2018.4.22f1
※ Unity2018.2以前のバージョンを使っている場合は、以下の手順とは異なるかと思いますので、ご注意ください。
手順
Unity + Android で実現するためには、
UnityPlayerActivity
を拡張して新しい Activity を定義する- 新しい Activity が使用されるように
AndroidManifest.xml
を修正する- 新しい Activity の
OnCreate()
にFLAG_SECURE
を設定するという手順が必要です。
1.
UnityPlayerActivity
を拡張する自分が作業中の Unityプロジェクトの
Assets/Plugins/Android
配下に新しい Activity を作成します。
例として、OverrideExample
クラスを作成します。ちなみに、 package name 以外のファイル内容は、Unity公式からそのままコピペしています。
UnityPlayerActivity Java コードの拡張 - Unity マニュアル
package name は、自分が設定した名前に置き換えてください。OverrideExample.javapackage 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/Android
にAndroidManifest.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.javapackage 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アプリでキャプチャーをされたくないときにする方法
- 投稿日:2021-01-12T10:04:20+09:00
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は必須
- 投稿日:2021-01-12T08:26:06+09:00
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.gradleplugins { 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'
を削除し、buildFeatures
にviewBinding = true
を追記します。app/build.gradleplugins { 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.nameStringParcelable を使用している場合
ソースコード内で
@Parcelize
アノテーションを使用している場合は、plugins
にid 'kotlin-parcelize'
を追記します。app/build.gradleplugins { id 'com.android.application' id 'kotlin-android' id 'kotlin-parcelize' id 'kotlin-kapt' id 'dagger.hilt.android.plugin' } ...参考 URL