정해진 기간이 지나면 만료되는 Android 평가판 애플리케이션 만들기


답변:


186

현재 대부분의 개발자는 다음 세 가지 기술 중 하나를 사용하여이를 수행합니다.

첫 번째 접근 방식은 쉽게 우회 할 수 있습니다. 앱을 처음 실행할 때 날짜 / 시간을 파일, 데이터베이스 또는 공유 기본 설정에 저장하고 그 후 앱을 실행할 때마다 평가 기간이 종료되었는지 확인합니다. 제거하고 다시 설치하면 사용자가 다른 평가 기간을 가질 수 있기 때문에 우회하기 쉽습니다.

두 번째 접근 방식은 우회하기가 더 어렵지만 여전히 우회 할 수 있습니다. 하드 코딩 된 시한 폭탄을 사용하십시오. 기본적으로이 접근 방식을 사용하면 평가판의 종료 날짜를 하드 코딩하고 앱을 다운로드하고 사용하는 모든 사용자가 동시에 앱을 사용할 수 없게됩니다. 구현하기 쉽고 대부분의 경우 세 번째 기술의 문제를 겪고 싶지 않았기 때문에이 접근 방식을 사용했습니다. 사용자는 휴대폰에서 수동으로 날짜를 변경하여이를 피할 수 있지만 대부분의 사용자는 이러한 작업을 수행하는 데 어려움을 겪지 않습니다.

세 번째 기술은 당신이하고 싶은 일을 진정으로 성취 할 수있는 유일한 방법입니다. 서버를 설정해야하며 애플리케이션이 시작될 때마다 앱이 전화기 고유 식별자 를 서버로 보냅니다. 서버에 해당 전화 ID에 대한 항목이 없으면 새 항목을 만들고 시간을 기록합니다. 서버에 전화 ID에 대한 항목이있는 경우 평가 기간이 만료되었는지 간단한 확인을 수행합니다. 그런 다음 평가판 만료 확인 결과를 애플리케이션에 다시 전달합니다. 이 접근 방식은 피할 수 없어야하지만 웹 서버 등을 설정해야합니다.

항상 onCreate에서 이러한 검사를 수행하는 것이 좋습니다. 만료가 끝나면 앱의 정식 버전에 대한 시장 링크 가 포함 된 AlertDialog가 팝업 됩니다. "확인"버튼 만 포함하고 사용자가 "확인"을 클릭하면 "finish ()"를 호출하여 활동을 종료합니다.


2
환상적인 대답입니다. 두 번째 옵션이 아마도 최고라고 생각합니다. 안타깝게도 Google 자체는 소규모 및 대규모 브랜드 개발자가 더 많은 Android 애플리케이션을 생산하도록 장려 할 수 있기 때문에 일종의 라이선스 시스템을 제공하지 않습니다.
Tom

8
또한 시작 중에 확인하지 않습니다. 당신의 목표는 사용자를 처벌하는 것이 아니라 앱을 판매하는 것입니다. (단지 보너스 일뿐입니다.) 실행 중 2 분마다 확인하도록 설정 한 경우 사용자가 무언가를 시작하도록 한 다음 지불해야한다는 것을 깨닫게됩니다. 당신이 정말 쉽게 지불하고 직장에 복귀한다면 (안드로이드에서 할 수 있는지 잘 모르겠습니다) 나는 당신이 onCreate에서 확인하는 것보다 더 많이 팔 것이라고 생각합니다.
Whaledawg 2009-06-16

4
@Whaledawg : 서버는 전화 ID와 최초 실행 시간을 데이터베이스에 저장하고 있기 때문에 나중에 비교하기 때문에 자신의 서버를 실행해야합니다. 또한 확인을 할 때 순전히 개발자의 선호도이므로 하드를 사용했습니다. 훌륭한 결과를 가진 게임에서 코딩 된 시한 폭탄. 전체 앱이로드되지만 사용자는 표시된 대화 상자와 만 상호 작용할 수 있습니다. 해당 대화 상자에는 사용자를 게임의 구매 페이지로 직접 연결하는 버튼이 있습니다. 사용자들은 그 게임이 안드로이드 마켓 개장 이후 상위 10 개 유료 앱에 포함 되었기 때문에 AFAIK를 신경 쓰지 않는 것 같습니다.
snctln

11
추가 서버 설정으로 인해 옵션 3을 사용하기를 꺼리는 사람이라면 Parse.com을 살펴보십시오. 동기화입니다.
Joel Skrepnek 2012

3
평가판의 하드 코드 종료 날짜는 무엇을 의미합니까? 그것은 당신이 미래에 다른 하드 코딩 된 날짜를 가진 평가판 앱의 새로운 버전을 영원히 계속 출시한다는 것을 의미합니까?
Jasper

21

Android Studio 프로젝트에 간단히 드롭 할 수 있는 Android 평가판 SDK 를 개발했으며 오프라인 유예 기간을 포함하여 모든 서버 측 관리를 처리합니다.

사용하려면 간단히

메인 모듈에 라이브러리 추가 build.gradle

dependencies {
  compile 'io.trialy.library:trialy:1.0.2'
}

