2012年12月6日木曜日

グラデーション画像からAndroidのXMLを自動生成する

このエントリーをはてなブックマークに追加
Androidでは、画像ファイルをそのままアプリにはめこむのではなく、XMLでスタイルを定義することで、画像を作成することができます。

ここで、デザイナが下記のような画像ファイルでボタンを作成したとします。

 
が、エンジニアは、ここで困ります。XMLでグラデーションを描画するためには、グラデーションの始まりと終わりの色の16進数を入力する必要があるからです。エンジニアがカラーピッカーを立ち上げ、RGBを取得し、16進数に変換する・・・なんて非常に手間がかかります。

というわけで、画像を引数として与えると、XMLを生成してくれるPythonスクリプトを書きました。画像ライブラリとしてPILが必要です。
GitHub

python generateDrawableXML.py [img_path]
#! /usr/bin/env python
# -*- coding: utf-8 -*-

import sys
from PIL import Image

def getCenterList(_list, order=1):
    if order > 0:
        return [i for (i, x) in enumerate(_list) if x == max(_list)]
    else:
        return [i for (i, x) in enumerate(_list) if x == min(_list)]

def getHexColor(rgb):
    return '#FF' + getHexStr(rgb[0]) + getHexStr(rgb[1]) + getHexStr(rgb[2])

def getHexStr(color):
    hexStr = hex(color)[2:].upper()
    return hexStr if len(hexStr) == 2 else '0' + hexStr

def main(path):
    img = Image.open(path)
    img = img.resize((1, img.size[1]))
    img = img.convert("RGB")

    rgbs        = list(img.getdata())
    height      = len(rgbs)
    startRGB    = rgbs[0]
    endRGB      = rgbs[len(rgbs) - 1]
    centerRGB   = None
    centerIndex = 0

    rs, gs, bs = [], [], []
    for rgb in rgbs:
        rs.append(rgb[0])
        gs.append(rgb[1])
        bs.append(rgb[2])

    for i in [-1, 1]:
        centerSet = set(getCenterList(rs, i)) & set(getCenterList(gs, i)) & set(getCenterList(bs, i))
        if len(centerSet) == 0: continue
        index = list(centerSet)[0]
        if index != 0 and index != (height - 1):
            centerRGB   = rgbs[index]
            centerIndex = index

    centerStr = ""
    if centerRGB:
        centerStr = """
        android:centerColor="%s"
        android:centerY="%s"
        """ % (getHexColor(centerRGB), round(float(centerIndex) / float(height - 1), 1))


    print """
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:angle="270"
        android:type="linear"
        android:startColor="%s"
        android:endColor="%s"%s
        />
</shape>""" % (getHexColor(startRGB), getHexColor(endRGB), centerStr)

if __name__ == '__main__':
    argvs = sys.argv
    if len(argvs) != 2:
        print("python " + argvs[0] + " [img_path]")
    else:
        main(argvs[1])

実行すると下記XMLが生成されます。
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:angle="270"
        android:type="linear"
        android:startColor="#FF330F55"
        android:endColor="#FFC45D19"
        />
</shape>

また、center要素がある画像でも大丈夫。

<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="rectangle">
    <gradient
        android:angle="270"
        android:type="linear"
        android:startColor="#FFFFC081"
        android:endColor="#FFFFA349"
        android:centerColor="#FFFF8101"
        android:centerY="0.6"
        />
</shape>

※横方向のグラデーションや、円型のグラデーションには対応していません。

2012年12月2日日曜日

ジオメディアサミット2012に参加した

このエントリーをはてなブックマークに追加
 普段、プログラムをペッと貼るだけのblogだけど、たまにはこういうのも書いてみる。

