phicdy devlog

Androidアプリ開発やその他技術系の記事をたまに書きます

AndroidとかiOSとかモバイル多め。その他技術的なことも書いていきます。

Nature Remoを買った

去年の11/5に注文して12/23に届いた。 13000円。 今在庫を見てみたら2~3日後配達になっていてかなり在庫が充実している・・・

nature.global

f:id:phicdy:20180302193055j:plain

(置き方がこれでいいかは疑問が残る・・・)

いわゆるスマートリモコンで赤外線リモコンで操作できる家電(エアコンとかテレビとか)をスマホGoogle Homeなどから操作できるものである。

良かったところ

エアコンの登録が楽

他のスマートリモコンを使ったことはないのでNature Remoが特化している機能ではないかもしれない。Nature Remoのアプリを開いて1つのキーを登録するだけでエアコンの種類が判別され、全操作が自動的にできるようになった。

エアコンのオン・オフを細かく設定できる

これはNature Remoに特化している話ではない。 通常のエアコンのリモコンだと何時間後に切る or 何時間後につけるのどちらか1つしか設定できない。 Nature Remoの場合IFTTTと連携すればを下記のように細かく設定できる。

  • 平日朝7時半に暖房19℃でつける
  • 平日朝8時半に切る
  • 平日夜6時に暖房21℃でつける
  • 休日朝10時に暖房19℃でつける
  • 毎日夜12時に切る

それに加えてNature RemoアプリにGeo Fencingの設定があるので、家を離れたらエアコンを切るようにしている。

悪かったところ

購入後に無線ルーターを買い替えたためSSIDが変わった。どうやらNature Remo側にSSIDを変更する機能がなく、初期化しか方法がなかったので設定が全部やり直しになってしまった。

おわりに

買ってからしばらく経つが、かなり満足している。もっと早く買っておけばよかった・・・

java.lang.RuntimeException: com.android.builder.dexing.DexArchiveMergerException: Unable to merge dex

UIテストを実行しようとしたらエラー・・・

f:id:phicdy:20180206222343p:plain

ひとまずRun with --stacktraceを押してみる。

...
Caused by: com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536
    at com.android.dx.merge.DexMerger$8.updateIndex(DexMerger.java:565)
    at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:276)
    at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:574)
    at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:166)
    at com.android.dx.merge.DexMerger.merge(DexMerger.java:198)
    at com.android.builder.dexing.DexArchiveMergerCallable.call(DexArchiveMergerCallable.java:61)
    ... 1 more

これはいわゆる65536メソッド問題のようです。

UIテスト周りのbuild.gradleを見直したところ以下は消せそうだったので消し、hamcrest-libraryのメソッドはorg.hamcrest.CoreMatchersのメソッドに置き換えました。

testImplementation 'org.hamcrest:hamcrest-library:1.3'
uiTestImplementation 'com.android.support:support-annotations:25.3.1'
uiTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2'

これでメソッド数が減ってUIテストが実行できるようになりました(なんでExpressoのテストないのに入れてたんだろ)

Google App ScriptでWebスクレイピングしたら楽だった

とある商品の価格を監視したくてなんかいい方法ないかと考えていたところ、Google App Scriptでやったという記事を見たのを思い出してやってみた。

実装

スプレッドシートを作成→ツール→スクリプトエディタで作成開始。 今回はデバッグ用に実行ボタンを追加する関数と実行関数を書いた。 中身はWebサイトのHTMLを取ってきて正規表現で価格を取り出す。前回の結果をA1のセルに保存しておいて下がったら自分のGmailに送るというシンプルな実装。

function addExecuteToMenu() {
  var sheet = SpreadsheetApp.getActiveSpreadsheet();
  var entries = [
    {
      name : "Execute",
      functionName : "myFunction"
    }
  ];
  sheet.addMenu("Execute script", entries);
};

function myFunction () {
  // Change URL
  var html = UrlFetchApp.fetch('http:/xxxxxxxxxx').getContentText();
  // Change for the site content you want to get
  var reg = /<div class="price">&yen;([\s\S]*?)<\/div>/i; 
  var match = reg.exec(html);
  var price = match[1];
  price = price.replace(",", "")
  
  var book = SpreadsheetApp.getActiveSpreadsheet();
  // Get previous price from A1 in sheet1
  var sheetData = book.getSheetByName("sheet1");
  var prevPrice = sheetData.getRange(1, 1).getValue()
  if (prevPrice > price) {
    // Send Gmail
    var email = Session.getActiveUser().getEmail();
    var subject = "Price down";
    var body = 'Price: ' + prevPrice + ' -> ' + price;
    GmailApp.sendEmail(email, subject, body);
  }
  // Store current price to A1 in sheet1
  sheetData.getRange(1, 1).setValue(price);
}

定期実行

スクリプトエディタ上で↓のボタンを押し、

f:id:phicdy:20180206221217p:plain

↓のように設定すれば完了

f:id:phicdy:20180206221225p:plain

参考

Overview of Google Apps Script  |  Apps Script  |  Google Developers

qiita.com

qiita.com

qiita.com

totoを予想するアプリをGoogle Playに公開できなかった話

結論:前もって規約を読んでおけ

Gambling | Restricted Content - Developer Policy Center

現在は公開中ですが、最初に公開しようとしたときには公開できませんでした。

play.google.com

