Tạo ứng dụng dùng thử Android hết hạn sau một khoảng thời gian cố định


103

Tôi có một ứng dụng mà tôi muốn tung ra thị trường dưới dạng ứng dụng Trả phí. Tôi muốn có phiên bản khác sẽ là phiên bản "dùng thử" với thời hạn nói là 5 ngày?

Làm thế nào tôi có thể tiếp tục làm điều này?


Google thực sự nên hỗ trợ điều này trong Dịch vụ Play!
powder366.

@ powder366 thực sự thì Google có hỗ trợ điều này, xem developer.android.com/google/play/billing/…
Yazazzello

Câu trả lời:


186

Hiện tại hầu hết các nhà phát triển thực hiện điều này bằng cách sử dụng một trong 3 kỹ thuật sau.

Cách tiếp cận đầu tiên dễ bị phá vỡ, lần đầu tiên bạn chạy ứng dụng, hãy lưu ngày / giờ vào tệp, cơ sở dữ liệu hoặc tùy chọn được chia sẻ và mỗi lần bạn chạy ứng dụng sau đó, hãy kiểm tra xem thời gian dùng thử đã kết thúc chưa. Điều này rất dễ bị phá vỡ vì việc gỡ cài đặt và cài đặt lại sẽ cho phép người dùng có một thời gian dùng thử khác.

Cách tiếp cận thứ hai khó vượt qua hơn, nhưng vẫn có thể vượt qua được. Sử dụng một quả bom hẹn giờ được mã hóa cứng. Về cơ bản với cách tiếp cận này, bạn sẽ có mã cứng ngày kết thúc dùng thử và tất cả người dùng tải xuống và sử dụng ứng dụng sẽ ngừng có thể sử dụng ứng dụng cùng một lúc. Tôi đã sử dụng cách tiếp cận này vì nó dễ thực hiện và phần lớn tôi không cảm thấy muốn trải qua sự cố của kỹ thuật thứ ba. Người dùng có thể tránh điều này bằng cách thay đổi ngày trên điện thoại của họ theo cách thủ công, nhưng hầu hết người dùng sẽ không gặp khó khăn khi làm điều đó.

Kỹ thuật thứ ba là cách duy nhất mà tôi đã nghe nói về để thực sự có thể đạt được những gì bạn muốn làm. Bạn sẽ phải thiết lập một máy chủ và sau đó bất cứ khi nào ứng dụng của bạn được khởi động, ứng dụng của bạn sẽ gửi mã nhận dạng duy nhất của điện thoại tới máy chủ. Nếu máy chủ không có mục nhập cho id điện thoại đó thì nó sẽ tạo một mục mới và ghi chú thời gian. Nếu máy chủ có mục nhập cho id điện thoại thì nó sẽ kiểm tra đơn giản để xem liệu thời gian dùng thử đã hết hạn hay chưa. Sau đó, nó thông báo kết quả của việc kiểm tra hết hạn dùng thử trở lại ứng dụng của bạn. Cách tiếp cận này không thể vượt qua được, nhưng yêu cầu thiết lập một máy chủ web và như vậy.

Luôn luôn là một phương pháp hay để thực hiện các kiểm tra này trong onCreate. Nếu thời hạn đã kết thúc, một AlertDialog sẽ bật lên có liên kết thị trường đến phiên bản đầy đủ của ứng dụng. Chỉ bao gồm nút "OK" và khi người dùng nhấp vào "OK", hãy thực hiện lệnh gọi "finish ()" để kết thúc hoạt động.


2
Câu trả lời tuyệt vời. Giống như bạn nói, tôi cảm thấy lựa chọn thứ hai có thể là tốt nhất. Thật tiếc khi bản thân google không cung cấp một số loại hệ thống cấp phép vì nó có thể khuyến khích cả các nhà phát triển thương hiệu nhỏ và lớn hơn tạo ra nhiều ứng dụng Android hơn nữa.
Tom