ジオメディアサミット に参加してきた。面白い事例があったのでいくつか紹介

  1. CITO
    GPS情報を頼りに、ユーザ間で仮想の宝物を隠して見つけ合ったりするGeocachingというサービスがあるんだけど、宝探しをしている間はGPSから割り出した周辺をウロウロと歩きまわる必要がある。そこで、その間にゴミを拾おうというのがCITO (Cache In Trash Out)のコンセプト。
    ピリカ
    のユーザでも、犬の散歩中やラジオ体操に行く途中、ランニング中など、「ながら」でのゴミ拾いを行う方が多いので、ゴミ拾いはGeocachingとの相性が非常にいいと思う。

  2. Navitimeのカープローブ
    Navitimeでは、サービスCAR NAVITIMEのユーザから送信されるGPS測位データを収集し、渋滞情報の解析に役立てている。センサを持つモビリティの問題である電池寿命も、車から電源を確保できるので問題ない。データ量も圧倒的で、VICSを利用した渋滞情報とは違うアプローチで興味深い。

    参考までに、車ではなく人にセンサ機器を持たせ、周辺状況をセンシングするヒューマンプローブという手法も紹介。
    東京大学空間情報科学研究センターの中の人の流れプロジェクトでは、各都市圏のパーソントリップ調査データをもとに人の位置・時間情報を取得することができる。パーソントリップ調査データは、人が"いつ、どこからどこへ移動したか"を調査したもので、例えばAさんは「8:00に家を出発→8:15に◯◯駅発→8:40に△△駅着→9:00に会社着」などである。ただ、これだと地点間の移動のみで、実際にどのような経路を辿ったか?ということがわからないので、人の流れプロジェクトでは、途中経路を推測により補間して1分毎の人の移動経路を提供している。
    ただ、パーソントリップ調査データは、アンケートで実施しているため、正確性が乏しい。実際には、8:13に家を出たにも関わらず、アンケート上では8:15としたり、場合によっては8:00と書いてしまうこともある。つまり、切りの良い時刻にデータが集まってしまい、現実データと観測データが乖離してしまう。(ただ、これは2年ほど前の時点で、今はどうなっているか不明)
    最近だと、東日本大震災の際の人の移動経路を動画にしている。



  3. Geo-Fuse
    georepublic社のGeo-Fuseは、地域別統計データを地図上にマッピングする際の煩雑な作業をなくすサービスである。説明スライドはコチラ。プログラムを一切書くことなく、統計データのマッピングができて非常に便利。
    ちなみに東京23区別ピリカ数はコチラから見れます。

2012年11月28日水曜日

Google Apps Scriptでサービスを監視する

このエントリーをはてなブックマークに追加
Google Apps Script でゆるく自分のサービスを監視します。

  • レスポンスコードの確認
    2012-07-26 - 愛と勇気と缶ビール
    GASでは、cronタスクのような定期的な処理が簡単に書けます。このスクリプトは、外部からページアクセスをして、レスポンスコードが正しく返ってくるかをチェックし、正しく返ってこない場合には、メールを飛ばします。
    地味に面倒な作業なのですが、GASで数行スクリプトを書くだけで代替しています。
  • ツイートの保存
    Google Apps Script でツイートを集める | rockf.es blog
    自社サービスがオンライン上でどれだけバズっているかを知るために、Twitter検索をすることがあるかと思います。このスクリプトは、検索単語を仕掛けて定期的にツイートをスプレッドシートに保存します。
  •  Mixpanelのイベントチェック
    突然話の対象が、特定サービスに限定されてしまいますが、イベントログの解析ツールであるMixpanelのAPIを叩き、異常値が出ていないかをチェックします。例えば、「過去の数日間の平均に比べて、本日のページアクセス数が異常に多い」or「今日はサインアップが1件もない(どこかでエラーが発生している可能性がある)」などです。ここでは、「過去の数日間の平均に比べて、本日のページアクセス数が異常に多い」をチェックするスクリプトを記載します。
