2010年9月16日 星期四

【分享】- 來授權一下吧!實作引入 License Verification Library (LVL)

雖然網路上出現了一堆批評或破解 Android Market 授權保護機制 (License Verification Library (LVL)) 的文章,但是咧 ~ 該來的還是會來的 ~ Google 遲早會在 Android Market 硬推它的 LVL (在 Android Market 的 App 設定中的 COPY PROTECT 下面就會看到有一行紅色的警示訊息 @_@):

The copy protection feature will be deprecated soon, please use licensing service instead.

漁郎近日實作了將 LVL 塞到現有 App 內並使用 Ant 與 Proguard 編譯的作法,跟 Google 的作法有些不一樣,卻符合漁郎偷懶的習性 >_< \\\ ... 貼上來自我記錄一下,免得以後忘記 @_@,亦與大夥兒分享一下,有不對的地方別 K 我喔 .... ^_^
PS. 如您不想用 Ant 與 Proguard 編譯您的程式,應可不用漁郎的方法喔!



[實作環境]

1. Eclipse 3.4
2. Android SDK 1.5 到 2.2 加載
3. Proguard 4.5.1
4. Market Licensing package, revision 1 (以下簡稱 LVL)

[實作步驟]

1. 先依 Google 網站的說明,將 LVL 套件內的 library 目錄以  Create project from existing source 的方式 建立新專案在 Eclipse 的 Workspace 中,並編譯 (Build Project) 該 LVL 專案,並在編譯後,到該 library 的目錄的 bin 下,將 com 整個目錄壓成 .zip 檔,並將該檔的檔名改為 LicenseChecker,副檔名改為 .jar (基本上 zip 跟 jar 的格式是一樣滴.... @_@)。

2. 將該 LicenseChecker.jar 複製到您現有專案的各個 libs 目錄下 (因為要用 Ant 編譯,目前只測出此法可行 @_@)。

3. 在各個專案的 Properties -> Java Build Path -> Libraries 中加入該 jar 的所在位置與檔名 (各個專案 libs 中的 LicenseChecker.jar)。

4. 在各個專案 (本例為 HelpMeSOSnR 專案) 的 build.xml 的 Proguard 程序段中要加入以下的參數 (本例為 com.e68club.android.HelpMeSOSnR) :

-keep public class com.e68club.android.HelpMeSOSnR.ActiveLicenseChecker
-keep public class com.e68club.android.HelpMeSOSnR.ActiveBootGoGo

5. 在各個專案的 Androidmanifest.xml 中要加入以下的參數:


<activity android:name=".ActiveLicenseChecker"
android:label="@string/app_name"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>


<activity android:name=".ActiveBootGoGo"
android:label="@string/app_name"
android:screenOrientation="portrait">
</activity>


並將原本主要 Activity 宣告內的 <intent-filter></<intent-filter> 段移除.

且加入以下的參數


<!-- Devices >= 3 have version of Android Market that supports licensing. -->
<uses-sdk android:minSdkVersion="3" />


<!-- Required permission to check licensing. -->
<uses-permission android:name="com.android.vending.CHECK_LICENSE" />
<uses-permission android:name="android.Settings.Secure.ANDROID_ID" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />


6. 複製 active_boots.xml 到 res/values 中。

7. 複製 e68club.3gp (改為您使用的開場動畫 3gp 影片) 到 res/raw 中。

8. 複製 avtive_boot.xml 到 res/layout 中。

9. 複製 ActiveBootGoGo.java 與 ActiveLicenseChecker.java 到 src 中 (本例是放到在 src/com.e68club.android.HelpMeSOSnR 下面)。

10. 編輯 ActiveBootGoGo.java,並將 startMainActivity() 內的 Intent(this, abc.class) 之 class 改為您要執行的主程式類別名稱。

11. 用 Ant 以 build.xml 編譯程式並測試。