주요 활동의 onCreate()방법으로 라이브러리 초기화

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

    //Initialize the library and check the current trial status on every launch
    Trialy mTrialy = new Trialy(mContext, "YOUR_TRIALY_APP_KEY");
    mTrialy.checkTrial(TRIALY_SKU, mTrialyCallback);
}

콜백 핸들러를 추가합니다.

private TrialyCallback mTrialyCallback = new TrialyCallback() {
    @Override
    public void onResult(int status, long timeRemaining, String sku) {
        switch (status){
            case STATUS_TRIAL_JUST_STARTED:
                //The trial has just started - enable the premium features for the user
                 break;
            case STATUS_TRIAL_RUNNING:
                //The trial is currently running - enable the premium features for the user
                break;
            case STATUS_TRIAL_JUST_ENDED:
                //The trial has just ended - block access to the premium features
                break;
            case STATUS_TRIAL_NOT_YET_STARTED:
                //The user hasn't requested a trial yet - no need to do anything
                break;
            case STATUS_TRIAL_OVER:
                //The trial is over
                break;
        }
        Log.i("TRIALY", "Trialy response: " + Trialy.getStatusMessage(status));
    }

};

평가판을 시작하려면 mTrialy.startTrial("YOUR_TRIAL_SKU", mTrialyCallback); 앱 키로 전화하면 평가판 SKU는 Trialy 개발자 대시 보드 에서 찾을 수 있습니다 .


데이터를 활성화해야합니까?
Sivaram Boina

trialy는 신뢰할 수 없습니다
아미르 도라

1
@AmirDe 안녕 아미르, 당신을 위해 작동하지 않는 것이 무엇인지 알려주시겠습니까? 나는 도와 드리겠습니다 Trialy 1000 + 사용자를위한 큰 노력 support@trialy.io

@Nick은 내 기기가 Android 롤리팝을 실행하는 이유를 모르겠습니다. 대시 보드에서 하루를 설정하면 몇 분 후에 하루가 음수 값으로 표시됩니다. 아직 대시 보드에 며칠이 남아 있지만 평가판이 만료되었다고합니다. 나는 또한 누가 장치에서 테스트했는데, 누가에서 잘 작동하는 것 같습니다. 어쩌면 일부 이전 안드로이드 버전 호환성 문제가 있습니다
아미르 도라

1
2016 년부터이 서비스를 사용하고 있으며 매번 잘 작동합니다. 공식 프로젝트에서도 이것을 사용했습니다. 이것은 받아 들여진 대답이어야합니다.
Tariq Mahmood

17

이것은 오래된 질문이지만 어쨌든 누군가에게 도움이 될 것입니다.

가장 단순한 접근 방식 ( 앱을 제거 / 재설치하거나 사용자가 기기의 날짜를 수동으로 변경 하면 실패 함) 을 사용하려는 경우 다음과 같이 할 수 있습니다.

private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
private final long ONE_DAY = 24 * 60 * 60 * 1000;

@Override
protected void onCreate(Bundle state){
    SharedPreferences preferences = getPreferences(MODE_PRIVATE);
    String installDate = preferences.getString("InstallDate", null);
    if(installDate == null) {
        // First run, so save the current date
        SharedPreferences.Editor editor = preferences.edit();
        Date now = new Date();
        String dateString = formatter.format(now);
        editor.putString("InstallDate", dateString);
        // Commit the edits!
        editor.commit();
    }
    else {
        // This is not the 1st run, check install date
        Date before = (Date)formatter.parse(installDate);
        Date now = new Date();
        long diff = now.getTime() - before.getTime();
        long days = diff / ONE_DAY;
        if(days > 30) { // More than 30 days?
             // Expired !!!
        }
    }

    ...
}

실제로 SharedPreferences 및 Android 2.2 Froyo 이상을 사용하는 경우 Google 데이터 동기화를위한 데이터 백업 API를 구현하는 한 사용자는 설정> 애플리케이션으로 이동하여 기기를 제거하거나 제거하여이 문제를 피할 수 없어야합니다. 데이터 지우기. 또한, 날짜에 방법은 getTime없습니다 getTimeInMillis.
Tom

동의하지 않습니다. 사용자가 기기의 날짜를 수동으로 변경하는 경우에도 실패하고 사용자가 데이터를 수동으로 삭제하면 어떻게 되나요?
Mohammed Azharuddin Shaikh

@Caner 이것은 sharedprefrence로 chek하는 것이 좋지만 사용자는 설정-> 응용 프로그램 관리자에서 메모리를 지우고 재사용하면 어떻게 될까요?
CoronaPintu 2013 년

@CoronaPintu,이 접근 방식은 firebase에서도 시도 할 수 있으므로 완벽한 조합이 될 것이며 평가 기간은 앱이 제거 될 것입니다.
Noor Hossain

접근 방식을 "strangetimes"답변과 결합하고 추가하면 접근 방식이 완벽 해집니다.
누르 후세인

10

이 질문과 snctln 의 답변은 저의 학사 논문으로 방법 3을 기반으로 한 솔루션을 작업하도록 영감을주었습니다. 나는 현재 상태가 생산적인 사용을위한 것이 아니라는 것을 알고 있지만 그것에 대해 어떻게 생각하는지 듣고 싶습니다! 그런 시스템을 사용 하시겠습니까? 클라우드 서비스로 보시겠습니까 (서버 구성에 문제가 없음)? 보안 문제 또는 안정성 이유가 걱정 되십니까?