API_KEY      = "YOUR_MIXPANEL_API_KEY";
API_SECRET   = "YOUR_MIXPANEL_API_SECRET";
MAIL_ADDRESS = "YOUR_MAIL_ADDRESS";
EVENTS       = ["EVENT_NAME1", "EVENT_NAME2", "EVNET_NAME3"];
RATIO        = 1.5;

data = {
  'unit'     : "day",
  'interval' : 15,
  'type'     : "general"
}

function main() {
  for(var i = 0; i < EVENTS.length; i++) {
    check(EVENTS[i]);
  }
}

function check(event) {
  var API_URL = "http://mixpanel.com/api/2.0/events/?";
  var response, jsonString, object, series = [], values = [], i = 0, ave = 0;

  try {
    data['event'] = '["' + event + '"]';
    data['sig']   = getSig(data);
    data['event'] = encodeURIComponent(data['event']);

    response = UrlFetchApp.fetch(API_URL + makeParam(data));
    if (!response) {
      Logger.log("no response");
      return;
    }

    jsonString = response.getContentText();
    object     = Utilities.jsonParse(jsonString);
    series     = object.data.series;
    values     = object.data.values[event];

    for(i = 0; i < series.length - 1; i++) {
      ave += values[series[i]];
    }
    ave /= (series.length - 1);
    ave = Math.floor(ave);
    lastValue = values[series[series.length - 1]];

    if (lastValue >= ave * RATIO && !exist(event, ave)) {
      write(event, ave, lastValue);
      sendMail(event, ave, lastValue);
    }
    Logger.log("done!");
  } catch(e) {
    Logger.log(e);
    response = null;
  }
}

function getSig(hash) {
  var keys = [];
  var kv   = "";

  var currentDate = new Date;
  var expire = Math.floor(currentDate.getTime() / 1000) + 600;

  delete hash['sig'];
  hash['api_key'] = API_KEY;
  hash['expire'] = expire;
  hash['format'] = "json";
  
  for (var k in hash) keys.push(k);
  keys.sort();
  var length = keys.length;
  for(var i = 0; i < length; i++){
    kv += keys[i] + "=" + hash[keys[i]];
  }
  return getMD5Hash(kv + API_SECRET);
}

function getMD5Hash(input) {
  var rawHash = Utilities.computeDigest(Utilities.DigestAlgorithm.MD5, input);
  var txtHash = '';
  for (j = 0; j < rawHash.length; j++) {
    var hashVal = rawHash[j];
    if (hashVal < 0)
      hashVal += 256; 
    if (hashVal.toString(16).length == 1)
      txtHash += "0";
    txtHash += hashVal.toString(16);
  }
  return txtHash;
}

function makeParam(hash) {
  var param = "";
  for(var key in hash) {
    param += key + "=" + hash[key] + "&";
  }
  param = param.substring(0, param.length-1);
  return param;
}

function exist(event, ave) {
  var sheet = SpreadsheetApp.getActiveSheet();
  var row   = sheet.getLastRow();
  var cell, i, _event, _ave, _date;

  for(i = 1; i <= row; i++) {
    _event = sheet.getRange('A'+ i).getValue();
    _ave   = sheet.getRange('B'+ i).getValue();
    _date  = sheet.getRange('D'+ i).getValue();
    if(event == _event && ave == _ave && getDateWithFormat() == _date) {
      return true;
    }
  }
  return false;
}

function write(event, ave, lastValue) {
  var sheet = SpreadsheetApp.getActiveSheet();
  sheet.appendRow([event, ave, lastValue, getDateWithFormat()]);
}

function sendMail(event, ave, lastValue) {
  MailApp.sendEmail(
    MAIL_ADDRESS,
    "#alert " + event + " event is greather than the average value",
    "event name: " + event + ", average: " + ave + ", lastValue: " + lastValue
  );
}

function getDateWithFormat() {
  var date = Utilities.formatDate(new Date(), "JST", "yyyy/MM/dd");
  return '"' + date + '"';
} 

