2016年2月29日 星期一

Bootstrap 外觀的自動完成選單


jQuery 有一個 Autocomplete 小部件,但它不及用開的 Bootstrap 外型美觀;我今日把它們合而為一。

首先為編寫 Autocomplete.css
.ui-autocomplete {
 position: absolute;
 z-index: 1000;
 cursor: default;
 padding: 0;
 margin-top: 2px;
 list-style: none;
 background-color: #ffffff;
 border: 1px solid #cccccc
 -webkit-border-radius: 5px;
 -moz-border-radius: 5px;
 border-radius: 5px;
 -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
 -moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
 box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
}
.ui-autocomplete > li {
  padding: 3px 20px;
}
.ui-autocomplete > li.ui-state-focus {
  background-color: #dddddd;
}
.ui-helper-hidden-accessible {
  display: none;
}

.ui-autocomplete a  {
 color: #555555;
}
.ui-autocomplete a:link  {
 color: #555555;
}
.ui-autocomplete a:hover  {
 color: #337ab7;
 text-decoration: none;
}
.ui-autocomplete a:active  {
 color: #555555;
}
.ui-autocomplete a:visited  {
 color: #555555;
}

在 HTML 加入:
<link type="text/css" rel="stylesheet" href="../__components__/jquery-ui-1.9.2.custom/css/autocomplete.css" />

當然還需要加入 jQuery-ui:
<script type="text/javascript" src="../__components__/jquery-ui-1.9.2.custom/js/jquery-ui-1.9.2.custom.min.js"></script>

輸入欄的代碼是:
<label for="dashboardModalOutlet">Outlet Name</label>
<input id="dashboardModalOutlet" type="text" class="form-control" placeholder="Outlet Name" data-toggle="tooltip" data-placement="top" title="Outlet Name" /><br>

網頁啟動時會以 AJAX 向服務器索取數據。得到的數據儲存在 _outletArray 中,只有「編號」及「名稱」會用得到。因此建立 _outletIDArray 給 Autocomplete 之用。考慮到從清單點選項目後需要進行處理,還是在 _outletIDArray 中一併加入索引數值。雖然輸入的只是「編號」,但為了方便使用者,選單中每一項目均顯示了「編號」及「名稱」。所以當點擊項目後,數值需要只保留「編號」,因而加入了「select」部份的代碼。
_outletArray = dataArray;

//  Update dashboard auto-outlet-complete content
_outletIDArray = new Array();
for (var i=0; i<_outletArray.length; i++)  {

 var row = _outletArray[i];
 var outletID = row["outletID"];
 var outlet = row["outlet"];

 _outletIDArray[i] = {label:outletID+" ("+outlet+")", idx:i};
}

$("#dashboardModaloutletID").autocomplete({
 source:_outletIDArray,
 select:function(event, ui)  {
  var index = ui.item.idx;
  var row = _outletArray[index];

  var outletID = row["outletID"];
  $(this).val(outletID);

  //  Update outlet name field
  var outlet = row["outlet"];
  $("#dashboardModalOutlet").val(outlet);
  return false;
 }
});

2016年2月28日 星期日

Robi 模型


近期日元貶值,港元變得划算,有不少朋友都到日本旅遊。早前託朋友到日本尋找 Robi 模型,第一個朋友找不到,第二個朋友也找不到。在網上搜索過,很多地方都賣完了,只有某個地方有售少量存貨,所以都預計買不到。第三個朋友過日本,他是機械人發燒友,一定知道出售 Robi 模型,立即託他購入。最後,當然成功了!


早幾天拿到手,已經急不及待拼砌。朋友的「真。Robi」還有兩期便完期,還笑說花 70 天去砌 Robi 模型。我真的很想訂購「真。Robi」,但總價錢要 HK$10,863,目前很吃力。還是先以模型滿足一下吧。


這套模型附送了一把模型剪刀。除了用在 Robi 模型外,還能用在其他模型上。


Robi 模型的步驟不多,容易裝嵌。花了一個小時左右便完成。它的比例是 2:1,亦即是原大的一半大小。把扭蛋版的 Robi 放在一起,感覺很特別。要是能把「真。Robi」放在一起就更好了。

2016年2月26日 星期五

Android 下的圓形進度介面


在 Android 中的基本介面都已經學習及使用過,是時候學點進階版。我選了圓形進度介面。

要製作圓形進度,得先決定外觀是怎麼樣。我希望有個幼細的整圓,代表 100% 的情況;而粗圓代表進度,按百份比顯示的。那麼,我需要先定義以上設計的 Drawable:

shape_circular_thin.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape
            android:useLevel="true"
            android:innerRadiusRatio="2.3"
            android:shape="ring"
            android:thickness="3sp">
            <solid android:color="#ff00ff" />
        </shape>
    </item>

    <item android:id="@android:id/progress">
        <shape
            android:useLevel="true"
            android:innerRadiusRatio="2.3"
            android:shape="ring"
            android:thickness="3sp">
            <solid android:color="#ffffff" />
        </shape>
    </item>
</layer-list>

shape_circular.xml
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@android:id/background">
        <shape
            android:useLevel="true"
            android:innerRadiusRatio="2.3"
            android:shape="ring"
            android:thickness="1sp">
            <solid android:color="#ff00ff" />
        </shape>
    </item>

    <item android:id="@android:id/progress">
        <shape
            android:useLevel="true"
            android:innerRadiusRatio="2.3"
            android:shape="ring"
            android:thickness="1sp">
            <solid android:color="#ffffff" />
        </shape>
    </item>
</layer-list>
有了 Drawable 之後,便是定義 XML。當中是使用「ProgressBar」元素。不過,Drawable 的進度起點是由三點鐘方向順時針轉動,而我則希望是十二點鐘方向順轉;所以在 XML 中要加入「android:rotate="-90"」。目前我使用的進度是把圓形分成七份,代表一星期七天;考慮到日後使用到這個元素時可能會有更密的單位,所以我把 360 度乘二作為最大值,亦即是 720 度。只要把數值按比例乘大到 720 便能正確顯示。

2016年2月25日 星期四

紅外線檢查線路


家中的三叔電視遙控器有一半按鈕沒反應,在鴨寮街也找不到相同的型號,在不確定能使用,而且又索價 HK$160 的情況下,我還是不買。


昨天到深水埗商談開班的事宜,順道購買了一顆紅外線檢查元件。淘寶賣 RMB$0.6,華輝卻賣 HK$12。只能說:「錢,要留給別人賺」。回家後砌了上面的線路來測試元件是否能正常運作。過程順利,元件亦沒有問題。下一步是把它跟 Arduino 連接,讀取目前遙控器還能發出的訊號,嘗試憑這些訊號找回失落了的按鍵內容。

2016年2月24日 星期三

Not-Bag 在商業電台雷霆 881《大玩派》節目錄音


上週三,Not-Bag 其中一位創辦人到了商業電台雷霆 881《大玩派》節目接受訪問。正式向外公佈 Not-Bag 的人事變動。這是經過剪接的錄音,多謝支持。

2016年2月23日 星期二

ShowMuse 獲得香港科技園「科技企業投資基金」投資


由 BeyondZ 投資、開發及營運的《ShowMuse》在上年十一月上架。目前的運作已經交由科學園及前海的同事負責。較早前得到香港科技園加碼注資,昨天正式向外公佈:「香港科技園「科技企業投資基金」完成首個投資項目」及「Kiss and tell: learning app ShowMuse gets HK$3 mln from Hong Kong Science and Technology Park’s pilot fund」。

2016年2月22日 星期一

ESP8266-12E


很多人認為 ESP8266 是在 IoT 世界裡非常實用的晶片。原因是它有一片 WiFi 模組,能簡單地建立網頁服務器,讓不同的裝置接入並進行操作。它本身能獨立使用,又或者以 UART 連接其他基板,如:Arduino。它帶有數個 GPIO 接口,能滿足到基本的需要。由於 ESP8266 是使用正常的網頁格式,所以不論是電腦、平板、還是手機都能支援,價格更只是 HK$30 左右,實在非常大眾化。這顆 ESP8266 是 12E 型號,已經到手數個月,最近才有點時間可以用來研究。不過,看來要直接將程式寫入,還需要 FDTI 編程器才可...。

2016年2月21日 星期日

解決 Xcode 的「Symbols tool failed」問題


昨晚在生成 Distribution IPA 時,遇到兩個問題。其中一個是「Symbols tool failed」錯誤。查看 Log 記錄,發現以下訊息:
2016-02-20 22:27:16 +0000 [MT] Presenting: Error Domain=IDEFoundationErrorDomain Code=1 "Symbols tool failed" UserInfo={NSLocalizedDescription=Symbols tool failed}

要解決的話,需要在輸出 .ipa 時,取消介面上的「Include app symbols for your application to receive symbolicated crash logs from Apple」打勾。

2016年2月20日 星期六

解決「This certificate has an invalid issuer」問題


今晚在輸出 .ipa 時,無論使用甚麼證書、甚麼帳號、甚麼 Provisioning 都不成功。花了兩小時,才找到方法。在 Keychain Access 內發現「Apple Worldwide Developer Relations Certification Authority」證書出現「This certificate has an invalid issuer」錯誤。解決方法是在 Keychain Access 內把舊的 WWDRCA 證書刪除。留意的是在 Login 項目下及 System 項目下都有這張證書,兩張都要刪除。下載新的 WWDRCA 證書,並雙擊進行安裝。

