2019年1月27日日曜日

Android 7.0以降だとSAF経由でGoogleDriveへファイル作成ができないことがある

はじめに


最近Androidアプリのメンテナンスをしていなかったので一番の稼ぎ頭であるNudiaの不具合修正を行うことにしたが、結構大変だったので内容を残しておく。

不具合の内容と調査結果


不具合の内容


アプリバージョン 2.3.0でAndroid 7.0以降の端末で作成した画像ファイルをGoogleDrive上に保存しようとすると0 byteの空ファイルができる。

調べてみると、stackoverflowの方に同じ悩みを抱えている人がいたが原因と対策は分かっていない様だった。

調査結果


当初、Android Nougatからアプリ間のファイル共有のポリシーが厳しくなったことにより、プログラム側での処理が足りていないのかと思っていたが、ファイルの保存方法はAndroid公式のSAFに紹介されている手順に従っているため、他に原因があるはずだが全く見当が付かなかった。

そして施行錯誤をする中で、なぜかGoogleDriveを再起動した直後の1回だけ保存が成功することがわかった。

ちなみに公式のSAFのドキュメントでACTION_OPEN_DOCUMENTインテントとACTION_CREATE_DOCUMENTインテントを紹介してCATEGORY_OPENABLEと組み合わせることで次の3パターンのファイルwrite操作が可能になる。


CATEGORY_OPENABLE ありCATEGORY_OPENABLE なし
ACTION_CREATE_DOCUMENT
新規にファイルを作成
空ファイル作成
ACTION_OPEN_DOCUMENT
既存ファイルを上書き


アプリ側ではACTION_CREATE_DOCUMENTとCATEGORY_OPENABLEを指定しているのだが、何故か2回目以降だとCATEGORY_OPENABLEが効いていなかった。
またlogcatでGoogleDriveの動作を監視してみると、再起動直後のみアプリでの保存操作後ActivityManagerから何かしらのIntentがGoogleDriveへ飛んでいることが分かった。
しかしこれ以上の原因については分からなかったため、暫定対応としてAndroid Nougat以上ではGoogleDriveへの保存をさせないように制限を付けることにした。

もし今後さらなる原因がわかったら、恒久的な修正をリリースします。


最後に、これまでアプリをご利用くださっていた方にはご不便をおかけして申し訳ありませんでした。



2019年1月26日土曜日

GoogleDirveでのStackEdit解除方法



はじめに


AndroidからiOSアプリへ開発を切り替えたのをきっかけにこのブログではなくQiitaの方へ投稿をしていたのですが、プログラミング以外のことも書きたいのでこっちを再開しようかと考えています。

Qiitaの投稿はMarkdownだったため、そちらで慣れているとBloggerでもMarkdownで書きたくなります。調べてみるとStackEditというWebツールを使えばMarkdown形式で書けるとのことで導入して見ましたが次の理由でやっぱりやめました。

StackEditについて気になった点


  • 画像を投稿に含める場合、先にどこかにアップしてそのURLを指定しなければならない(ドラッグ&ドロップで自動でアップロードしてくれない)

  • StackEditで作ったドキュメントだけとは言え、自分のGoogleDriveへのアクセスを付けるのが嫌

  • そもそも、GoogleDrive上でStackEditドキュメントを作成しようとしたときに確認されるアカウント紐付けで、「Restrict access」を選択しないとGoogleDrive上の全てのアクセス権を要求されるのが物凄く嫌



そのためGoogleDriveに設定したStackEditを解除することにしましたが、やり方が分からず戸惑ったのでやり方を残しておきます。

StackEditの解除方法

  • GoogleDriveを開く

  • 右上の設定アイコンを選択し、設定を開く

  • 左のペインよりアプリの管理を選択し、一覧の中にあるStackEditを選び「ドライブから切断」をクリックする







2017年7月27日木曜日

複数積んだFragmentBackStackを一度にクリアする方法


Androidアプリを作るとき昔はActivityを複数作ってIntentで画面切替をしていた。
今は一つのActivityの中にFragmentを突っ込んでFragmentを切り替えることで画面切替をするようにしている。(一般的にもこの方法が今はメジャーなはず)

FragmentA、FragmentB、FragmentCと3つのFragmentがあったとして、
Fragmentでの画面切替は普通こんな感じで行う。

FragmentManager fragmentManager = getFragmentManager();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.container, FragmentA.newInstance() );
transaction.addToBackStack(null);
transaction.commit();

commit時にBackStackに積んでいるので
[A] -> [B] -> [C] と画面切替をした後バックキーを押す度に

[C] -> [B]

[B] -> [A]

と切り替わる。

しかし、一度に [C] -> [A]に戻りたいときどうすれば良いかを分らなかった。

