Medoly ver.1.0.7

Medoly – Google Play の Android アプリ

ちょっと微調整…。
あんまり頻繁にアップデートすると鬱陶しそうなので、今後はちょっと抑えめに。

修正内容

再生中に着信があった場合、着信終了後の再開をオプション化

以前から、停止状態でも電話着信時に再生が開始されてしまうことがあるという問題がありました。着信時に前回再生してた曲が流れ始めるので、うっかり受話ボタンを押そうものなら通話中にバックグラウンドで曲が流れるという、人によっては大変に恐ろしい不具合なのですが、事象の再現性がなく、今ひとつ要因に確信が持てないでいました。これは機種依存の可能性もあり、私の使用しているISW 13HTでしか発生しないかもしれません。
ただ、再生開始のメソッドを呼ぶ場所は限られているので、恐らくここだろう、というかここしかないだろう、という目星はついていました。「受話前に音楽が再生されていた場合は、通話終了後に再生を再開する」という処理を入れていたのですが、この辺のイベントやフラグの処理が想定外の順序で走っているのかもしれません。
少しコードを修正した上で、電話後に再生を再開する/しないの判断はユーザー側に委ねた方が良いという考えもあり、とりあえず受話後に再生を再開する処理をオプション扱いにしました。設定画面で切り替えられます。
これでしばらく様子見ということで。

歌詞の前後に空白を入れるように変更

歌詞を表示させた際、歌詞が画面端に詰まってしまう場合に1行間を開けるようにしました。単に個人的な見やすさの問題です。

Medoly ver.1.0.6 リリース

修正内容

歌詞のオフセット保存機能追加

これは要望が上がったので追加。
再生中に歌詞の表示タイミング(オフセット)を調整することができますが、これを曲毎に毎回リセットせず、保存できるようにしました。ただ、これはズレがある歌詞に対して微調整をするための機能だったので、オプション扱いとします。設定画面でリセットする/しないは切り替えられます。

再生キューのサムネイル作成処理を修正

サムネイルの作成処理を少し見直しました。エラーのサムネイルを読み飛ばすようにしたので、エラーとなるサムネイルが沢山存在する場合に、再生キューのスクロールが高速化されます。…されるはずです。

再生キューのアニメーションスクロールの廃止

Ver. 1.0.2で修正したアニメーションスクロールですが、再生キューに多量の登録があると、アニメーションスクロールがまともに追従しなくなる事がわかったので、中途半端に動くぐらいなら邪魔なのでやめます。スクロール量に応じて切り替える事も考えましたが、基準が分からない上に端末毎に異なる可能性もあるので…。
smoothScrollToPositionFromTopメソッドは、大量のスクロールには全く向かないものでした。もっと細かくクロールさせる用途向けの処理ですね。

メディアスキャン処理の変更

Android 4.4でメディアスキャンがエラーとなっていたので修正。Intent.ACTION_MEDIA_MOUNTEDをsendBroadcastさせていたのですが、これはあまりよろしくないようで…。処理を修正して、MediaScannerConnection#scanFileを実行するようにしました。
 

通知アイコンの表示タイミング変更

これは私の環境だけかもしれませんが、停止時の通知アイコンを表示させていると、アプリを終了させtも無関係なタイミングで通知アイコンが再表示されてしまう問題があったため、通知アイコンを表示させるタイミングを少し調整しました。

アニメーションONの状態で初回タブ切換時に、タブが一緒にアニメーションしてしまう問題修正

アプリ起動時に、初回タブ画面が再生キューの状態からタブを切り替えると、タブ画面ではなくタブウィジェットがタブ画面に引っ張られるようにアニメーションしてしまうという問題がありました。原因がさっぱり分からなかったのですが、tabhostのタブ変更イベント( TabHost.OnTabChangeListener )の設定タイミングを、タブ登録の前に持ってきたところ発生しなくなりました。
操作開始前にTabHost.OnTabChangeListener を1回以上発生させておく必要があるのかなぁ、という予測。

その他

メッセージ等修正。

その他現在認識している問題

 