2016年2月16日 星期二

鍵盤支架

Monitor Hook for Keyboard by pacess on Sketchfab


同事離職了,需要頂下他們的工作,我的檯面也因而多了三台電腦。細小的空間不容許放這麼多東西,只好想想辦法。放置三個鍵盤及三隻滑鼠還是可以,但第四份則是問題。我發現中間的顯示器背面有一個凸了出來的地方可以善加利用,於是的設計出以下部件:


部件的勾形設計可以抓實外框:


只要左右兩邊放置部件,便能把鍵盤放在屏幕之上:

2016年2月15日 星期一

一年前的創業點子

經濟大氣氛不明朗、百物騰貴、物價飛漲。五年前能輕鬆過活的薪水,到現在已經生活艱難。作為一家之柱,得想辦法增加收入。

在春節假期中,我發現兩年前創作出來的東西竟然有人把它變成了生意。在 Log-on 找到它的影子、在信和中心也有。一年前,我也曾想過把它商業化,但卻欠缺毅力。今天,想再次努力。我不期望帶來豐厚的回報,只希望帶來穩定的收入。今天跟公司的租客談論過,他可以幫忙尋找供應商;在 Facebook 的朋友也給了我很多的意見;我可以把這個獨創新事業想得更多更深。目前最大的難度是特點及自動化問題。作為機械人愛好者,當然要把事情自動化。希望新的一年有順利的發展!

2016年2月14日 星期日

為 PHP 檔加密(二)

早幾天編寫的「為 PHP 檔加密」,在應用上遇到一個情況,就是我寫的 PHP 很多時都會匯入其他 PHP 檔。要是匯入的 PHP 已被加密,那麼輸出來的 PHP 將會是加密中有加密;這會導致程式無法正常執行。原本我打算把被匯入的那些 PHP 加密就算數了事,但那些 PHP 多數是數據庫的中介程式,工作簡單,沒有需要保護的必要。那麼,該如何是好?我想出以下方法。

通常我是以「require()」指令來匯入其他 PHP,而這個功能就像是把匯入 PHP 嵌入到當前 PHP 內一樣。既然如此,那麼索性做這樣的處理,把匯入 PHP 嵌入後才加密,便不會出現「加密中有加密」的情況。我把程式改了一下:
<?php
//----------------------------------------------------------------------------------------
//  PHP File Obfuscator v1.20
//  This program will compress a PHP file and name it .min.php
//----------------------------------------------------------------------------------------
//  Written by Pacess HO
//  Copyright 2016 Pacess Studio.  All rights reserved.
//----------------------------------------------------------------------------------------

//----------------------------------------------------------------------------------------
//  1.00 - Search all .php in current and its child directories and apply encryption.
//  1.10 - Search all .php in current directory only since there are some .php do not need
//         encryption.  Moreover, this version will load all "require(xxx)" php and inject
//         into the opened .php which prevent encryption not working with require.
//  1.20 - Just encrypted .php which not encrypted yet, ignore those encrypted .php, add
//         encryption date time as well.

//----------------------------------------------------------------------------------------
function getDirectoryArray($directory)  {
 $resultArray = array(); 
 $fileArray = scandir($directory); 
 foreach ($fileArray as $key => $value)  {

  //  Skip . & ..
  if (in_array($value, array(".", "..")))  {continue;}

  $filePath = $directory.DIRECTORY_SEPARATOR.$value;
  if (is_dir($filePath) == false)  {$resultArray[] = $filePath;}
  }
 } 
 return $resultArray; 
} 

//=========================================================================================
//  Main program
$encryptedPHPHeader = "<?php\n".
 "//----------------------------------------------------------------------------------------\n".
 "//  Written by Pacess HO\n".
 "//  Encrypted at ".date("Y-m-d H:i:s")."\n".
 "//  Copyright 2015-2016 Pacess Studio.  All rights reserved.\n".
 "//----------------------------------------------------------------------------------------\n".
 "ob_start();";
$encryptedPHPFooter = 'eval(gzuncompress(base64_decode($code)));$v=ob_get_contents();ob_end_clean(); ?>';
$encrypedPHPCount = 0;

$thisPHPFile = baseName(__FILE__);

echo("\n");
echo("----------------------------------------------------------------\n");
echo("--  PHP File Obfuscator v1.20                                 --\n");
echo("----------------------------------------------------------------\n");
echo("--  Written by Pacess HO                                      --\n");
echo("--  Copyright 2015-2016 Pacess Studio.  All rights reserved.  --\n");
echo("----------------------------------------------------------------\n");
echo("\n");