8
Ngoài ra, tôi sẽ không kiểm tra trong quá trình khởi động. Mục tiêu của bạn là bán ứng dụng chứ không phải trừng phạt người dùng (đó chỉ là một phần thưởng;) Nếu bạn đặt nó kiểm tra 2 phút một lần trong khi chạy, bạn để người dùng bắt đầu làm điều gì đó và sau đó nhận ra họ phải trả tiền. Nếu bạn thực sự dễ dàng thanh toán và quay lại làm việc (tôi không chắc bạn có thể làm được trong Android hay không), tôi nghĩ bạn sẽ bán được nhiều hơn sau đó kiểm tra trong quá trình onCreate.
Whaledawg

4
@Whaledawg: Bạn cần phải chạy máy chủ của riêng mình vì máy chủ đang lưu trữ id điện thoại và thời gian chạy lần đầu tiên trong cơ sở dữ liệu sau đó được so sánh với sau này. Ngoài ra, khi bạn kiểm tra hoàn toàn là tùy chọn của nhà phát triển, tôi đã sử dụng khó bom hẹn giờ được mã hóa trong một trò chơi với kết quả tuyệt vời. Toàn bộ ứng dụng sẽ tải, nhưng người dùng chỉ có thể tương tác với hộp thoại được nhìn thấy, có một nút trên hộp thoại đó đưa người dùng trực tiếp đến trang mua trò chơi. Người dùng dường như không bận tâm đến AFAIK vì trò chơi đó đã nằm trong 10 ứng dụng trả phí hàng đầu kể từ khi Android Market mở cửa.
snctln

11
Đối với bất kỳ ai miễn cưỡng sử dụng tùy chọn 3 vì thiết lập máy chủ bổ sung, hãy xem Parse.com - đó là một đồng bộ.
Joel Skrepnek

3
Ngày kết thúc dùng thử mã cứng nghĩa là gì? Điều đó có nghĩa là bạn sẽ tiếp tục phát hành các phiên bản mới của ứng dụng dùng thử mãi mãi với các ngày mã hóa cứng khác nhau trong tương lai?
Jasper

21

Tôi đã phát triển SDK dùng thử Android mà bạn có thể chỉ cần đưa vào dự án Android Studio của mình và nó sẽ đảm nhận tất cả việc quản lý phía máy chủ cho bạn (bao gồm cả thời gian gia hạn ngoại tuyến).

Để sử dụng nó, chỉ cần

Thêm thư viện vào mô-đun chính của bạn build.gradle

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

Khởi tạo thư viện trong onCreate()phương thức hoạt động chính của bạn

@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);
}

Thêm một trình xử lý gọi lại:

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));
    }

};

Để bắt đầu dùng thử, hãy gọi mTrialy.startTrial("YOUR_TRIAL_SKU", mTrialyCallback); Khóa ứng dụng của bạn và SKU dùng thử có thể được tìm thấy trong bảng điều khiển dành cho nhà phát triển Trialy của bạn .


Nó yêu cầu dữ liệu được kích hoạt?
Sivaram Boina

Trialy không đáng tin cậy
Amir Dora

1
@AmirDe Xin chào Amir, bạn có thể cho tôi biết điều gì không phù hợp với bạn không? Tôi rất vui được trợ giúp, support@trialy.io Trialy đang hoạt động hiệu quả cho hơn 1000 người dùng
Nick

@Nick không biết lý do tại sao thiết bị của tôi đang chạy android lollipop, khi tôi đặt ngày từ bảng điều khiển, sau đó ngày hiển thị ở giá trị âm sau vài phút, nó cho biết bản dùng thử đã hết hạn, mặc dù tôi vẫn còn trong bảng điều khiển nhiều ngày. Tôi cũng đã thử nghiệm trên thiết bị nougat, dường như hoạt động tốt trên naugat. có thể nó có một số vấn đề tương thích với phiên bản Android cũ hơn
Amir Dora

1
Tôi đang sử dụng dịch vụ này từ năm 2016, nó hoạt động tốt mọi lúc. Tôi cũng đã sử dụng điều này trong các dự án chính thức của mình. Điều này nên được chấp nhận câu trả lời.
Tariq Mahmood

17

Đây là một câu hỏi cũ nhưng dù sao, có thể điều này sẽ giúp ích cho ai đó.