このスクリプトは、イベント、EVENT_NAME1,2,3について、過去2週間の平均よりも本日の値が1.5倍よりも多い場合、スプレッドシートに内容を記録し、アラートメールを飛ばします。





2012年10月11日木曜日

XcodeでWombat風のカラーテーマを使う方法

このエントリーをはてなブックマークに追加


XcodeでvimのWombat風のカラーテーマを使う方法についてです。

1. 下記、コードをwombat.dvtcolorthemeとして保存
2. {$HOME}/Library/Developer/Xcode/UserData/FontAndColorThemes/ にファイルを移動
3. XcodeのPreferences>Font & Colors>Themeよりwombatを選ぶ
4. フォントはSource Editorにて一括選択後、フォントを変更することで、一括変換可能



    DVTConsoleDebuggerInputTextColor
    0 0 0 1
    DVTConsoleDebuggerInputTextFont
    Menlo-Bold - 11.0
    DVTConsoleDebuggerOutputTextColor
    0 0 0 1
    DVTConsoleDebuggerOutputTextFont
    Menlo-Regular - 11.0
    DVTConsoleDebuggerPromptTextColor
    0.317071 0.437736 1 1
    DVTConsoleDebuggerPromptTextFont
    Menlo-Bold - 11.0
    DVTConsoleExectuableInputTextColor
    0 0 0 1
    DVTConsoleExectuableInputTextFont
    Menlo-Regular - 11.0
    DVTConsoleExectuableOutputTextColor
    0 0 0 1
    DVTConsoleExectuableOutputTextFont
    Menlo-Bold - 11.0
    DVTConsoleTextBackgroundColor
    0.999899 1 0.999842 1
    DVTConsoleTextInsertionPointColor
    0 0 0 1
    DVTConsoleTextSelectionColor
    0.576266 0.81005 1 1
    DVTDebuggerInstructionPointerColor
    0.705792 0.8 0.544 1
    DVTSourceTextBackground
    0.141 0.141 0.141
    DVTSourceTextBlockDimBackgroundColor
    0.5 0.5 0.5 1
    DVTSourceTextInsertionPointColor
    0.396 0.396 0.396
    DVTSourceTextInvisiblesColor
    0.5 0.5 0.5 1
    DVTSourceTextSelectionColor
    0.267 0.267 0.267
    DVTSourceTextSyntaxColors
    
        xcode.syntax.attribute
        0.906 0.965 0.855
        xcode.syntax.character
        0.584 0.894 0.329
        xcode.syntax.comment
        0.600 0.588 0.545
        xcode.syntax.comment.doc
        0.600 0.588 0.545
        xcode.syntax.comment.doc.keyword
        0.600 0.588 0.545
        xcode.syntax.identifier.class
        0.792 0.902 0.510
        xcode.syntax.identifier.class.system
        0.792 0.902 0.510
        xcode.syntax.identifier.constant
        0.898 0.471 0.427
        xcode.syntax.identifier.constant.system
        0.898 0.471 0.427
        xcode.syntax.identifier.function
        0.792 0.902 0.510
        xcode.syntax.identifier.function.system
        0.792 0.902 0.510
        xcode.syntax.identifier.macro
        0.898 0.471 0.427
        xcode.syntax.identifier.macro.system
        0.898 0.471 0.427
        xcode.syntax.identifier.type
        0.792 0.902 0.510
        xcode.syntax.identifier.type.system
        0.792 0.902 0.510
        xcode.syntax.identifier.variable
        0.792 0.902 0.510
        xcode.syntax.identifier.variable.system
        0.792 0.902 0.510
        xcode.syntax.keyword
        0.541 0.776 0.949
        xcode.syntax.mark
        1.000 0.500 0.000
        xcode.syntax.number
        0.898 0.471 0.427
        xcode.syntax.plain
        1.000 1.000 1.000
        xcode.syntax.preprocessor
        0.894 0.471 0.427
        xcode.syntax.string
        0.584 0.894 0.329
        xcode.syntax.url
        0.906 0.965 0.855
        xcode.syntax.url.mail
        0.100 0.100 1.000
    
    DVTSourceTextSyntaxFonts
    
        xcode.syntax.attribute
        Monaco - 11.0
        xcode.syntax.character
        Monaco - 11.0
        xcode.syntax.comment
        Monaco - 11.0
        xcode.syntax.comment.doc
        Monaco - 11.0
        xcode.syntax.comment.doc.keyword
        Monaco - 11.0
        xcode.syntax.keyword
        Monaco - 11.0
        xcode.syntax.number
        Monaco - 11.0
        xcode.syntax.plain
        Monaco - 11.0
        xcode.syntax.preprocessor
        Monaco - 11.0
        xcode.syntax.string
        Monaco - 11.0
        xcode.syntax.url
        Monaco - 11.0
    


