Creazione di un'applicazione di prova Android che scade dopo un periodo di tempo fisso


103

Ho un'applicazione che voglio lanciare sul mercato come app a pagamento. Vorrei avere un'altra versione che sarebbe una versione "di prova" con un limite di tempo diciamo, 5 giorni?

Come posso fare per farlo?


Google dovrebbe davvero supportare questo in Play Services!
polvere 366

@ powder366 in realtà Google lo supporta, vedi developer.android.com/google/play/billing/…
Yazazzello

Risposte:


186

Attualmente la maggior parte degli sviluppatori esegue questa operazione utilizzando una delle seguenti 3 tecniche.

Il primo approccio è facilmente aggirabile, la prima volta che esegui l'app salva la data / ora in un file, database o preferenze condivise e ogni volta che esegui l'app dopo quel controllo per vedere se il periodo di prova è terminato. Questo è facile da aggirare perché la disinstallazione e la reinstallazione consentiranno all'utente di avere un altro periodo di prova.

Il secondo approccio è più difficile da aggirare, ma comunque aggirabile. Usa una bomba a orologeria hardcoded. Fondamentalmente con questo approccio verrà codificata una data di fine per la prova e tutti gli utenti che scaricano e utilizzano l'app smetteranno di essere in grado di utilizzare l'app allo stesso tempo. Ho usato questo approccio perché è facile da implementare e per la maggior parte non avevo voglia di affrontare i guai della terza tecnica. Gli utenti possono aggirare questo problema modificando manualmente la data sul proprio telefono, ma la maggior parte degli utenti non si prenderà la briga di fare una cosa del genere.

La terza tecnica è l'unico modo di cui ho sentito parlare per essere veramente in grado di realizzare ciò che vuoi fare. Dovrai configurare un server, quindi ogni volta che la tua applicazione viene avviata la tua app invia l' identificatore univoco del telefono al server. Se il server non ha una voce per quell'ID telefono, ne crea una nuova e annota l'ora. Se il server ha una voce per l'ID del telefono, esegue un semplice controllo per vedere se il periodo di prova è scaduto. Quindi comunica i risultati del controllo della scadenza della prova alla tua applicazione. Questo approccio non dovrebbe essere aggirabile, ma richiede la creazione di un server web e simili.

È sempre buona norma eseguire questi controlli nel file onCreate. Se la scadenza è terminata, viene visualizzato un popup di AlertDialog con un collegamento di mercato alla versione completa dell'app. Includere solo un pulsante "OK" e, una volta che l'utente fa clic su "OK", effettuare una chiamata a "finish ()" per terminare l'attività.


2
Risposta fantastica. Come dici tu, penso che la seconda opzione sia forse la migliore. È un peccato che Google stesso non offra una sorta di sistema di licenze in quanto potrebbe incoraggiare gli sviluppatori di marchi piccoli e grandi a produrre ancora più applicazioni Android.
Tom

8
Inoltre, non controllerei durante l'avvio. Il tuo obiettivo è vendere l'app, non punire l'utente (questo è solo un bonus;) Se hai impostato il controllo ogni 2 minuti durante la corsa, lasci che l'utente inizi a fare qualcosa e poi realizzi che dovrebbe pagare. Se rendi davvero facile pagare e tornare al lavoro (non sono sicuro se puoi su Android) penso che venderai di più rispetto a controllare durante onCreate.
Whaledawg

4
@Whaledawg: è necessario eseguire il proprio server perché il server memorizza l'ID del telefono e l'ora della prima esecuzione in un database che viene quindi confrontato con il successivo.Anche quando si esegue il controllo è puramente la preferenza dello sviluppatore, ho usato l'hard bomba a orologeria codificata in un gioco con ottimi risultati. L'intera app verrà caricata, ma l'utente può interagire solo con la finestra di dialogo visualizzata, c'è un pulsante in quella finestra di dialogo che porta l'utente direttamente alla pagina di acquisto del gioco. Agli utenti non sembra importare AFAIK poiché quel gioco è stato tra le prime 10 app a pagamento dall'apertura dell'Android Market.
snctln

11
Per chiunque sia riluttante a utilizzare l'opzione 3 a causa della configurazione del server aggiuntiva, dai un'occhiata a Parse.com: è una sincronizzazione.
Joel Skrepnek

