Medoly – Google Play の Android アプリ
修正内容
再生中に着信があった場合、着信終了後の再開をオプション化
少しコードを修正した上で、電話後に再生を再開する/しないの判断はユーザー側に委ねた方が良いという考えもあり、とりあえず受話後に再生を再開する処理をオプション扱いにしました。設定画面で切り替えられます。
これは要望が上がったので追加。
再生中に歌詞の表示タイミング(オフセット)を調整することができますが、これを曲毎に毎回リセットせず、保存できるようにしました。ただ、これはズレがある歌詞に対して微調整をするための機能だったので、オプション扱いとします。設定画面でリセットする/しないは切り替えられます。
サムネイルの作成処理を少し見直しました。エラーのサムネイルを読み飛ばすようにしたので、エラーとなるサムネイルが沢山存在する場合に、再生キューのスクロールが高速化されます。…されるはずです。
アプリ起動時に、初回タブ画面が再生キューの状態からタブを切り替えると、タブ画面ではなくタブウィジェットがタブ画面に引っ張られるようにアニメーションしてしまうという問題がありました。原因がさっぱり分からなかったのですが、tabhostのタブ変更イベント( TabHost.OnTabChangeListener )の設定タイミングを、タブ登録の前に持ってきたところ発生しなくなりました。
操作開始前にTabHost.OnTabChangeListener を1回以上発生させておく必要があるのかなぁ、という予測。
以下の内容は、現在のところ自分で認識している問題です。原因調査中です。
再生キューのサムネイル生成時に、エラーが発生する場合があったので修正しました。
こちらで再現することができないのですが、AndroidのDBにあるメディア情報と実際のファイル状態に齟齬があると発生するような感じです。とりあえずエラーの発生箇所は分かるので、問題が起きないようにプログラムを変更しました。
再生キューの問題に絡み、再生失敗時に再生に失敗したメディアを自動的に再生キューから削除します。
標準では削除しませんが、次に再生順が来た時は自動的に読み飛ばします(再生キューを直接タップすれば再度再生します)。なお、これは再生キューから外れると解除されます。また、アプリを起動した際に再生キューを再読込する場合にも解除されます。(要は、再生キューに保存されたフラグがクリアされた場合。)
再生キューの再生済み状態が保存されない場合があったので修正しました。これは、アプリを2回再起動すると発生します。
要因は、起動中の意図しないタイミングで再生済みがクリアされた状態で設定が保存されていたためです。対策としては、起動時に再生済み状態を保存するように修正しました。また、これに伴い全体的に設定の保存タイミングを調整しました。
再生位置を1秒毎に保存し、次回起動時に再生位置のリジュームを行うようにしました。今までも、画面を閉じた際や停止時に再生位置を保存していたのですが、これだとバックグラウンド再生時に全く保存されるタイミングが無いという…。適当に実装してたものですが、きちんとレジュームできるように作り直し。
再生キューのスクロール量が多いと、最後まで完全にスクロールされない場合がありました。これは、AndroidでsmoothScrollToPositionFromTopを利用すると発生する場合があるそうです。なお、設定でアニメーションを無効にすると、setSelectionFromTopを使用するので、この影響はありません。
原因はAndroid側の問題のようで、対応策としてはイベントで再スクロールさせています。ただ、これでも、スクロールに失敗したり、スクロールが連続して発生するために妙な動きをする場合があるので、少し様子を見てみます。これがダメなようならアニメーションを完全にカットしてsetSelectionFromTopのみに絞った方が良いかもししれません。個人的に、スクロールアニメーションは、上/下のスクロールが視覚的に認識しやすいので、あまり無くしたくはないのですが…。
これは、将来的なAndroidのバージョンで治る可能性もあります。
再生キューを編集モードにした上で、キュー項目を削除すると、選択中メディアに自動的にスクロールする問題を修正しました。
再生キューの内容に変化があった場合に、全てスクロールする処理になっていたため、項目削除時にもスクロールが発生していました。これに対し、スクロールする場所を個別に指定して、発生タイミングを限定しました。
…単にnullチェックを怠っていただけです。タイトルがnullの場合はファイル名を表示、アーティストがnullの場合は何も表示しないように変更しました。
元々、問題発生時にエラーをメールで送信してもらう機能をつけていたのですが、Androidアプリには標準でエラーレポート機能が備わっているため、不要なので削除しました。
…単純に標準のエラーレポート機能を知らなかっただけです。初めてアプリ作ったので、この辺の知識が足りてませんでした。すいません…。
ただ、開発用のデバックビルドでは動作するようにしています。開発用なので、普通の人の目に触れることはありません。
なお、実際にエラー発生時はエラーレポートを送信していただけると非常に助かります。
実装してたエラーレポート機能 |
その他、諸々の修正をしてますが、細かいので省略。というか、忘れました。
ファイル読込みがエラーとなる問題と、再生順ボタンを押すと落ちる問題を修正しました。
ハッキリ言うと、デバッグ用コードの消し忘れです…。 すいません。
先日、2013-10-08に、Android音楽プレイヤー のMedolyver.1.0.0を公開した事に伴い、せっかくなのでここを再利用してサポート等を行いたいと思います。
本アプリは8月末頃から作り始めましたので、約3ヶ月かかりましたが、 無事公開することが出来ました。初のAndroidアプリ開発だったので拙い部分や至らない点もあるかと思います。まだ最初に想定してた機能の8割ぐらいまでしか実装されていませんし、作ってる最中も色々と考えが浮かんできたので、まだまだ作り足りない感じです。何とか頑張って形にしてみたいと思いますので、今後ともよろしくお願いします。
設定の書き込み・読み込みを行うための抽象クラス。
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 アカウントが機能追加されたので、こちらでブログを作成してみる。