上記ファイルは、
// TODO: better name : wombat風Xcodeカラーテーマ - // TODO: better name
をXcode4用に修正したものです。


2012年7月27日金曜日

スマフォアプリのランキングデータを調べてみた

このエントリーをはてなブックマークに追加
Google Play(Android マーケット)では、エンターテイメント, ショッピング, スポーツなど様々なカテゴリがあり、ユーザはその中から、アプリを入手することができます。

アプリ製作者は折角作ったアプリなので、たくさんのユーザに使って欲しいはず。そこで、大事なのはカテゴリ内のランキング。ランキングを上げることは、アプリのインストール数を増やすためには、非常に重要であり、ランキングを上げるために別カテゴリにアプリを移動することもあります。

と言った経緯から、Google Playのアプリケーション内の各カテゴリのTOP120について、タイトル、順位、評価、評価数、インストール数、ダウンロードページ、アプリの紹介文章について、調べてみました。(※ゲームおよび有料アプリを除く)

ファイルはこちら



ついでに、アプリ名とアプリの紹介文章について、「名詞」をカウントし、どのような単語が検索で引っかかりやすいかを調べてみました。
この単語以外のキーワードで、自分のアプリの紹介文章を書けば、検索した際にヒットする確率が向上します。


ファイルはこちら


2012年5月16日水曜日

surround.vimの拡張

このエントリーをはてなブックマークに追加

デフォルトのsurround.vimでは、囲み文字で囲まれた文字を削除する場合は、「di'」、「di"」の様に囲み文字を指定する必要がある。

・di"
"Hello World" → ""
・di'
'Hello World' → ''

自動的に囲み文字を判別してくれて、削除してくれたら便利だな、と思ったので、スクリプトを書いてみた

nnoremap ci :call ExSurround("ci")
nnoremap di :call ExSurround("di")
function! s:ExSurround(cmd)
    let pattern = "'\"{[("
    let front = strpart(getline("."), 0, col("."))
    let max = -1
    for pat in split(pattern, '.\zs')
        let pos = strridx(front, pat)
        if pos > max
            let max = pos
        endif
    endfor
    if max >= 0
        let sorround = strpart(front, max, 1)
        call feedkeys(a:cmd . sorround, 'n')
    endif
endfunction


・di
"Hello World" → ""
'Hello World' → ''

対応している文字は'、"、{、[、(


2012年5月14日月曜日

第3回 開発コンテスト24に参加した

このエントリーをはてなブックマークに追加
今年のテーマは「一日の終わりを楽しくするもの」

この記事では、どんな流れで開発を行ったかについて書きたいと思います。

[作品]








吾輩は読書中である。明日の朝は早い。

[ブレスト]

開始から1時間ほどブレストしました。
僕はいつもスライドにブレスト内容を書き出しながら、適宜まとめます。