3
Cosa si intende per data di fine hardcode del periodo di prova? Ciò significa che continuerai a rilasciare nuove versioni dell'app di prova per sempre con date diverse con hardcoded in futuro?
Jasper

21

Ho sviluppato un SDK di prova Android che puoi semplicemente inserire nel tuo progetto Android Studio e si occuperà di tutta la gestione lato server per te (compresi i periodi di grazia offline).

Per usarlo, semplicemente

Aggiungi la libreria al tuo modulo principale build.gradle

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

Inizializza la libreria nel onCreate()metodo della tua attività principale

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

Aggiungi un gestore di callback:

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

};

Per avviare una versione di prova, chiama la mTrialy.startTrial("YOUR_TRIAL_SKU", mTrialyCallback); chiave dell'app e lo SKU di prova è disponibile nella dashboard per sviluppatori Trialy .


Richiede che i dati siano abilitati?
Sivaram Boina

trialy non è affidabile
Amir Dora

1
@AmirDe Ciao Amir, potresti farmi sapere cosa non funziona per te? Sono felice di aiutare, support@trialy.io Trialy funziona alla grande per oltre 1000 utenti
Nick

@ Nick non conosce il motivo per cui il mio dispositivo esegue Android Lollipop, quando imposto il giorno dalla dashboard, il giorno viene visualizzato in valore negativo dopo pochi minuti, si dice che la prova è scaduta, anche se ho ancora molti giorni nella dashboard. Ho provato anche su nougat, sembra funzionare bene su naugat. forse ha alcuni problemi di compatibilità con versioni precedenti di Android
Amir Dora

1
Utilizzo questo servizio dal 2016, funziona sempre bene. L'ho usato anche nei miei progetti ufficiali. Questa dovrebbe essere una risposta accettata.
Tariq Mahmood,

17

Questa è una vecchia domanda ma comunque, forse questo aiuterà qualcuno.

Nel caso in cui desideri seguire l' approccio più semplicistico (che fallirà se l'app viene disinstallata / reinstallata o l'utente cambia manualmente la data del dispositivo), ecco come potrebbe essere:

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

    ...
}

In realtà, se stai utilizzando SharedPreferences e Android 2.2 Froyo o versioni successive, a condizione che tu implementi le API di backup dei dati per la sincronizzazione dei dati di Google, l'utente non dovrebbe essere in grado di sfuggire a questo su dispositivi o disinstallando, solo andando su Impostazioni> Applicazioni e cancellazione dei dati. Inoltre, il metodo su Date getTimenon lo è getTimeInMillis.
Tom

Non sono d'accordo, anche questo fallirà se l'utente modifica manualmente la data del dispositivo e cosa succede se l'utente cancella i dati manualmente?
Mohammed Azharuddin Shaikh

@Caner questo è buono da controllare con sharedprefrence ma l'utente cancellerà la memoria da seting-> application manager e riutilizzerà, quindi cosa farà?
CoronaPintu

@CoronaPintu, in modo che questo approccio venga provato anche con Firebase, questo renderà la combinazione perfetta e il periodo di prova finirà anche l'app verrà disinstallata.
Noor Hossain

combina e aggiungi l'approccio con la risposta "strangetimes", questo renderà l'approccio perfetto.
Noor Hossain

10

Questa domanda e la risposta di snctln mi hanno ispirato a lavorare su una soluzione basata sul metodo 3 come mia tesi di laurea. So che lo stato attuale non è per un utilizzo produttivo, ma mi piacerebbe sapere cosa ne pensi! Usereste un tale sistema? Ti piacerebbe vederlo come un servizio cloud (non avendo problemi con la configurazione di un server)? Preoccupato per problemi di sicurezza o motivi di stabilità?

Non appena ho terminato la procedura di bachelor, voglio continuare a lavorare sul software. Quindi ora è il momento che ho bisogno del tuo feedback!

Il codice sorgente è ospitato su GitHub https://github.com/MaChristmann/mobile-trial