12. 注意! 如您是安裝自己在測試的版本,也就是當您安裝的程式之 VersionCode 跟 Android Market 上的不一樣時,記得將 Android Market 之 profile 內的 License response 設定改為 LICENSED,不然 License Checker 會檢查 License response 的設定 ,如果 License response 仍是設定為 Respond normally ,則程式在 applicationError 事件中,會有 NOT_MARKET_MANAGED 的錯誤訊息產生。

[附件內容]

1. active_boots.xml 內容 (string)


<?xml version="1.0" encoding="UTF-8"?>
<resources>
    <!-- License checking status messages -->
    <string name="check_license">Check license</string>
    <string name="checking_license">Checking license...</string>
    <string name="dont_allow">Don\'t allow the user access</string>
    <string name="allow">Allow the user access</string>
    <string name="application_error">Application error: %1$s</string>


    <!-- Unlicensed dialog messages -->
    <string name="unlicensed_dialog_title">Application not licensed</string>
    <string name="unlicensed_dialog_body">This application is not licensed. Please get new version app or purchase it from Android Market.</string>
    <string name="get_button">Get app</string>
    <string name="quit_button">Exit</string>
    
    <!-- program load -->
    <string name="program_load">Program Load</string>
    <string name="program_loading">Program Loading...</string>
</resources>


2. avtive_boot.xml 內容 (layout)


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  android:layout_width="fill_parent"
  android:layout_height="fill_parent"
  android:layout_gravity="center"
  android:gravity="center"
  >
  <VideoView
    android:id="@+id/e68clubVideoView" 
    android:layout_width="fill_parent" 
    android:layout_height="fill_parent"
   android:layout_gravity="center"
  />
</LinearLayout>


3. ActiveLicenseChecker,java 內容


package com.e68club.android.HelpMeSOSnR;


import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings.Secure;
import android.util.Log;


import com.android.vending.licensing.AESObfuscator;
import com.android.vending.licensing.LicenseChecker;
import com.android.vending.licensing.LicenseCheckerCallback;
import com.android.vending.licensing.ServerManagedPolicy;


public class ActiveLicenseChecker extends Activity {

    // 複製自 Android Market 之 profile 內之 Public key 字碼
private static final String BASE64_PUBLIC_KEY =
"Your License Public KEY string";


// 自定亂數
private static final byte[] SALT = new byte[] {
     33, -9, -18, 66, -73, -2, 99, 9, 12, 22, 21, 27, 30, -111, -107, 28, -75, 112, 66
    };

private LicenseChecker mChecker;
private LicenseCheckerCallback mLicenseCheckerCallback;

private ProgressDialog myDialog = null;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);


// Android Market 線上授權檢查程式碼  ...... start .............................

// Try to use more data here. ANDROID_ID is a single point of attack.
String deviceId = Secure.getString(getContentResolver(), Secure.ANDROID_ID);

// Library calls this when it's done.
mLicenseCheckerCallback = new MyLicenseCheckerCallback();

// Construct the LicenseChecker with a policy.
mChecker = new LicenseChecker(this, new ServerManagedPolicy(this,
new AESObfuscator(SALT, getPackageName(), deviceId)),
BASE64_PUBLIC_KEY);

// Android Market 線上授權檢查程式碼  ...... end ...............................

myDialog = ProgressDialog.show(
ActiveLicenseChecker.this,
getString(R.string.check_license),
getString(R.string.checking_license), true);

new Thread() {
public void run() {
try {
// 執行 Android Market 線上授權檢查
mChecker.checkAccess(mLicenseCheckerCallback);
} catch (Exception e) {
e.printStackTrace();

// 卸載所建立的 myDialog 物件
myDialog.dismiss();

// 結束這個 Activity
finish();
}
}
}.start(); // 啟動 Thread
}

@Override
protected Dialog onCreateDialog(int id) {
// We have only one dialog.
return new AlertDialog.Builder(this)
.setTitle(R.string.unlicensed_dialog_title)
.setCancelable(false)
.setMessage(R.string.unlicensed_dialog_body)
.setPositiveButton(R.string.get_button,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
Intent marketIntent = new Intent(
Intent.ACTION_VIEW,
Uri.parse("http://market.android.com/details?id="
+ getPackageName()));
// 開啟瀏覽器, 導向 Android Market 找該 App
startActivity(marketIntent);

// 結束這個 Activity
finish();
}
})
.setNegativeButton(R.string.quit_button,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
// 結束這個 Activity
finish();
}
}).create();
}