학사 과정을 마치는 즉시 소프트웨어 작업을 계속하고 싶습니다. 이제 여러분의 피드백이 필요합니다!

소스 코드는 GitHub https://github.com/MaChristmann/mobile-trial에서 호스팅됩니다.

시스템에 대한 몇 가지 정보 :-시스템에는 Android 라이브러리, node.js 서버 및 여러 평가판 앱 및 게시자 / 개발자 계정을 관리하기위한 구성 기의 세 부분이 있습니다.

  • 시간 기반 평가판 만 지원하며 전화 ID가 아닌 사용자 (Play 스토어 또는 기타) 계정을 사용합니다.

  • Android 라이브러리의 경우 Google Play 라이선스 확인 라이브러리를 기반으로합니다. node.js 서버에 연결하도록 수정했으며 추가로 라이브러리는 사용자가 시스템 날짜를 변경했는지 인식하려고 시도합니다. 또한 검색된 평가판 라이센스를 AES 암호화 된 공유 기본 설정에 캐시합니다. 구성자를 사용하여 캐시의 유효 시간을 구성 할 수 있습니다. 사용자가 "데이터를 지우는"경우 라이브러리는 서버 측 검사를 강제합니다.

  • 서버는 https를 사용하고 있으며 라이센스 확인 응답에 디지털 서명도합니다. CRUD 평가판 앱 및 사용자 (게시자 및 개발자)를위한 API도 있습니다. Licensing Verfication Library 개발자와 유사한 개발자는 테스트 결과와 함께 평가판 앱에서 동작 구현을 테스트 할 수 있습니다. 따라서 구성자에서 라이센스 응답을 "라이센스 있음", "라이센스 없음"또는 "서버 오류"로 명시 적으로 설정할 수 있습니다.

  • 새로운 기능으로 앱을 업데이트하는 경우 모든 사람이 다시 시도 할 수 있기를 원할 수 있습니다. 구성 관리자에서이를 트리거해야하는 버전 코드를 설정하여 만료 된 라이선스가있는 사용자의 평가판 라이선스를 갱신 할 수 있습니다. 예를 들어 사용자가 버전 코드 3에서 앱을 실행하고 있고 버전 코드 4의 기능을 사용해 보길 원합니다. 그가 앱을 업데이트하거나 다시 설치하면 서버가 마지막으로 시도한 버전을 알고 있기 때문에 전체 평가 기간을 다시 사용할 수 있습니다. 시각.

  • 모든 것은 Apache 2.0 라이선스하에 있습니다.


2
당신은 방금 내 하루를 구했습니다. 수고 해주셔서 감사합니다. 한 번만 얻은 암호화 된 구성을 사용하고 애플리케이션 내부에 공개 키를 유지하는 동일한 솔루션을 작성하려고했습니다. 그래서 중요한 문제는 어떻게 라이센스를 부여합니까? 여러 번의 허가를받지 않으려면 휴대 전화와 함께 휴대하는 고유 ID가 필요할 수 있습니다.
user2305886 dec.

6

이를 수행하는 가장 쉽고 가장 좋은 방법은 BackupSharedPreferences를 구현하는 것입니다.

앱을 제거하고 다시 설치하더라도 기본 설정은 유지됩니다.

설치 날짜를 기본 설정으로 저장하기 만하면됩니다.

이론은 다음과 같습니다. http://developer.android.com/reference/android/app/backup/SharedPreferencesBackupHelper.html

예 : Android SharedPreferences 백업이 작동하지 않음


3
사용자는 시스템 설정에서 백업을 비활성화 할 수 있습니다.
Patrick

5

접근 방식 4 : 애플리케이션 설치 시간을 사용합니다.

API 레벨 9 (Android 2.3.2, 2.3.1, Android 2.3, GINGERBREAD)부터 .NET Framework에는 firstInstallTimelastUpdateTimePackageInfo있습니다.

자세히 알아보기 : Android에서 앱 설치 시간을 얻는 방법


snctln의 답변의 방법 1을 쉽게 우회하지 않고 안정적으로 사용할 수 있습니까? 아니면 동일하거나 유사한 문제가 있습니까?
jwinn

이 방법을 테스트했습니다. 좋은 점은 데이터가 삭제 되어도 작동한다는 것입니다. 나쁜 점은 제거 / 재설치로 피할 수 있다는 것입니다.
Jean-Philippe Jodoin 2014 년

확인 : 내 Pixel에서 앱을 제거하고 다시 설치하면 처음 설치할 때 재설정됩니다. 사용자가 재판을 다시 설정하는 그게 아주 사소한 있습니다
파비안 Streitel

3

이제 최신 버전의 Android 무료 평가판 구독이 추가되었으므로 무료 평가판 기간 동안 앱 내에서 구독을 구매 한 후에 만 ​​앱의 모든 기능을 잠금 해제 할 수 있습니다. 이렇게하면 사용자가 평가 기간 동안 앱을 사용할 수 있으며, 평가 기간 후에도 앱이 제거되면 구독 금액이 귀하에게 이체됩니다. 나는 시도하지 않았지만 아이디어를 공유했습니다.