Alcune informazioni sul sistema: - Il sistema ha tre parti, una libreria Android, un server node.js e un configuratore per la gestione di più app di prova e account publisher / sviluppatore.

  • Supporta solo prove basate sul tempo e utilizza il tuo account (Play Store o altro) anziché un ID telefono.

  • Per la libreria Android, si basa sulla libreria di verifica delle licenze di Google Play. L'ho modificato per connettermi al server node.js e inoltre la libreria cerca di riconoscere se un utente ha cambiato la data di sistema. Inoltre, memorizza nella cache una licenza di prova recuperata nelle Preferenze condivise crittografate AES. È possibile configurare il tempo di validità della cache con il configuratore. Se un utente "cancella i dati", la libreria forza un controllo lato server.

  • Il server utilizza https e firma digitale anche per la risposta al controllo della licenza. Ha anche un'API per le app di prova CRUD e gli utenti (editore e sviluppatore). Simile alla libreria di verifica delle licenze, gli sviluppatori possono testare l'implementazione del proprio comportamento nell'app di prova con il risultato del test. Quindi nel configuratore puoi impostare esplicitamente la risposta della tua licenza su "con licenza", "non con licenza" o "errore del server".

  • Se aggiorni la tua app con una nuova funzionalità da brivido, potresti volere che tutti possano riprovare. Nel configuratore è possibile rinnovare la licenza di prova per gli utenti con licenze scadute impostando un codice di versione che dovrebbe attivarlo. Ad esempio, l'utente sta eseguendo la tua app su versioncode 3 e vuoi che provi le funzionalità di versioncode 4. Se aggiorna l'app o la reinstalla, è in grado di utilizzare di nuovo il periodo di prova completo perché il server sa su quale versione l'ha provata per ultima tempo.

  • Tutto è sotto la licenza Apache 2.0


2
Mi hai appena salvato la giornata, grazie per il duro lavoro. Ho solo pensato di scrivere la stessa soluzione di utilizzare la configurazione crittografata ottenuta una sola volta e di mantenere la chiave pubblica all'interno dell'applicazione. Quindi problema grave che posso vedere solo come si concede una licenza? Potresti comunque aver bisogno di un ID univoco che porti con un telefono per evitare più sovvenzioni.
user2305886

6

Il modo più semplice e migliore per farlo è implementare BackupSharedPreferences.

Le preferenze vengono mantenute, anche se l'app viene disinstallata e reinstallata.

Salva semplicemente la data di installazione come preferenza e sei a posto.

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

Ecco l'esempio: Android SharedPreferences Backup non funzionante


3
L'utente può disabilitare il backup nelle impostazioni di sistema.
Patrick

5

Approccio 4: utilizzare il tempo di installazione dell'applicazione.

Dal livello API 9 (Android 2.3.2, 2.3.1, Android 2.3, GINGERBREAD) ci sono firstInstallTime e lastUpdateTime in formato PackageInfo.

Per saperne di più: Come ottenere il tempo di installazione dell'app da Android


Il metodo 1 dalla risposta di snctln può essere utilizzato con questo in modo affidabile, senza essere facilmente aggirato, o ha lo stesso o un problema simile?
jwinn

Ho testato questo metodo. Il lato positivo è che funziona anche se i dati vengono cancellati. Il lato negativo è che può essere aggirato con una disinstallazione / reinstallazione.
Jean-Philippe Jodoin

posso confermare: sul mio Pixel, disinstallando e reinstallando l'app si ripristina la prima volta di installazione. ciò rende abbastanza banale per gli utenti resettare il processo
Fabian Streitel

3

Ora nella versione recente dell'abbonamento di prova gratuito di Android è stato aggiunto, puoi sbloccare tutte le funzionalità della tua app solo dopo aver acquistato l'abbonamento all'interno dell'app per un periodo di prova gratuito. Ciò consentirà all'utente di utilizzare la tua app per un periodo di prova, se l'app è ancora disinstallata dopo il periodo di prova, il denaro dell'abbonamento ti verrà trasferito. Non ho provato, ma solo condividendo un'idea.

Ecco la documentazione


2
Vorrei che funzionasse per acquisti singoli. Funziona solo per gli abbonamenti, che possono essere annuali o mensili.
jwinn

3

A mio parere, il modo migliore per farlo è semplicemente utilizzare Firebase Realtime Database:

1) Aggiungi il supporto Firebase alla tua app

2) Seleziona "Autenticazione anonima" in modo che l'utente non debba registrarsi o sapere cosa stai facendo. Questo è garantito per il collegamento all'account utente attualmente autenticato e quindi funzionerà su tutti i dispositivi.

3) Utilizza l'API del database in tempo reale per impostare un valore per "data_installazione". Al momento del lancio, recupera semplicemente questo valore e usalo.