private class MyLicenseCheckerCallback implements LicenseCheckerCallback {
@Override
public void allow() {
// 授權檢查成功, 是合法授權程式
if (isFinishing()) {
// Don't update UI if Activity is finishing.
return;
}

Log.d("LicenseChecker","Allow");


// 取得授權正常, 執行主程式
// Should allow user access.
startMainActivity();
}


@Override
public void dontAllow() {
// 授權檢查失敗, 是非法授權的程式, 也可能是第一次取得授權卻沒連上網路所致
if (isFinishing()) {
// Don't update UI if Activity is finishing.
return;
}

// 卸載所建立的 myDialog 物件
myDialog.dismiss();

Log.d("LicenseChecker","Do Not Allow");


// 秀出無授權, 請使用者點選離開或引導使用者到 Android Market 購買軟體或下載新版軟體
showDialog(0);
}


@Override
public void applicationError(ApplicationErrorCode errorCode) {
// 授權檢查異常, 從網路到主機取得授權失敗, 可能是主機沒設定測試授權回應 (例如: LICENSED)
if (isFinishing()) {
// Don't update UI if Activity is finishing.
return;
}

// 卸載所建立的 myDialog 物件
myDialog.dismiss();

// 取得授權有異常, 以可能是使用者的版本與 Android 上的版本不符, 
// 請使用者點選離開或引導使用者到 Android Market 購買軟體或下載新版軟體
showDialog(0);
}
}


private void startMainActivity() {
// 轉執行主要程式, 並結束本授權檢查程式
// 可將此處的 ActiveBootGoGo.class 換成您的主程式碼之 MainActivity.class, 以跳過開場動畫程式
startActivity(new Intent(this, ActiveBootGoGo.class));

// 卸載所建立的 myDialog 物件
myDialog.dismiss();

// 結束這個 Activity
finish();
}


@Override
protected void onDestroy() {
super.onDestroy();
// 終止授權檢查元件
mChecker.onDestroy();
}
}


4. ActiveBootGoGo.java 內容


package com.e68club.android.HelpMeSOSnR;


import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Bundle;
import android.widget.VideoView;


public class ActiveBootGoGo extends Activity {

private ProgressDialog myDialog = null;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// 如啟用這段, 則在 onResume 那段要註記掉 ----------------------- start ---------------
// 程式開始後, 先播完開場動畫檔, 再啟動主程式
setContentView(R.layout.active_boot);
Context context = ActiveBootGoGo.this;

Uri uri = Uri.parse (
"android.resource://"+context.getPackageName()+"/"+R.raw.e68club
);

VideoView video = (VideoView) findViewById(R.id.e68clubVideoView);
video.setVideoURI(uri);
video.requestFocus();

// 開始撥放影片
video.start();

// 影片撥完後會執行的 OnCompletionListener
video.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer arg0) {
startMainActivity();
}
});
// 如啟用這段, 則在 onResume 那段要註記掉 ----------------------- end -----------------
}

@Override
protected void onResume() {
super.onResume();

// 如啟用這段, 則上面那段要註記掉 ----------------------- start ---------------
// 程式開始後, 直接啟動主程式
/*
startMainActivity();
*/
// 如啟用這段, 則上面那段要註記掉 ----------------------- end -----------------
}


private void startMainActivity() {
// 記得要改為您要執行的主程式類別
final Intent intent = new Intent(this, HelpMeSOSnR.class);

myDialog = ProgressDialog.show(
ActiveBootGoGo.this,
getString(R.string.program_load),
getString(R.string.program_loading), true);

new Thread() {
public void run() {
try {
// 轉執行主要程式, 並結束本程式
startActivity(intent);

// 卸載所建立的 myDialog 物件
myDialog.dismiss();

// 結束這個 Activity
finish();
} catch (Exception e) {
e.printStackTrace();

// 卸載所建立的 myDialog 物件
myDialog.dismiss();

// 結束這個 Activity
finish();
}
}
}.start(); // 啟動 Thread
}
}


