Erstellen einer Android-Testanwendung, die nach einem festgelegten Zeitraum abläuft


103

Ich habe eine Anwendung, die ich als kostenpflichtige App auf den Markt bringen möchte. Ich hätte gerne eine andere Version, die eine "Test" -Version mit einem Zeitlimit von beispielsweise 5 Tagen wäre.

Wie kann ich das machen?


Google sollte dies in Play Services wirklich unterstützen!
Pulver366

@ Pulver366 tatsächlich unterstützt Google dies, siehe developer.android.com/google/play/billing/…
Yazazzello

Antworten:


186

Derzeit erreichen die meisten Entwickler dies mit einer der folgenden drei Techniken.

Der erste Ansatz lässt sich leicht umgehen. Wenn Sie die App zum ersten Mal ausführen, speichern Sie Datum und Uhrzeit in einer Datei, Datenbank oder freigegebenen Einstellungen. Jedes Mal, wenn Sie die App nach dieser Ausführung ausführen, überprüfen Sie, ob der Testzeitraum abgelaufen ist. Dies ist leicht zu umgehen, da der Benutzer durch Deinstallation und Neuinstallation eine weitere Testphase erhalten kann.

Der zweite Ansatz ist schwerer zu umgehen, aber immer noch zu umgehen. Verwenden Sie eine hartcodierte Zeitbombe. Grundsätzlich wird bei diesem Ansatz ein Enddatum für die Testversion festgelegt, und alle Benutzer, die die App herunterladen und verwenden, können die App nicht mehr gleichzeitig verwenden. Ich habe diesen Ansatz verwendet, weil er einfach zu implementieren ist und ich größtenteils keine Lust hatte, die Probleme der dritten Technik zu lösen. Benutzer können dies umgehen, indem sie das Datum auf ihrem Telefon manuell ändern. Die meisten Benutzer werden sich jedoch nicht die Mühe machen, so etwas zu tun.

Die dritte Technik ist der einzige Weg, von dem ich gehört habe, um wirklich das erreichen zu können, was Sie tun möchten. Sie müssen einen Server einrichten. Wenn Ihre Anwendung gestartet wird, sendet Ihre App die eindeutige Kennung des Telefons an den Server. Wenn der Server keinen Eintrag für diese Telefon-ID hat, erstellt er einen neuen und notiert die Uhrzeit. Wenn der Server einen Eintrag für die Telefon-ID hat, wird einfach überprüft, ob der Testzeitraum abgelaufen ist. Anschließend werden die Ergebnisse der Testablaufprüfung an Ihre Anwendung zurückübermittelt. Dieser Ansatz sollte nicht umgehbar sein, erfordert jedoch die Einrichtung eines Webservers und dergleichen.

Es wird immer empfohlen, diese Überprüfungen in onCreate durchzuführen. Wenn der Ablauf hat eine Alertdialog mit einem Pop - up beendet Markt Link auf die Vollversion der App. Fügen Sie nur eine Schaltfläche "OK" hinzu. Wenn der Benutzer auf "OK" klickt, rufen Sie "finish ()" auf, um die Aktivität zu beenden.


2
Fantastische Antwort. Wie Sie sagen, ich denke, die zweite Option ist möglicherweise die beste. Es ist eine Schande, dass Google selbst kein Lizenzsystem anbietet, da es sowohl kleine als auch größere Markenentwickler dazu ermutigen kann, noch mehr Android-Anwendungen zu produzieren.
Tom

8
Außerdem würde ich beim Start nicht nachsehen. Ihr Ziel ist es, die App zu verkaufen und den Benutzer nicht zu bestrafen (das ist nur ein Bonus;). Wenn Sie sie so eingestellt haben, dass sie während des Betriebs alle 2 Minuten überprüft wird, lassen Sie den Benutzer etwas tun und erkennen dann, dass er zahlen sollte. Wenn Sie es wirklich einfach machen, zu bezahlen und wieder an die Arbeit zu gehen (ich bin mir nicht sicher, ob Sie dies in Android können), werden Sie wahrscheinlich mehr verkaufen als bei onCreate zu prüfen.
Whaledawg