Ho fatto lo stesso e funziona benissimo. Sono stato in grado di testarlo durante la disinstallazione / reinstallazione e il valore nel database in tempo reale rimane lo stesso. In questo modo il tuo periodo di prova funziona su più dispositivi utente. Puoi persino modificare la data di installazione in modo che l'app "reimposti" la data di prova per ogni nuova versione principale.

AGGIORNAMENTO : Dopo aver testato un po 'di più, sembra anonimo Firebase sembra allocare un ID diverso nel caso in cui tu abbia dispositivi diversi e non è garantito tra le reinstallazioni: / L'unico modo garantito è usare Firebase ma collegarlo al loro google account. Questo dovrebbe funzionare, ma richiederebbe un passaggio aggiuntivo in cui l'utente deve prima accedere / registrarsi.

Finora ho finito con un approccio leggermente meno elegante di controllare semplicemente le preferenze di backup e una data memorizzata nelle preferenze al momento dell'installazione. Questo funziona per le app incentrate sui dati in cui è inutile per una persona reinstallare l'app e reinserire tutti i dati aggiunti in precedenza, ma non funzionerebbe per un gioco semplice.


Ho gli stessi requisiti per la mia app Android e ho il mio database / server web. Gli utenti non richiedono un accesso, quindi stavo pianificando di salvare l'ID dispositivo con installed_date, funzionerebbe?
user636525

@strangetimes, penso che la tua soluzione funzioni meglio, puoi fornire maggiori informazioni? grazie
DayDayHappy

Questo thread stackoverflow.com/q/41733137/1396068 suggerisce che dopo aver reinstallato l'app si ottiene un nuovo ID utente, ovvero un nuovo periodo di prova
Fabian Streitel

3

Dopo aver esaminato tutte le opzioni in questo e in altri thread, questi sono i miei risultati

Preferenze condivise, database Può essere cancellato nelle impostazioni di Android, perso dopo la reinstallazione di un'app. Può essere eseguito il backup con il meccanismo di backup di Android e verrà ripristinato dopo una reinstallazione. Il backup potrebbe non essere sempre disponibile, sebbene dovrebbe essere sulla maggior parte dei dispositivi

Memoria esterna (scrittura su un file) Non influenzata da una cancellazione dalle impostazioni o da una reinstallazione se non si scrive nella directory privata dell'applicazione . Ma: richiede di chiedere all'utente il permesso in fase di esecuzione nelle versioni più recenti di Android, quindi questo è probabilmente fattibile solo se hai comunque bisogno di tale autorizzazione. Può anche essere eseguito il backup.

PackageInfo.firstInstallTime Viene ripristinato dopo una reinstallazione ma stabile tra gli aggiornamenti

Accedi a qualche account Non importa se è il loro account Google tramite Firebase o uno sul tuo server: la prova è vincolata all'account. La creazione di un nuovo account ripristinerà la prova.

Accesso anonimo a Firebase Puoi accedere a un utente in modo anonimo e archiviarne i dati in Firebase. Ma a quanto pare una reinstallazione dell'app e forse altri eventi non documentati potrebbero fornire all'utente un nuovo ID anonimo , ripristinando il tempo di prova. (Google stesso non fornisce molta documentazione su questo)

ANDROID_ID Potrebbe non essere disponibile e potrebbe cambiare in determinate circostanze , ad esempio il ripristino dei dati di fabbrica. Le opinioni sul fatto che sia una buona idea usarlo per identificare i dispositivi sembrano differire.

ID pubblicità di Play Può essere reimpostato dall'utente. Può essere disabilitato dall'utente disattivando il monitoraggio degli annunci.

InstanceID Reimposta su una reinstallazione . Ripristino in caso di un evento di sicurezza. Può essere ripristinato dalla tua app.

Quale (combinazione di) metodi funziona per te dipende dalla tua app e da quanto impegno pensi che il John medio dedicherà per ottenere un altro periodo di prova. Consiglierei di evitare di utilizzare solo Firebase anonimi e ID pubblicità a causa della loro instabilità. Un approccio a più fattori sembra che darà i migliori risultati. I fattori disponibili dipendono dalla tua app e dalle sue autorizzazioni.