後記:


近日在 Android Developers Blog 中有篇文章說將 Market Licensing 程式庫的
com.android.vending.licensing.LicenseValidator.java 內之程式段 public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) {
   //......
} 以 CRC 函數改寫後會讓破解者較難搞定, 經測試是可用的, 但就不知效果如何了 @_@ ~~
以下是漁郎改寫後的程式碼 :

public void verify(PublicKey publicKey, int responseCode, String signedData, String signature) {
        String userId = null;
        // Skip signature check for unsuccessful requests
        ResponseData data = null;
        if (responseCode == LICENSED || responseCode == NOT_LICENSED ||
                responseCode == LICENSED_OLD_KEY) {
            // Verify signature.
            try {
                Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
                sig.initVerify(publicKey);
                sig.update(signedData.getBytes());


                if (!sig.verify(Base64.decode(signature))) {
                    Log.e(TAG, "Signature verification failed.");
                    handleInvalidResponse();
                    return;
                }
            } catch (NoSuchAlgorithmException e) {
                // This can't happen on an Android compatible device.
                throw new RuntimeException(e);
            } catch (InvalidKeyException e) {
                handleApplicationError(ApplicationErrorCode.INVALID_PUBLIC_KEY);
                return;
            } catch (SignatureException e) {
                throw new RuntimeException(e);
            } catch (Base64DecoderException e) {
                Log.e(TAG, "Could not Base64-decode signature.");
                handleInvalidResponse();
                return;
            }


            // Parse and validate response.
            try {
                data = ResponseData.parse(signedData);
            } catch (IllegalArgumentException e) {
                Log.e(TAG, "Could not parse response.");
                handleInvalidResponse();
                return;
            }


            if (data.responseCode != responseCode) {
                Log.e(TAG, "Response codes don't match.");
                handleInvalidResponse();
                return;
            }


            if (data.nonce != mNonce) {
                Log.e(TAG, "Nonce doesn't match.");
                handleInvalidResponse();
                return;
            }


            if (!data.packageName.equals(mPackageName)) {
                Log.e(TAG, "Package name doesn't match.");
                handleInvalidResponse();
                return;
            }


            if (!data.versionCode.equals(mVersionCode)) {
                Log.e(TAG, "Version codes don't match.");
                handleInvalidResponse();
                return;
            }


            // Application-specific user identifier.
            userId = data.userId;
            if (TextUtils.isEmpty(userId)) {
                Log.e(TAG, "User identifier is empty.");
                handleInvalidResponse();
                return;
            }
        }


     // 以下這段易被駭客破解, 因 0x0 表 LICENSED, 0x1 表 NOT_LICENSED ,
        // 因此, 依照 Android Developers Blog 上的網友建議的改法, 改寫成以下程式碼


        java.util.zip.CRC32 crc32 = new java.util.zip.CRC32();
        crc32.update(responseCode);
        long transformedResponseCode = crc32.getValue();


        if (transformedResponseCode == 3523407757l) {
            // ... put unrelated application code here ...
            // crc32(LICENSED) == 3523407757
            Log.e(TAG, "CORI APP LICENSED!");
            LicenseResponse limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
            handleResponse(limiterResponse, data);
        } else if (transformedResponseCode == 100745590l) {
            // ... put unrelated application code here ...
            // crc32(LICENSED_OLD_KEY) == 1007455905
            Log.e(TAG, "CORI APP OLD LICENSED!");
            LicenseResponse limiterResponse = mDeviceLimiter.isDeviceAllowed(userId);
            handleResponse(limiterResponse, data);
        } else if (transformedResponseCode == 2768625435l) {
            // ... put unrelated application code here ...
            // crc32(NOT_LICENSED) == 2768625435
            Log.e(TAG, "CORI APP NOT LICENSED!");
         handleResponse(LicenseResponse.NOT_LICENSED, data);
        } else {
         switch (responseCode) {
             case ERROR_CONTACTING_SERVER:
             Log.w(TAG, "Error contacting licensing server.");
             handleResponse(LicenseResponse.RETRY, data);
             break;
             case ERROR_SERVER_FAILURE:
             Log.w(TAG, "An error has occurred on the licensing server.");
             handleResponse(LicenseResponse.RETRY, data);
             break;
             case ERROR_OVER_QUOTA:
             Log.w(TAG, "Licensing server is refusing to talk to this device, over quota.");
             handleResponse(LicenseResponse.RETRY, data);
             break;
             case ERROR_INVALID_PACKAGE_NAME:
             handleApplicationError(ApplicationErrorCode.INVALID_PACKAGE_NAME);
             break;
             case ERROR_NON_MATCHING_UID:
             handleApplicationError(ApplicationErrorCode.NON_MATCHING_UID);
             break;
             case ERROR_NOT_MARKET_MANAGED:
             handleApplicationError(ApplicationErrorCode.NOT_MARKET_MANAGED);
             break;
             default:
             Log.e(TAG, "Unknown response code for license check.");
                 handleInvalidResponse();
         }
        }
    }