다음은 문서입니다.


2
나는 이것이 단일 구매에 효과가 있었으면 좋겠다. 연간 또는 월간 구독에 대해서만 작동합니다.
jwinn 2014 년

3

제 생각에는이 작업을 수행하는 가장 좋은 방법은 Firebase 실시간 데이터베이스를 사용하는 것입니다.

1) 앱에 Firebase 지원 추가

2) '익명 인증'을 선택하면 사용자가 가입 할 필요가 없으며 자신이 무엇을하는지 알지 못합니다. 이것은 현재 인증 된 사용자 계정에 대한 링크가 보장되므로 여러 장치에서 작동합니다.

3) Realtime Database API를 사용하여 'installed_date'값을 설정합니다. 시작시이 값을 검색하여 사용하면됩니다.

나는 똑같이 해왔고 훌륭하게 작동합니다. 제거 / 재설치를 통해이를 테스트 할 수 있었고 실시간 데이터베이스의 값은 동일하게 유지됩니다. 이렇게하면 평가 기간이 여러 사용자 장치에서 작동합니다. 앱이 새로운 주요 릴리스마다 평가판 날짜를 '재설정'하도록 install_date 버전을 지정할 수도 있습니다.

업데이트 : 조금 더 테스트 한 후 익명의 Firebase는 다른 기기를 가지고 있고 재설치 사이에 보장되지 않는 경우 다른 ID를 할당하는 것 같습니다 : / 유일한 보장 된 방법은 Firebase를 사용하지만 Google에 연결하는 것입니다. 계정. 이것은 작동하지만 사용자가 먼저 로그인 / 가입해야하는 추가 단계가 필요합니다.

지금까지 백업 된 기본 설정과 설치시 기본 설정에 저장된 날짜를 확인하는 약간 덜 우아한 접근 방식으로 끝났습니다. 이는 사용자가 앱을 다시 설치하고 이전에 추가 한 모든 데이터를 다시 입력하는 것이 무의미한 데이터 중심 앱에서 작동하지만 간단한 게임에서는 작동하지 않습니다.


내 안드로이드 앱에 대한 동일한 요구 사항이 있으며 자체 데이터베이스 / 웹 서버가 있습니다. 사용자는 로그인이 필요하지 않으므로 installed_date와 함께 장치 ID를 저장할 계획이었습니다.
user636525

@strangetimes, 귀하의 솔루션이 가장 잘 작동한다고 생각합니다. 더 많은 정보를 제공 할 수 있습니까? 감사
DayDayHappy

이 스레드 stackoverflow.com/q/41733137/1396068는 응용 프로그램을 다시 설치 한 후 새 사용자 ID, 즉 새로운 시험 기간 얻을 것을 제안
파비안 Streitel

3

이 스레드와 다른 스레드의 모든 옵션을 살펴본 후 이것이 내 결과입니다.

공유 환경 설정, 데이터베이스 안드로이드 설정에서 지울 수 있으며 앱 재설치 후 손실됩니다. 안드로이드의 백업 메커니즘으로 백업 할 수 있으며 재설치 후 복원됩니다. 백업이 항상 사용 가능한 것은 아니지만 대부분의 장치에 있어야합니다.

외부 저장소 (파일에 쓰기) 설정에서 삭제하거나 애플리케이션의 개인 디렉터리에 쓰지 않는 경우 다시 설치해도 영향을받지 않습니다 . 그러나 : 최신 안드로이드 버전 에서 런타임시 사용자에게 권한을 요청 해야하므로이 권한이 필요한 경우에만 가능합니다. 백업 할 수도 있습니다.

PackageInfo.firstInstallTime 은 재설치 후 재설정되지만 업데이트간에 안정적입니다.

일부 계정에 로그인 Firebase를 통한 Google 계정이든 자체 서버에있는 계정이든 상관 없습니다. 평가판은 계정에 바인딩됩니다. 새 계정을 만들면 평가판이 재설정됩니다.

Firebase 익명 로그인 사용자를 익명으로 로그인하고 Firebase에 데이터를 저장할 수 있습니다. 그러나 분명히 앱을 다시 설치하고 문서화되지 않은 기타 이벤트가 사용자에게 새로운 익명 ID를 제공하여 평가판 시간을 재설정 할 수 있습니다. (Google 자체는 이에 대한 많은 문서를 제공하지 않습니다)

ANDROID_ID 사용할 수 없으며 특정 상황 (예 : 공장 초기화)에서 변경 될 수 있습니다 . 이것을 사용하여 장치를 식별하는 것이 좋은 생각인지에 대한 의견은 다른 것 같습니다.

Play 광고 ID 는 사용자가 재설정 할 수 있습니다. 사용자가 광고 추적을 선택 해제하여 비활성화 할 수 있습니다.

재설치시 InstanceID 재설정 . 보안 이벤트의 경우 재설정합니다. 앱에서 재설정 할 수 있습니다.

어떤 (조합) 방법이 당신에게 효과가 있는지는 당신의 앱과 평균적인 John이 다른 시험 기간을 얻기 위해 얼마나 많은 노력을 기울일 지에 따라 다릅니다. 불안정성 때문에 익명의 Firebase 및 광고 ID 사용하지 않는 것이 좋습니다 . 다중 요소 접근 방식은 최상의 결과를 얻을 수있는 것처럼 보입니다. 사용할 수있는 요소는 앱 및 권한에 따라 다릅니다.