Trong trường hợp bạn muốn sử dụng cách tiếp cận đơn giản nhất ( sẽ không thành công nếu ứng dụng được gỡ cài đặt / cài đặt lại hoặc người dùng thay đổi ngày của thiết bị theo cách thủ công), đây có thể là:

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 !!!
        }
    }

    ...
}

Thực ra nếu bạn đang sử dụng SharedPreferences và Android 2.2 Froyo trở lên, miễn là bạn triển khai các API sao lưu dữ liệu cho Đồng bộ hóa dữ liệu của Google, người dùng sẽ không thể thoát khỏi điều này trên các thiết bị hoặc bằng cách gỡ cài đặt, chỉ bằng cách đi tới Cài đặt> Ứng dụng và xóa dữ liệu. Ngoài ra, phương pháp trên Ngày getTimekhông getTimeInMillis.
Tom

Không đồng ý, điều này cũng sẽ không thành công nếu người dùng thay đổi ngày của Thiết bị theo cách thủ công và điều gì xảy ra nếu Người dùng xóa dữ liệu theo cách thủ công?
Mohammed Azharuddin Shaikh

@Caner này tốt để chek bằng sharedprefrence nhưng người dùng xóa bộ nhớ khỏi seting-> trình quản lý ứng dụng và sử dụng lại thì sẽ làm gì?
CoronaPintu

@CoronaPintu, vì vậy cách tiếp cận này cũng sẽ được thử với firebase, điều này sẽ tạo nên sự kết hợp hoàn hảo và thời gian dùng thử sẽ qua ngay cả khi ứng dụng sẽ được gỡ cài đặt.
Noor Hossain

kết hợp và thêm phương pháp tiếp cận với câu trả lời "kỳ lạ", điều này sẽ làm cho phương pháp tiếp cận trở nên hoàn hảo.
Noor Hossain

10

Câu hỏi này và câu trả lời của snctln đã truyền cảm hứng cho tôi nghiên cứu một giải pháp dựa trên phương pháp 3 làm luận văn cử nhân của tôi. Tôi biết tình trạng hiện tại không phải để sử dụng hiệu quả nhưng tôi rất muốn nghe bạn nghĩ gì về nó! Bạn sẽ sử dụng một hệ thống như vậy? Bạn có muốn xem nó như một dịch vụ đám mây (không gặp sự cố khi định cấu hình máy chủ) không? Lo ngại về các vấn đề bảo mật hoặc lý do ổn định?

Ngay sau khi tôi hoàn thành thủ tục cử nhân, tôi muốn tiếp tục làm việc trên phần mềm. Vì vậy, bây giờ là lúc tôi cần phản hồi của bạn!

Mã nguồn được lưu trữ trên GitHub https://github.com/MaChristmann/mobile-trial

Một số thông tin về hệ thống: - Hệ thống có ba phần, thư viện Android, máy chủ node.js và một bộ cấu hình để quản lý nhiều ứng dụng dùng thử và tài khoản nhà xuất bản / nhà phát triển.

  • Nó chỉ hỗ trợ các bản dùng thử dựa trên thời gian và nó sử dụng tài khoản (cửa hàng chơi hoặc tài khoản khác) của bạn thay vì ID điện thoại.

  • Đối với thư viện Android, nó dựa trên thư viện xác minh cấp phép của Google Play. Tôi đã sửa đổi nó để kết nối với máy chủ node.js và thêm vào đó, thư viện cố gắng nhận ra nếu người dùng đã thay đổi ngày hệ thống. Nó cũng lưu vào bộ nhớ cache một giấy phép dùng thử đã được truy xuất trong Tùy chọn Chia sẻ được mã hóa AES. Bạn có thể định cấu hình thời gian hợp lệ của bộ đệm với trình cấu hình. Nếu người dùng "xóa dữ liệu", thư viện sẽ buộc kiểm tra phía máy chủ.

  • Máy chủ đang sử dụng https và cũng đang ký kỹ thuật số phản hồi kiểm tra giấy phép. Nó cũng có một API cho các ứng dụng và người dùng dùng thử CRUD (nhà xuất bản và nhà phát triển). Tương tự như các nhà phát triển Thư viện xác minh cấp phép có thể kiểm tra việc triển khai hành vi của họ trong ứng dụng dùng thử với kết quả thử nghiệm. Vì vậy, bạn trong trình cấu hình, bạn có thể đặt phản hồi giấy phép của mình thành "được cấp phép", "không được cấp phép" hoặc "lỗi máy chủ".

  • Nếu cập nhật ứng dụng của mình bằng một tính năng mới, bạn có thể muốn mọi người thử lại. Trong trình cấu hình, bạn có thể gia hạn giấy phép dùng thử cho những người dùng có giấy phép hết hạn bằng cách đặt mã phiên bản sẽ kích hoạt điều này. Ví dụ: người dùng đang chạy ứng dụng của bạn trên mã phiên bản 3 và bạn muốn anh ta thử các tính năng của mã phiên bản 4. Nếu anh ta cập nhật ứng dụng hoặc cài đặt lại ứng dụng, anh ta có thể sử dụng lại toàn bộ thời gian dùng thử vì máy chủ biết anh ta đã dùng thử phiên bản nào gần đây nhất thời gian.

  • Mọi thứ đều theo giấy phép Apache 2.0


