ページ

2014/03/20

[Android]AlarmManagerの設定がクリアされる場合とその対処法

ほとんど以下の記事で書いた日付変更の検知だけで実装できます。
[Android]日付や時刻・タイムゾーンの変更を検知するBroadcastReceiver | DevAchieve
その際の注意点として AlarmManager は設定がクリアされる場合があるということです。
基本的には以下の記事で書いたことと同じです。
[Android]Serviceを停止されても自動的に再起動して常駐させる | DevAchieve

AlarmManager の設定がクリアされる場合とその対処法

アプリがアップデートされて AlarmManager の設定がクリアされる場合
以下の記事の BroadcastReceiver で AlarmManager を再設定する。
[Android]アプリがアップデートしたことを検知するBroadcastReceiver | DevAchieve

端末が再起動されて AlarmManager の設定がクリアされる場合
以下の記事の BroadcastReceiver で AlarmManager を再設定する。
[Android]端末の起動完了を検知するBroadcastReceiver | DevAchieve

参考: Taosoftware: Android AlarmManager 4 アラームが消えるとき

2014/03/19

[Android]Serviceを停止されても自動的に再起動して常駐させる

ユーザーに嫌われる常駐 Service の作り方。
Service が停止されるタイミングはいくつかあるので
その全てで Service を再起動するようにすれば常駐サービスを作ることはできます。

Service が停止される場合とその対処法

アプリがアップデートされて Service が停止する場合
以下の記事の BroadcastReceiver で startService する。
[Android]アプリがアップデートしたことを検知するBroadcastReceiver | DevAchieve

端末が再起動されて Service が停止している場合
以下の記事の BroadcastReceiver で startService する。
[Android]端末の起動完了を検知するBroadcastReceiver | DevAchieve

OSがメモリなどのリソースが少なくなり Service が強制的に停止された場合
public class RevivalService extends Service {
    @Override
    public int onStartCommand(Intent intent, int flags, int startId){
        return Service.START_STICKY;
    }
}
Service#onStartCommand で返すフラグによる動作の違いについては以下の記事が詳しいです。
Yukiの枝折: Android:Serviceの基本とonStartCommandの戻り値による動作の違い
onStartCommand で START_STICKY を使うと
強制終了からの再起動時に intent に null になっているという問題があります。
実験的に作ったサンプルプログラムでは、「戻る」コマンドを実行した程度では、このような現象は発生しない。
この問題をサンプルプログラムで再現するには、DDMSのStop Processを使って、プロセスを強制終了させれば良い。そうすれば、しばらく経つと、Serviceが起動してくる。
onStartメソッド又はonStartCommandメソッドを実行してしまったServiceで、この問題は発生する。
bindServiceメソッドだけを使って生成したServiceでは、(onStartメソッド又はonStartCommandメソッドは実行されないので)このような問題は発生しない。
Androyer in Japan: onStartCommandとその戻値について

参考: kino's blog: Android Service を自動的に再起動する方法

ユーザー操作により[設定 > アプリ > 実行中]から Service が停止された場合
以下のコードのように Service#onDestroy で startService すれば再起動可能です。
public class RevivalService extends Service {    
    @Override
    public void onDestroy(){
        super.onDestroy();
        startService(new Intent(this, RevivalService.class));
    }
}
ですが、ユーザーの確固たる意志で Service を停止しているものを無視して再起動するのですから
アンインストールされたり☆1のレビューが増えたりすることは覚悟した上で実装することになります。
本当にそこまで実装する必要があるか要件と向き合う必要があると思います。

おまけ

別プロセスの2つの Service で相互に bindService すれば落ちないようです。
ITで何かできないかを考えてみる Androidで死なないServiceを実装してみた
こちらはこちらで少し気持ち悪い気がしますね。

2014/03/18

[Android]アプリがアップデートしたことを検知するBroadcastReceiver

自分のアプリをアップデートした際に Service や AlarmManager が止まるので
再起動するために Broadcast Intent を受信したいと思います。

パッケージ関連の BroadcastIntent は
以下の操作でそれぞれのアクションが入った Intent が飛ぶようです。
Google Play から新規にアプリをインストール
android.intent.action.ACTION_PACKAGE_ADDED

アプリをアンインストール
android.intent.action.ACTION_PACKAGE_REMOVED

アプリをアップデート
android.intent.action.PACKAGE_REMOVED
android.intent.action.PACKAGE_ADDED
android.intent.action.PACKAGE_REPLACED