내 앱의 경우 공유 환경 설정 + firstInstallTime + 환경 설정 백업이 가장 덜 방해가되지만 충분히 효과적인 방법이라는 것을 알았습니다. 공유 기본 설정에서 평가판 시작 시간을 확인하고 저장 한 후에 만 ​​백업을 요청해야합니다. 공유 Prefs의 값은 firstInstallTime보다 우선해야합니다. 그런 다음 사용자는 앱을 다시 설치하고 한 번 실행 한 다음 앱의 데이터를 삭제하여 평가판을 재설정해야합니다. 이는 상당히 많은 작업입니다. 백업 전송이없는 장치에서는 사용자가 간단히 재설치하여 평가판을 재설정 할 수 있습니다.

이 접근 방식을 확장 가능한 라이브러리 로 만들었습니다 .


1

정의 에 따라 시장에 나와있는 모든 유료 Android 앱은 구매 후 24 시간 동안 평가할 수 있습니다.

24 시간 후에 '제거'로 변경되는 '제거 및 환불'버튼이 있습니다.

이 버튼이 너무 눈에 잘 띕니다!


17
환불 기간은 이제 15 분입니다.
Intrications

1

동일한 문제를 검색하는 동안이 질문을 보았습니다 .http : //www.timeapi.org/utc/now와 같은 무료 날짜 API 또는 다른 날짜 API를 사용하여 트레일 앱의 만료 여부를 확인할 수 있다고 생각 합니다. 이 방법은 데모를 제공하고 지불에 대해 걱정하고 수정 기간 데모가 필요한 경우 효율적입니다. :)

아래 코드를 찾으십시오.

public class ValidationActivity extends BaseMainActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

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

private void processCurrentTime() {
    if (!isDataConnectionAvailable(ValidationActivity.this)) {
        showerrorDialog("No Network coverage!");
    } else {
        String urlString = "http://api.timezonedb.com/?zone=Europe/London&key=OY8PYBIG2IM9";
        new CallAPI().execute(urlString);
    }
}

private void showerrorDialog(String data) {
    Dialog d = new Dialog(ValidationActivity.this);
    d.setTitle("LS14");
    TextView tv = new TextView(ValidationActivity.this);
    tv.setText(data);
    tv.setPadding(20, 30, 20, 50);
    d.setContentView(tv);
    d.setOnDismissListener(new OnDismissListener() {
        @Override
        public void onDismiss(DialogInterface dialog) {
            finish();
        }
    });
    d.show();
}

private void checkExpiry(int isError, long timestampinMillies) {
    long base_date = 1392878740000l;// feb_19 13:8 in GMT;
    // long expiryInMillies=1000*60*60*24*5;
    long expiryInMillies = 1000 * 60 * 10;
    if (isError == 1) {
        showerrorDialog("Server error, please try again after few seconds");
    } else {
        System.out.println("fetched time " + timestampinMillies);
        System.out.println("system time -" + (base_date + expiryInMillies));
        if (timestampinMillies > (base_date + expiryInMillies)) {
            showerrorDialog("Demo version expired please contact vendor support");
            System.out.println("expired");
        }
    }
}

private class CallAPI extends AsyncTask<String, String, String> {
    @Override
    protected void onPreExecute() {
        // TODO Auto-generated method stub
        super.onPreExecute();
    }

    @Override
    protected String doInBackground(String... params) {
        String urlString = params[0]; // URL to call
        String resultToDisplay = "";
        InputStream in = null;
        // HTTP Get
        try {
            URL url = new URL(urlString);
            HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream());
            resultToDisplay = convertStreamToString(in);
        } catch (Exception e) {
            System.out.println(e.getMessage());
            return e.getMessage();
        }
        return resultToDisplay;
    }

    protected void onPostExecute(String result) {
        int isError = 1;
        long timestamp = 0;
        if (result == null || result.length() == 0 || result.indexOf("<timestamp>") == -1 || result.indexOf("</timestamp>") == -1) {
            System.out.println("Error $$$$$$$$$");
        } else {
            String strTime = result.substring(result.indexOf("<timestamp>") + 11, result.indexOf("</timestamp>"));
            System.out.println(strTime);
            try {
                timestamp = Long.parseLong(strTime) * 1000;
                isError = 0;
            } catch (NumberFormatException ne) {
            }
        }
        checkExpiry(isError, timestamp);
    }

} // end CallAPI

public static boolean isDataConnectionAvailable(Context context) {
    ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
    NetworkInfo info = connectivityManager.getActiveNetworkInfo();
    if (info == null)
        return false;

    return connectivityManager.getActiveNetworkInfo().isConnected();
}

public String convertStreamToString(InputStream is) throws IOException {
    if (is != null) {
        Writer writer = new StringWriter();

        char[] buffer = new char[1024];
        try {
            Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
            int n;
            while ((n = reader.read(buffer)) != -1) {
                writer.write(buffer, 0, n);
            }
        } finally {
            is.close();
        }
        return writer.toString();
    } else {
        return "";
    }
}

@Override
public void onClick(View v) {
    // TODO Auto-generated method stub

}
}