2
Bạn vừa cứu được ngày của tôi, cảm ơn bạn đã làm việc chăm chỉ. Tôi chỉ nghĩ viết cùng một giải pháp sử dụng cấu hình mã hóa thu được chỉ một lần và giữ khóa công khai bên trong ứng dụng. Vì vậy, vấn đề chính tôi có thể thấy chỉ làm thế nào để bạn cấp giấy phép? Bạn vẫn có thể cần một số id duy nhất mang theo điện thoại để tránh nhiều lần được cấp.
user2305886 28/12/13

6

Cách dễ nhất và tốt nhất để làm điều này là triển khai BackupSharedPreferences.

Các tùy chọn được giữ nguyên, ngay cả khi ứng dụng được gỡ cài đặt và cài đặt lại.

Chỉ cần lưu ngày cài đặt làm tùy chọn và bạn đã sẵn sàng.

Đây là lý thuyết: http://developer.android.com/reference/android/app/backup/SharedPreferencesBackupHelper.html

Đây là ví dụ: Android SharedPreferences Backup không hoạt động


3
Người dùng có thể tắt sao lưu trong cài đặt hệ thống.
Patrick

5

Cách 4: sử dụng thời gian cài đặt ứng dụng.

Vì API cấp 9 (Android 2.3.2, 2.3.1, Android 2.3, GINGERBREAD) đã có firstInstallTimelastUpdateTime trong PackageInfo.

Để đọc thêm: Cách lấy thời gian cài đặt ứng dụng từ android


Có thể sử dụng phương pháp 1 từ câu trả lời của snctln với điều này một cách đáng tin cậy, mà không dễ bị phá vỡ, hoặc nó có vấn đề giống hoặc tương tự không?
jwinn

Tôi đã thử nghiệm phương pháp này. Mặt tốt là nó hoạt động ngay cả khi dữ liệu bị xóa. Mặt xấu là nó có thể bị phá vỡ bằng cách gỡ cài đặt / cài đặt lại.
Jean-Philippe Jodoin

có thể xác nhận: trên Pixel của tôi, việc gỡ cài đặt và cài đặt lại ứng dụng sẽ đặt lại thời gian cài đặt đầu tiên. mà làm cho nó khá tầm thường cho người sử dụng để thiết lập lại thử nghiệm
Fabian Streitel

3

Bây giờ trong phiên bản gần đây của đăng ký dùng thử miễn phí android đã được thêm vào, bạn có thể mở khóa tất cả các tính năng của ứng dụng chỉ sau khi mua đăng ký trong ứng dụng trong thời gian dùng thử miễn phí. Điều này sẽ cho phép người dùng sử dụng ứng dụng của bạn trong thời gian dùng thử, nếu ứng dụng vẫn được gỡ cài đặt sau thời gian dùng thử thì tiền đăng ký sẽ được chuyển cho bạn. Tôi đã không cố gắng, nhưng chỉ chia sẻ một ý tưởng.

Đây là tài liệu