基本的なアプローチ方法は二つ
・一日の終りにする楽しくないものを楽しくする
・一日の終りにする楽しいものをより楽しくする
これについて、それぞれ列挙し、その後詳細に考えました。 ただ、去年の場合のように詳細を詰めることなく、応募内容のものが自分で使いたいし、シンプルで作りやすい&わかりやすいと思ったので、アイデア出しはこの程度で終わりました。

[開発]

開発はGoogle App Engine上で動作するプログラムを書きました。
基本的な動作手順は
1. 青空文庫にアクセスして、ZIPファイルをダウンロードする
2. ZIPファイルからテキストファイルを抽出
3. 文字コードをSJISからUTF-8に変換
4. テキストファイルを段落毎に区切り、各段落の文字数をカウント
5. 段落の前に、文字数とそれに応じた推定読了時間を記載
6. 再度ZIP化してリクエストに応答する
となります。ZIP/UNZIPと文字コード変換に少し詰まりましたが、昼過ぎには完成。大体100行程度のプログラムになりました。

[開発2]

コア部分の開発後、Webページを作り、説明&フォームからのダウンロードできるようにしました。ここでは、Twitter Boostrapを使いました。CSSは10行ほど書き換えて、後は説明文を記載。大体HTMLを100行ほど書きました。

[開発3]

とてもシンプルなサービスで自分でも結構気に入ったのですが、折角時間もあるので、Androidからのクライアントを作りました。と言っても、文庫一覧をリストに表示して、選択された文庫のURLでAPIを叩きに行くだけ。200行ほどのプログラム。

[サービス名]

サービス名は非常に重要だと思います。何のサービスか、何が嬉しいかを伝える必要があります。そこで、夏目漱石の吾輩は猫であるの冒頭をもじってタイトルを「吾輩は読書中である。明日の朝は早い。」、サブタイトルを「どこで読書をやめればよいか、とんと見当がつかぬ。」としました。結構悩んだのですが、審査員コメントでも触れて頂き非常に嬉しかったです。

[ムービー制作]

開発コンテストの趣旨からは離れているのですが、紹介ムービーも作りました。こちらはiMovieやらPhotoshopやらを駆使。去年の作品では音楽があったのですが、今回は自分で作ったモノだけにしようと思い、音楽はなくしました。慣れの問題もあるのですが、正直なところ、ムービー制作に一番時間を使いました。

[まとめ]

24時間と短い時間ですので、コア部分の実装を出来る限り小さくすることが重要かと思いました。コア部分が出来上がったら、縦や横に実装をすすめる。つまり、いつ開発をストップしても作品として提出できる、といった状態を作り出します。
これにより、バグにハマって抜け出せなくなっても、作品を提出できないというリスクを回避できます。そして、余った時間に応じて作品のクオリティーを上げることができます。

ただ、少しシンプル過ぎたかなーと思います。24時間を使い切ることなく、終わってしまった。
GAEやらBoostrapやらのお陰で、めんどくさい作業や書くべきコード量がガクンと減りましたね。まぁ、楽なのは良いことですが。


昨年に引き続き、参加させて頂きとても楽しく過ごせました。開催元のCookpad社とその中で"休日にも関わらず"働いてくださった社員の皆様に感謝です!!



2012年4月21日土曜日

AndroidでWebViewを使用する際の注意点

このエントリーをはてなブックマークに追加
Androidのアプリ内でWebViewを使用する際に、サイトページ内に、window.open(url)やwindow.location.href=urlを使用したが画面遷移があると、うまく画面遷移ができない。

解決策は、shouldOverrideUrlLoadingをオーバーライドして、各種サービスの起動を明記する。

