Stworzenie aplikacji próbnej na Androida, która wygasa po określonym czasie

103

Mam aplikację, która chce trafić na rynek jako aplikacja płatna. Chciałbym mieć inną wersję, która byłaby wersją „próbną” z ograniczeniem czasowym powiedzmy 5 dni?

Jak mam to zrobić?

Tomek
źródło
Google naprawdę powinno to wspierać w usługach Play!
powder366
@ powder366 faktycznie Google to obsługuje, patrz developer.android.com/google/play/billing/…
Yazazzello,

Odpowiedzi:

186

Obecnie większość programistów dokonuje tego za pomocą jednej z trzech poniższych technik.

Pierwsze podejście można łatwo obejść, przy pierwszym uruchomieniu aplikacji zapisz datę / godzinę w pliku, bazie danych lub wspólnych preferencjach i za każdym razem, gdy uruchomisz aplikację po tym sprawdzeniu, czy okres próbny dobiegł końca. Można to łatwo obejść, ponieważ odinstalowanie i ponowna instalacja pozwoli użytkownikowi na kolejny okres próbny.

Drugie podejście jest trudniejsze do obejścia, ale nadal można je obejść. Użyj zakodowanej bomby zegarowej. Zasadniczo dzięki temu podejściu ustalisz na stałe datę zakończenia okresu próbnego, a wszyscy użytkownicy, którzy pobierają i używają aplikacji, przestaną być w stanie korzystać z aplikacji w tym samym czasie. Użyłem tego podejścia, ponieważ jest ono łatwe do wdrożenia i przeważnie nie miałem ochoty przechodzić przez kłopoty związane z trzecią techniką. Użytkownicy mogą to obejść, ręcznie zmieniając datę w telefonie, ale większość użytkowników nie będzie miała problemu z zrobieniem czegoś takiego.

Trzecia technika to jedyny sposób, o jakim słyszałem, aby naprawdę móc osiągnąć to, co chcesz. Będziesz musiał skonfigurować serwer, a następnie za każdym razem, gdy Twoja aplikacja jest uruchamiana, aplikacja wysyła unikalny identyfikator telefonu do serwera. Jeśli serwer nie ma wpisu dla tego identyfikatora telefonu, tworzy nowy i zapisuje godzinę. Jeśli serwer ma wpis dla identyfikatora telefonu, wykonuje proste sprawdzenie, czy okres próbny wygasł. Następnie przekazuje wyniki sprawdzenia wygaśnięcia okresu próbnego z powrotem do aplikacji. Tego podejścia nie można obejść, ale wymaga skonfigurowania serwera internetowego i tym podobnych.

Zawsze dobrze jest przeprowadzać te sprawdzenia w onCreate. Jeśli wygaśnięcie zakończyło się wyskakujące okienko AlertDialog z linkiem rynkowym do pełnej wersji aplikacji. Dołącz tylko przycisk „OK”, a gdy użytkownik kliknie „OK”, wywołaj funkcję „finish ()”, aby zakończyć działanie.

snctln
źródło
2
Fantastyczna odpowiedź. Tak jak mówisz, czuję, że druga opcja jest prawdopodobnie najlepsza. Szkoda, że ​​sami Google nie oferuje żadnego systemu licencjonowania, ponieważ może to zachęcić zarówno małych, jak i większych twórców marek do tworzenia jeszcze większej liczby aplikacji na Androida.
Tom
8
Nie sprawdzałbym też podczas uruchamiania. Twoim celem jest sprzedaż aplikacji, a nie karanie użytkownika (to tylko bonus;) Jeśli ustawisz sprawdzanie co 2 minuty podczas działania, pozwalasz użytkownikowi zacząć coś robić, a potem zdajesz sobie sprawę, że powinien zapłacić. Jeśli naprawdę ułatwisz płacenie i wrócisz do pracy (nie jestem pewien, czy możesz w Androidzie), myślę, że sprzedasz więcej niż sprawdzając podczas onCreate.
Whaledawg
4
@Whaledawg: Musisz uruchomić swój własny serwer, ponieważ serwer przechowuje identyfikator telefonu i czas pierwszego uruchomienia w bazie danych, która jest następnie porównywana z późniejszymi. Również kiedy sprawdzasz to wyłącznie preferencje programisty, użyłem twardej zakodowana bomba zegarowa w grze ze świetnymi wynikami. Cała aplikacja by się załadowała, ale użytkownik może wchodzić w interakcję tylko z wyświetlonym oknem dialogowym. W tym oknie dialogowym znajduje się przycisk, który przenosi użytkownika bezpośrednio do strony zakupu gry. Wydaje się, że użytkownikom nie przeszkadza AFAIK, ponieważ ta gra znajduje się w pierwszej dziesiątce płatnych aplikacji od czasu otwarcia Android Market.
snctln
11
Dla każdego, kto niechętnie korzysta z opcji 3 z powodu dodatkowej konfiguracji serwera, spójrz na Parse.com - to synchronizacja.
Joel Skrepnek
3
Co oznacza ustalona data zakończenia okresu próbnego? Czy to oznacza, że ​​w przyszłości będziesz wydawać nowe wersje aplikacji próbnej z różnymi zapisanymi na stałe datami?
Jasper
21