アプリデータを消去
(設定->アプリケーション->アプリケーションの管理->アプリを選択->データを消去)
android.intent.action.PACKAGE_RESTARTED
android.intent.action.PACKAGE_DATA_CLEARED

(プリインアプリの)アップデートのアンインストール
android.intent.action.PACKAGE_REMOVED
android.intent.action.PACKAGE_ADDED
android.intent.action.PACKAGE_REPLACED


ACTION_MY_PACKAGE_REPLACED (API Level 12)は更新したアプリ自体に飛んでくるもののようなので、他のものとはちょっと扱いが違いますが便利そうなIntentですね。アプリ更新したときの更新情報を表示したりするのに使えそう。
ITで何かできないかを考えてみる Android PACKAGE関連のbroadcast intentを試してみた

アップデートの場合は android.intent.action.PACKAGE_REPLACED を捕まえてあげればいいのですが、
自分のアプリだけでなく他のアプリのアップデートなども飛んできてしまいますので
以下のようにチェックする必要があります。
public class UpdateBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent){
        String action = intent.getAction();
        String packagePath = intent.getDataString(); // package:app.package.name
        if(Intent.ACTION_PACKAGE_REPLACED.equals(action)
            && packagePath.equals("package:" + context.getPackageName())){
            // App updated!
        }
    }
}
<receiver android:name=".UpdateBroadcastReceiver">
    <intent-filter>
        <action android:name="android.intent.action.PACKAGE_REPLACED" />
        <data android:scheme="package" />
    </intent-filter>
</receiver>

もう少し未来になると API Level 12 以上の端末ばかりになって
自分のアプリのアップデートだけで飛んでくる
ACTION_MY_PACKAGE_REPLACED が使えるようになると思います。
public class UpdateBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent){
        String action = intent.getAction();
        String packagePath = intent.getDataString(); // package:app.package.name
        if(Intent.ACTION_MY_PACKAGE_REPLACED.equals(action)){
            // App updated!
        }
    }
}
<receiver android:name=".UpdateBroadcastReceiver">
    <intent-filter>
        <action android:name="android.intent.action.ACTION_MY_PACKAGE_REPLACED" />
    </intent-filter>
</receiver>

アプリの更新って Google Play で自動に実行されたりするので、
コレで拾って更新通知のダイアログ風な Activity を開いたらリテンションに繋がりそう。
まぁ意図せず画面を奪うことになるからイラッと来て
アンインストールされそうだからやめたほうがいいだろうけど。

2014/03/17

[Android]テキストや画像をLINEに送る

とあるサービスが流行ると周りのサービスは
そのサービスにシェアできるようになってほしいという要望が
多方面から寄せられますね。
以前は Twitter/Facebook が最低限シェア先として欲しいとされていましたが、
最近は LINE に送るを実装して欲しいという要望が多くなっています。

ということで対応コード。

public static Intent getShareTextToLineIntent(String text){
    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_VIEW);
    intent.setData(Uri.parse("line://msg/text/" + text));
    return intent;
}

public static Intent getShareImageToLineIntent(String filePath){
    Intent intent = new Intent();
    intent.setAction(Intent.ACTION_VIEW);
    intent.setData(Uri.parse("line://msg/image/" + filePath));
    return intent;
}
LINE をインストールしていなかった場合は動かないので注意。
Intent を送れるかチェックして対応する必要があります。
このコードには1つ問題点があって、
このコードで LINE を起動して投稿した後に
もう一度このコードで LINE を起動すると、その前に投稿した画面が出て正常に投稿フローに乗れません。
起動側の Intent のフラグをうまく設定すればいいのか LINE 側の対応が必要なのかよくわかっていないです。
Intent のフラグの意味がいまいちわかっていないのでコレを設定すればいけそうだとかあったら教えて下さい。

設置方法|LINEで送るボタン
line://msg/<CONTENT TYPE>/<CONTENT KEY>

パラメータ名 パラメータ値 説明
<CONTENT TYPE> text テキスト情報を送るときに指定します。
image 画像を送るときに指定します。

  • ※Androidの場合、イメージはローカルファイルになります。パラメータ設定方法は、下の<CONTENT KEY>の説明を参照ください。
<CONTENT KEY> [任意の値] パーセントエンコーディング(utf-8)したテキスト情報の値を指定します。

原則、ページタイトル、ページURLの指定が可能です。

  • ※Androidから画像を送信する場合、line://msg/image/(localfilepath)の形式で、LINE側から読み取り可能なローカルファイルのイメージのパスを指定してして下さい。
  • ※ページに関係ない情報の指定はガイドラインで禁止されています。