下記例では、urlが、「mailto:」で始まった時はメールアプリを起動する暗黙的インテントを発行し、TwitterやFacebookのシェアボタンをタップした時には、画面遷移先を読み込むようにしている。

        wb = (WebView) findViewById(R.id.event_wb);
        wb.setWebChromeClient(new WebChromeClient());
        wb.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView webView, String url) {
                Intent intent = null;
                if(url.substring(0, 7).equals("mailto:"))
                intent = new Intent(Intent.ACTION_SENDTO, Uri.parse(url));
                else if(url.contains("twitter") || url.contains("facebook"))
                    intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                if(intent != null) {
                    startActivity(intent);
                    webView.reload();   
                }
                return true;
            }
        });
        WebSettings settings = wb.getSettings();
        settings.setJavaScriptEnabled(true);

2012年4月12日木曜日

Androidのリバースエンジニアリング方法

このエントリーをはてなブックマークに追加

apktool
dex2jar
jad

基本コマンド

[apk→smali]
apktool d LogTest.apk Smali

[apk→classes.dex]
unzip LogTest.apk -d Unzip

[classes.dex→classes_dex2jar.jar]
./dex2jar.sh ./Unzip/classes.dex

[classes_dex2jar.jar→class]
unzip ./Unzip/classes_dex2jar.jar -d ./Classes

[class→java]
./jad -8 -d Src -s .java -r ~ **/*.class


【LogTestActivity.java - 元ファイル】
package com.ayaki.log;

import android.app.Activity;
import android.os.Bundle;

public class LogTestActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

String str = "ABC";
}
}


【LogTestActivity.smali - 取得したSmaliファイル】
.class public Lcom/ayaki/log/LogTestActivity;
.super Landroid/app/Activity;
.source "LogTestActivity.java"


# direct methods
.method public constructor ()V
.locals 0

.prologue
.line 6
invoke-direct {p0}, Landroid/app/Activity;->()V

return-void
.end method


# virtual methods
.method public onCreate(Landroid/os/Bundle;)V
.locals 2
.parameter "savedInstanceState"

.prologue
.line 10
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

.line 11
const/high16 v1, 0x7f03

invoke-virtual {p0, v1}, Lcom/ayaki/log/LogTestActivity;->setContentView(I)V

.line 13
const-string v0, "ABC"

.line 16
.local v0, str:Ljava/lang/String;
return-void
.end method


【LogTestActivity.java - 逆コンパイルJavaファイル】
package com.ayaki.log;

import android.app.Activity;
import android.os.Bundle;

public class LogTestActivity extends Activity
{

public LogTestActivity()
{
}

public void onCreate(Bundle bundle)
{
super.onCreate(bundle);
setContentView(0x7f030000);
}
}


動的な動作の変更

Smaliは可逆性があるので、.apk→.smali→編集→.apkが可能。
例えば、Log.d("MYLOG", str);に相当するものを書いてみる。



【LogTestActivity.smali - 修正後】
.class public Lcom/ayaki/log/LogTestActivity;
.super Landroid/app/Activity;
.source "LogTestActivity.java"


# direct methods
.method public constructor ()V
.locals 0

.prologue
.line 6
invoke-direct {p0}, Landroid/app/Activity;->()V

return-void
.end method


# virtual methods
.method public onCreate(Landroid/os/Bundle;)V
.locals 2
.parameter "savedInstanceState"

.prologue
.line 10
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V

.line 11
const/high16 v1, 0x7f03

invoke-virtual {p0, v1}, Lcom/ayaki/log/LogTestActivity;->setContentView(I)V

.line 13
const-string v0, "ABC"

const-string v2, "MYLOG"

invoke-static {v2, v0}, Landroid/util/Log;->d(Ljava/lang/String;Ljava/lang/String;)I

.line 16
.local v0, str:Ljava/lang/String;
return-void
.end method

実行

[smali→apk]
apktool b Smali LogTestNew.apk

[keystoreの作成]
keytool -genkey -keystore test.keystore -validity 10000 -alias test

[認証]
jarsigner -keystore test.keystore -verbose LogTestNew.apk test

[デバイスへのインストール]
adb install -r LogTestNew.apk

[Log]
adb logcat -s MYLOG

これで、ABCと表示されれば、改変成功!