Opracowałem Android Trial SDK, który możesz po prostu wrzucić do swojego projektu Android Studio i zajmie się całym zarządzaniem po stronie serwera (w tym okresami prolongaty offline).

Aby z niego skorzystać, po prostu

Dodaj bibliotekę do głównego modułu build.gradle

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

Zainicjuj bibliotekę w onCreate()metodzie swojego głównego działania

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

Dodaj moduł obsługi wywołań zwrotnych:

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

};

Aby rozpocząć okres próbny, zadzwoń do mTrialy.startTrial("YOUR_TRIAL_SKU", mTrialyCallback); swojego klucza aplikacji, a wersję próbną SKU można znaleźć na pulpicie programisty Trialy .

Nacięcie
źródło
Wymaga włączenia danych?
Sivaram Boina
trialy nie jest wiarygodne
Amir Dora
1
@AmirDe Cześć Amir, czy możesz mi powiedzieć, co nie działa dla Ciebie? Chętnie pomogę, [email protected] Trialy działa świetnie dla ponad 1000 użytkowników
Nick
@Nick nie wie, dlaczego na moim urządzeniu działa Android Lollipop, kiedy ustawiam dzień z pulpitu nawigacyjnego, a po kilku minutach dzień pokazuje wartość ujemną, mówi, że okres próbny wygasł, mimo że mam jeszcze wiele dni w panelu. Testowałem również na urządzeniu z nugatem, wydaje się, że działa dobrze na naugacie. może ma jakieś problemy ze zgodnością ze starszymi wersjami Androida
Amir Dora
1
Korzystam z tej usługi od 2016 roku, za każdym razem działa dobrze. Wykorzystałem to również w moich oficjalnych projektach. To powinna zostać zaakceptowana odpowiedź.
Tariq Mahmood
17

To stare pytanie, ale może komuś to pomoże.

Jeśli chcesz zastosować najbardziej uproszczone podejście (które się nie powiedzie, jeśli aplikacja zostanie odinstalowana / ponownie zainstalowana lub użytkownik ręcznie zmieni datę urządzenia), może to wyglądać następująco:

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

    ...
}
Caner
źródło
Cóż, w rzeczywistości, jeśli używasz SharedPreferences i Androida 2.2 Froyo lub nowszego, o ile zaimplementujesz interfejsy API kopii zapasowych danych do synchronizacji danych Google, użytkownik nie powinien być w stanie uciec z tego na różnych urządzeniach lub odinstalować, tylko przechodząc do Ustawienia> Aplikacje i czyszczenie danych. Również metoda w Date getTimenie jest getTimeInMillis.
Tom
Nie zgadzam się, to również się nie powiedzie, jeśli użytkownik ręcznie zmieni datę urządzenia, a także co, jeśli użytkownik usunie dane ręcznie?
Mohammed Azharuddin Shaikh
@Caner, dobrze jest to sprawdzić za pomocą Sharedprefrence, ale użytkownik powinien wyczyścić pamięć z ustawiania-> menedżera aplikacji i ponownie użyć, co wtedy zrobi?
CoronaPintu
@CoronaPintu, więc to podejście będzie również próbować z firebase, będzie to idealne połączenie, a okres próbny zakończy się nawet odinstalowaniem aplikacji.
Noor Hossain
połącz i dodaj podejście z odpowiedzią „strangetimes”, dzięki czemu podejście będzie idealne.
Noor Hossain
10