4
@Whaledawg: Sie müssen Ihren eigenen Server ausführen, da der Server die Telefon-ID und den Zeitpunkt der ersten Ausführung in einer Datenbank speichert, die dann mit später verglichen wird. Auch wenn Sie die Prüfung durchführen, ist dies eine reine Präferenz des Entwicklers codierte Zeitbombe in einem Spiel mit großartigen Ergebnissen. Die gesamte App würde geladen, aber der Benutzer kann nur mit dem angezeigten Dialogfeld interagieren. In diesem Dialogfeld befindet sich eine Schaltfläche, die den Benutzer direkt zur Kaufseite für das Spiel führt. Benutzer scheinen AFAIK nichts auszumachen, da dieses Spiel seit der Eröffnung des Android Market zu den Top 10 der kostenpflichtigen Apps gehört.
Snctln

11
Wer Option 3 aufgrund des zusätzlichen Server-Setups nicht verwenden möchte, sollte sich Parse.com ansehen - es ist eine Synchronisierung.
Joel Skrepnek

3
Was ist mit dem Hardcode-Enddatum der Testversion gemeint? Bedeutet das, dass Sie auch in Zukunft neue Versionen der Test-App mit unterschiedlichen fest codierten Daten für immer veröffentlichen werden?
Jasper

21

Ich habe ein Android-Test-SDK entwickelt, das Sie einfach in Ihr Android Studio-Projekt einfügen können und das die gesamte serverseitige Verwaltung für Sie übernimmt (einschließlich Offline-Kulanzfristen).

Um es einfach zu benutzen

Fügen Sie die Bibliothek Ihrem Hauptmodul hinzu build.gradle

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

Initialisieren Sie die Bibliothek in der onCreate()Methode Ihrer Hauptaktivität

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

Fügen Sie einen Rückruf-Handler hinzu:

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

};

Um eine Testversion zu starten, rufen Sie mTrialy.startTrial("YOUR_TRIAL_SKU", mTrialyCallback); Ihren App-Schlüssel an. Die Test-SKU finden Sie in Ihrem Trialy-Entwickler-Dashboard .


Es müssen Daten aktiviert werden?
Sivaram Boina

Trialy ist nicht zuverlässig
Amir Dora

1
@AmirDe Hallo Amir, kannst du mich wissen lassen, was bei dir nicht funktioniert? Ich helfe gerne, support@trialy.io Trialy funktioniert hervorragend für mehr als 1000 Benutzer
Nick

@ Nick weiß nicht, warum auf meinem Gerät Android Lollipop ausgeführt wird. Wenn ich den Tag über das Dashboard einstelle, wird der Tag nach einigen Minuten als negativer Wert angezeigt. Die Testversion ist abgelaufen, obwohl ich noch viele Tage im Dashboard habe. Ich habe auch auf Nougat-Gerät getestet, scheint auf Naugat gut zu funktionieren. Vielleicht hat es einige ältere Android-Version Kompatibilitätsprobleme
Amir Dora

1
Ich benutze diesen Service seit 2016, er funktioniert jedes Mal einwandfrei. Ich habe dies auch in meinen offiziellen Projekten verwendet. Dies sollte als Antwort akzeptiert werden.
Tariq Mahmood

17

Dies ist eine alte Frage, aber vielleicht hilft das jemandem.

Wenn Sie den einfachsten Ansatz wählen möchten (der fehlschlägt, wenn die App deinstalliert / neu installiert wird oder der Benutzer das Gerätedatum manuell ändert), könnte dies folgendermaßen aussehen:

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

    ...
}

Wenn Sie SharedPreferences und Android 2.2 Froyo oder höher verwenden, sollte der Benutzer, solange Sie die Datensicherungs-APIs für die Google-Datensynchronisierung implementieren, nicht in der Lage sein, dies geräteübergreifend oder durch Deinstallation zu umgehen, sondern nur unter Einstellungen> Anwendungen und Daten löschen. Auch die Methode am Datum ist getTimenicht getTimeInMillis.
Tom

Nicht einverstanden, dies schlägt auch fehl, wenn der Benutzer das Gerätedatum manuell ändert und was passiert, wenn der Benutzer die Daten manuell löscht?
Mohammed Azharuddin Shaikh

@Caner Dies ist gut, um mit Sharedprefrence zu chek, aber Benutzer wird Speicher aus seting-> Aplication Manager löschen und wiederverwenden, was wird dann tun?
CoronaPintu

@CoronaPintu, damit dieser Ansatz auch mit Firebase versucht, wird dies die perfekte Kombination ergeben und der Testzeitraum wird sogar über die App deinstalliert.
Noor Hossain

Kombinieren Sie den Ansatz und fügen Sie ihn mit der Antwort "Strangetimes" hinzu. Dadurch wird der Ansatz perfekt.
Noor Hossain