規約によるとオンライン ギャンブルを推進するコンテンツおよびサービスについては英国、アイルランド、フランスのみ許可されています。 よってtotoのように日本向けのギャンブル?系のアプリをGoogle Playに公開することができません。 もともとこのアプリは自動でtotoを予想をした後にアプリ内のWebViewでtotoのページを開いてJavaScriptでその予想をページに反映させていました。 そしてその後ユーザがログインすればそのままtotoを買える仕組みでした。 恐らくこの部分が規約に引っかかったと思われ、公開を止められました。

Google Play にアプリをご送信いただきまして、誠にありがとうございます。審査の結果、toto予想(パッケージ ID com.phicdy.totoanticipation)は賭博行為に関するポリシーに違反していると判断されたため、否承認となりました。この送信がアップデートであった場合は、引き続き以前のバージョンのアプリが Google Play に掲載されます。 再度審査を受けるためにアプリをご送信いただく場合は、次の手順に沿ってご対応ください。 アプリでの賭け事やギャンブルに本物の通貨を使用する場合は、必ず AO(成人のみ)または IARC と同等のレーティングにする(なお、コンテンツ レーティング質問票への回答が再度必要になる場合があります) 賭博行為に関するポリシーの要件をよく読み、アプリが要件を満たしていることを確認する(すべての要件を満たしていない場合、Google Play ではアプリが許可されません) Play Console にログインし、アプリを再送信する なお、再送信していただいたアプリがデベロッパー プログラム ポリシーに違反している場合は、もう一度否承認になることがございます(または、Google Play から削除されることもあります)。 ポリシーをご確認のうえ、今回の否承認が誤りだと思われる場合は、お手数ですが弊社のポリシー サポートチームまでお問い合わせください。担当者が 2 営業日以内にご連絡いたします。 どうぞよろしくお願いいたします。 Chrissy Google Play 審査チーム

仕方ないのでtotoのページを外部ブラウザで開くようにしたところ無事審査に通り、公開されました。 ただ、予想がページに反映されなくなってしまったので正直意味あるのかなこのアプリ・・・という感じになってしまいした。残念。 GitHub上にでもその機能を有効化したapkを置いておこうかなと思います。

KotilnでTheoryとDataPointsを使ったJUnitのパラメータテストを書く

Kotlinに何も考えずにTheoryとDataPointsを使ったテストを書こうとするとデータがJUnitから参照できずエラーになる。 Kotlinにstaticはないのでcompanion objectを作って@JvmFieldをつけることでJUnitから参照できるようになる。

@RunWith(Theories::class)
class SampleTest {

    companion object {
        @DataPoints
        @JvmField
        val data = listOf(
                1 to 1,
                2 to 4,
                3 to 9
        )
    }

    @Theory
    fun testSquare(testData: Pair<Int, Int>) {
        val result = Calc().square(testData.first)
        assertEquals("expected:" + testData.second + " , result: " + result, 
                      testData.second, result)
    }
}

【Android】Activityを2重で開かないようにする

なにも考えずに実装を行うとActivityが2重で開いてしまうことがある。 例えばボタンを押して次のActivityが開くとき、そのActivityやIntentに設定をしていなければ、素早く連打することで2重に開くことができる。

修正方法

launchModeは指定しないとstandardに設定される。singleTopはほとんどstandardと同じだが、タスクのスタックの一番上に既存のActivityのインスタンスが既に存在する場合挙動が異なる。

"standard" モードと "singleTop" モードの違いは、1 点だけです。 "standard" アクティビティが新たなインテントを受け取るたびに、クラスの新しいインスタンスが作成され、そのインテントに応答します。各インスタンスは 1 つのインテントを処理します。同様に、"singleTop" アクティビティの新しいインスタンスは、新しいインテントを処理するために作成されることもあります。 しかし、ターゲット タスクのスタックの一番上に、既存のアクティビティのインスタンスが既に存在する場合、そのインスタンスが新しいインテントを受け取ります(onNewIntent() の呼び出し)。新しいインスタンスは作成されません。その他の状況では、たとえば、"singleTop" アクティビティの既存のインスタンスがターゲット タスク内に存在するもののスタックの一番上に配置されていない場合、あるいはスタックの一番上に存在するもののターゲット タスク内ではない場合、新しいインスタンスが作成され、スタック上にプッシュされます。

つまり連打で1回Activityを開き2回目が開かれたとしても、そのActivityを使いまわし、onNewIntent()が呼ばれるという流れになる。これでActivityが2重で開くのを防ぐことができる。

ただstartActivityForResult()の場合はこれが効かず毎回新しいActivityが生成される。未調査だが恐らく結果を次のActivityが受け取るという特性上、Activityごとに結果が違うだろうということで新しいインスタンスが作られるのではないかと思う。これはstartActivityForResult()に渡すIntentにFLAG_ACTIVITY_CLEAR_TOPを指定してあげれば解決できる。

タスクがA→B→C→DとあったときにDからBをFLAG_ACTIVITY_CLEAR_TOP付きで呼ぶと、新しいインスタンスを作らずにC, Dを終了してA→Bとする。今回のケースの場合、A→B→C→DでEをstartActivityForResult()で呼び、A→B→C→D→Eとなった直後にEを呼ぶので、新しいインスタンスを作らずにA→B→C→D→Eのままになる。

最後にアンインストールダイアログなどAndroidのシステムのActivityを呼ぶときも同様にFLAG_ACTIVITY_CLEAR_TOPを付ければ同様に解決できる。これらはsingleTopをアプリ側から指定することができない。