To pytanie i odpowiedź snctln zainspirowały mnie do pracy nad rozwiązaniem opartym na metodzie 3 jako moja praca licencjacka. Wiem, że obecny status nie służy do produktywnego użytku, ale chciałbym usłyszeć, co o nim myślisz! Czy użyłbyś takiego systemu? Czy chciałbyś widzieć to jako usługę w chmurze (nie masz problemu z konfiguracją serwera)? Martwisz się o kwestie bezpieczeństwa lub przyczyny związane ze stabilnością?

Zaraz po zakończeniu procedury licencjackiej chcę kontynuować pracę nad oprogramowaniem. Więc teraz czas na twoją opinię!

Kod źródłowy jest hostowany na GitHub https://github.com/MaChristmann/mobile-trial

Kilka informacji o systemie: - System składa się z trzech części, biblioteki Android, serwera node.js i konfiguratora do zarządzania wieloma aplikacjami próbnymi i kontami wydawcy / programisty.

  • Obsługuje tylko okresowe wersje próbne i używa konta (sklepu Play lub innego) zamiast identyfikatora telefonu.

  • W przypadku biblioteki Android jest ona oparta na bibliotece weryfikacji licencji Google Play. Zmodyfikowałem go, aby połączyć się z serwerem node.js i dodatkowo biblioteka próbuje rozpoznać, czy użytkownik zmienił datę systemową. Buforuje również pobraną licencję próbną w preferencjach wspólnych z szyfrowaniem AES. Za pomocą konfiguratora możesz skonfigurować aktualny czas pamięci podręcznej. Jeśli użytkownik „wyczyści dane”, biblioteka wymusi sprawdzenie po stronie serwera.

  • Serwer używa protokołu HTTPS, a także podpisuje cyfrowo odpowiedź sprawdzającą licencję. Posiada również API dla aplikacji próbnych CRUD i użytkowników (wydawcy i programisty). Podobnie jak w przypadku Biblioteki weryfikacji licencji, programiści mogą testować implementację swojego zachowania w aplikacji próbnej z wynikiem testu. Dlatego w konfiguratorze możesz jawnie ustawić odpowiedź licencji na „licencjonowana”, „nie licencjonowana” lub „błąd serwera”.

  • Jeśli zaktualizujesz swoją aplikację za pomocą niesamowitej nowej funkcji, możesz chcieć, aby wszyscy mogli spróbować ponownie. W konfiguratorze możesz odnowić licencję próbną dla użytkowników z wygasłymi licencjami, ustawiając kod wersji, który powinien to wywołać. Na przykład użytkownik uruchamia Twoją aplikację z kodem wersji 3 i chcesz, aby wypróbował funkcje kodu wersji 4. Jeśli zaktualizuje aplikację lub ją ponownie zainstaluje, będzie mógł ponownie skorzystać z pełnego okresu próbnego, ponieważ serwer wie, na której wersji ostatnio ją wypróbował czas.

  • Wszystko jest objęte licencją Apache 2.0

Martin Christmann
źródło
2
Właśnie uratowałeś mi dzień, dziękuję za ciężką pracę. Pomyślałem, że napiszę to samo rozwiązanie wykorzystujące zaszyfrowaną konfigurację uzyskaną tylko raz i zachowując klucz publiczny w aplikacji. Więc poważny problem, widzę tylko, jak przyznajesz licencję? Nadal możesz potrzebować unikalnego identyfikatora noszącego telefon, aby uniknąć wielokrotnych dotacji.
user2305886
6

Najłatwiejszym i najlepszym sposobem na to jest zaimplementowanie BackupSharedPreferences.

Preferencje są zachowywane, nawet jeśli aplikacja zostanie odinstalowana i ponownie zainstalowana.

Po prostu zapisz datę instalacji jako preferencję i gotowe.

Oto teoria: http://developer.android.com/reference/android/app/backup/SharedPreferencesBackupHelper.html

Oto przykład: Kopia zapasowa SharedPreferences systemu Android nie działa

pstorli
źródło
3
Użytkownik może wyłączyć tworzenie kopii zapasowych w ustawieniach systemu.
Patrick
5