10

Diese Frage und die Antwort von snctln haben mich dazu inspiriert, an einer Lösung zu arbeiten, die auf Methode 3 als meine Bachelorarbeit basiert. Ich weiß, dass der aktuelle Status nicht für den produktiven Gebrauch bestimmt ist, aber ich würde gerne hören, was Sie darüber denken! Würden Sie ein solches System verwenden? Möchten Sie es als Cloud-Dienst sehen (ohne Probleme bei der Konfiguration eines Servers)? Besorgt über Sicherheitsprobleme oder Stabilitätsgründe?

Sobald ich das Bachelor-Verfahren abgeschlossen habe, möchte ich weiter an der Software arbeiten. Jetzt ist es an der Zeit, dass ich Ihr Feedback brauche!

Der Quellcode wird auf GitHub https://github.com/MaChristmann/mobile-trial gehostet

Einige Informationen zum System: - Das System besteht aus drei Teilen, einer Android-Bibliothek, einem node.js-Server und einem Konfigurator zum Verwalten mehrerer Test-Apps und Publisher- / Entwicklerkonten.

  • Es unterstützt nur zeitbasierte Testversionen und verwendet Ihr (Play Store oder anderes) Konto anstelle einer Telefon-ID.

  • Für die Android-Bibliothek basiert sie auf der Google Play-Bibliothek zur Überprüfung der Lizenzierung. Ich habe es geändert, um eine Verbindung zum Server node.js herzustellen, und außerdem versucht die Bibliothek zu erkennen, ob ein Benutzer das Systemdatum geändert hat. Außerdem wird eine abgerufene Testlizenz in AES-verschlüsselten Shared Preferences zwischengespeichert. Sie können die gültige Zeit des Caches mit dem Konfigurator konfigurieren. Wenn ein Benutzer Daten "löscht", erzwingt die Bibliothek eine serverseitige Überprüfung.

  • Der Server verwendet https und signiert die Antwort auf die Lizenzprüfung auch digital. Es hat auch eine API für CRUD-Test-Apps und Benutzer (Herausgeber und Entwickler). Ähnlich wie bei Licensing Verfication Library können Entwickler ihre Verhaltensimplementierung in der Test-App mit Testergebnissen testen. So können Sie im Konfigurator Ihre Lizenzantwort explizit auf "lizenziert", "nicht lizenziert" oder "Serverfehler" setzen.

  • Wenn Sie Ihre App mit einer neuen Funktion aktualisieren, möchten Sie möglicherweise, dass jeder sie erneut versuchen kann. Im Konfigurator können Sie die Testlizenz für Benutzer mit abgelaufenen Lizenzen erneuern, indem Sie einen Versionscode festlegen, der dies auslösen soll. Beispielsweise führt der Benutzer Ihre App mit Versionscode 3 aus und Sie möchten, dass er die Funktionen von Versionscode 4 ausprobiert. Wenn er die App aktualisiert oder neu installiert, kann er die vollständige Testphase erneut verwenden, da der Server weiß, auf welcher Version er sie zuletzt ausprobiert hat Zeit.

  • Alles unterliegt der Apache 2.0-Lizenz


2
Du hast gerade meinen Tag gerettet, danke für die harte Arbeit. Ich dachte nur daran, die gleiche Lösung zu schreiben, bei der die verschlüsselte Konfiguration nur einmal verwendet wird und der öffentliche Schlüssel in der Anwendung bleibt. So großes Problem kann ich nur sehen, wie Sie eine Lizenz gewähren? Möglicherweise benötigen Sie noch eine eindeutige ID, die Sie mit einem Telefon tragen müssen, um Mehrfachzuschüsse zu vermeiden.
user2305886

6

Der einfachste und beste Weg, dies zu tun, ist die Implementierung von BackupSharedPreferences.

Die Einstellungen bleiben erhalten, auch wenn die App deinstalliert und neu installiert wird.

Speichern Sie einfach das Installationsdatum als Präferenz und Sie können loslegen.

Hier ist die Theorie: http://developer.android.com/reference/android/app/backup/SharedPreferencesBackupHelper.html

Hier ist das Beispiel: Android SharedPreferences Backup funktioniert nicht


3
Der Benutzer kann die Sicherung in den Systemeinstellungen deaktivieren.
Patrick

5

Ansatz 4: Verwenden Sie die Installationszeit der Anwendung.

