Visual Studio Community 2015 でAndroidアプリを作成する(Xamarin)

普段はモバイル開発とは全く無縁だが、Xamarinが無償提供になったようなのでとりあえずどんなものか試してみる。
ドキュメントは、Creating Mobile Apps with Xamarin.Forms Book First Edition - Xamarin からPDFがダウンロードできる。これがあれば大体のことはできそうだが、1000ページ超でサクッと試すには不向き。

とっかかりやすいチュートリアルとして、Getting Started with Xamarin.Forms - Xamarinを試してみる。
「Hello, Xamarin.Forms」ー「Quickstart」から始める。


  1. 新規プロジェクトの作成
    VisualStudioを起動して、新規プロジェクトを作成する。
    f:id:foohogehoge:20160427214454p:plain
  2. テンプレートの選択
    新しいプロジェクト ダイアログで、「Blank App (Xamarin.Forms Portable)」を選択する。
    プロジェクト名は、チュートリアルに従って「Phoneword」とした。
    f:id:foohogehoge:20160427214715p:plain
  3. Windowsのプラットフォーム
    f:id:foohogehoge:20160427215004p:plain
    チュートリアルには記載されていないが、Windowsのプラットフォームを選択するダイアログがある。2つしかバージョンが選択できなかったので、Target Versionを新しいほう/Minimum Version を古いほうにした。
  4. 空のプロジェクトが作成された
    f:id:foohogehoge:20160427215641p:plain
    なぜか無数のエラーが……ほとんどUWPから発生しているようだ。。。
  5. エラーを解消していく
    以前にニュースリーダーか何かで「テンプレートから作成した直後はエラーの嵐」だとチラ見した記憶があったので適当にググる。下記サイトがヒットしたので参考にさせていただく。
    ytabuchi.hatenablog.com

  6. ソリューションを右クリックして「NuGetパッケージの復元」
    f:id:foohogehoge:20160427221514p:plain

  7. PCLプロジェクトのリビルド

    PCL プロジェクトを[リビルド]

    だそうだが、PCL???と思ったので調べてみると下記が見つかった。
    PCL (Portable Class Library) - Xamarin 3 の新しいコード共有テクニック : XLsoft エクセルソフト
    PCLとは全プラットフォームで共通して使えるライブラリのことで、”移植可能”と書いてあるプロジェクトのようだ。PCL+固有プラットフォームのプロジェクト の組み合わせでアプリケーションを構築する……のかな。。。
    リビルドすると問題なく成功する。


  8. UWPをリビルドする。これも問題なく成功した。
    警告が10個残っているが、とりあえず無視して先へ進む。

  9. PCLプロジェクトを右クリックして新規項目を追加を選択

    f:id:foohogehoge:20160427223500p:plain


  10. Visual C# > Cross-Platform から Forms XML Page を選択。 名前はチュートリアルに従って ”MainPage” とする。

    f:id:foohogehoge:20160427224034p:plain


  11. 追加したMainPage.xamlに元から書かれているコードをすべて消して下記を貼り付ける

    <?xml version="1.0" encoding="UTF-8"?>
    <ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
                 xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                 x:Class="Phoneword.MainPage">
    	<ContentPage.Padding>
    		<OnPlatform x:TypeArguments="Thickness"
                        iOS="20, 40, 20, 20"
                        Android="20, 20, 20, 20"
                        WinPhone="20, 20, 20, 20" />
    	</ContentPage.Padding>
    	<ContentPage.Content>
    		<StackLayout VerticalOptions="FillAndExpand"
                         HorizontalOptions="FillAndExpand"
                         Orientation="Vertical"
                         Spacing="15">
    			<Label Text="Enter a Phoneword:" />
    			<Entry x:Name="phoneNumberText" Text="1-855-XAMARIN" />
    			<Button x:Name="translateButon" Text="Translate" Clicked="OnTranslate" />
    			<Button x:Name="callButton" Text="Call" IsEnabled="false" Clicked="OnCall" />
    		</StackLayout>
    	</ContentPage.Content>
    </ContentPage>
    


  12. ソリューションエクスプローラーでMainPage.xamlを展開して、MainPage.xaml.csを開く

    f:id:foohogehoge:20160427230019p:plain


  13. MainPage.xaml.csの内容を下記で置き換える

    using System;
    using System.Threading.Tasks;
    using Xamarin.Forms;
    
    namespace Phoneword {
        public partial class MainPage : ContentPage {
            string translatedNumber;
    
            public MainPage() {
                InitializeComponent();
            }
    
            void OnTranslate(object sender, EventArgs e) {
                translatedNumber = Core.PhonewordTranslator.ToNumber(phoneNumberText.Text);
                if (!string.IsNullOrWhiteSpace(translatedNumber)) {
                    callButton.IsEnabled = true;
                    callButton.Text = "Call " + translatedNumber;
                }
                else {
                    callButton.IsEnabled = false;
                    callButton.Text = "Call";
                }
            }
    
            async void OnCall(object sender, EventArgs e) {
                if (await this.DisplayAlert(
                        "Dial a Number",
                        "Would you like to call " + translatedNumber + "?",
                        "Yes",
                        "No")) {
                    var dialer = DependencyService.Get<IDialer>();
                    if (dialer != null)
                        dialer.Dial(translatedNumber);
                }
            }
        }
    }
    

    エラーが出るが、この後のステップで解消されそうなので無視して次へ進む。


  14. ソリューションエクスプローラーからApp.csを開いて下記の内容で置き換える。

    using System;
    using Xamarin.Forms;
    
    namespace Phoneword {
        public class App : Application {
            public App() {
                MainPage = new Phoneword.MainPage();
            }
    
            protected override void OnStart() {
                // Handle when your app starts
            }
    
            protected override void OnSleep() {
                // Handle when your app sleeps
            }
    
            protected override void OnResume() {
                // Handle when your app resumes
            }
        }
    }
    


  15. PCLプロジェクトにC#のクラスを新規追加する。名前は”PhoneTranslator”として内容を下記で置き換える。

    using System.Text;
    
    namespace Core {
        public static class PhonewordTranslator {
            public static string ToNumber(string raw) {
                if (string.IsNullOrWhiteSpace(raw))
                    return null;
    
                raw = raw.ToUpperInvariant();
    
                var newNumber = new StringBuilder();
                foreach (var c in raw) {
                    if (" -0123456789".Contains(c))
                        newNumber.Append(c);
                    else {
                        var result = TranslateToNumber(c);
                        if (result != null)
                            newNumber.Append(result);
                        // Bad character?
                        else
                            return null;
                    }
                }
                return newNumber.ToString();
            }
    
            static bool Contains(this string keyString, char c) {
                return keyString.IndexOf(c) >= 0;
            }
    
            static readonly string[] digits = {
                "ABC", "DEF", "GHI", "JKL", "MNO", "PQRS", "TUV", "WXYZ"
            };
    
            static int? TranslateToNumber(char c) {
                for (int i = 0; i < digits.Length; i++) {
                    if (digits[i].Contains(c))
                        return 2 + i;
                }
                return null;
            }
        }
    }
    


  16. PCLプロジェクトに IDialer インターフェイスを追加して下記の内容とする。

    using System;
    
    namespace Phoneword
    {
        public interface IDialer
        {
            bool Dial(string number);
        }
    }
    


  17. 次のステップはiPhon用のコーディングだが、実機がないのでパスしてAndroidの項へ飛ぶ。と、その前にMainPage.xaml.csでコンパイルエラーが出ていて気になるので解決しておく。
    チュートリアルはここから各プラットフォームのコーディングを行って終了のようだ。
    つまり、現時点で共通部分であるPCLプロジェクトにコンパイルエラーが出ているのはマズいはずである。
    「InitializeComponentメソッドがない」と怒られている。WindowsForm開発でもなじみのあるメソッドだが、どこに実装されているのか?おそらくMainPage.xamlあたりにあるはずだが……探してもない。コピペミスを疑ってチュートリアルからMainPage.xamlを再度貼り付けなおしたらエラーが消えた。
    InitializeComponentの実体を探すとobj\Debugの下に「Phoneword.MainPage.xaml.g.cs」が作成されておりそこに定義してあった。
    ……このエラーはこの後のステップでも度々発生するが、xamlを編集して保存するとOKな様子。

  18. Androidプロジェクトに PhoneDialer クラスを追加して以下の内容で置き換える。

    using Android.Content;
    using Android.Telephony;
    using Phoneword.Droid;
    using System.Linq;
    using Xamarin.Forms;
    
    using Uri = Android.Net.Uri;
    
    [assembly: Dependency(typeof(PhoneDialer))]
    
    namespace Phoneword.Droid {
        public class PhoneDialer : IDialer {
            public bool Dial(string number) {
                var context = Forms.Context;
                if (context == null)
                    return false;
    
                var intent = new Intent(Intent.ActionCall);
                intent.SetData(Uri.Parse("tel:" + number));
    
                if (IsIntentAvailable(context, intent)) {
                    context.StartActivity(intent);
                    return true;
                }
    
                return false;
            }
    
            public static bool IsIntentAvailable(Context context, Intent intent) {
                var packageManager = context.PackageManager;
    
                var list = packageManager.QueryIntentServices(intent, 0)
                    .Union(packageManager.QueryIntentActivities(intent, 0));
    
                if (list.Any())
                    return true;
    
                var manager = TelephonyManager.FromContext(context);
                return manager.PhoneType != PhoneType.None;
            }
        }
    }
    


  19. ソリューションエクスプローラーでAndroidプロジェクトのプロパティをダブルクリック
    f:id:foohogehoge:20160428004216p:plain

  20. Android Manifest を選択し、Required permissionsから「CALL_PHONE」にチェックする。
    f:id:foohogehoge:20160428004622p:plain
    また、Package name が空だとエラーダイアログが表示されるので、Package name に "Phoneword" と入力する。

  21. 他のプラットフォームは追々試すとして、とりあえずAndroidだけビルドしてみる。
    「"ConvertResourcesCases"タスクが予期せずに失敗しました」

  22. ググったところ、ソリューションのパスに日本語が含まれているとNGらしいのでソリューションを移動してリトライしたところうまくビルドできた。

  23. 普段使用しているNexsus5を開発者モードにし、USBデバッグ可にしてPCと接続する。
    開発者モードは、Nexsus5の設定>端末情報 で表示されるビルド番号を連打すると使用できるようになる。
    設定>開発者向けオプションからUSBデバッグをONにする。

  24. VisualStudioでAndroidプロジェクトをスタートアッププロジェクトに設定し、デバッグ実行する。
    しばらくすると、Nexsus側に画面が表示された。

    f:id:foohogehoge:20160428013902j:plain