Podejście 4: użyj czasu instalacji aplikacji.

Od poziomu 9 interfejsu API (Android 2.3.2, 2.3.1, Android 2.3, GINGERBREAD ) istnieją firstInstallTime i lastUpdateTime w PackageInfo.

Więcej informacji: Jak uzyskać czas instalacji aplikacji z Androida

18446744073709551615
źródło
Czy metoda 1 z odpowiedzi snctln może być używana z tym niezawodnie, bez łatwego obejścia, czy też ma ten sam lub podobny problem?
jwinn
Przetestowałem tę metodę. Dobra strona jest taka, że ​​działa nawet po wyczyszczeniu danych. Zła strona polega na tym, że można go obejść przez odinstalowanie / ponowną instalację.
Jean-Philippe Jodoin
mogę potwierdzić: na moim Pixelu odinstalowanie i ponowne zainstalowanie aplikacji resetuje się przy pierwszej instalacji. to sprawia, że ​​resetowanie wersji próbnej jest dla użytkowników dość trywialne
Fabian Streitel
3

Teraz, w najnowszej wersji bezpłatnej subskrypcji próbnej Androida, możesz odblokować wszystkie funkcje aplikacji dopiero po wykupieniu subskrypcji w aplikacji na bezpłatny okres próbny. Pozwoli to użytkownikowi korzystać z Twojej aplikacji przez okres próbny, jeśli aplikacja będzie nadal odinstalowana po okresie próbnym, pieniądze z subskrypcji zostaną przesłane do Ciebie. Nie próbowałem, po prostu dzielę się pomysłem.

Oto dokumentacja

Vins
źródło
2
Chciałbym, żeby to działało w przypadku pojedynczych zakupów. Działa tylko w przypadku subskrypcji, które mogą być roczne lub miesięczne.
jwinn
3

Moim zdaniem najlepszym sposobem na to jest po prostu skorzystanie z bazy danych czasu rzeczywistego Firebase:

1) Dodaj obsługę Firebase do swojej aplikacji

2) Wybierz „Uwierzytelnianie anonimowe”, aby użytkownik nie musiał się rejestrować ani nawet wiedzieć, co robisz. To gwarantuje połączenie z aktualnie uwierzytelnionym kontem użytkownika i będzie działać na różnych urządzeniach.

3) Użyj interfejsu API bazy danych czasu rzeczywistego, aby ustawić wartość parametru „installed_date”. W czasie uruchamiania po prostu pobierz tę wartość i użyj jej.

Zrobiłem to samo i działa świetnie. Udało mi się to przetestować podczas odinstalowywania / ponownej instalacji, a wartość w bazie danych czasu rzeczywistego pozostaje taka sama. W ten sposób okres próbny działa na wielu urządzeniach użytkowników. Możesz nawet ustawić wersję swojej install_date, aby aplikacja `` resetowała '' datę wersji próbnej dla każdej nowej wersji głównej.

AKTUALIZACJA : po przeprowadzeniu dalszych testów wydaje się, że wydaje się, że Firebase anonimowo przydziela inny identyfikator w przypadku, gdy masz różne urządzenia i nie ma gwarancji między kolejnymi instalacjami: / Jedynym gwarantowanym sposobem jest użycie Firebase, ale powiązanie go z ich Google konto. To powinno działać, ale wymagałoby dodatkowego kroku, w którym użytkownik musi najpierw zalogować się / zarejestrować.

Jak dotąd skończyłem z nieco mniej eleganckim podejściem, polegającym na prostym sprawdzaniu kopii zapasowej preferencji i daty zapisanej w preferencjach podczas instalacji. Działa to w przypadku aplikacji zorientowanych na dane, w przypadku których nie ma sensu ponowne instalowanie aplikacji i ponowne wprowadzanie wszystkich wcześniej dodanych danych, ale nie działa to w przypadku prostej gry.

strangetimes
źródło
Mam te same wymagania co do mojej aplikacji na Androida i mam własną bazę danych / serwer WWW. Użytkownicy nie wymagają logowania, więc planowałem zapisać identyfikator urządzenia z zainstalowaną datą, czy to zadziała?
user636525
@strangetimes, myślę, że Twoje rozwiązanie działa najlepiej, czy możesz podać więcej informacji? dzięki
DayDayHappy
Ten wątek stackoverflow.com/q/41733137/1396068 sugeruje, że po ponownej instalacji aplikacji otrzymasz nowy identyfikator użytkownika, tj. Nowy okres próbny
Fabian Streitel
3