以下の内容は、現在のところ自分で認識している問題です。原因調査中です。

  • 登録時に二重登録されてしまう場合がある。
  • 使ってないのにバッテリーを異常に消費することがある。
  • 停止状態でも受話時に再生が再開されてしまう。
  • Android 4.4でスクロールつまみが表示されない。Andoroid 4.4のバグっぽい。対応するか否かは考え中。

Medoly ver.1.0.5 リリース

昨日公開したバージョンに速攻でエラーレポートが上がってたので、その修正…。
レポート上げてくれた人、どなたかは存じませんがありがとうございます。

サムネイル生成時にエラーが発生する問題を修正

再生キューのサムネイル生成時に、エラーが発生する場合があったので修正しました。
こちらで再現することができないのですが、AndroidのDBにあるメディア情報と実際のファイル状態に齟齬があると発生するような感じです。とりあえずエラーの発生箇所は分かるので、問題が起きないようにプログラムを変更しました。

Medoly 1.0.2 リリース

Medolyのバージョン1.0.2をリリースしました。今回は以下のような変更があります。

メディアが再生できない問題を修正

音楽ファイルのタグ(メタ情報)の読込み失敗時に、再生その物がに失敗する問題がありました。失敗時は、メタデータの読込みをせずに再生を開始します。こんな所のエラー処理が杜撰だったのは、自分でも流石にどうかと思います。

再生に失敗したメディアが再生キューに存在する際の問題を修正

再生できないメディアが再生キューに登録されていた場合、再生キューの順序制御や停止に問題が発生するので、内部の処理を色々見直しました。 これに伴い、再生キューに関わる色々な問題が修正されています。
ただ、自分が再生キューの状態遷移をきちんと把握しきれてないため、まだちょっと怪しいかもしれません…。

再生失敗したメディアを再生キューから除外するオプションを追加

再生キューの問題に絡み、再生失敗時に再生に失敗したメディアを自動的に再生キューから削除します。
標準では削除しませんが、次に再生順が来た時は自動的に読み飛ばします(再生キューを直接タップすれば再度再生します)。なお、これは再生キューから外れると解除されます。また、アプリを起動した際に再生キューを再読込する場合にも解除されます。(要は、再生キューに保存されたフラグがクリアされた場合。)

再生済み状態が保存されない問題

再生キューの再生済み状態が保存されない場合があったので修正しました。これは、アプリを2回再起動すると発生します。
要因は、起動中の意図しないタイミングで再生済みがクリアされた状態で設定が保存されていたためです。対策としては、起動時に再生済み状態を保存するように修正しました。また、これに伴い全体的に設定の保存タイミングを調整しました。

再生位置を保存

再生位置を1秒毎に保存し、次回起動時に再生位置のリジュームを行うようにしました。今までも、画面を閉じた際や停止時に再生位置を保存していたのですが、これだとバックグラウンド再生時に全く保存されるタイミングが無いという…。適当に実装してたものですが、きちんとレジュームできるように作り直し。

再生キューのスクロールが中途半端に終わる問題修正

再生キューのスクロール量が多いと、最後まで完全にスクロールされない場合がありました。これは、AndroidでsmoothScrollToPositionFromTopを利用すると発生する場合があるそうです。なお、設定でアニメーションを無効にすると、setSelectionFromTopを使用するので、この影響はありません。

Issue 36062 – android – AbsListView.smoothScrollToPositionFromTop does not scroll correctly when position is the next visible item – Android Open Source Project – Issue Tracker – Google Project Hosting
listview – smoothScrollToPosition after notifyDataSetChanged not working in android – Stack Overflow
listview – Android smoothScrollTo not invoking onScrollStateChanged – Stack Overflow

原因はAndroid側の問題のようで、対応策としてはイベントで再スクロールさせています。ただ、これでも、スクロールに失敗したり、スクロールが連続して発生するために妙な動きをする場合があるので、少し様子を見てみます。これがダメなようならアニメーションを完全にカットしてsetSelectionFromTopのみに絞った方が良いかもししれません。個人的に、スクロールアニメーションは、上/下のスクロールが視覚的に認識しやすいので、あまり無くしたくはないのですが…。
これは、将来的なAndroidのバージョンで治る可能性もあります。