msys2を更新したらPATH設定が消えた??

msys2からupdate-coreコマンドが消えたあとあたりのタイミングで、
msys2を起動してもWindows環境変数のPATHを認識しなくなった。

gitはmsys2でインストールしたものではなく別途Git for Windowsをインストールしているのでちょっと困った。

msys2の起動に使用していたバッチファイル(msys2_shell.bat)を確認すると
「start_shell.cmd」を呼び出すだけのシンプルなものに変更されていた。
「start_shell.cmd」の中身を確認してみると、

rem To export full current PATH from environment into MSYS2 use '-use-full-path' parameter
rem or uncomment next line
rem set MSYS2_PATH_TYPE=inherit

 と書かれていた。

つまり、msys2_shell.batでstart_shell.cmdを呼び出す際に -use-full-path を指定するか
start_shell.cmdを編集して

rem set MSYS2_PATH_TYPE=inherit 

 を

set MSYS2_PATH_TYPE=inherit 

 にすればOKらしい。
start_shell.cmdを呼び出す際に -use-full-path を指定したところGit for Windowsが使えるようになった。

oracleからメールを送る

こんな感じで。重い処理の最後にメール送信するようにしとくとか。

declare
  type ADDRESSES IS TABLE OF VARCHAR2(40);
  procedure SendMail(mail_from    in varchar2,
                     mail_to      in ADDRESSES,
                     mail_subject in varchar2,
                     mail_text    in varchar2) is
    smtp  utl_smtp.connection;
    MAIL_SERVER     constant varchar2(40)   := 'MAIL_SERVER';     -- メールサーバ名、または、IP
    SMTP_PORT       constant binary_integer := 25;
    CRLF            constant varchar2(4)    := UTL_TCP.CRLF;
    TAB             constant varchar2(2)    := chr(9);
    MIME_CHARSET    constant varchar2(250)  := 'ISO-2022-JP';
    CONV_CHARSET    constant varchar2(250)  := 'ISO2022-JP';
    SUBJECT_LENGTH  constant number         := 14; -- Subjectは14文字ずつ区切って送信

    len number;
    pos number;
  begin
  
    /* メール送信 */
    smtp := utl_smtp.open_connection(MAIL_SERVER, 25);
    utl_smtp.helo(smtp, MAIL_SERVER);
    utl_smtp.mail(smtp, mail_from);
  
    for idx in mail_to.FIRST .. mail_to.LAST loop
      utl_smtp.rcpt(smtp, mail_to(idx));
    end loop;
    utl_smtp.open_data(smtp);
  
    utl_smtp.write_data(smtp, 'To:');
    for idx in mail_to.FIRST .. mail_to.LAST loop
      utl_smtp.write_data(smtp, mail_to(idx));
      utl_smtp.write_data(smtp, ', ');
      utl_smtp.write_data(smtp, CRLF);
      utl_smtp.write_data(smtp, TAB);
    end loop;
    utl_smtp.write_data(smtp, CRLF);
  
    utl_smtp.write_data(smtp, 'From:' || mail_from || CRLF);
    utl_smtp.write_data(smtp, 'Subject:');
    -- Subjectはある程度区切って送信しないと文字化けする
    pos := 1;
    len := length(mail_subject);
    while (pos <= len) loop
      if (pos > 1) then 
        utl_smtp.write_data(smtp, CRLF||TAB);
      end if;
      utl_smtp.write_data(smtp, '=?'||MIME_CHARSET||'?B?');
      utl_smtp.write_raw_data(smtp,
        utl_encode.base64_encode(
          utl_raw.cast_to_raw(convert(substr(mail_subject, pos, SUBJECT_LENGTH), CONV_CHARSET))
        )
      );
      utl_smtp.write_data(smtp, '?=');
      pos := pos + SUBJECT_LENGTH;
    end loop;
    utl_smtp.write_data(smtp, CRLF);
    utl_smtp.write_data(smtp, 'MIME-Version: 1.0' || CRLF);
    utl_smtp.write_data(smtp, 'Content-Type: text/plain;' || CRLF);
    utl_smtp.write_data(smtp, TAB || 'charset: ' || MIME_CHARSET || CRLF);
    utl_smtp.write_data(smtp, 'Content-Transfer-Encoding: base64' || CRLF);
    utl_smtp.write_data(smtp, CRLF);
    utl_smtp.write_raw_data(smtp,
      utl_encode.BASE64_ENCODE(
        utl_raw.cast_to_raw(mail_text)
      ));
    utl_smtp.close_data(smtp);
    utl_smtp.quit(smtp);
  
  end SendMail;