調べたところ、BackStackはFragmentのインスタンスを保存しているのではなく、transactionで行ったことを記録しているだけらしく、popBackStack()が実行されると記録されたことと逆のことが実行される。

例えば、replace(FragmentB)(中身はremove(FragmentA)してadd(FragmentB))が記録されていた場合
その逆のremove(FragmentB)してadd(FragmentA)が実行されるらしい。


知らなかったーーーー。

それならば、話は簡単で[C] -> [A]に一度のバックキーで戻るにはFragmentBackStackの数が変わったことをイベントトリガとして、BackStackの数が0に成るまでpopBackStackを呼んでやれば良い。

FragmentBackStackの数の変更はFragmentManager.OnBackStackChangedListnerを実現することで検知できる。

以下サンプルコード

public class MainActivity implements FragmentManager.OnBackStackChangedListener{
    private int fragmentBackStackCnt = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        FragmentManager fragmentManager = getFragmentManager();
        fragmentManager.beginTransaction().add(R.id.container, FragmentA.newInstance()).commit();
        fragmentManager.addOnBackStackChangedListener(this);
    }

    @Override
    public void onBackStackChanged() {
        FragmentManager fragmentManager = getFragmentManager();
        int count = fragmentManager.getBackStackEntryCount();

        if (fragmentBackStackCnt > count && count != 0) {
            // backStackが0になる(検索画面になるまで)fragmentをpopする
            fragmentManager.popBackStack();
        }
        fragmentBackStackCnt = count;
    }

}

参考ページ:
Android Fragment トランザクション - バックスタックの落とし穴

2016年12月2日金曜日

iOSアプリのNavigationBarにメニューアイコンをつける

やりたいこと

下のスクリーンショットにあるようにNavigationBarにメニューを示すアイコンを表示したい

やり方

  • IoniconsSwiftを使ってアイコン画像をロード
  • ロードした画像を使ってUIBarButtonItemインスタンスを作成
  • インスタンスをnavigationItemにsetLeftBarButtonItemsしてやるだけ

実際のコード

override func viewDidLoad() {
    super.viewDidLoad()
    
    // NavigationBarの高さ × 0.8にアイコンのサイズを指定する
    let barHeight : Int = Int((self.navigationController?.navigationBar.frame.size.height)! * 0.8)
    
    // Ioniconよりメニューアイコン画像をロード
    let menuIcon = UIImage.imageWithIonicon(ionicon.AndroidMenu, color: UIColor.black, iconSize: CGFloat(barHeight), imageSize: CGSize(width: barHeight, height: barHeight))

    // アイコン画像からUIBarButtonItemを生成
    let leftNavEditButtonItem = UIBarButtonItem(image: menuIcon, style: .plain, target: self, action: #selector(MyViewController.doSomething))

    // UIBarButtonItemをセット
    self.navigationItem.setLeftBarButtonItems([leftNavEditButtonItem], animated: true)
}

2016年11月22日火曜日

UIButtonに渡すselfはinit後に渡そうね


最近、Androidを離れてSwiftでiOSアプリを作成していますが、次の点にはまったのでメモ 

やりたいこと

  •  イベントに応じてNavigationBarに動的にボタンを追加したり消したりする 


失敗した例

  • UIBarButtonItemのインスタンスを静的メンバとして保持しておく 
  • イベントに応じて保持しているインスタンスをセットしたりアンセットする 
class MyViewController: UIViewController {
    var rightNavEditButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(MyViewController.test))
    
    func test() {
        print("hogehoge")
    }

    func occurSomeEvent(_ calendar: FSCalendar, didSelect date: Date) {
        self.navigationItem.setRightBarButtonItems([rightNavEditButtonItem!], animated: true)
    }
}

 これだと、UIBarButtonItemをnewしたタイミングで引数に渡しているselfが決まっていない(インスタンス化されていない)ため
コード的にエラーはなくともselectorで指定したメソッドが呼ばれない

 正しい例

  • UIBarButtonItemのインスタンスをインスタンスメンバとして保持しておく
  •  viewDidLoadなどselfが決まっている状態でUIBarButtonItemをnewする
  •  イベントに応じて保持しているインスタンスをセットしたりアンセットする
class MyViewController: UIViewController {
    var rightNavEditButtonItem : UIBarButtonItem?
    
