Social.framework & Account.framework
社内のスマホエンジニア勉強会で発表したのでブログにしときます。
サンプルアプリはGitHubに公開しているのでどうぞ。
https://github.com/i2key/SocialframeworkExample
最初のほうは当たり前なツイートシートとかの内容なので、試してて引っかかったところのみ掲載します。
Facebookに対してAPIコールを行う(SLRequestを使う)場合、パーミッションをFacebookの指定する順序で取得しないといけません。
基本的には以下の順にパーミッションをとる必要があります。
Step 1: Request basic profile information
ex) email, user_birthday , user_location
Step 2: Request read permissions
ex) user_about_me, read_stream
Step 3: Request publish permissions
ex) publish_actions, publish_stream
参考:https://developers.facebook.com/docs/howtos/ios-6/
そのため、最初からemail,publish_action のような組み合わせではパーミッション取得時に以下のようなエラーがでます。
The Facebook server could not fulfill this access request: The app must ask for a basic read permission at install time.
例えば、FacebookのAPIをSLRequestで叩いてウォールポストを行いたい場合。
準備として・・・
FacebookのAPP登録画面(https://developers.facebook.com/apps)にてAppIDを発行します。
・「アプリをFacebookに統合する方法を選択」にて「ネイティブiOSアプリ」のバンドルIDを入力します。
・「詳細設定」の「認証」でAppTypeを「Native/Desktop」。AppSecretinClientを「いいえ」
初めにemailのパーミッションを取得します。(上記step1)
ACAccountType *facebookType = [self.accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook]; NSDictionary *options = @{ ACFacebookAppIdKey : @"AppID", ACFacebookPermissionsKey : @[@"email"], ACFacebookAudienceKey : ACFacebookAudienceOnlyMe}; [self.accountStore requestAccessToAccountsWithType:facebookType options:options completion:^(BOOL granted, NSError *error) { if(granted){ //ActionSheet表示する処理 } }]
次にpublish_actionsのパーミッションを取得して(上記step3)、ポストします。
ACAccountStore *accountStore = [[ACAccountStore alloc] init]; ACAccountType *facebookType = [accountStore accountTypeWithAccountTypeIdentifier:ACAccountTypeIdentifierFacebook]; NSDictionary *options = @{ACFacebookAppIdKey : @"AppID", ACFacebookPermissionsKey : @[@"publish_actions"], ACFacebookAudienceKey : ACFacebookAudienceOnlyMe}; [accountStore requestAccessToAccountsWithType:facebookType options:options completion:^(BOOL granted, NSError *error) { if(granted){ NSString *urlStr = [NSString stringWithFormat:@"https://graph.facebook.com/me/feed"]; NSURL *url = [NSURL URLWithString:urlStr]; NSDictionary *params = @{@"message" : @"hogehoge"}; SLRequest *request = [SLRequest requestForServiceType:SLServiceTypeFacebook requestMethod:SLRequestMethodPOST URL:url parameters:params]; [request setAccount:self.selectedAccount]; [request performRequestWithHandler:^(NSData *response, NSHTTPURLResponse *urlResponse, NSError *error){ NSLog(@"response:%@",[[NSString alloc]initWithData:response encoding:NSUTF8StringEncoding]); }]; } }];
これでエラーが出ずにFacebookAPIを利用したポストが出来ます。
また、上記のようにやってもどうしてもエラーが出る場合は、iPhone側の設定にて、アプリとFacebookの連携の許可を一旦解除して、再度アプリからaccountStoreにアクセスすると直る場合があります。
Amazon Linux にJenkins入れたときのメモ
yumにJenkinsリポジトリ登録してインストール
$ sudo wget -O /etc/yum.repos.d/jenkins.repo http://pkg.jenkins-ci.org/redhat/jenkins.repo $ sudo rpm --import http://pkg.jenkins-ci.org/redhat/jenkins-ci.org.key $ sudo yum install jenkins
cf : https://wiki.jenkins-ci.org/display/JENKINS/Installing+Jenkins+on+RedHat+distributions
HOST名/jenkinsで開けるように
/etc/sysconfig/jenkins内で
JENKINS_ARGS="--prefix=/jenkins";
<Location /jenkins> Order allow,deny Allow from all ProxyPass http://localhost:8080/jenkins ProxyPassReverse http://localhost:8080/jenkins </Location>
設定後 httpdリスタート
Jenkinsのユーザ設定(デフォルトだとJenkinsになるが、任意に変えたい場合)
/etc/sysconfig/jenkins
JENKINS_USER="hogeUser" //例えばhogeUserでJenkins動かす場合
パーミッションの変更
$ chown -R hogeUser /var/lib/jenkins $ chown -R hogeUser /var/log/jenkins $ chown -R hogeUser /var/cache/jenkins
Jenkins strat/stop
$ sudo service jenkins start/stop/restart
Port指定してPlay!を起動するコマンド
1.2系は設定ファイルでもできるけど、ここではコマンド指定のメモ。たまに忘れるのでメモ。
Play 1.2系
play run --http.port=8080
Play 2.0系
play "run 8080"
または
sbt stage target/start -Dhttp.port=8080
TwitterAPI勉強会で発表してきました。 #twtr_hack
8月にTwitterAPI勉強会で発表したスライドを載せていなかったので、
思い出迷子にならないように載せときます。
しかしながら、既にiOS6のSocialFrameworkが出ていたり、リバースオースも申請しないでも使えるようになったりと情報鮮度は著しく悪化しているので、参考にはなりません。
あくまで、思い出記録用。
第三回Playframework勉強会で発表してきました。#play_ja
第三回Playframework勉強会で発表してきました。
当日のtoggetterはこちら。
内容は、今回私が開発したソーシャル音楽アプリAttaccaでPlay2.0+Javaを選択した経緯がメインになります。
こんなアプリです。
発表資料はRECRUITの黒田として発表した部分は省いております。i2key個人として発表した部分のみにしております。ご了承ください。
UST録画はこちら。
Video streaming by Ustream
17:14 〜 43:00が私の発表になります。
最後のAction Invoker Actorsの値についてですが、やはり512はやりすぎwwwと@Masahitoさんにご指摘をうけました。まずは2倍の24x2の48にして、徐々にあげて行き、閾値をみないとねーと。仰る通りです><
今迄J2EEなTomcat脳だったので、スレッドプールはリクエスト毎に消費する感覚で色々やってましたが、Playでは、ノンブロッキングに役割毎にスレッドプールが待機していて、リクエストを裁く感覚(Netty + Akka)が必要だと再認識しました。
また、@ikeike443さんから頂いたご指摘のとおり、多分、中身の処理でブロッキングしちゃってると思いますので、改善できたらと思っております。
スライドにはhozumiさんの日記のnettyの基礎のみをリンクをいれましたが、Nettyの仕組みの概要を掴むために以下も参考にさせていただきました。
ありがとうございます。
当日は池田さんをはじめ日本Playユーザグループの皆様、及び勉強会に参加された皆様ありがとうございました。とても勉強させていただきました。
これからも精進いたします。
女子中高生とTwitter4Jについて発表してきました。#twtr_hack
TwitterAPI勉強会にて、以下のLTをしてきました。
10分の発表なのに、スライド60枚以上作ってしまったので、どうなるかと思いましたが、想定通り1分6枚ペースで時間内に無事?終わりました。
また、タイトルで釣ることを目的にしていたので、中身薄くてゴメンナサイ。
当日のTogetterはこちら
http://togetter.com/li/292726
全体のまとめはこちら
第6回Twitter API勉強会を開催いたしました - ビデオ・スライドまとめ #twtr_hack
発表資料は以下になります。
Ustを録画してくださったので、以下に張っておきます。
TwitterAPIのエラーが多いので、それに対応するコードがカオスになってしまうから、旨いリトライのやり方あったら教えて下さいと言ったら普通にTwitter4Jに実装されていたという恥ずかしいオチでしたが、勉強になりました。リファレンス読めってことですね。ありがとうございました。
.@i2key Twitter4Jでhttp.retryCount パラメータを1以上に設定すればリトライしてくれます twitter4j.org/en/configurati… #twtr_hack
— ゆーすけさん (@yusukey) 4月 24, 2012
関連情報
インフラ構成で軽く触れましたが、AWSのAutoScalingについてはこちらで記事を書いていますので、ご参考になれば。
Amazon WebserviceのAuto Scalingの設定について
Scalaで自動フォロー返し書いたら凄く簡単になった件はこちらに記事を書いています。ご参考になれば。
Javaで書かれたTwitterの自動フォロー返しを、Scalaで書いてみた #twitter4j #twtr_hack
今回MyBatisというORマッパー(HibernateほどなORマップではない)を使いました。某SIerの社内フレームワークはiBatisを使っていて、SQL文をxmlに記述していく(またはエクセル方眼紙からそのxmlを自動生成している)と思いますが、MyBatisはxmlを捨ててアノテーションでSQLを記述します。
※ちなみにMyBatisはiBatisが大人の事情でApache系列からGoogleCodeProjectに移ることになった際にネーミングが変わっています。
私は、自分でSQLを書きたい派なので、JPA的なインタフェースが少し苦手(笑)で、SQL感が強いMyBatisを採用しました。SpringのTransactionTemplateでもいいじゃんとなるのですが、MyBatisの付属品である、Generatorが便利だったので。
http://code.google.com/p/mybatis/wiki/Generator
データベースにあるテーブル情報から単純なCRUDアクセスパターンはSQL及びそれをラップしたDAOクラスを自動生成してくれます。
例えば、userId、userName、photoUrlのカラムを持つUSERテーブルに対してCRUD生成を行うと以下のようなクラスが生成されます。
public interface UserMapper { /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ @SelectProvider(type=UserSqlProvider.class, method="countByExample") int countByExample(UserExample example); /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ @DeleteProvider(type=UserSqlProvider.class, method="deleteByExample") int deleteByExample(UserExample example); /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ @Delete({ "delete from user", "where userId = #{userId,jdbcType=VARCHAR}" }) int deleteByPrimaryKey(UserKey key); /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ @Insert({ "insert into user (userId, userName, ", "photoUrl)", "values (#{userId,jdbcType=VARCHAR}, #{userName,jdbcType=VARCHAR}, ", "#{photoUrl,jdbcType=LONGVARCHAR})" }) int insert(UserWithBLOBs record); /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ @InsertProvider(type=UserSqlProvider.class, method="insertSelective") int insertSelective(UserWithBLOBs record); /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ @Select({ "select", "userId, userName, photoUrl", "from user", "where userId = #{userId,jdbcType=VARCHAR}" }) @Results({ @Result(column="userId", property="userId", jdbcType=JdbcType.VARCHAR, id=true), @Result(column="userName", property="userName", jdbcType=JdbcType.VARCHAR), @Result(column="photoUrl", property="photoUrl", jdbcType=JdbcType.LONGVARCHAR) }) UserWithBLOBs selectByPrimaryKey(UserKey key); /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ @UpdateProvider(type=UserSqlProvider.class, method="updateByExampleSelective") int updateByExampleSelective(@Param("record") UserWithBLOBs record, @Param("example") UserExample example); /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ @UpdateProvider(type=UserSqlProvider.class, method="updateByExampleWithBLOBs") int updateByExampleWithBLOBs(@Param("record") UserWithBLOBs record, @Param("example") UserExample example); /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ @UpdateProvider(type=UserSqlProvider.class, method="updateByExample") int updateByExample(@Param("record") User record, @Param("example") UserExample example); /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ @UpdateProvider(type=UserSqlProvider.class, method="updateByPrimaryKeySelective") int updateByPrimaryKeySelective(UserWithBLOBs record); /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ @Update({ "update user", "set userName = #{userName,jdbcType=VARCHAR},", "photoUrl = #{photoUrl,jdbcType=LONGVARCHAR}", "where userId = #{userId,jdbcType=VARCHAR}" }) int updateByPrimaryKeyWithBLOBs(UserWithBLOBs record); /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ @Update({ "update user", "set userName = #{userName,jdbcType=VARCHAR}", "where userId = #{userId,jdbcType=VARCHAR}" }) int updateByPrimaryKey(User record); }
こちらが動的にSQLをつくる必要があるものです。上記クラスからアノテーション経由で利用されます。
public class UserSqlProvider { /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ public String countByExample(UserExample example) { BEGIN(); SELECT("count (*)"); FROM("user"); applyWhere(example, false); return SQL(); } /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ public String deleteByExample(UserExample example) { BEGIN(); DELETE_FROM("user"); applyWhere(example, false); return SQL(); } /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ public String insertSelective(UserWithBLOBs record) { BEGIN(); INSERT_INTO("user"); if (record.getUserId() != null) { VALUES("userId", "#{userId,jdbcType=VARCHAR}"); } if (record.getUserName() != null) { VALUES("userName", "#{userName,jdbcType=VARCHAR}"); } if (record.getPhotoUrl() != null) { VALUES("photoUrl", "#{photoUrl,jdbcType=LONGVARCHAR}"); } return SQL(); } /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ public String updateByExampleSelective(Map<String, Object> parameter) { UserWithBLOBs record = (UserWithBLOBs) parameter.get("record"); UserExample example = (UserExample) parameter.get("example"); BEGIN(); UPDATE("user"); if (record.getUserId() != null) { SET("userId = #{record.userId,jdbcType=VARCHAR}"); } if (record.getUserName() != null) { SET("userName = #{record.userName,jdbcType=VARCHAR}"); } if (record.getPhotoUrl() != null) { SET("photoUrl = #{record.photoUrl,jdbcType=LONGVARCHAR}"); } applyWhere(example, true); return SQL(); } /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ public String updateByExampleWithBLOBs(Map<String, Object> parameter) { BEGIN(); UPDATE("user"); SET("userId = #{record.userId,jdbcType=VARCHAR}"); SET("userName = #{record.userName,jdbcType=VARCHAR}"); SET("photoUrl = #{record.photoUrl,jdbcType=LONGVARCHAR}"); UserExample example = (UserExample) parameter.get("example"); applyWhere(example, true); return SQL(); } /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ public String updateByExample(Map<String, Object> parameter) { BEGIN(); UPDATE("user"); SET("userId = #{record.userId,jdbcType=VARCHAR}"); SET("userName = #{record.userName,jdbcType=VARCHAR}"); UserExample example = (UserExample) parameter.get("example"); applyWhere(example, true); return SQL(); } /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ public String updateByPrimaryKeySelective(UserWithBLOBs record) { BEGIN(); UPDATE("user"); if (record.getUserName() != null) { SET("userName = #{userName,jdbcType=VARCHAR}"); } if (record.getPhotoUrl() != null) { SET("photoUrl = #{photoUrl,jdbcType=LONGVARCHAR}"); } WHERE("userId = #{userId,jdbcType=VARCHAR}"); return SQL(); } /** * This method was generated by MyBatis Generator. * This method corresponds to the database table user * * @mbggenerated Mon Oct 03 15:41:07 JST 2011 */ protected void applyWhere(UserExample example, boolean includeExamplePhrase) { if (example == null) { return; } String parmPhrase1; String parmPhrase1_th; String parmPhrase2; String parmPhrase2_th; String parmPhrase3; String parmPhrase3_th; if (includeExamplePhrase) { parmPhrase1 = "%s #{example.oredCriteria[%d].allCriteria[%d].value}"; parmPhrase1_th = "%s #{example.oredCriteria[%d].allCriteria[%d].value,typeHandler=%s}"; parmPhrase2 = "%s #{example.oredCriteria[%d].allCriteria[%d].value} and #{example.oredCriteria[%d].criteria[%d].secondValue}"; parmPhrase2_th = "%s #{example.oredCriteria[%d].allCriteria[%d].value,typeHandler=%s} and #{example.oredCriteria[%d].criteria[%d].secondValue,typeHandler=%s}"; parmPhrase3 = "#{example.oredCriteria[%d].allCriteria[%d].value[%d]}"; parmPhrase3_th = "#{example.oredCriteria[%d].allCriteria[%d].value[%d],typeHandler=%s}"; } else { parmPhrase1 = "%s #{oredCriteria[%d].allCriteria[%d].value}"; parmPhrase1_th = "%s #{oredCriteria[%d].allCriteria[%d].value,typeHandler=%s}"; parmPhrase2 = "%s #{oredCriteria[%d].allCriteria[%d].value} and #{oredCriteria[%d].criteria[%d].secondValue}"; parmPhrase2_th = "%s #{oredCriteria[%d].allCriteria[%d].value,typeHandler=%s} and #{oredCriteria[%d].criteria[%d].secondValue,typeHandler=%s}"; parmPhrase3 = "#{oredCriteria[%d].allCriteria[%d].value[%d]}"; parmPhrase3_th = "#{oredCriteria[%d].allCriteria[%d].value[%d],typeHandler=%s}"; } StringBuilder sb = new StringBuilder(); List<Criteria> oredCriteria = example.getOredCriteria(); boolean firstCriteria = true; for (int i = 0; i < oredCriteria.size(); i++) { Criteria criteria = oredCriteria.get(i); if (criteria.isValid()) { if (firstCriteria) { firstCriteria = false; } else { sb.append(" or "); } sb.append('('); List<Criterion> criterions = criteria.getAllCriteria(); boolean firstCriterion = true; for (int j = 0; j < criterions.size(); j++) { Criterion criterion = criterions.get(j); if (firstCriterion) { firstCriterion = false; } else { sb.append(" and "); } if (criterion.isNoValue()) { sb.append(criterion.getCondition()); } else if (criterion.isSingleValue()) { if (criterion.getTypeHandler() == null) { sb.append(String.format(parmPhrase1, criterion.getCondition(), i, j)); } else { sb.append(String.format(parmPhrase1_th, criterion.getCondition(), i, j,criterion.getTypeHandler())); } } else if (criterion.isBetweenValue()) { if (criterion.getTypeHandler() == null) { sb.append(String.format(parmPhrase2, criterion.getCondition(), i, j, i, j)); } else { sb.append(String.format(parmPhrase2_th, criterion.getCondition(), i, j, criterion.getTypeHandler(), i, j, criterion.getTypeHandler())); } } else if (criterion.isListValue()) { sb.append(criterion.getCondition()); sb.append(" ("); List<?> listItems = (List<?>) criterion.getValue(); boolean comma = false; for (int k = 0; k < listItems.size(); k++) { if (comma) { sb.append(", "); } else { comma = true; } if (criterion.getTypeHandler() == null) { sb.append(String.format(parmPhrase3, i, j, k)); } else { sb.append(String.format(parmPhrase3_th, i, j, k, criterion.getTypeHandler())); } } sb.append(')'); } } sb.append(')'); } } if (sb.length() > 0) { WHERE(sb.toString()); } } }
使うにはSpringのBean定義に以下を書くだけ
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configLocation" value="classpath:MyBatisConfig.xml" /> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="sqlSessionFactory" ref="sqlSessionFactory"/> <property name="basePackage" value= "生成したDAOがおかれるパッケージ" /> </bean>
生成されたDAOクラスは、当然GenerationGapパターンを適用できるので、自動生成したクラスをスーパークラスとして、個別要件(手動による追記)をextendsした側に記述してあげることで、CRUDの再生成が可能です。
TwitterハッカソンでNFCによるTwitterアイコンチェンジャーを作成しました。#twtr_hack #twitter4j #nfchack
Twitterハッカソンに参加してきました。場所はTwitter Japan!
(場所はアーク森ビルといわれ、勘違いして六本木ヒルズに行き、インフォメーションカウンターのおねーさんにTwitterJapanどこですか?と聞いて、ザワザワしたのは言うまでもない。)
トゥギャッターはこちら → 第0回 Twitterハッカソン #twtr_hack
@yusukeyさんのブログはこちら →第0回Twitterハッカソンを開催しました #twtr_hack
@bina1204さんのブログはこちら →第0回 Twitter Hack に参加した
@makotoworldさんのブログはこちら →第0回Twitterハッカソンに参加して
@johtaniさんのブログはこちら →第0回 Twitter Hack #twtr_hack に遊びに行きました。
私がつくったのはNFC契機によるTwitterのアイコンチェンジャーです。
Twitterのアイコンに現在のステータスを埋め込んだら、MSNメッセンジャーとかでいう現在のステータス(退席中、取り込み中的な)が表現できるのではないかと。
また、ステータスのキーとして、SUICAをAndroidにタッチすれば、「電車移動中」とTwitterアイコンに示され、
同様に、社員証をタッチすれば「仕事中!」とアイコンに示されるようにしました。Twitterのアイコンかえるくらい、ボタン操作ではなくワンタッチで済ませたい。
これがSUICAタッチ後のアイコン。
遅刻しまくったし時間が無かったので強引なオレオレ実装ですが、ソースはGitHubにアップしておきました。
https://github.com/i2key/NFCxTwitter4J
また、TwitterAPIを使うとかなりの確率でステータスコード4XXや500が返ってくるのだけど、
以下のように何回かリトライさせるような実装で対応するしかないのでしょうか。
皆さんどのように書いているのか非常に気になります。
UpdateProfileImageTask.java
//4XXや5XXでても3回まではリトライする int continuousErrorCount = 0; while(true){ try { Log.i(TAG, "START."); //やりたいのはこれだけ。アイコン画像のアップロード twitter.updateProfileImage(bis); } catch (TwitterException e) { Integer errorCode = e.getStatusCode(); if(errorCode.toString().startsWith("5") || errorCode.toString().startsWith("4")){ continuousErrorCount++; if(continuousErrorCount < 4){ Log.e(TAG, "STATUS CODE is 4XX , 5XX. Try " + continuousErrorCount + "times."); continue; }else{ //リトライ4回目で終了(もう無理あきらめる) Log.e(TAG, "STATUS CODE is 4XX , 5XX. Try " + continuousErrorCount + "times."); return false; } }else{ //STATUS CODE = 3XX , 2XXのときはリトライなしで終了 Log.e(TAG, "STATUS CODE is 3XX , 2XX. Try " + continuousErrorCount + "times."); return false; } } //成功したら終了 Log.i(TAG, "SUCCESS. Try " + continuousErrorCount + "times."); break; }
頂いたノベルティ(ステッカー、ボールペン、クリアファイル、Tシャツ)
最後になりましたが、@yusukeyさん、ピザごちそうさまでした!