begin
  SendMail('sender', ADDRESSES('mail@example.com'), 'Subject', 'Hello,World.');
end;
/

msys2でgithubへpushできない

最近は、自宅も職場もmsys2+Git for Windowsで安定している。
githubにプロジェクトを作っていざpushしようとすると、

bash: /dev/tty: No such device or address
error: failed to execute prompt script (exit code 1)
fatal: could not read Username for 'https://github.com': Invalid argument

 のようなエラーが発生した。

試しに、Git for Windows付属のGitBashで試すと

Username for 'https://github.com':  

 のようにプロンプトが表示され、問題なくpushできた。

……しかしどうしてもzshを使いたい。
msys2の何かが問題なわけだがエラーメッセージをググっても要領を得ない。
ttyという単語から、ダメもとで

winpty git push origin master

 のようにしてみるとうまくいった。

チェックアウトしてあるすべてのブランチをプル

# チェックアウトしてあるすべてのブランチをプル
function gp() {
    current=$(git rev-parse --abbrev-ref HEAD)
    for b in $(git for-each-ref refs/heads --format='%(refname:short)')
    do
        git branch -vv | grep "${b}.*behind" > /dev/null
        if [ "$?" -eq 0 ]
        then
            git checkout $b&&git pull
        fi
    done
    git checkout -q $current
}

zshでredmineのチケットタイトルを取得する

function get-redmine-subject() {
    echo $(curl -s -X GET http://[redmineのアドレス]/issues/$1.xml | sed -n -e 's/.*<subject>\(.*\)<\/subject>.*/\1/p')
}

のような関数を.zshrcに作成する。

% get-redmine-subject 1234

とすると、チケット番号1234番のタイトルが取得できる。