2014/03/16

[Git][GitHub]hub コマンドでターミナルから Pull Request を送る

[Git][GitHub]Pull Request を送る手順、あるいは GPUImage for Android に PullRequest を送った話 | DevAchieve を書く際に色々調べていたら
以前導入した hub コマンドで Pull Request を送る方法があったのでまとめてみました。
hub: github/hub
導入記事: HomeBrew を zsh で使えるようにする。ついでに hub コマンドも。
参考記事: GitHubユーザーのためのhubコマンド - Qiita

Pull Request したいリポジトリをクローン

hub clone octocat/Spoon-Knife
Cloning into 'Spoon-Knife'...
remote: Reusing existing pack: 10, done.
remote: Total 10 (delta 0), reused 0 (delta 0)
Receiving objects: 100% (10/10), done.
Resolving deltas: 100% (1/1), done.
Checking connectivity... done

作業ディレクトリに移動

cd Spoon-Knife

作業用ブランチを切る

git checkout -b Spife
Switched to a new branch 'Spife'

編集してコミット

vi README.md
git add README.md
git commit -m "Spife is spoon, it's also knife."
[Spife 0758961] Spife is spoon, it's also knife.
1 file changed, 1 insertion(+)

リポジトリをフォーク

hub fork
Updating wada811
From github.com:wada811/Spoon-Knife
* [new branch] master -> wada811/master
new remote: wada811

変更をプッシュ

git push wada811 Spife
Counting objects: 5, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 388 bytes | 0 bytes/s, done.
Total 3 (delta 1), reused 0 (delta 0)
To git@github.com:wada811/Spoon-Knife.git
* [new branch] Spife -> Spife

Pull Request を送る

hub pull-request
https://github.com/octocat/Spoon-Knife/pull/1778

hub コマンドを入れたものの全然使ってなかったので積極的に使っていこう。

2014/03/15

[Git][GitHub]Pull Request を送る手順、あるいは GPUImage for Android に Pull Request を送った話

CyberAgent/android-gpuimageを使ってみたら
大きめの画像を Bitmap に変換する際に結構時間がかかっていたので
ソースコードを見てみたらPixelBuffer#convertToBitmap
上下反転を直すために全ピクセルをループ回していました。

これでは画像が大きくなるほど処理時間が長くなってしまうので
一旦上下反転した画像を Bitmap に変換して、
その Bitmap を Matrixで更に上下反転させて直しました。
change the way to convert upside down image · ec82668 · wada811/android-gpuimage

GitHub で Pull Request を送る手順

1. リポジトリをフォークする

GitHub で fork する。

2. リポジトリをクローンする

git clone git@github.com:octocat/Spoon-Knife.git

3. 作業ディレクトリに移動する

cd Spoon-Knife

4. 作業用ブランチを切る

git checkout -b Spife

5. ファイルを編集してコミットする

vi README
git add README
git commit README -m "Spife is a spoon, it is also a knife."

6. フォーク元のリポジトリの更新に追従する

6-1. フォーク元のリポジトリの登録(初回のみ)
git remote add upstream git://github.com/octocat/Spoon-Knife.git
6-2. commit 前の差分がある場合は退避する
git stash
6-3. master ブランチに移動する
git checkout master
6-4. upstream/master の変更をローカルの master に pull する
git pull upstream master
6-5. 作業用ブランチに移動する
git checkout Spife
6-6. fork 元の最新版まで rebase する (コンフリクトしたら頑張りましょう)
git rebase master Spife
6-7. stash で退避した差分を元に戻して作業を継続 (コンフリクトしたら頑張りましょう)
git stash pop

7. Pull Request 用に作業用ブランチの commit を1つにまとめる

7-1. 作業用ブランチに移動する
git checkout Spife
7-2. 作業用ブランチを元に Pull Request 用ブランチを切る
git checkout -b featureSpife
7-3. 作業用ブランチ上の fork 元からの差分を1 commitにまとめる
git rebase -i master

8. Pull Request 用ブランチを push する

git push origin featureSpife

9. Pull Request する

GitHub から Pull Request する。