$fileArray = getDirectoryArray(".");
foreach ($fileArray as $key => $filePath)  {
 echo("#$key ");

 $tail = strtolower(substr($filePath, -8));
 if ($tail == ".min.php")  {
  echo("Skip [$filePath] because it is already compressed.\n");
  continue;
 }

 $tail = strtolower(substr($filePath, -4));
 if ($tail != ".php")  {
  echo("Skip [$filePath] because it is not a PHP file.\n");
  continue;
 }

 //  Check if this .php file, if yes then skip
 if (strpos($filePath, $thisPHPFile) !== false)  {
  echo("Skip [$filePath] because it is current tool file.\n");
  continue;
 }

 //  It is .php file, encrypt now!
 $data = "ob_end_clean();?>";

 //----------------------------------------------------------------------------------------
 //  Somehow encryption is not work if PHP include another encrypted PHP, so I read PHP
 //  source code here and inject those require-PHPs into the source
 $phpContent = php_strip_whitespace($filePath);

 //  Check if PHP already encrypted
 $position = stripos($phpContent, "eval(gzuncompress(base64_decode(");
 if ($position !== false)  {
  echo("Skip [$filePath] because it is already encrypted.\n");
  continue;
 }

 $patternStart = "require(";
 $patternEnd = ");";

 $lengthPatternStart = strlen($patternStart);
 $lengthPatternEnd = strlen($patternEnd);

 $count = 0;
 $finish = false;
 while ($finish == false)  {

  //  Just for safe, never include so much files
  $count++;
  if ($count > 50)  {$finish = true;}

  $position = stripos($phpContent, $patternStart);
  if ($position === false)  {$finish = true;}
  else  {

   //  +1/-1 because there is a quote ""
   $startPoint = $position+$lengthPatternStart+1;
   $endPoint = stripos($phpContent, $patternEnd, $startPoint);
   if ($endPoint === false)  {$finish = true;}
   else  {

    $pathArray = pathinfo($filePath);
    $length = $endPoint-$startPoint-1;
    $requiredFilename = substr($phpContent, $startPoint, $length);
    $requiredFilePath = $pathArray["dirname"]."/".$requiredFilename;

    //  Read required file content
    $requiredFileContent = php_strip_whitespace($requiredFilePath);

    //  Inject the content to PHP file
    $length = ($endPoint+$lengthPatternEnd)-$position;
    $requiredCommand = substr($phpContent, $position, $length);
    $phpContent = str_replace($requiredCommand, " ?>".$requiredFileContent."<?php ", $phpContent);
   }
  }
 }

 //  Since injection may produce PHP-CLOSE-PHP-OPEN pattern, remove it now
 $length = strlen($phpContent);
 $lastLength = 0;
 while ($length != $lastLength)  {
  $lastLength = $length;
  $phpContent = str_replace("?><?php", "", $phpContent);
  $length = strlen($phpContent);
 }

 $data .= $phpContent;

 //----------------------------------------------------------------------------------------
 //  Compress data
 $zipData = gzcompress($data, 9);
 $base64Data = base64_encode($zipData);
 $encryptedPHPContent = '$code=\''.$base64Data.'\';';

 $outputContent = $encryptedPHPHeader.$encryptedPHPContent.$encryptedPHPFooter;

 $outputPath = substr($filePath, 0, -4).".min.php";
 $result = file_put_contents($outputPath, $outputContent);
 echo("Encoding [$filePath]...");
 echo("[$outputPath]...$result bytes written.\n");

 $encrypedPHPCount++;
}
echo("\nTotal $encrypedPHPCount PHP files have been encrypted.\n~ Finish ~\n\n");
?>

2016年2月13日 星期六

把 srf 格式轉換成 .ts 實驗


由於 .srf 不是正常的影片檔,需要進行處理才可以直接觀看。參考了 http://www.ivonet.it/Linux/Samsung.LED.TV.PVR.Recording.As.MKV 網頁的方法把 .srf 格式轉換成 .ts 格式。

1. 首先安裝好 gcc
yum install gcc
2. 然後找個位置
cd ~
3. 下載解碼器源碼
git clone https://github.com/marvin0815/drmdecrypt.git
4. 編譯解碼器
gcc -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -c aes.c -o aes.o
gcc -D_FILE_OFFSET_BITS=64 -D_LARGEFILE_SOURCE -D_LARGEFILE64_SOURCE -c DRMDecrypt.c -o DRMDecrypt.o
gcc -o drmdecrypt aes.o DRMDecrypt.o
5. 轉換到 .srf 檔案目錄
cd /home/pacess/CONTENTS/
6. 執行轉換工作
/root/drmdecrypt/drmdecrypt 20160202125505.srf