Po przejrzeniu wszystkich opcji w tym i innych wątkach, oto moje ustalenia

Wspólne preferencje, baza danych Można wyczyścić w ustawieniach Androida, utracić po ponownej instalacji aplikacji. Można zarchiwizować za pomocą mechanizmu tworzenia kopii zapasowych Androida i zostanie przywrócona po ponownej instalacji. Kopia zapasowa może nie zawsze być dostępna, chociaż powinna znajdować się na większości urządzeń

Pamięć zewnętrzna (zapis do pliku) Nie dotyczy wyczyszczenia ustawień lub ponownej instalacji, jeśli nie zapisujemy w prywatnym katalogu aplikacji . Ale: wymaga, abyś poprosił użytkownika o pozwolenie w czasie wykonywania w nowszych wersjach Androida, więc jest to prawdopodobnie możliwe tylko wtedy, gdy i tak potrzebujesz tego uprawnienia. Można również zarchiwizować.

PackageInfo.firstInstallTime jest resetowany po ponownej instalacji, ale stabilny między aktualizacjami

Zaloguj się na jakieś konto Nie ma znaczenia, czy jest to konto Google przez Firebase, czy jedno na Twoim własnym serwerze: wersja próbna jest powiązana z kontem. Utworzenie nowego konta spowoduje zresetowanie okresu próbnego.

Anonimowe logowanie do Firebase Możesz zalogować użytkownika anonimowo i przechowywać jego dane w Firebase. Ale najwyraźniej ponowna instalacja aplikacji i być może inne nieudokumentowane zdarzenia mogą dać użytkownikowi nowy anonimowy identyfikator , resetując jego czas próbny. (Same Google nie dostarczają dużo dokumentacji na ten temat)

ANDROID_ID Może być niedostępny i może ulec zmianie w pewnych okolicznościach , np. Przywrócenie ustawień fabrycznych. Opinie na temat tego, czy warto używać tego do identyfikacji urządzeń, wydają się różne.

Identyfikator reklamowy Play Może zostać zresetowany przez użytkownika. Może zostać wyłączone przez użytkownika poprzez rezygnację ze śledzenia reklam.

Reset ID instancji podczas ponownej instalacji . Zresetuj w przypadku zdarzenia związanego z bezpieczeństwem. Można zresetować za pomocą aplikacji.

To, która (kombinacja) metod działa w Twoim przypadku, zależy od Twojej aplikacji i od tego, ile wysiłku, według Ciebie, przeciętny Jan włoży w uzyskanie kolejnego okresu próbnego. Zalecałbym unikanie używania tylko anonimowych identyfikatorów Firebase i Advertising ID ze względu na ich niestabilność. Wydaje się, że podejście wieloczynnikowe przyniesie najlepsze rezultaty. Dostępność dostępnych czynników zależy od aplikacji i jej uprawnień.

W przypadku mojej własnej aplikacji okazało się, że wspólne preferencje + firstInstallTime + kopia zapasowa preferencji są najmniej uciążliwą, ale także wystarczająco skuteczną metodą. Musisz upewnić się, że zażądasz kopii zapasowej tylko po sprawdzeniu i zapisaniu czasu rozpoczęcia okresu próbnego we wspólnych preferencjach. Wartości we wspólnych prefs muszą mieć pierwszeństwo przed firstInstallTime. Następnie użytkownik musi ponownie zainstalować aplikację, uruchomić ją raz, a następnie wyczyścić dane aplikacji, aby zresetować wersję próbną, co jest dość dużo pracy. Jednak na urządzeniach bez zapasowego transportu użytkownik może zresetować wersję próbną, po prostu przeinstalowując.

Udostępniłem to podejście jako rozszerzalną bibliotekę .

Fabian Streitel
źródło
1

Z definicji wszystkie płatne aplikacje na Androida dostępne na rynku można oceniać przez 24 godziny od zakupu.

Dostępny jest przycisk „Odinstaluj i zwróć środki”, który po 24 godzinach zmienia się na „Odinstaluj”.