2
Tôi ước điều này làm việc cho các lần mua đơn lẻ. Chỉ hoạt động cho các đăng ký, có thể là hàng năm hoặc hàng tháng.
jwinn

3

Theo tôi, cách tốt nhất để làm điều này là chỉ cần sử dụng Cơ sở dữ liệu thời gian thực của Firebase:

1) Thêm hỗ trợ Firebase vào ứng dụng của bạn

2) Chọn 'Xác thực ẩn danh' để người dùng không phải đăng ký hoặc thậm chí biết bạn đang làm gì. Điều này được đảm bảo liên kết với tài khoản người dùng hiện đã được xác thực và do đó sẽ hoạt động trên các thiết bị.

3) Sử dụng API cơ sở dữ liệu thời gian thực để đặt giá trị cho 'install_date'. Tại thời điểm khởi chạy, chỉ cần truy xuất giá trị này và sử dụng giá trị này.

Tôi cũng đã làm như vậy và nó hoạt động rất tốt. Tôi đã có thể kiểm tra điều này trong quá trình gỡ cài đặt / cài đặt lại và giá trị trong cơ sở dữ liệu thời gian thực vẫn giữ nguyên. Bằng cách này, thời gian dùng thử của bạn hoạt động trên nhiều thiết bị người dùng. Bạn thậm chí có thể tạo phiên bản install_date của mình để ứng dụng 'đặt lại' Ngày dùng thử cho mỗi bản phát hành chính mới.

CẬP NHẬT : Sau khi thử nghiệm thêm một chút, dường như Firebase ẩn danh sẽ phân bổ một ID khác trong trường hợp bạn có các thiết bị khác nhau và không được đảm bảo giữa các lần cài đặt lại: / Cách đảm bảo duy nhất là sử dụng Firebase nhưng buộc nó với google của họ tài khoản. Điều này sẽ hoạt động, nhưng sẽ yêu cầu thêm một bước mà trước tiên người dùng cần đăng nhập / đăng ký.

Do đó, tôi đã kết thúc với một cách tiếp cận kém thanh lịch hơn một chút là chỉ cần kiểm tra các tùy chọn đã sao lưu và ngày được lưu trữ trong tùy chọn khi cài đặt. Điều này hoạt động đối với các ứng dụng tập trung vào dữ liệu trong đó việc một người cài đặt lại ứng dụng và nhập lại tất cả dữ liệu đã thêm trước đó là vô nghĩa, nhưng sẽ không hoạt động đối với một trò chơi đơn giản.


Tôi có yêu cầu tương tự của bạn cho ứng dụng Android của tôi và tôi có cơ sở dữ liệu / máy chủ web của riêng mình. Người dùng không yêu cầu đăng nhập, Vì vậy, tôi đã dự định lưu ID thiết bị với ngày_cài_đặt, điều này có hiệu quả không?
user636525

@strangetimes, tôi nghĩ giải pháp của bạn hoạt động tốt nhất, bạn có thể cung cấp thêm thông tin không? cảm ơn
DayDayHappy 13/02/17

Chủ đề này stackoverflow.com/q/41733137/1396068 gợi ý rằng sau khi cài đặt lại ứng dụng bạn sẽ có được một ID người dùng mới, Tức là một thời gian dùng thử mới
Fabian Streitel

3

Sau khi xem xét tất cả các tùy chọn trong chủ đề này và các chủ đề khác, đây là những phát hiện của tôi

Tùy chọn chia sẻ, cơ sở dữ liệu Có thể bị xóa trong cài đặt Android, bị mất sau khi cài đặt lại ứng dụng. Có thể được sao lưu với cơ chế sao lưu của Android và sẽ được khôi phục sau khi cài đặt lại. Sao lưu có thể không phải lúc nào cũng có sẵn, mặc dù phải có trên hầu hết các thiết bị

Bộ nhớ ngoài (ghi vào tệp) Không bị ảnh hưởng bởi việc xóa khỏi cài đặt hoặc cài đặt lại nếu chúng tôi không ghi vào thư mục riêng của ứng dụng . Nhưng: yêu cầu bạn yêu cầu người dùng cho phép họ trong thời gian chạy trong các phiên bản android mới hơn, vì vậy điều này có lẽ chỉ khả thi nếu bạn cần sự cho phép đó. Cũng có thể được sao lưu.