Per la mia app ho trovato le preferenze condivise + firstInstallTime + backup delle preferenze il metodo meno invadente ma anche abbastanza efficace. Devi assicurarti di richiedere un backup solo dopo aver controllato e memorizzato l'ora di inizio della prova nelle preferenze condivise. I valori nelle preferenze condivise devono avere la precedenza su firstInstallTime. Quindi l'utente deve reinstallare l'app, eseguirla una volta e quindi cancellare i dati dell'app per ripristinare la versione di prova, il che è un bel po 'di lavoro. Sui dispositivi senza un trasporto di backup, l'utente può ripristinare la versione di prova semplicemente reinstallandola.

Ho reso disponibile questo approccio come libreria estensibile .


1

Per definizione, tutte le app Android a pagamento sul mercato possono essere valutate per 24 ore dopo l'acquisto.

C'è un pulsante "Disinstalla e rimborsa" che cambia in "Disinstalla" dopo 24 ore.

Direi che questo pulsante è troppo prominente!


17
Tieni presente che il periodo di rimborso è ora di soli 15 minuti.
Intrications

1

Mi sono imbattuto in questa domanda mentre cercavo lo stesso problema, penso che possiamo utilizzare l'API di data gratuita come http://www.timeapi.org/utc/now o qualche altra API di data per verificare la scadenza dell'app trail. in questo modo è efficiente se si desidera consegnare la demo e si è preoccupati per il pagamento e si richiede una demo di permanenza fissa. :)

trova il codice qui sotto

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

}
}

la sua soluzione di lavoro .....


certo che puoi usare, ma in quel caso solo la tua attività principale sarebbe in grado di controllare la scadenza.
RQube

0

Ecco come ho fatto la mia, ho creato 2 app una con attività di prova l'altra senza,

ho caricato quello senza attività di prova nel Play Store come app a pagamento,

e quello con attività di prova come app gratuita.

L'app gratuita al primo avvio ha opzioni per la prova e l'acquisto in negozio, se l'utente seleziona l'acquisto in negozio reindirizza al negozio per l'utente per l'acquisto, ma se l'utente fa clic su prova, lo porta all'attività di prova

NB: ho usato l'opzione 3 come @snctln ma con modifiche

in primo luogo , non dipendevo dall'ora del dispositivo, ho ottenuto il mio tempo dal file php che fa la registrazione di prova al db,

in secondo luogo , ho utilizzato il numero di serie del dispositivo per identificare in modo univoco ciascun dispositivo,

infine , l'app dipende dal valore dell'ora restituito dalla connessione al server e non dal suo tempo, quindi il sistema può essere aggirato solo se il numero di serie del dispositivo viene modificato, il che è abbastanza stressante per un utente.

quindi ecco il mio codice (per l'attività di prova):

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


}

Il mio file php ha questo aspetto (è una tecnologia 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;
        }

    }

quindi sull'attività principale utilizzo la preferenza condivisa (installDate creata in attività di prova) per monitorare il numero di giorni rimanenti e se i giorni sono trascorsi blocco l'interfaccia utente dell'attività principale con un messaggio che li porta in negozio per l'acquisto.

L'unico lato negativo che vedo qui è che se un utente Rogue acquista l'app a pagamento e decide di condividerla con app come Zender, condividere file o persino ospitare il file apk direttamente su un server affinché le persone possano scaricarlo gratuitamente. Ma sono sicuro che presto modificherò questa risposta con una soluzione o un collegamento alla soluzione.

Spero che questo salvi un'anima ... un giorno

Codifica felice ...


0

L' opzione 3 di @snctln può essere facilmente eseguita aggiungendo un file php a un server web con php e mysql installati come molti di loro hanno installato.

Dal lato Android un identificatore (l'ID del dispositivo, l'account google o quello che vuoi) viene passato come argomento nell'URL utilizzando HttpURLConnection e il php restituisce la data della prima installazione se esiste nella tabella o inserisce una nuova riga e restituisce la data corrente.

Funziona bene per me.

Se ho tempo posterò del codice!

In bocca al lupo !


aspetta ma perdi il tuo ID univoco dopo la reinstallazione dell'app? !!
Maksim Kniazev

Questo ID identifica l'hardware, il telefono stesso, l'utente non lo vede e non può modificarlo. Se reinstalla l'app, il servizio web php rileverà che si tratta dello stesso telefono. D'altra parte, se l'utente cambia il telefono, godrà di un nuovo periodo.
Lluis Felisart
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.