自動スクロール設定をONにしていると、項目削除時にスクロールしてしまう問題修正

再生キューを編集モードにした上で、キュー項目を削除すると、選択中メディアに自動的にスクロールする問題を修正しました。
再生キューの内容に変化があった場合に、全てスクロールする処理になっていたため、項目削除時にもスクロールが発生していました。これに対し、スクロールする場所を個別に指定して、発生タイミングを限定しました。

アクションバーのタイトルやアーティストが null になる問題修正

…単にnullチェックを怠っていただけです。タイトルがnullの場合はファイル名を表示、アーティストがnullの場合は何も表示しないように変更しました。

メール送信機能を無効化

元々、問題発生時にエラーをメールで送信してもらう機能をつけていたのですが、Androidアプリには標準でエラーレポート機能が備わっているため、不要なので削除しました。
…単純に標準のエラーレポート機能を知らなかっただけです。初めてアプリ作ったので、この辺の知識が足りてませんでした。すいません…。
ただ、開発用のデバックビルドでは動作するようにしています。開発用なので、普通の人の目に触れることはありません。
なお、実際にエラー発生時はエラーレポートを送信していただけると非常に助かります。

実装してたエラーレポート機能

検索条件の見直し

検索時に、タイトル項目から再検索すると検索結果が正しく反映されない問題を修正しました。

再生停止時に、通知アイコンを表示しないオプションを追加

個人的には、再生停止時に通知バーから再呼び出しが出来るので便利なのですが、鬱陶しいと感じる場合もあると思うので、切り替えられるようにオプションに追加しました。

その他、諸々の修正をしてますが、細かいので省略。というか、忘れました。


今回はそんな感じで。
設定項目を多少いじってるので、設定画面の設定項目も増え画面も間延びして不格好なのですが、余力があればもう少し整理します…。

追記

設定画面が開けない問題があることが分かったので、修正しました…。申し訳ありません。
現在、Ver. 1.0.3となります。

さらに追記

ファイル読込みがエラーとなる問題と、再生順ボタンを押すと落ちる問題を修正しました。
ハッキリ言うと、デバッグ用コードの消し忘れです…。 すいません。

Medoly ver.1.0.0 公開

Medoly

先日、2013-10-08に、Android音楽プレイヤー のMedolyver.1.0.0を公開した事に伴い、せっかくなのでここを再利用してサポート等を行いたいと思います。

本アプリは8月末頃から作り始めましたので、約3ヶ月かかりましたが、 無事公開することが出来ました。初のAndroidアプリ開発だったので拙い部分や至らない点もあるかと思います。まだ最初に想定してた機能の8割ぐらいまでしか実装されていませんし、作ってる最中も色々と考えが浮かんできたので、まだまだ作り足りない感じです。何とか頑張って形にしてみたいと思いますので、今後ともよろしくお願いします。

AbstractSettings.cs

設定の書き込み・読み込みを行うための抽象クラス。
Visual Studio標準の設定保存・読み込みクラスが、あまりにも使いにくいので作成。

using System;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml.Serialization;
using Microsoft.Win32;



/// 
/// アプリケーションの設定情報を操作する抽象クラス。
/// 
/// 
/// 設定の書き出し、読み込み、削除を行う。
/// 操作対象は、継承先で定義されたpublicインスタンスフィールド。
/// 使用可能な形式を次に示す。
/// 
/// 
///     INI:
///     INIファイル(*.ini)を使用する。対象はプリミティブ型のみ。
/// 
/// 
///     XML:
///     XMLファイル(*.xml)を使用する。対象はシリアライズ可能なオブジェクト。
/// 
/// 
///     レジストリ:
///     レジストリを使用する。対象はプリミティブ型のみ。
/// 
/// 
/// 
/// 次の規則に従って初期値が設定される。
/// 
/// 
///     ディレクトリパス:
///     呼び出し元アセンブラディレクトリ。(変更可能)
/// 
/// 
///     ファイル名/キー:
///     呼び出し元アセンブラ名 + 拡張子。(変更可能)
/// 
/// 
///     セクション名/サブキー:
///     クラス名。
/// 
/// 
///     設定名/データ名:
///     変数名。
/// 
/// 
/// 
public abstract class AbstractSettings
{