PackageInfo.firstInstallTime được đặt lại sau khi cài đặt lại nhưng ổn định trên các bản cập nhật

Đăng nhập vào một số tài khoản Không thành vấn đề nếu đó là tài khoản Google của họ thông qua Firebase hay một tài khoản trong máy chủ của riêng bạn: bản dùng thử được ràng buộc với tài khoản. Tạo tài khoản mới sẽ đặt lại thời gian dùng thử.

Đăng nhập ẩn danh trong Firebase Bạn có thể đăng nhập ẩn danh một người dùng và lưu trữ dữ liệu cho họ trong Firebase. Nhưng có vẻ như việc cài đặt lại ứng dụng và có thể các sự kiện không có giấy tờ khác có thể cung cấp cho người dùng một ID ẩn danh mới , đặt lại thời gian dùng thử của họ. (Bản thân Google không cung cấp nhiều tài liệu về điều này)

ANDROID_ID Có thể không khả dụng và có thể thay đổi trong một số trường hợp nhất định , chẳng hạn như khôi phục cài đặt gốc. Các ý kiến ​​về việc liệu có nên sử dụng điều này để xác định thiết bị hay không dường như khác nhau.

ID quảng cáo Play có thể đặt lại . Có thể bị người dùng vô hiệu hóa bằng cách chọn không theo dõi quảng cáo.

InstanceID Đặt lại khi cài đặt lại . Đặt lại trong trường hợp có sự kiện bảo mật. Có thể được đặt lại bằng ứng dụng của bạn.

Phương pháp (kết hợp) nào phù hợp với bạn tùy thuộc vào ứng dụng của bạn và vào mức độ nỗ lực mà bạn nghĩ John trung bình sẽ bỏ ra để đạt được một thời gian dùng thử khác. Tôi khuyên bạn nên bỏ qua việc chỉ sử dụng Firebase ẩn danh và ID quảng cáo do chúng không ổn định. Cách tiếp cận đa yếu tố có vẻ như sẽ mang lại kết quả tốt nhất. Những yếu tố nào có sẵn cho bạn phụ thuộc vào ứng dụng của bạn và quyền của nó.

Đối với ứng dụng của riêng tôi, tôi thấy các tùy chọn được chia sẻ + firstInstallTime + sao lưu các tùy chọn là phương pháp ít xâm phạm nhất nhưng cũng đủ hiệu quả. Bạn phải đảm bảo rằng bạn chỉ yêu cầu sao lưu sau khi kiểm tra và lưu trữ thời gian bắt đầu dùng thử trong các tùy chọn được chia sẻ. Các giá trị trong Prefs được chia sẻ phải được ưu tiên hơn firstInstallTime. Sau đó, người dùng phải cài đặt lại ứng dụng, chạy nó một lần và sau đó xóa dữ liệu của ứng dụng để đặt lại bản dùng thử, khá nhiều công việc. Tuy nhiên, trên các thiết bị không có phương tiện dự phòng, người dùng có thể đặt lại bản dùng thử bằng cách cài đặt lại.

Tôi đã cung cấp cách tiếp cận đó như một thư viện có thể mở rộng .


1

Theo định nghĩa, tất cả các ứng dụng Android trả phí trên thị trường có thể được đánh giá trong 24 giờ sau khi mua.

Có nút 'Gỡ cài đặt và hoàn lại tiền' thay đổi thành 'Gỡ cài đặt' sau 24 giờ.

Tôi cho rằng nút này quá nổi bật!


17
Lưu ý thời gian hoàn tiền bây giờ chỉ là 15 phút.
Phức tạp

1

Tôi gặp câu hỏi này trong khi tìm kiếm cùng một vấn đề, tôi nghĩ chúng ta có thể sử dụng api ngày miễn phí như http://www.timeapi.org/utc/now hoặc một số api ngày khác để kiểm tra xem ứng dụng đường mòn đã hết hạn hay chưa. cách này hiệu quả nếu bạn muốn cung cấp bản demo và lo lắng về việc thanh toán và yêu cầu bản demo thời hạn sửa chữa. :)