작동하는 솔루션 .....


사용할 수 있지만이 경우 주요 활동 만 만료를 확인할 수 있습니다.
RQube 2015

0

다음은 제가 어떻게 진행했는지, 하나는 평가판 활동이있는 앱 2 개를 만들고 다른 하나는없는 앱을 만들었습니다.

평가판 활동이없는 것을 유료 앱으로 플레이 스토어에 업로드했는데,

그리고 무료 앱으로 평가판 활동이있는 사람.

최초 실행시 무료 앱에는 평가판 및 스토어 구매 옵션이 있으며, 사용자가 스토어 구매를 선택하면 사용자가 구매할 수 있도록 스토어로 리디렉션되지만 사용자가 평가판을 클릭하면 평가판 활동으로 이동합니다.

NB : @snctln과 같은 옵션 3을 사용했지만 수정했습니다.

첫째 , 나는 장치 시간에 의존하지 않고 db에 대한 평가판 등록을 수행하는 PHP 파일에서 시간을 얻었습니다.

둘째 , 장치 일련 번호를 사용하여 각 장치를 고유하게 식별했습니다.

마지막으로 , 앱은 자체 시간이 아닌 서버 연결에서 반환 된 시간 값에 의존하므로 기기 일련 번호가 변경된 경우에만 시스템을 우회 할 수 있으며 이는 사용자에게 매우 스트레스입니다.

그래서 여기에 내 코드가 있습니다 (평가판 활동).

package com.example.mypackage.my_app.Start_Activity.activity;

import android.Manifest;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.telephony.TelephonyManager;
import android.view.KeyEvent;
import android.widget.TextView;

import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.Volley;
import com.example.onlinewisdom.cbn_app.R;
import com.example.mypackage.my_app.Start_Activity.app.Config;
import com.example.mypackage.my_app.Start_Activity.data.TrialData;
import com.example.mypackage.my_app.Start_Activity.helper.connection.Connection;
import com.google.gson.Gson;

import org.json.JSONObject;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import cn.pedant.SweetAlert.SweetAlertDialog;

public class Trial extends AppCompatActivity {
    Connection check;
    SweetAlertDialog pDialog;
    TextView tvPleaseWait;
    private static final int MY_PERMISSIONS_REQUEST_READ_PHONE_STATE = 0;

    String BASE_URL = Config.BASE_URL;
    String BASE_URL2 = BASE_URL+ "/register_trial/"; //http://ur link to ur API

    //KEY
    public static final String KEY_IMEI = "IMEINumber";

    private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
    private final long ONE_DAY = 24 * 60 * 60 * 1000;