    #region 設定情報

    /// セクション名。
    protected readonly string SECTION_NAME;
    /// 設定情報を定義(継承先のpublicインスタンスプロパティを設定情報と定義する)。
    protected readonly PropertyInfo[] SETTINGS_PROPERTIES;

    /// 呼び出し元のアセンブリパス。
    protected readonly string EXEC_PATH;
    /// 設定ファイルの絶対ディレクトリパス。
    protected string DIR_PATH;
    /// 設定ファイルの拡張子を除くファイル名。
    protected string FILE_NAME;

    /// 
    /// 新しいインスタンスを初期化する。
    /// 
    public AbstractSettings()
    {
        // 設定情報
        SECTION_NAME = this.GetType().Name;
        SETTINGS_PROPERTIES = this.GetType().GetProperties(BindingFlags.Public|BindingFlags.Instance|BindingFlags.DeclaredOnly);
        
        // パス情報
        EXEC_PATH = Assembly.GetExecutingAssembly().Location;
        DIR_PATH  = Path.GetDirectoryName(EXEC_PATH);
        FILE_NAME = Path.GetFileName(EXEC_PATH);
    }

    /// 
    /// 設定ファイルのディレクトリパスを変更する。
    /// 
    /// ディレクトリパス(絶対パス/相対パス)。public void SetDirPath(string dir)
    {
        // 指定が無効の場合は何もしない。
        if (dir == null || dir == "") return;

        if (Path.IsPathRooted(dir))
            DIR_PATH = dir; // 絶対パス
        else
            DIR_PATH = Path.Combine(DIR_PATH, dir); // 相対パス
    }

    /// 
    /// 設定ファイルのファイル名を変更する。
    /// 
    /// 拡張子を除くファイル名。public void SetFileName(string file)
    {
        // 指定が無効の場合は何もしない。
        if (file == null || file == "") return;

        FILE_NAME = file;
    }

    #endregion



    #region INIファイル

    /// 
    /// INIファイルのパスを取得する。
    /// 
    public string IniPath
    {
        get
        {
            return Path.Combine(DIR_PATH, FILE_NAME + ".ini");
        }
    }

    /// 
    /// INIファイルから設定を読み込む。
    /// 
    public void ReadIni()
    {
        string path = IniPath;
        if (!File.Exists(path)) return;

        foreach (PropertyInfo info in SETTINGS_PROPERTIES)
        {
            // プリミティブ型またはString型以外の値は無視
            if (!info.PropertyType.IsPrimitive &&
                info.PropertyType != typeof(string))
                continue;
            
            // INIファイルの読み込み
            StringBuilder value = new StringBuilder(256);
            GetPrivateProfileString(
                SECTION_NAME, info.Name, "", value, (uint)value.Capacity, path);
            if (value.Length > 0)
                info.SetValue(this, Convert.ChangeType(value.ToString(), info.PropertyType, null), null);
        }
    }

    /// 
    /// INIファイルに設定を書き込む。
    /// 
    public void WriteIni()
    {
        string path = IniPath;

        foreach (PropertyInfo info in SETTINGS_PROPERTIES)
        {
            // プリミティブ型またはString型意外の値は無視
            if (!info.PropertyType.IsPrimitive &&
                info.PropertyType != typeof(string))
                continue;

            // INIファイルの書き込み
            string value = info.GetValue(this, null).ToString();
            if (info.PropertyType == typeof(string))
                value = """ + value + """;
            WritePrivateProfileString(
                SECTION_NAME, info.Name, value, path);
        }
    }

    /// 
    /// INIファイルを削除する。
    /// 
    public void DeleteIni()
    {
        string path = IniPath;
        File.Delete(path);
    }