21 則留言:

  1. LVL形同虛設呢

    1.一分鐘內快速處理掉
    2.再者好像是要收費程式才能用LVL
    3.至於proguard也是參考用的

    最近總是跟朋友說android的java code就是open source

    回覆刪除
  2. 哈哈 ^_^ 有同感~ 只是寥勝於無 >_< ...

    回覆刪除
  3. Android Developers Blog 中有篇文章說改寫 Market Licensing 程式庫的部份程式碼可防被破解, 就不知效果如何了 ? @_@ ... (已測試並將程式碼貼在文章尾端)

    回覆刪除
  4. 感覺好像不需要改到那邊,我之前研究的時候,是改他的callback function裡面要做的事情,比如說如果他check完成之後發現是非法版本,會跳到market,我就把那段code passby,換成合法版本會做的事情

    回覆刪除
  5. 所以說... 不用改到 Google 的 LVL 程式庫原始碼就可避開 LVL 被破解的危險? 可否請您幫忙看看這個網友是說啥 @_@ : http://android-developers.blogspot.com/2010/09/securing-android-lvl-applications.html ... 謝啦! ^_^

    回覆刪除
  6. 看起來應該是LVL會去GOOGLE拿responseCode,來判斷是否是合法授權,一般改法都是改那個reponseCode,讓她丟出LICENSED,該文章提到再判斷responseCode的時候,可以加上CRC檢查..我還是覺得完全沒作用

    如果我要改,直接把整個switch拿掉,不用檢查,不管結果為何,直接丟出LICENSED即可

    另外他也提到可以在callback function做些額外判斷,這邊也是老樣子

    老實說java code出來的東西就是沒安全性,等於是open source,要加code,刪code也是相當容易

    回覆刪除
  7. 也對! ^_^ .. 所以... 還是只能當作是"安慰"自己的 >_<\\\

    回覆刪除
  8. 最近做了幾種方法,應該可以有效防止做java反組議的破解.

    一口氣把三四種方法全都放到我的軟體上,到時再來看看成效如何.但目前我已經破解不了我自己的軟體了XD

    有個很簡單的方法是一般人要修改你的APK都要重新sign過,從新sign的key基本上是不太可能一樣(因為你sign的時候是用primary key)

    步驟大概如下:
    1.JAVA中取得自己APK的signatures(從PackageManager裡面可以拿到)
    2.程式必需要強制使用JNI(某段function即可,也就是說程式一定會跑到的地方,把她搬到C code,不見得要很複雜,就是程式的處理核心必要動作即可
    3.跑JNI的時候,把signaturest傳到C層,邊處理順便檢查,只要JAVA或JNI被改掉,signatures都會不一樣,就會被抓包,在C層可以做一些奇怪的事情之類的

    這樣目前看起來唯二的解法就是
    A.反組arm code,這不是不能,但入門比較高,至少要有多年IDA反組經驗吧,這種人是不怎多,至少比android java的dex來說少多了
    B.拔掉jni,全改到java,但這部分有點難,因為很了解code才有辦法,如果jni裡面做的事情太多估計還是有難度.

    總結就是要逼要破解的話,一定得改到C層,如果她有能力改C層,自然也有能力改android java,那怎麼防範都是沒效果囉

    感覺這樣搭配LVL應該就可以降低不法之徒的欲望吧

    就像賣車子鎖的人常說:天下沒有打不開的鎖,但你的鎖要是很難開,小偷會去找比較好開或沒裝鎖的人

    回覆刪除
  9. Gpc , 您這段 "小偷會去找比較好開或沒裝鎖的人" 形容的非常確切 , 但就不知會不會激起那些 “害"客 們好奇好鬥的劣根性了... @@ , 漁郎來試試您的方法看看, 搞不好可以舉一反三 . ^_^ ...

    回覆刪除
  10. 今天仔細把LVL的code trace完了,感覺動刀的地方不多,漏洞倒是幾十個,而且我發現LVL會造成每次用都要上網驗證,如果時間超過一陣子(timeout),就會重新驗証的說,也就是說沒一直上網的話,如果突然要用就不能用了,這樣不會被user罵死嗎

    我之前7天,30天檢查一次,都被狂罵了,還寫信來罵說"我出國,沒網路,怎麼用呢!" 沒想到剛看LVL 預設居然timeout是1分鐘...上個廁所回來又得重新上網驗證了

    回覆刪除
  11. 耶 !? 我測過在第一次認證後, 第二次開啓程式後都不會再上網認證的 (關了網路測試的結果), 但前提是: 在手機上的程式版本 (VersionCode) 要跟 Android Market 上架的程式版本一樣. (難道我有漏了啥嗎 ?! 好可怕 !?)

    回覆刪除
  12. 我的軟體沒有上架 我是用我market帳號測試的說
    連線之後 過一陣子就等驗證 不然就爆掉了

    他有個timeout值 你可以trace一下LVL的source

    回覆刪除
  13. 沒上架的會有問題, 有個國外的網友, 在上架後 (沒發佈), 他跟您一樣狀況的問題就解了, 或許您可試試. (他的軟體也是沒上架時, 會有跟您一樣的問題)
    參考: http://groups.google.com.tw/group/android-developers/browse_thread/thread/6e07229f51983c8d
    他有回我信, 說上架後 (沒發佈), 問題就解了. 參考參考.

    回覆刪除
  14. 意思是說 上架後 只要使用者 連上網一次 驗證 之後就不再需要了嗎 就算沒有網路?

    回覆刪除
  15. 依照漁郎測試的結果, 只要是已上架的程式, 在第一次認證後, 關掉網路的情況下, 的確不會再上網認證, 但前提是在認證時(第一次開啓程式), 手機上的程式版次 (VersionCode) 一定要跟 Android Market 上的一樣 (實測結果), 不然, 就會有認了再認的情況產生. 我想應該是沒上架的程式, 不會發給正式認證所致 (我猜 Android Market 當作未上架的程式是在測試, 所以, 發出的憑證是暫用的)

    回覆刪除
  16. 我實際測試了....不行

    說一下我的做法

    修改 com.android.vending.licensing.ServerManagedPolicy 裡面的 private void setValidityTimestamp(String validityTimestamp)


    lValidityTimestamp = System.currentTimeMillis() + 1* MILLIS_PER_MINUTE;
    裡面的 1改成 0.001 表是他的TS是1ms,簡單的說 一旦驗證過 1ms之後又得重新驗證!!!

    再來是com.android.vending.licensing.LicenseValidator 裡面的 public void verify(PublicKey publicKey, int responseCode, String signedData, String signature)

    第一行加個印LOG吧!
    Log.i("gpc","code=" +responseCode);


    然後一些就如正常 上傳market 收費軟體(但我自己沒有買) 再Market裡面的Edit Profile改成LINCENSED

    VersionCode跟MARKET完全一樣,

    跑程式,首先 不開網路 跑程式 LOG印出257
    callback function跑dontAllow()

    再來開網路 LOG印出0 callback function跑allow()

    再來關網路 LOG印出257
    callback function跑dontAllow()

    所以實際測試證明,沒網路就不能用了

    除非在Policy裡面
    ValidityTimestamp = System.currentTimeMillis() + 1* MILLIS_PER_MINUTE;
    把TS加長,但預設的是1分鐘...

    當然在1分鐘之內 是不會認了再認 但問題是超過一分鐘呢????

    不曉得是哪裡出了問題?

    回覆刪除
  17. Gpc 兄, 以下是漁郎測試的 Logs , 請您參考參考囉 ^_^ :

    == 第一次開啓程式 (開啓網路) ======================
    10-09 19:12:41.037: INFO/LicenseChecker(3432): Binding to licensing service.
    10-09 19:12:41.867: INFO/LicenseChecker(3432): Calling checkLicense on service for com.e68club.android.GodDonggangKingWen
    10-09 19:12:41.877: INFO/LicenseChecker(3432): Start monitoring timeout.
    10-09 19:12:43.167: INFO/LicenseChecker(3432): Received response.
    10-09 19:12:43.167: INFO/LicenseChecker(3432): Clearing timeout.
    10-09 19:12:43.457: DEBUG/LicenseChecker(3432): Allow

    == 第二次開啓程式 (關閉網路) ======================
    10-09 19:15:00.527: INFO/LicenseChecker(3495): Using cached license response
    10-09 19:15:00.527: DEBUG/LicenseChecker(3495): Allow

    == 第三次開啓程式 (關閉網路) ====================
    10-09 19:32:54.287: INFO/LicenseChecker(3949): Using cached license response
    10-09 19:32:54.287: DEBUG/LicenseChecker(3949): Allow

    回覆刪除
  18. 真是怪異 都是 LicenseChecker 裡面的程式 莫非我們用的LVL是不一樣的 驚!

    回覆刪除
  19. 大概看到問題了,是在ServerManagedPolicy.java裡面,當他設定ts的時候,如果沒設成功,就會以目前時間+1分

    但還是不知道為什會這樣 我現在上架了 Version Code 一樣,就差沒有自己買自己的軟體 但就是只能維持一分鐘,超過一分鐘又得上網,不然就會callback跑到dontallow




    private void setValidityTimestamp(String validityTimestamp) {
    Long lValidityTimestamp;
    try {
    lValidityTimestamp = Long.parseLong(validityTimestamp);
    } catch (NumberFormatException e) {
    // No response or not parsable, expire in one minute.
    Log.w(TAG, "License validity timestamp (VT) missing, caching for a minute");
    lValidityTimestamp = System.currentTimeMillis() + 1* MILLIS_PER_MINUTE;
    validityTimestamp = Long.toString(lValidityTimestamp);
    }

    mValidityTimestamp = lValidityTimestamp;
    mPreferences.putString(PREF_VALIDITY_TIMESTAMP, validityTimestamp);
    }

    回覆刪除
  20. Trevor Johns
    檢視個人資料 翻譯為中文 (繁體)
    更多選項 2010年8月3日, 上午4時24分

    If you use a test response (in other words, you're using a developer/test
    account and have the Market publisher console set to something other than
    "Respond Normally" for licensing requests), the server doesn't send any
    response "extras".

    This means that your local cache will expire after 1 minute -- which is the
    minimum cache lifetime as enforced by ServerManagedPolicy.

    --
    Trevor Johns
    Google Developer Programs, Android
    http://developer.android.com

    On Sat, Jul 31, 2010 at 3:17 PM, sblantipodi

    回覆刪除
  21. 感謝 400 提供的資料,被系統放到「垃圾留言」中了~ 怪怪的系統 @@ :

    谷歌大神翻譯如下:如果您使用的測試響應(換句話說,您使用的是一個開發人員/測試帳戶,並有市場出版商控制台設置以外的東西“正常響應”授權請求),服務器不會發送任何回應“群眾演員”。這意味著,您的本地緩存將於1分鐘後 - 這是由ServerManagedPolicy執行最小緩存壽命。

    回覆刪除