詳しい説明は GitHubへpull requestする際のベストプラクティス - hnwの日記 を読んで下さい。
僕は CyberAgent/android-gpuimage が1年近く放置されていたのと、変更箇所が1ヶ所だけだったので
上の手順の 6 と 7 を省いて実行しました。
CyberAgent さんは Pull Request が来ると Travis CI が動くようです。
Travis CI を使ったことがないのでよくわからないのですが、
結果は設定ファイルがないよ!ということでその後うまく動かなかったのかエラーになりました。
Travis CI - CyberAgent/android-gpuimage - Could not find .travis.yml, using standard configuration.
他の人の Pull Request も同様に失敗しているのでこちら側由来のエラーではないはず。
Pull Request しても反応がないのだったら他で連絡しても多分反応ないし放置することにしました。

Bad End.

おまけ

Pull Request はこれで2回目だけど手順も全然覚えてないし、
git レベルが低くてコミットをまとめるとかコンフリクトを解決するとか
あまりやらないことは記事を書けるレベルに達していないので
とりあえず全体の流れだけ記事に書いてみました。
Pull Request する機会は早々ないと思いますが、これで次からは多少すんなりできるかな?

もっと git レベルを上げるために Fork しない Pull Request で色々な操作を練習して慣れていきたい。
GitHub初心者はForkしない方のPull Requestから入門しよう - QNYP Blog

2014/03/13

[Android]画像を合成する - Image synthesis using PorterDuff

Android で画像を PorterDuff を使って
合成する方法がようやくわかったので
AndroidLibrary@wada811 に BitmapUtils#synthesize(Bitmap, Bitmap, PorterDuff.Mode) を
追加しました。

Porter さんと Duff さんがまとめた(※)
画像合成のルールが
PorterDuff.Mode クラスで指定できます。
(※Alpha compositing - Wikipedia, the free encyclopedia)

PorterDuffXfermode クラスというものがありますが
Xfermode に PorterDuff.Mode を入れているだけでした。
Xfermode とは Transfer mode という意味です。

PorterDuff.Mode のコメントはわかりにくいですが、
以下のような意味があると思われます。
Sa: ソース画像のアルファ、Sc: ソース画像のカラー
Da: 合成先画像のアルファ、Dc: 合成先画像のカラー
[Ra, Rc]: [結果画像のアルファ, 結果画像のカラー]
public class PorterDuffXfermode extends Xfermode {
    /**
     * @hide
     */
    public final PorterDuff.Mode mode;

    /**
     * Create an xfermode that uses the specified porter-duff mode.
     *
     * @param mode           The porter-duff mode that is applied
     */
    public PorterDuffXfermode(PorterDuff.Mode mode) {
        this.mode = mode;
        native_instance = nativeCreateXfermode(mode.nativeInt);
    }
    
    private static native int nativeCreateXfermode(int mode);
}


/**
 * Xfermode is the base class for objects that are called to implement custom
 * "transfer-modes" in the drawing pipeline. The static function Create(Modes)
 * can be called to return an instance of any of the predefined subclasses as
 * specified in the Modes enum. When an Xfermode is assigned to an Paint, then
 * objects drawn with that paint have the xfermode applied.
 */
public class Xfermode {

    protected void finalize() throws Throwable {
        try {
            finalizer(native_instance);
        } finally {
            super.finalize();
        }
    }

    private static native void finalizer(int native_instance);

    int native_instance;
}


public class PorterDuff {

    // these value must match their native equivalents. See SkPorterDuff.h
    public enum Mode {
        /** [0, 0] */
        CLEAR       (0),
        /** [Sa, Sc] */
        SRC         (1),
        /** [Da, Dc] */
        DST         (2),
        /** [Sa + (1 - Sa)*Da, Rc = Sc + (1 - Sa)*Dc] */
        SRC_OVER    (3),
        /** [Sa + (1 - Sa)*Da, Rc = Dc + (1 - Da)*Sc] */
        DST_OVER    (4),
        /** [Sa * Da, Sc * Da] */
        SRC_IN      (5),
        /** [Sa * Da, Sa * Dc] */
        DST_IN      (6),
        /** [Sa * (1 - Da), Sc * (1 - Da)] */
        SRC_OUT     (7),
        /** [Da * (1 - Sa), Dc * (1 - Sa)] */
        DST_OUT     (8),
        /** [Da, Sc * Da + (1 - Sa) * Dc] */
        SRC_ATOP    (9),
        /** [Sa, Sa * Dc + Sc * (1 - Da)] */
        DST_ATOP    (10),
        /** [Sa + Da - 2 * Sa * Da, Sc * (1 - Da) + (1 - Sa) * Dc] */
        XOR         (11),
        /** [Sa + Da - Sa*Da,
             Sc*(1 - Da) + Dc*(1 - Sa) + min(Sc, Dc)] */
        DARKEN      (12),
        /** [Sa + Da - Sa*Da,
             Sc*(1 - Da) + Dc*(1 - Sa) + max(Sc, Dc)] */
        LIGHTEN     (13),
        /** [Sa * Da, Sc * Dc] */
        MULTIPLY    (14),
        /** [Sa + Da - Sa * Da, Sc + Dc - Sc * Dc] */
        SCREEN      (15),
        /** Saturate(S + D) */
        ADD         (16),
        OVERLAY     (17);