    /// 
    /// INIファイル読み込み関数宣言。(Win32API)
    /// 
    /// セクション名。/// キー名。/// デフォルト値。/// 値。/// 値のサイズ。/// INIファイルパス。/// 成功の場合は取得した文字数。
    [DllImport("kernel32.dll", EntryPoint = "GetPrivateProfileString")]
    protected static extern uint GetPrivateProfileString(
            string lpApplicationName,
            string lpEntryName,
            string lpDefault,
            StringBuilder lpReturnedString,
            uint nSize,
            string lpFileName);

    /// 
    /// INIファイル書き込み関数宣言。(Win32API)
    /// 
    /// セクション名。/// キー名。/// 値。/// INIファイルパス。/// 成功の場合は0以外。
    [DllImport("kernel32.dll", EntryPoint = "WritePrivateProfileString")]
    protected static extern uint WritePrivateProfileString(
            string lpApplicationName,
            string lpEntryName,
            string lpEntryString,
            string lpFileName);

    #endregion



    #region XMLファイル

    /// 
    /// XMLファイルのパスを取得する。
    /// 
    public string XmlPath
    {
        get
        {
            return Path.Combine(DIR_PATH, FILE_NAME + ".xml");
        }
    }

    /// 
    /// XMLファイルから設定を読み込む。
    /// 
    public void ReadXml()
    {
        string path = XmlPath;
        if (!File.Exists(path)) return;

        // XMLファイルの読み込み
        XmlSerializer serializer = new XmlSerializer(this.GetType());
        using (FileStream inputStream = new FileStream(path, FileMode.Open))
        {
            Object set = serializer.Deserialize(inputStream);
            foreach (PropertyInfo info in SETTINGS_PROPERTIES)
            {
                info.SetValue(this, Convert.ChangeType(info.GetValue(set, null), info.PropertyType, null), null);
            }
        }
    }

    /// 
    /// XMLファイルに設定を書き込む。
    /// 
    public void WriteXml()
    {
        string path = XmlPath;

        // XMLファイルの書き込み
        XmlSerializer serializer = new XmlSerializer(this.GetType());
        using (FileStream outputStream = new FileStream(path, FileMode.Create))
        {
            serializer.Serialize(outputStream, this);
        }
    }

    /// 
    /// XMLファイルを削除する。
    /// 
    public void DeleteXml()
    {
        string path = XmlPath;
        File.Delete(path);
    }

    #endregion



    #region レジストリ

    /// 
    /// レジストリのキーを取得する。
    /// 
    public string RegistryPath
    {
        get
        {
            return "Software" + FILE_NAME + "" + SECTION_NAME;
        }
    }

    /// 
    /// レジストリから設定を読み込む。
    /// 
    public void ReadRegistry()
    {
        string path = RegistryPath;

        using (RegistryKey key = Registry.CurrentUser.OpenSubKey(path, false))
        {
            if (key == null) return;

            foreach (PropertyInfo info in SETTINGS_PROPERTIES)
            {
                // プリミティブ型またはString型以外の値は無視
                if (!info.PropertyType.IsPrimitive &&
                    info.PropertyType != typeof(string))
                    continue;

                // レジストリの読み込み
                object value = key.GetValue(info.Name);
                if (value != null)
                    info.SetValue(this, Convert.ChangeType(value, info.PropertyType, null), null);
            }
        }
    }

    /// 
    /// レジストリに設定を書き込む。
    /// 
    public void WriteRegistry()
    {
        string path = RegistryPath;

        using (RegistryKey key = Registry.CurrentUser.CreateSubKey(path))
        {
            foreach (PropertyInfo info in SETTINGS_PROPERTIES)
            {
                // プリミティブ型またはString型意外の値は無視
                if (!info.PropertyType.IsPrimitive &&
                    info.PropertyType != typeof(string))
                    continue;

                // レジストリの書き込み
                string value = info.GetValue(this, null).ToString();
                key.SetValue(info.Name, info.GetValue(this, null));
            }
        }
    }
    
    /// 
    /// レジストリを削除する。
    /// 
    public void DeleteRegistry(string path)
    {
        string paht = RegistryPath;
        Registry.CurrentUser.DeleteSubKeyTree(path);
    }

    #endregion

}

開始

Google Apps アカウントが機能追加されたので、こちらでブログを作成してみる。