tìm mã bên dưới

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

}
}

giải pháp làm việc của nó .....


chắc chắn bạn có thể sử dụng, nhưng trong trường hợp đó, chỉ hoạt động chính của bạn mới có thể kiểm tra xem đã hết hạn.
RQube

0

Đây là cách tôi đã làm về của mình, tôi đã tạo 2 ứng dụng, một ứng dụng có hoạt động thử nghiệm, ứng dụng kia không có,

tôi đã tải một ứng dụng không có hoạt động dùng thử lên cửa hàng Play dưới dạng ứng dụng trả phí,

và ứng dụng có hoạt động dùng thử dưới dạng ứng dụng miễn phí.

Ứng dụng miễn phí khi ra mắt lần đầu tiên có các tùy chọn để dùng thử và mua tại cửa hàng, nếu người dùng chọn mua tại cửa hàng, ứng dụng đó sẽ chuyển hướng đến cửa hàng để người dùng mua nhưng nếu người dùng nhấp vào dùng thử, nó sẽ đưa họ đến hoạt động dùng thử

NB: Tôi đã sử dụng tùy chọn 3 như @snctln nhưng có sửa đổi

đầu tiên , tôi không phụ thuộc vào thời gian thiết bị, tôi có thời gian từ tệp php đăng ký dùng thử sang db,

thứ hai , tôi đã sử dụng số sê-ri của thiết bị để nhận dạng duy nhất từng thiết bị,

cuối cùng , ứng dụng phụ thuộc vào giá trị thời gian trả về từ kết nối máy chủ chứ không phải thời gian của chính nó, vì vậy hệ thống chỉ có thể bị phá vỡ nếu số sê-ri của thiết bị bị thay đổi, điều này khá căng thẳng đối với người dùng.

vì vậy đây là mã của tôi (cho Hoạt động dùng thử):

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);
    }


}

Tệp php của tôi trông như thế này (nó là một công nghệ 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;
        }

    }

sau đó trên hoạt động chính, tôi sử dụng tùy chọn được chia sẻ (installDate được tạo trong hoạt động dùng thử) để theo dõi số ngày còn lại và nếu hết ngày, tôi sẽ chặn giao diện người dùng hoạt động chính bằng một thông báo đưa họ đến cửa hàng để mua.

Mặt trái duy nhất tôi thấy ở đây là nếu người dùng Rogue mua ứng dụng trả phí và quyết định chia sẻ với các ứng dụng như Zender, chia sẻ tệp hoặc thậm chí lưu trữ tệp apk trực tiếp trên máy chủ để mọi người tải xuống miễn phí. Nhưng chắc chắn rằng tôi sẽ sớm chỉnh sửa câu trả lời này với một giải pháp cho điều đó hoặc một liên kết đến giải pháp.

Hy vọng điều này sẽ cứu một linh hồn ... một ngày nào đó

Mã hóa vui vẻ ...


0

@snctln tùy chọn 3 có thể dễ dàng thực hiện thêm tệp php vào máy chủ web có cài đặt php và mysql như nhiều người trong số họ có.

Từ phía Android, một số nhận dạng (ID thiết bị, tài khoản google hoặc bất cứ thứ gì bạn muốn) được chuyển làm đối số trong URL bằng cách sử dụng HttpURLConnection và php trả về ngày cài đặt đầu tiên nếu nó tồn tại trong bảng hoặc nó chèn một hàng mới và nó trả về ngày hiện tại.

Việc này ổn với tôi.

Nếu tôi có thời gian, tôi sẽ đăng một số mã!

Chúc may mắn !


đợi nhưng bạn có mất id duy nhất của mình sau khi cài đặt lại ứng dụng không? !!
Maksim Kniazev

ID này xác định phần cứng, điện thoại chính nó, người dùng không nhìn thấy nó và không thể thay đổi nó. Nếu anh ta cài đặt lại ứng dụng, dịch vụ web php sẽ phát hiện ra rằng đó là cùng một điện thoại. Mặt khác, nếu người dùng đổi điện thoại, anh ta sẽ được hưởng một giai đoạn mới.
Lluis Felisart
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.