Seit API Level 9 (Android 2.3.2, 2.3.1, Android 2.3, GINGERBREAD) gibt es firstInstallTime und lastUpdateTime in PackageInfo.

Lesen Sie mehr: So erhalten Sie App-Installationszeit von Android


Kann Methode 1 aus der Antwort von snctln damit zuverlässig angewendet werden, ohne leicht umgangen zu werden, oder hat sie das gleiche oder ein ähnliches Problem?
Jwinn

Ich habe diese Methode getestet. Die gute Seite ist, dass es auch funktioniert, wenn Daten gelöscht werden. Die schlechte Seite ist, dass es durch eine Deinstallation / Neuinstallation umgangen werden kann.
Jean-Philippe Jodoin

kann bestätigen: Auf meinem Pixel wird durch Deinstallieren und erneutes Installieren der App die erste Installationszeit zurückgesetzt. Das macht es für Benutzer ziemlich trivial, die Testversion zurückzusetzen
Fabian Streitel

3

Jetzt, da die aktuelle Version des kostenlosen Android-Testabonnements hinzugefügt wurde, können Sie alle Funktionen Ihrer App erst freischalten, nachdem Sie das Abonnement innerhalb der App für einen kostenlosen Testzeitraum gekauft haben. Auf diese Weise kann der Benutzer Ihre App für einen Testzeitraum verwenden. Wenn die App nach dem Testzeitraum noch deinstalliert ist, wird das Abonnementgeld an Sie überwiesen. Ich habe es nicht versucht, sondern nur eine Idee geteilt.

Hier ist die Dokumentation


2
Ich wünschte, dies würde für Einzelkäufe funktionieren. Funktioniert nur für Abonnements, die jährlich oder monatlich sein können.
Jwinn

3

Meiner Meinung nach ist der beste Weg, dies zu tun, einfach die Firebase-Echtzeitdatenbank zu verwenden:

1) Fügen Sie Ihrer App Firebase-Unterstützung hinzu

2) Wählen Sie "Anonyme Authentifizierung", damit sich der Benutzer nicht anmelden oder gar wissen muss, was Sie tun. Dies wird garantiert mit dem aktuell authentifizierten Benutzerkonto verknüpft und funktioniert somit geräteübergreifend.

3) Verwenden Sie die Echtzeitdatenbank-API, um einen Wert für 'installiertes_Datum' festzulegen. Rufen Sie zum Startzeitpunkt einfach diesen Wert ab und verwenden Sie ihn.

Ich habe das gleiche getan und es funktioniert großartig. Ich konnte dies bei Deinstallation / Neuinstallation testen und der Wert in der Echtzeitdatenbank bleibt gleich. Auf diese Weise funktioniert Ihre Testphase auf mehreren Benutzergeräten. Sie können Ihr Installationsdatum sogar versionieren, sodass die App das Testdatum für jede neue Hauptversion zurücksetzt.

UPDATE : Nachdem Sie ein bisschen mehr getestet haben, scheint es, dass anonymes Firebase eine andere ID zuzuweisen scheint, falls Sie andere Geräte haben, und es wird nicht garantiert, dass es zwischen Neuinstallationen installiert wird: / Die einzige garantierte Möglichkeit besteht darin, Firebase zu verwenden, aber es an Google zu binden Konto. Dies sollte funktionieren, würde jedoch einen zusätzlichen Schritt erfordern, bei dem sich der Benutzer zuerst anmelden / anmelden muss.

Bisher habe ich einen etwas weniger eleganten Ansatz gefunden, bei dem einfach die gesicherten Einstellungen und ein Datum überprüft werden, das bei der Installation in den Einstellungen gespeichert wurde. Dies funktioniert für datenzentrierte Apps, bei denen es für eine Person sinnlos ist, die App erneut zu installieren und alle zuvor hinzugefügten Daten erneut einzugeben, dies würde jedoch für ein einfaches Spiel nicht funktionieren.


Ich habe die gleichen Anforderungen für meine Android-App und ich habe meine eigene Datenbank / Webserver. Benutzer benötigen keine Anmeldung. Ich hatte also vor, die Geräte-ID mit dem installierten_Datum zu speichern. Würde dies funktionieren?
user636525

@strangetimes, ich denke, Ihre Lösung funktioniert am besten. Können Sie weitere Informationen bereitstellen? danke
DayDayHappy