把轉換出來後的 .ts 拉到 VLC / QuickTime 都無法成功播放,只能誤認為音頻格式,而且聲音也無法順利解碼。要成功看到影片,還要繼續努力!

2016年2月11日 星期四

讀取 Smart TV 錄影檔


早前託弟弟用 Samsung Smart TV 把《視點 31》節目錄起來;在拜年時把那 USB 硬碟拿到手。可是無論是 Mac 還是 Windows 都無法讀取。在 Linux 下接上硬碟後執行「file -SL /dev/sd*」便能得知它的分割格或是 EXT4,不是常用的電腦格式。就算在 Mac 安裝了 FUSE for OS X 還是不行。現在很多裝置都運行 Linux,特別是 Samsung,於是把硬碟接上 CentOS 一試。利用「mount /dev/sdb1 /mnt」指令把硬碟安裝到 /mnt 目錄。


查看一下,果真能正常讀取硬碟內的檔案。從檔名看出日期是 2016 年 2 月 2 日,正是錄影的時間。


當中的 20160202125505.mta 是一個 XML 格式檔案,看來是儲存了影片不同時間的畫面截圖。


那個「InlineMedia」標記後的是 Base64 資料,利用 Code Beautify 把資料轉換成圖像,確定判斷正確。

2016年2月10日 星期三

把 Button 當作 ToggleButton 使用


今日嘗試製作過濾形式的選單;點擊其中一顆按鈕,它會開著,其他的會關閉。最合適應該是 ToggleButton,但用 Button 感覺上較為得心應手,於是選擇了 Button。外觀方面,今次在 XML 中定義了「All」按鈕,其他的改為程序中生成;很容便做到:
//  Add filter button
Drawable drawable = resources.getDrawable(R.drawable.button_filter);
int size = subCategoryList.size();
for (int i=0; i<size; i++)  {

   category = subCategoryList.get(i);
   int subCategoryID = category.categoryID();

   String string = category.categoryName();
   string = _appManager.jsonStringForKey(string);

   LinearLayout.LayoutParams layoutParameter = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
   layoutParameter.setMarginStart(margin);

   String tag = Integer.toString(BUTTON_TAG_ALL+1+subCategoryID);

   button = new Button(_activity);
   button.setId(BUTTON_TAG_ALL+1+subCategoryID);
   button.setTag(tag);
   button.setText(string);
   button.setTextColor(color);
   button.setBackground(drawable);
   button.setLayoutParams(layoutParameter);
   button.setPadding(horizontal, vertical, horizontal, vertical);
   button.setOnClickListener(_onClickListener);
   button.setSelected(false);
   if (_typefaceSourceSanProLight != null)  {button.setTypeface(_typefaceSourceSanProLight);}
   linearLayout.addView(button);

   //  Add button object to array for ON/OFF use
   _buttonArray.add(button);
}
可是卻發生狀況。除了「All」按鈕能正常的開及關外,其他的不正常。例如明明點擊的是「Water」,「Syrup」卻開著了;當放開手時,「Water」開著,「Syrup」還是開著。功能上能正常過濾下方的 ListView,但外觀上卻不對。花了整個下午,最終發現錯誤是由 Drawable 導致。Drawable 那句要放在迴圈內。把不變的 Drawable 送在迴圈外,是為了效能的考量;原來在 Android 下 Drawable 那句是會變的。改成以下順序便解決了問題。又上了一課。
//  Add filter button
int size = subCategoryList.size();
for (int i=0; i<size; i++)  {

   category = subCategoryList.get(i);
   int subCategoryID = category.categoryID();

   String string = category.categoryName();
   string = _appManager.jsonStringForKey(string);

   LinearLayout.LayoutParams layoutParameter = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
   layoutParameter.setMarginStart(margin);

   String tag = Integer.toString(BUTTON_TAG_ALL+1+subCategoryID);

   Drawable drawable = resources.getDrawable(R.drawable.button_filter);

   button = new Button(_activity);
   button.setId(BUTTON_TAG_ALL+1+subCategoryID);
   button.setTag(tag);
   button.setText(string);
   button.setTextColor(color);
   button.setBackground(drawable);
   button.setLayoutParams(layoutParameter);
   button.setPadding(horizontal, vertical, horizontal, vertical);
   button.setOnClickListener(_onClickListener);
   button.setSelected(false);
   if (_typefaceSourceSanProLight != null)  {button.setTypeface(_typefaceSourceSanProLight);}
   linearLayout.addView(button);

   //  Add button object to array for ON/OFF use
   _buttonArray.add(button);
}