Twierdzę, że ten przycisk jest zbyt widoczny!

AlexJReid
źródło
17
Pamiętaj, że okres zwrotu wynosi teraz tylko 15 minut.
Intrications
1

Natknąłem się na to pytanie, szukając tego samego problemu, myślę, że możemy skorzystać z darmowego interfejsu API dat, takiego jak http://www.timeapi.org/utc/now lub innego interfejsu API do sprawdzania daty wygaśnięcia aplikacji. ten sposób jest skuteczny, jeśli chcesz dostarczyć wersję demonstracyjną i martwisz się o płatność i potrzebujesz wersji demo z ustalonym okresem zatrudnienia. :)

znajdź kod poniżej

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

}
}

jego działające rozwiązanie .....

RQube
źródło
na pewno możesz użyć, ale w takim przypadku tylko Twoja główna działalność będzie w stanie sprawdzić datę ważności.
RQube
0

Oto jak poszedłem do mojego, stworzyłem 2 aplikacje, jedną z próbną aktywnością, a drugą bez,

przesłałem ten bez aktywności próbnej do sklepu Play jako płatną aplikację,

i ten z aktywnością próbną jako darmową aplikacją.

Bezpłatna aplikacja przy pierwszym uruchomieniu ma opcje zakupu wersji próbnej i zakupu w sklepie, jeśli użytkownik wybierze zakup w sklepie, przekierowuje do sklepu, aby użytkownik mógł dokonać zakupu, ale jeśli użytkownik kliknie wersję próbną, przeniesie go do aktywności próbnej

NB: użyłem opcji 3, jak @snctln, ale z modyfikacjami

po pierwsze , nie zależałem od czasu urządzenia, mam swój czas z pliku php, który wykonuje próbną rejestrację do bazy danych,

po drugie , użyłem numeru seryjnego urządzenia, aby jednoznacznie zidentyfikować każde urządzenie,

wreszcie aplikacja zależy od wartości czasu zwróconej z połączenia z serwerem, a nie od własnego czasu, więc system można obejść tylko wtedy, gdy numer seryjny urządzenia zostanie zmieniony, co jest dość stresujące dla użytkownika.

więc oto mój kod (dla działania próbnego):

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


}

Mój plik php wygląda następująco (jest to technologia 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;
        }

    }

następnie w głównym działaniu używam preferencji współdzielonych (installDate utworzona w działaniu próbnym), aby monitorować liczbę pozostałych dni, a jeśli dni się skończyły, blokuję główny interfejs aktywności za pomocą komunikatu, który prowadzi ich do sklepu w celu zakupu.

Jedyną wadą, jaką tutaj widzę, jest to, że jeśli użytkownik Rogue kupi płatną aplikację i zdecyduje się udostępnić ją aplikacjom takim jak Zender, udostępnić plik lub nawet hostować plik apk bezpośrednio na serwerze, aby ludzie mogli go pobrać za darmo. Ale jestem pewien, że wkrótce edytuję tę odpowiedź, podając rozwiązanie tego problemu lub link do rozwiązania.

Mam nadzieję, że to uratuje duszę ... któregoś dnia

Miłego kodowania ...

The Dead Guy
źródło
0

@snctln opcję 3 można łatwo zrobić, dodając plik php do serwera internetowego z zainstalowanymi php i mysql, tak jak wiele z nich ma.

Ze strony Androida identyfikator (identyfikator urządzenia, konto Google lub cokolwiek chcesz) jest przekazywany jako argument w adresie URL za pomocą HttpURLConnection, a php zwraca datę pierwszej instalacji, jeśli istnieje w tabeli lub wstawia nowy wiersz i zwraca aktualną datę.

U mnie działa dobrze.

Jeśli będę miał czas, wyślę kod!

Powodzenia !

Lluis Felisart
źródło
czekaj, ale czy tracisz swój unikalny identyfikator po ponownej instalacji aplikacji? !!
Maksim Kniazev
Ten identyfikator identyfikuje sprzęt, sam telefon, użytkownik go nie widzi i nie może go zmienić. Jeśli ponownie zainstaluje aplikację, usługa internetowa php wykryje, że jest to ten sam telefon. Z drugiej strony, jeśli użytkownik zmieni telefon, będzie cieszył się nowym okresem.
Lluis Felisart