Dieser Thread stackoverflow.com/q/41733137/1396068 schlägt vor, dass Sie nach der Neuinstallation der App eine neue Benutzer-ID erhalten, dh eine neue Testphase
Fabian Streitel

3

Nachdem ich mir alle Optionen in diesem und anderen Threads angesehen habe, sind dies meine Ergebnisse

Gemeinsame Einstellungen , Datenbank Kann in den Android-Einstellungen gelöscht werden und geht nach einer Neuinstallation der App verloren. Kann mit dem Backup-Mechanismus von Android gesichert werden und wird nach einer Neuinstallation wiederhergestellt. Backup ist möglicherweise nicht immer verfügbar, sollte jedoch auf den meisten Geräten vorhanden sein

Externer Speicher (Schreiben in eine Datei) Nicht betroffen von einem Löschen der Einstellungen oder einer Neuinstallation, wenn nicht in das private Verzeichnis der Anwendung geschrieben wird . Aber: erfordert, dass Sie den Benutzer zur Laufzeit in neueren Android-Versionen um seine Erlaubnis bitten , daher ist dies wahrscheinlich nur möglich, wenn Sie diese Erlaubnis trotzdem benötigen. Kann auch gesichert werden.

PackageInfo.firstInstallTime Wird nach einer Neuinstallation zurückgesetzt, ist jedoch über Updates hinweg stabil

Bei einem Konto anmelden Es spielt keine Rolle, ob es sich um das Google-Konto über Firebase oder eines auf Ihrem eigenen Server handelt: Die Testversion ist an das Konto gebunden. Durch das Erstellen eines neuen Kontos wird die Testversion zurückgesetzt.

Anonyme Firebase-Anmeldung Sie können einen Benutzer anonym anmelden und Daten für ihn in Firebase speichern. Aber offenbar eine erneute Installation der App und vielleicht andere nicht dokumentierte Ereignisse können dem Benutzer eine neue anonyme ID geben , ihre Probezeit zurückzusetzen. (Google selbst liefert hierzu nicht viel Dokumentation)

ANDROID_ID ist möglicherweise nicht verfügbar und kann sich unter bestimmten Umständen ändern , z. B. durch Zurücksetzen auf die Werkseinstellungen. Die Meinungen darüber, ob es eine gute Idee ist, Geräte zu identifizieren, scheinen sich zu unterscheiden.

Play Advertising ID Kann vom Benutzer zurückgesetzt werden. Kann vom Nutzer deaktiviert werden, indem die Anzeigenverfolgung deaktiviert wird.

InstanceID Bei einer Neuinstallation zurücksetzen . Im Falle eines Sicherheitsereignisses zurücksetzen. Kann von Ihrer App zurückgesetzt werden.

Welche (Kombination von) Methoden für Sie funktionieren, hängt von Ihrer App ab und davon, wie viel Aufwand der durchschnittliche John Ihrer Meinung nach in eine weitere Testphase stecken wird. Ich würde empfehlen, aufgrund ihrer Instabilität nicht nur anonyme Firebase- und Werbe-IDs zu verwenden. Ein Multi-Faktor-Ansatz scheint die besten Ergebnisse zu erzielen. Welche Faktoren Ihnen zur Verfügung stehen, hängt von Ihrer App und ihren Berechtigungen ab.

Für meine eigene App empfand ich gemeinsame Einstellungen + firstInstallTime + Sicherung der Einstellungen als die am wenigsten aufdringliche, aber auch effektivste Methode. Sie müssen sicherstellen, dass Sie eine Sicherung erst anfordern, nachdem Sie die Teststartzeit in den freigegebenen Einstellungen überprüft und gespeichert haben. Werte in den freigegebenen Einstellungen müssen Vorrang vor firstInstallTime haben. Dann muss der Benutzer die App neu installieren, einmal ausführen und dann die Daten der App löschen, um die Testversion zurückzusetzen, was ziemlich viel Arbeit bedeutet. Auf Geräten ohne Backup-Transport kann der Benutzer die Testversion jedoch durch einfaches Neuinstallieren zurücksetzen.

Ich habe diesen Ansatz als erweiterbare Bibliothek zur Verfügung gestellt .


1

Per Definition können alle kostenpflichtigen Android-Apps auf dem Markt nach dem Kauf 24 Stunden lang bewertet werden.

Es gibt eine Schaltfläche "Deinstallieren und erstatten", die nach 24 Stunden in "Deinstallieren" geändert wird.