2016年2月9日 星期二

iOS SQLite vs Android SQLite

今日在寫 Android 的 SQLite 指令時,發現到一個奇怪的事情。同一句指令在 iOS 的 SQLite 下能順利執行,但在 Android 下卻不行。以下是在 iOS 版本的指令:
SELECT product.*, brand.brandName, category.categoryName FROM product, brand, category WHERE product.status>0 AND product.categoryID=? AND product.brandID=brand.brandID AND product.categoryID=category.categoryID ORDER BY orderNumber DESC, productID ASC
在 Android 下卻搜索不到結果。解決方法是加入「GROUP BY」,像是:
SELECT product.*, brand.brandName, category.categoryName FROM product, brand, category WHERE product.status>0 AND product.categoryID=? AND product.brandID=brand.brandID AND product.categoryID=category.categoryID GROUP BY productID ORDER BY orderNumber DESC, productID ASC
更奇怪的是在常用的 sqlitebrowser 也跟 Android 一樣。那麼,當初 iOS 版為可沒有問題?實在不得而知...。可能這個情況應該用 JOIN 較為合適。

2016年2月8日 星期一

ListView 的 Padding 處理


在介面設計中用到 ListView,為了更加美觀,我希望在上下左右加入邊框。在 XML 中的指令是「android:paddingRight="20dp"」。但是,這個 Padding 同時會影響顯示內容的範圍,如上圖。要解決的話,需要加入「android:clipToPadding="false"」。


2016年2月7日 星期日

如何讓 OnItemClickListener 及 OnClickListener 同時存在


我的 Android 程式中有一個 ListView 顯示一組內容;每一列有一行文字及一顆按鈕。原本在未加入按鈕之前,當我單擊其中一行時都會執行 OnItemClickListener;可是當加入按鈕後,點擊行中非按鈕的地方會沒反應,但點按鈕就會執行 OnClickListener。

網上找到有相同遭遇的人
A listView with textual, non-clickable views in it responds to Click events via the OnItemClickListener event.

But once you put a button in the ListView, this event no longer fires


起初以為沒有解決方法,豈料卻找到原因是當有按鈕出現時,會自動變成焦點,只要在 XML 中加入「android:focusable="false"」即可。

2016年2月6日 星期六

新開始

新年將至,終於捱過了艱辛的一年。這年學到很多東西,有新創立的 Not-Bag 事業、見過投資者、嘗試過股份分配及淡化、資金用盡、區域法院案件、大額裁員、甚至是退出事業。在新創事業中的拍檔有 70 後,也有 80 後。經歷了大半年,發現溝通上出了嚴重的問題,我認為這是「代溝」。同一件事、同一段說話、兩代的人會有不同的演譯;而且發生不只一次。公司也有 80 後及 90 後,以前未曾發生過這樣的事,唯一不同的是前者是拍檔,會左右到發展方向及決策;後者是同事,怎樣不同還是會按照決定了的方向前進。合作態度又是另一個問題。我很希望拍檔們互相配合、取長捨短、一起向前衝。我跟 70 後的拍檔做到了,大家很有默契,唯獨是 80 後的卻同步不來。還有面子問題、匯報問題、信任問題、信心問題...等。內部未解決時,阻疑著對外擴張的發展,更影響士氣。不過,昨天已經重新整合,惡夢已經過去;新開始即將到來。有些人,是寧願在生命中沒有出現過。

2016年2月5日 星期五

為 PHP 檔加密


現在開發手機應用程式,少不免會連接服務器取得最新數據。我喜歡用 .php 作為應用程式跟服務器的中介接口,貪他方便又資源豐富。有時客人希望自己管理服務器,但是我又不想被客戶簡單地拿後台的 .php 源代碼,於是參考了 https://www.dontbebad.com/blog/2013/04/simple-php-obfuscator/ 編寫出以下程式:
<?php
//----------------------------------------------------------------------------------------
//  PHP File Obfuscator
//  This program will compress a PHP file and name it .min.php
//----------------------------------------------------------------------------------------
//  Written by Pacess HO
//  Copyright 2016 Pacess Studio.  All rights reserved.
//----------------------------------------------------------------------------------------

//----------------------------------------------------------------------------------------
function getDirectoryArray($directory)  {
   $resultArray = array(); 
   $fileArray = scandir($directory); 
   foreach ($fileArray as $key => $value)  {

      //  Skip . & ..
      if (in_array($value, array(".", "..")))  {continue;}

      $filePath = $directory.DIRECTORY_SEPARATOR.$value;
      if (is_dir($filePath) == false)  {$resultArray[] = $filePath;}
      else  {

         $subdirectoryArray = getDirectoryArray($filePath);
         $resultArray = array_merge($resultArray, $subdirectoryArray);
      }
   } 
   return $resultArray; 
} 