    SharedPreferences preferences;
    String installDate;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_trial);

        preferences = getPreferences(MODE_PRIVATE);
        installDate = preferences.getString("InstallDate", null);

        pDialog = new SweetAlertDialog(this, SweetAlertDialog.PROGRESS_TYPE);
        pDialog.getProgressHelper().setBarColor(Color.parseColor("#008753"));
        pDialog.setTitleText("Loading...");
        pDialog.setCancelable(false);

        tvPleaseWait = (TextView) findViewById(R.id.tvPleaseWait);
        tvPleaseWait.setText("");

        if(installDate == null) {
            //register app for trial
            animateLoader(true);
            CheckConnection();
        } else {
            //go to main activity and verify there if trial period is over
            Intent i = new Intent(Trial.this, MainActivity.class);
            startActivity(i);
            // close this activity
            finish();
        }

    }

    public void CheckConnection() {
        check = new Connection(this);
        if (check.isConnected()) {
            //trigger 'loadIMEI'
            loadIMEI();
        } else {
            errorAlert("Check Connection", "Network is not detected");
            tvPleaseWait.setText("Network is not detected");
            animateLoader(false);
        }
    }

    public boolean onKeyDown(int keyCode, KeyEvent event) {
        //Changes 'back' button action
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            finish();
        }
        return true;
    }

    public void animateLoader(boolean visibility) {
        if (visibility)
            pDialog.show();
        else
            pDialog.hide();
    }

    public void errorAlert(String title, String msg) {
        new SweetAlertDialog(this, SweetAlertDialog.ERROR_TYPE)
                .setTitleText(title)
                .setContentText(msg)
                .show();
    }

    /**
     * Called when the 'loadIMEI' function is triggered.
     */
    public void loadIMEI() {
        // Check if the READ_PHONE_STATE permission is already available.
        if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE)
                != PackageManager.PERMISSION_GRANTED) {
            // READ_PHONE_STATE permission has not been granted.
            requestReadPhoneStatePermission();
        } else {
            // READ_PHONE_STATE permission is already been granted.
            doPermissionGrantedStuffs();
        }
    }


    /**
     * Requests the READ_PHONE_STATE permission.
     * If the permission has been denied previously, a dialog will prompt the user to grant the
     * permission, otherwise it is requested directly.
     */
    private void requestReadPhoneStatePermission() {
        if (ActivityCompat.shouldShowRequestPermissionRationale(this,
                Manifest.permission.READ_PHONE_STATE)) {
            // Provide an additional rationale to the user if the permission was not granted
            // and the user would benefit from additional context for the use of the permission.
            // For example if the user has previously denied the permission.
            new AlertDialog.Builder(Trial.this)
                    .setTitle("Permission Request")
                    .setMessage(getString(R.string.permission_read_phone_state_rationale))
                    .setCancelable(false)
                    .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                        public void onClick(DialogInterface dialog, int which) {
                            //re-request
                            ActivityCompat.requestPermissions(Trial.this,
                                    new String[]{Manifest.permission.READ_PHONE_STATE},
                                    MY_PERMISSIONS_REQUEST_READ_PHONE_STATE);
                        }
                    })
                    .setIcon(R.drawable.warning_sigh)
                    .show();
        } else {
            // READ_PHONE_STATE permission has not been granted yet. Request it directly.
            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_PHONE_STATE},
                    MY_PERMISSIONS_REQUEST_READ_PHONE_STATE);
        }
    }

    /**
     * Callback received when a permissions request has been completed.
     */
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {

        if (requestCode == MY_PERMISSIONS_REQUEST_READ_PHONE_STATE) {
            // Received permission result for READ_PHONE_STATE permission.est.");
            // Check if the only required permission has been granted
            if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // READ_PHONE_STATE permission has been granted, proceed with displaying IMEI Number
                //alertAlert(getString(R.string.permision_available_read_phone_state));
                doPermissionGrantedStuffs();
            } else {
                alertAlert(getString(R.string.permissions_not_granted_read_phone_state));
            }
        }
    }

    private void alertAlert(String msg) {
        new AlertDialog.Builder(Trial.this)
                .setTitle("Permission Request")
                .setMessage(msg)
                .setCancelable(false)
                .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        // do somthing here
                    }
                })
                .setIcon(R.drawable.warning_sigh)
                .show();
    }

    private void successAlert(String msg) {
        new SweetAlertDialog(this, SweetAlertDialog.SUCCESS_TYPE)
                .setTitleText("Success")
                .setContentText(msg)
                .setConfirmText("Ok")
                .setConfirmClickListener(new SweetAlertDialog.OnSweetClickListener() {
                    @Override
                    public void onClick(SweetAlertDialog sDialog) {
                        sDialog.dismissWithAnimation();
                        // Prepare intent which is to be triggered
                        //Intent i = new Intent(Trial.this, MainActivity.class);
                        //startActivity(i);
                    }
                })
                .show();
    }

    public void doPermissionGrantedStuffs() {
        //Have an  object of TelephonyManager
        TelephonyManager tm =(TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE);
        //Get IMEI Number of Phone  //////////////// for this example i only need the IMEI
        String IMEINumber = tm.getDeviceId();

        /************************************************
         * **********************************************
         * This is just an icing on the cake
         * the following are other children of TELEPHONY_SERVICE
         *
         //Get Subscriber ID
         String subscriberID=tm.getDeviceId();

         //Get SIM Serial Number
         String SIMSerialNumber=tm.getSimSerialNumber();

         //Get Network Country ISO Code
         String networkCountryISO=tm.getNetworkCountryIso();

         //Get SIM Country ISO Code
         String SIMCountryISO=tm.getSimCountryIso();

         //Get the device software version
         String softwareVersion=tm.getDeviceSoftwareVersion()

         //Get the Voice mail number
         String voiceMailNumber=tm.getVoiceMailNumber();


         //Get the Phone Type CDMA/GSM/NONE
         int phoneType=tm.getPhoneType();

         switch (phoneType)
         {
         case (TelephonyManager.PHONE_TYPE_CDMA):
         // your code
         break;
         case (TelephonyManager.PHONE_TYPE_GSM)
         // your code
         break;
         case (TelephonyManager.PHONE_TYPE_NONE):
         // your code
         break;
         }

         //Find whether the Phone is in Roaming, returns true if in roaming
         boolean isRoaming=tm.isNetworkRoaming();
         if(isRoaming)
         phoneDetails+="\nIs In Roaming : "+"YES";
         else
         phoneDetails+="\nIs In Roaming : "+"NO";


         //Get the SIM state
         int SIMState=tm.getSimState();
         switch(SIMState)
         {
         case TelephonyManager.SIM_STATE_ABSENT :
         // your code
         break;
         case TelephonyManager.SIM_STATE_NETWORK_LOCKED :
         // your code
         break;
         case TelephonyManager.SIM_STATE_PIN_REQUIRED :
         // your code
         break;
         case TelephonyManager.SIM_STATE_PUK_REQUIRED :
         // your code
         break;
         case TelephonyManager.SIM_STATE_READY :
         // your code
         break;
         case TelephonyManager.SIM_STATE_UNKNOWN :
         // your code
         break;

         }
         */
        // Now read the desired content to a textview.
        //tvPleaseWait.setText(IMEINumber);
        UserTrialRegistrationTask(IMEINumber);
    }

    /**
     * Represents an asynchronous login task used to authenticate
     * the user.
     */
    private void UserTrialRegistrationTask(final String IMEINumber) {
        JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.GET, BASE_URL2+IMEINumber, null,
                new Response.Listener<JSONObject>() {
                    @Override
                    public void onResponse(JSONObject response) {
                        Gson gson = new Gson();
                        TrialData result = gson.fromJson(String.valueOf(response), TrialData.class);
                        animateLoader(false);
                        if ("true".equals(result.getError())) {
                            errorAlert("Error", result.getResult());
                            tvPleaseWait.setText("Unknown Error");
                        } else if ("false".equals(result.getError())) {
                            //already created install/trial_start date using the server
                            // so just getting the date called back
                            Date before = null;
                            try {
                                before = (Date)formatter.parse(result.getResult());
                            } catch (ParseException e) {
                                e.printStackTrace();
                            }
                            Date now = new Date();
                            assert before != null;
                            long diff = now.getTime() - before.getTime();
                            long days = diff / ONE_DAY;
                            // save the date received
                            SharedPreferences.Editor editor = preferences.edit();
                            editor.putString("InstallDate", String.valueOf(days));
                            // Commit the edits!
                            editor.apply();
                            //go to main activity and verify there if trial period is over
                            Intent i = new Intent(Trial.this, MainActivity.class);
                            startActivity(i);
                            // close this activity
                            finish();
                            //successAlert(String.valueOf(days));
                            //if(days > 5) { // More than 5 days?
                                // Expired !!!
                            //}
                            }
                    }
                },
                new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        animateLoader(false);
                        //errorAlert(error.toString());
                        errorAlert("Check Connection", "Could not establish a network connection.");
                        tvPleaseWait.setText("Network is not detected");
                    }
                })

        {
            protected Map<String, String> getParams() {
                Map<String, String> params = new HashMap<String, String>();
                params.put(KEY_IMEI, IMEINumber);
                return params;
            }
        };

        RequestQueue requestQueue = Volley.newRequestQueue(this);
        requestQueue.add(jsonObjectRequest);
    }


}