    override func viewDidLoad() {
        rightNavEditButtonItem = UIBarButtonItem(barButtonSystemItem: .edit, target: self, action: #selector(MyViewController.test))
    }
    
    func test() {
        print("hogehoge")
    }

    func occurSomeEvent(_ calendar: FSCalendar, didSelect date: Date) {
        self.navigationItem.setRightBarButtonItems([rightNavEditButtonItem!], animated: true)
    }
}

2016年7月27日水曜日

Hot Ramenリリース

Hot Ramenというアプリをリリースしました。 お手持ちの画像に簡単に動く湯気を付けることができます。 画像はGIFとして保存できるのでTwitterなどへ投稿ができます。 料理以外にも人物の怒った姿を演出するのにも使えます。

ラーメンもホカホカ!
もう怒ったぞ!

2016年6月3日金曜日

バネの動きをするAction

Cocos2d-xでアプリを作っていて結局使わなかったがバネの動きを再現したActionを作ったので公開する。
 libcocos2dのソースの中に組み込んでやれば他のActionと同様に呼び出せるようになる。
ソースの中にdefineしてしまっているが自然長や初速度、摩擦係数などを変更すれば動きをもっと激しくすることも可能。

SpringMoveBy.h
#ifndef SPRINGMOVE_H_
#define SPRINGMOVE_H_

#include "cocos2d.h"

class CC_DLL SpringMoveBy : public cocos2d::ActionInterval
{
public:
    static SpringMoveBy* create(float duration, const cocos2d::Vec2& position, float mass, float k);

    //
    // Overrides
    //
    virtual SpringMoveBy* clone() const override;
    virtual SpringMoveBy* reverse(void) const override;
    virtual void startWithTarget(cocos2d::Node *target) override;
    /**
     * @param time In seconds.
     */
    virtual void update(float time) override;

CC_CONSTRUCTOR_ACCESS:
SpringMoveBy() {}
    virtual ~SpringMoveBy() {}

    /**
     * initializes the action
     * @param duration in seconds
     */
    bool initWithDuration(float duration, const cocos2d::Vec2& position, float mass, float k);

protected:
    cocos2d::Vec2 _startPosition;
    cocos2d::Vec2 _delta;
    float  _k;
    float   _mass;
    cocos2d::Vec2 _previousPos;
    float  _velocity;
    float  _accel;
    float  _position;

private:
    CC_DISALLOW_COPY_AND_ASSIGN(SpringMoveBy);
};

#endif /* SPRINGMOVE_H_ */

SpringMoveBy.cpp
#include "SpringMove.h"

USING_NS_CC;

//
// SpringMoveBy
//

#define GRAVITY 9.80665 // 重力加速度
#define NATURAL_LENGTH 0 // 自然長
#define FIRST_ACCEL GRAVITY // 初期加速度
#define FIRST_VEROCITY 60 // 初期速度
#define FIRST_POSITION 0 // 初期位置
#define FRICTION 0.996 // 摩擦係数

USING_NS_CC;

SpringMoveBy* SpringMoveBy::create(float duration, const Vec2& position, float mass, float k)
{
    SpringMoveBy *springMoveBy = new (std::nothrow) SpringMoveBy();
    springMoveBy->initWithDuration(duration, position, mass, k);
    springMoveBy->autorelease();

    return springMoveBy;
}

bool SpringMoveBy::initWithDuration(float duration, const Vec2& position, float mass, float k)
{
    CCASSERT(mass >=0, "Number of mass must be >= 0");

    if (ActionInterval::initWithDuration(duration) && mass>=0)
    {
        _delta = position;
        _mass = mass;
        _k = k;
        _velocity = FIRST_VEROCITY;
        _accel = FIRST_ACCEL;
        _position = FIRST_POSITION;

        return true;
    }

    return false;
}

SpringMoveBy* SpringMoveBy::clone() const
{
    // no copy constructor
    auto a = new (std::nothrow) SpringMoveBy();
    a->initWithDuration(_duration, _delta, _mass, _k);
    a->autorelease();
    return a;
}

void SpringMoveBy::startWithTarget(Node *target)
{
    ActionInterval::startWithTarget(target);
    _previousPos = _startPosition = target->getPosition();
}

void SpringMoveBy::update(float t) // _delta = 現在座標
{
    if (_target)
    {
     auto force = -_k * ( _position - NATURAL_LENGTH ) + _mass * GRAVITY;
     _accel = force / _mass;
     _velocity += _accel * t;
     _velocity = _velocity * FRICTION;
     _position += _velocity * t;

#ifdef CC_ENABLE_STACKABLE_ACTIONS
        Vec2 currentPos = _target->getPosition();

        Vec2 diff = currentPos - _previousPos;
        _startPosition = diff + _startPosition;

        Vec2 newPos = _startPosition;

        newPos = _startPosition + Vec2(0,_position);

        _target->setPosition(newPos);

        _previousPos = newPos;
#else
        _target->setPosition(_startPosition + Vec2(0, _position));
#endif // !CC_ENABLE_STACKABLE_ACTIONS

    }
}

SpringMoveBy* SpringMoveBy::reverse() const
{
    return SpringMoveBy::create(_duration, Vec2(-_delta.x, -_delta.y),
        _mass, _k);
}

動作例