//=========================================================================================
//  Main program
$encryptedPHPHeader = "<?php\n".
   "//----------------------------------------------------------------------------------------\n".
   "//  Written by Pacess HO\n".
   "//  Copyright 2016 Pacess Studio.  All rights reserved.\n".
   "//----------------------------------------------------------------------------------------\n".
   "ob_start();";
$encryptedPHPFooter = 'eval(gzuncompress(base64_decode($code)));$v=ob_get_contents();ob_end_clean(); ?>';
$encrypedPHPCount = 0;

echo("\n");
echo("-----------------------------------------------------------\n");
echo("--  PHP File Obfuscator v1.00                            --\n");
echo("-----------------------------------------------------------\n");
echo("--  Written by Pacess HO                                 --\n");
echo("--  Copyright 2016 Pacess Studio.  All rights reserved.  --\n");
echo("-----------------------------------------------------------\n");
echo("\n");

$fileArray = getDirectoryArray(".");
foreach ($fileArray as $key => $filePath)  {
   echo("#$key ");

   $tail = strtolower(substr($filePath, -8));
   if ($tail == ".min.php")  {
      echo("Skip [$filePath] because it is already compressed.\n");
      continue;
   }

   $tail = strtolower(substr($filePath, -4));
   if ($tail != ".php")  {
      echo("Skip [$filePath] because it is not a PHP file.\n");
      continue;
   }

   //  Check if this .php file, if yes then skip
   if (strpos($filePath, "phpEncrypt.php") !== false)  {
      echo("Skip [$filePath] because it is current tool file.\n");
      continue;
   }

   //  It is .php file, encrypt now!
   echo("Encoding [$filePath]...");
   $data = "ob_end_clean();?>";
   $data .= php_strip_whitespace($filePath);

   //  Compress data
   $zipData = gzcompress($data, 9);
   $base64Data = base64_encode($zipData);
   $encryptedPHPContent = '$code=\''.$base64Data.'\';';

   $outputPath = substr($filePath, 0, -4).".min.php";
   $result = file_put_contents($outputPath, $encryptedPHPHeader.$encryptedPHPContent.$encryptedPHPFooter);
   echo("[$outputPath]...$result bytes written.\n");

   $encrypedPHPCount++;
}
echo("\nTotal $encrypedPHPCount PHP files have been encrypted.\n~ Finish ~\n\n");
?>
執行方法是在 Terminal 中輸入 php phpEncrypt.php,就會將當前目錄以下的所有 .php 檔案加密,並儲存成 .min.php 檔案。

2016年2月4日 星期四

為 Android Studio 加入編譯計數器


在 iOS 的開發過程中,我會加入編譯自動計數器;現在開發 Android 程式,我也希望有相同的做法。在 Android Studio 中不難做到,只要在 app 目錄下加入 version.properties,內容:

然後在 app/build.grade 加入如下的代碼:
apply plugin: 'com.android.application'