내 PHP 파일은 다음과 같습니다 (REST-slim 기술).

/**
     * registerTrial
     */
    public function registerTrial($IMEINumber) {
        //check if $IMEINumber already exist
        // Instantiate DBH
        $DBH = new PDO_Wrapper();
        $DBH->query("SELECT date_reg FROM trials WHERE device_id = :IMEINumber");
        $DBH->bind(':IMEINumber', $IMEINumber);
        // DETERMINE HOW MANY ROWS OF RESULTS WE GOT
        $totalRows_registered = $DBH->rowCount();
        // DETERMINE HOW MANY ROWS OF RESULTS WE GOT
        $results = $DBH->resultset();

        if (!$IMEINumber) {
            return 'Device serial number could not be determined.';
        } else if ($totalRows_registered > 0) {
            $results = $results[0];
            $results = $results['date_reg'];
            return $results;
        } else {
            // Instantiate variables
            $trial_unique_id = es_generate_guid(60);
            $time_reg = date('H:i:s');
            $date_reg = date('Y-m-d');

            $DBH->beginTransaction();
            // opening db connection
            //NOW Insert INTO DB
            $DBH->query("INSERT INTO trials (time_reg, date_reg, date_time, device_id, trial_unique_id) VALUES (:time_reg, :date_reg, NOW(), :device_id, :trial_unique_id)");
            $arrayValue = array(':time_reg' => $time_reg, ':date_reg' => $date_reg, ':device_id' => $IMEINumber, ':trial_unique_id' => $trial_unique_id);
            $DBH->bindArray($arrayValue);
            $subscribe = $DBH->execute();
            $DBH->endTransaction();
            return $date_reg;
        }

    }

그런 다음 주요 활동에서 공유 환경 설정 (평가판 활동에서 생성 된 installDate)을 사용하여 남은 일 수를 모니터링하고 일이 끝나면 구매를 위해 상점으로 이동하는 메시지와 함께 주요 활동 UI를 차단합니다.

내가 여기서 볼 수있는 유일한 단점Rogue 사용자 가 유료 앱을 구입하고 Zender, 파일 공유와 같은 앱과 공유하기로 결정하거나 사람들이 무료로 다운로드 할 수 있도록 서버에서 직접 apk 파일을 호스팅하기로 결정한다는 것입니다. 그러나 곧 해결책이나 해결책에 대한 링크 로이 답변을 편집 할 것이라고 확신합니다.

이것이 영혼을 구하기를 바랍니다 ... 언젠가

해피 코딩 ...


0

@snctln 옵션 3은 php와 mysql이 많이 설치되어있는 웹 서버에 php 파일을 쉽게 추가 할 수 있습니다.

Android 측에서 식별자 (장치 ID, Google 계정 또는 원하는 모든 것)가 HttpURLConnection을 사용하여 URL의 인수로 전달되고 php는 테이블에있는 경우 첫 번째 설치 날짜를 반환하거나 새 행을 삽입하고 현재 날짜를 반환합니다.

그것은 나를 위해 잘 작동합니다.

시간이 있으면 코드를 게시하겠습니다!

행운을 빕니다 !


잠시만 기다려주세요. 앱 재설치 후 고유 ID를 잃어 버리나요? !!
Maksim Kniazev

이 ID는 하드웨어, 전화기 자체를 식별하며 사용자는이를 볼 수 없으며 변경할 수 없습니다. 그가 앱을 다시 설치하면 PHP 웹 서비스가 동일한 전화임을 감지합니다. 반면에 사용자가 전화를 변경하면 새로운 기간을 즐길 수 있습니다.
Lluis Felisart
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.