        Mode(int nativeInt) {
            this.nativeInt = nativeInt;
        }

        /**
         * @hide
         */
        public final int nativeInt;
    }
}

2014/03/05

[Android]日付や時刻・タイムゾーンの変更を検知するBroadcastReceiver

日時変更イベントで AppWidget を更新したかったので
日付や時刻・タイムゾーンの変更を検知する BroadcastReceiver を作ってみました。

時刻・タイムゾーンの変更を検知する

Intent#ACTION_TIME_CHANGED | Android Developers
Intent#ACTION_TIMEZONE_CHANGED | Android Developers
@Override
public void onReceive(Context context, Intent intent){
    String action = intent.getAction();
    if(Intent.ACTION_TIME_CHANGED.equals(action) || Intent.ACTION_TIMEZONE_CHANGED.equals(action)){
        // onTimeChanged!
    }
}
ユーザの設定で自動設定(ネットワークから提供されたタイムゾーン/日時を使用する設定)になっている場合は
接続するネットワークが変わると変更イベントが飛ぶので注意です。

日付の変更を検知する

Intent#ACTION_DATE_CHANGED | Android Developers というものがあるので
同じように行けるかと思いきや何故か検知することができません。
日付変更イベントが飛んでいないのか検知できないのかわかりませんが、
検知できないのなら自分で飛ばして自分で検知しよう!というのが一般的な対応策のようです。
AlarmManager#setAlarmManager#setExactで 0 時 0 分にイベントが飛ぶように設定して
検知する度に翌日にセットし直してやれば毎日日付変更のイベントが飛んで検知することができます。
@Override
public void onReceive(Context context, Intent intent){
    String action = intent.getAction();
    if(getDateChangedAction(context).equals(action)){
        onDateChanged(context);
        registerDateChangeReceiver(context);
    }
}

/**
 * 翌日の0時に {@link AlarmManager} をセットする
 */
@TargetApi(Build.VERSION_CODES.KITKAT)
public static void registerDateChangeReceiver(Context context){
    unregisterDateChangeReceiver(context);
    Calendar calendar = Calendar.getInstance(TimeZone.getDefault());
    calendar.set(Calendar.HOUR_OF_DAY, 0);
    calendar.set(Calendar.MINUTE, 0);
    calendar.set(Calendar.SECOND, 0);
    calendar.set(Calendar.MILLISECOND, 0);
    calendar.add(Calendar.DAY_OF_MONTH, 1);
    AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
    PendingIntent pendingIntent = createDateChangePendingIntent(context);
    if(AndroidUtils.isLessThanBuildVersion(Build.VERSION_CODES.KITKAT)){
        alarmManager.set(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent);
    }else{
        alarmManager.setExact(AlarmManager.RTC_WAKEUP, calendar.getTimeInMillis(), pendingIntent);
    }
}

/**
 * すでにセットされた Alarm を解除する
 */
public static void unregisterDateChangeReceiver(Context context){
    AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
    alarmManager.cancel(createDateChangePendingIntent(context));
}

/**
 * 日付変更の PendingIntent を生成する
 */
private static PendingIntent createDateChangePendingIntent(Context context){
    Intent intent = new Intent(getDateChangedAction(context));
    return PendingIntent.getBroadcast(context, 0, intent, 0);
}

public static String getDateChangedAction(Context context){
    return context.getPackageName() + ".action.DATE_CHANGED";
}
参考
Taosoftware: AlarmManager1 Android でCronみたいなことをするには
AlarmManagerがKitKatで省エネ動作になった - _development,

AndroidManifest.xml

<receiver
    android:name=".YourDateTimeChangedReceiver"
    >
    <intent-filter>
        <action android:name="your.package.name.action.DATE_CHANGED" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.TIMEZONE_CHANGED" />
        <action android:name="android.intent.action.TIME_SET" />
    </intent-filter>
</receiver>
AndroidManifest.xml はこんな感じです。

ソースコードは wada811/AndroidLibrary@wada811 に追加しました。