android {
    compileSdkVersion 21
    buildToolsVersion "23.0.1"

    def major = "1"
    def minor = "00"
    def versionFile = file('version.properties')
    if (versionFile.canRead())  {

        //  Load properties file
        def Properties properties = new Properties()
        properties.load(new FileInputStream(versionFile))

        //  Get build count and update it
        def buildCountString = properties['BUILD_COUNT']
        def buildCount = buildCountString.toInteger()+1

        //  Save new value back to properties file
        properties['BUILD_COUNT'] = buildCount.toString()
        properties.store(versionFile.newWriter(), null)

        defaultConfig {
            applicationId "com.pacess.amigocontroller"
            minSdkVersion 17
            targetSdkVersion 21
            versionCode 1
            versionName major+"."+minor+"."+buildCount.toString()
        }

    }  else  {
        throw new GradleException("### Unable to read version.properties...")
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile 'info.hoang8f:android-segmented:1.0.6'
}

2016年2月3日 星期三

Android 的本地通知經驗

繼續 Android 的學習。本來在 Fragment 內顯示 AlertDialog,但搞了很久也無法正常顯示,基於結構上的考量,把顯示的部份搬到上層 Activity 看來更好。現時的結構是 Activity 包著 Fragment 再包著目前的 Fragment。那麼要怎樣通知兩層上的 Activity?我選擇了用本地通知(LocalBroadcastManager)。但明明在程式其他地方用過,執行得好好地的代碼,搬到 Fragment 內用不了?搞了兩小時,才發現原來沒有第三個參數的 send() 有一個臭蟲,會跳過執行而沒有反應:
//
//  LocalNotification.java
//  AMIGOController
//
//  Created by Pacess on 02/04/2016.
//  Copyright (c) 2016 Pacess Studio. All rights reserved.
//

//--------------------------------------------------------------------------------------------------
//  0    1         2         3         4         5         6         7         8         9
//  567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890

package com.pacess.amigocontroller.utilities;

//--------------------------------------------------------------------------------------------------
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.v4.content.LocalBroadcastManager;

//==================================================================================================
public final class LocalNotification  {

    //  Defines
    public static final String CONFIRM_LOGOUT = "com.pacess.amigocontroller.confirmLogout";
    public static final String CONFIRM_RESET_PASSWORD = "com.pacess.amigocontroller.confirmResetPassword";
    public static final String ERROR_RESPONSE_FORMAT = "com.pacess.amigocontroller.errorResponseFormat";
    public static final String ERROR_STATUS = "com.pacess.amigocontroller.errorStatus";
    public static final String FORGOTPASSWORD_DONE = "com.pacess.amigocontroller.forgotPasswordDone";
    public static final String LANGUAGE_CHANGED = "com.pacess.amigocontroller.languageChanged";
    public static final String LOGIN_DONE = "com.pacess.amigocontroller.loginDone";
    public static final String SIGNUP_DONE = "com.pacess.amigocontroller.signUpDone";
    public static final String STARTAPP_DONE = "com.pacess.amigocontroller.startAppDone";
    public static final String UPDATE_HEADER = "com.pacess.amigocontroller.updateHeader";

    //--------------------------------------------------------------------------------------------------
    //  Static functions
    //--------------------------------------------------------------------------------------------------
    public static void send(Context context, String action)  {
        send(context, action, null);
    }

    //--------------------------------------------------------------------------------------------------
    public static void send(Context context, String action, Bundle data)  {
        if (data == null)  {return;}

        Intent intent = new Intent();
        intent.setAction(action);
        intent.putExtras(data);
        LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
    }
}

2016年2月1日 星期一

在 Apache 安裝多於一張 SSL 證書


之前的 SSL 證書都要收費,公司很少使用。但隨著改革風吹起,在網上開始找到一些免費的 SSL 證書簽發服務。正好在開發一個新項目,要是用上 SSL 的話就更好。於是我在一台已有 SSL 的服務器上安裝另一個子域名的 SSL 證書。起初出現「[warn] _default_ VirtualHost overlap on port 443, the first has precedence」警告。發現原來在 /etc/httpd/conf/httpd.conf 內欠了加入「NameVirtualHost *:443」。

game.sita-chan.com.conf
##  game.sita-chan.com Server Settings
<virtualhost *:80>
   ServerAdmin support@sita-chan.com
   ServerName game.sita-chan.com
   DocumentRoot /home/www/game.sita-chan.com
   <Directory "/home/www/game.sita-chan.com">
      AllowOverride All
      Order Allow,Deny
      Allow from All
   </Directory>
   ProxyRequests off
   <proxy *>
      Order deny,allow
      Allow from all
   </proxy>

   RewriteEngine On
   RewriteLog "/var/log/httpd/rewrite_log"
   RewriteLogLevel 4
</VirtualHost>

##  SSL related
<VirtualHost *:443>
   ServerAdmin support@sita-chan.com
   ServerName game.sita-chan.com:443

   ErrorLog /var/log/httpd/ssl_error_log.game.sita-chan.com
   TransferLog logs/ssl_access_log.game.sita-chan.com

   RewriteEngine On
   RewriteLog "/var/log/httpd/rewrite_log"
   RewriteLogLevel 4

   SSLEngine on
   SSLProtocol all -SSLv2
   SSLCipherSuite ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA;
   SSLCertificateFile /etc/pki/tls/certs/game.sita-chan.com/domain.crt
   SSLCertificateKeyFile /etc/pki/tls/certs/game.sita-chan.com/domain.key
   SSLCertificateChainFile /etc/pki/tls/certs/game.sita-chan.com/intermediate.pem

   DocumentRoot /home/www/game.sita-chan.com
   <Directory "/home/www/game.sita-chan.com">
      AllowOverride All
      Order Allow,Deny
      Allow from All
   </Directory>

   ProxyRequests off
   <proxy *>
      Order deny,allow
      Allow from all
   </proxy>
</VirtualHost>
以上寫法只作參考,不是一個好的做法。只要加裝 Proxy 把 HTTPS 請求改為 HTTP,就能避開一層加密算法。較好的做法是只保留 SSL 那段。