Ich würde behaupten, dieser Knopf ist viel zu prominent!


17
Beachten Sie, dass die Rückerstattungsfrist jetzt nur noch 15 Minuten beträgt.
Komplikationen

1

Ich bin auf diese Frage gestoßen, als ich nach dem gleichen Problem gesucht habe. Ich denke, wir können eine kostenlose Datums-API wie http://www.timeapi.org/utc/now oder eine andere Datums-API verwenden, um den Ablauf der Trail-App zu überprüfen. Dieser Weg ist effizient, wenn Sie die Demo liefern möchten und sich Sorgen um die Zahlung machen und eine Demo mit fester Amtszeit benötigen. :) :)

Finden Sie den Code unten

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

}
}

seine funktionierende Lösung .....


Sicher können Sie verwenden, aber in diesem Fall kann nur Ihre Hauptaktivität auf Ablauf prüfen.
RQube

0

Hier ist, wie ich meine gemacht habe, ich habe 2 Apps erstellt, eine mit Testaktivität, die andere ohne,

Ich habe die ohne Testaktivität hochgeladen, um den Store als kostenpflichtige App zu spielen.

und der mit Testaktivität als kostenlose App.

Die kostenlose App beim ersten Start bietet Optionen für den Test- und Store-Kauf. Wenn der Benutzer den Store-Kauf auswählt, wird sie zum Kauf an den Store weitergeleitet, aber wenn der Benutzer auf Test klickt, werden sie zur Testaktivität weitergeleitet

NB: Ich habe Option 3 wie @snctln verwendet, aber mit Änderungen

Erstens war ich nicht von der Gerätezeit abhängig. Ich habe meine Zeit aus der PHP-Datei erhalten, die die Testregistrierung für die Datenbank durchführt.

Zweitens habe ich die Seriennummer des Geräts verwendet, um jedes Gerät eindeutig zu identifizieren.

Schließlich hängt die App auf dem Zeitwert von der Server - Verbindung nicht seine eigene Zeit zurückgeführt, so kann das System nur umgangen werden , wenn die Seriennummer des Geräts geändert wird, die für einen Benutzer ziemlich stressig ist.

Also hier ist mein Code (für die Testaktivität):

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


}

Meine PHP-Datei sieht so aus (es ist eine REST-Slim-Technologie):

/**
     * 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;
        }

    }

Bei der Hauptaktivität verwende ich die gemeinsame Einstellung (in der Testaktivität erstelltes Installationsdatum), um die Anzahl der verbleibenden Tage zu überwachen. Wenn die Tage vorbei sind, blockiere ich die Benutzeroberfläche der Hauptaktivität mit einer Nachricht, die sie zum Kauf in den Laden bringt.

Der einzige Nachteil, den ich hier sehe, ist, dass wenn ein Rogue-Benutzer die kostenpflichtige App kauft und beschließt, sie mit Apps wie Zender zu teilen, die APK-Datei direkt auf einem Server zu teilen oder sogar zu hosten, damit die Leute sie kostenlos herunterladen können. Aber ich bin mir sicher, dass ich diese Antwort bald mit einer Lösung dafür oder einem Link zur Lösung bearbeiten werde.

Hoffe das rettet eine Seele ... eines Tages

Viel Spaß beim Codieren ...


0

@snctln Option 3 kann einfach durchgeführt werden, indem eine PHP-Datei zu einem Webserver hinzugefügt wird, auf dem PHP und MySQL installiert sind, wie viele von ihnen.

Von der Android-Seite wird eine Kennung (die Geräte-ID, das Google-Konto oder was auch immer Sie wollen) als Argument in der URL mit HttpURLConnection übergeben und das PHP gibt das Datum der ersten Installation zurück, wenn es in der Tabelle vorhanden ist oder eine neue Zeile und einfügt es gibt das aktuelle Datum zurück.

Es funktioniert gut für mich.

Wenn ich Zeit habe, werde ich einen Code posten!

Viel Glück !


Warten Sie, aber verlieren Sie Ihre eindeutige ID nach der Neuinstallation der App? !!
Maksim Kniazev

Diese ID identifiziert die Hardware, das Telefon selbst, der Benutzer sieht es nicht und kann es nicht ändern. Wenn er die App neu installiert, erkennt der PHP-Webdienst, dass es sich um dasselbe Telefon handelt. Wenn der Benutzer hingegen das Telefon wechselt, genießt er einen neuen Zeitraum.
Lluis Felisart
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.