Crear una aplicación de prueba de Android que caduca después de un período de tiempo fijo

103

Tengo una aplicación que quiero lanzar al mercado como una aplicación de pago. Me gustaría tener otra versión que sería una versión de "prueba" con un límite de tiempo de, digamos, 5 días.

¿Cómo puedo hacer esto?

Tom
fuente
¡Google realmente debería admitir esto en Play Services!
powder366
@ powder366 en realidad Google admite esto, consulte developer.android.com/google/play/billing/…
Yazazzello

Respuestas:

186

Actualmente, la mayoría de los desarrolladores logran esto utilizando una de las siguientes 3 técnicas.

El primer enfoque se evita fácilmente, la primera vez que ejecuta la aplicación, guarde la fecha / hora en un archivo, base de datos o preferencias compartidas y cada vez que ejecute la aplicación después de eso, verifique si el período de prueba ha finalizado. Esto es fácil de eludir porque desinstalar y reinstalar le permitirá al usuario tener otro período de prueba.

El segundo enfoque es más difícil de eludir, pero aún así eludible. Utilice una bomba de tiempo codificada de forma rígida. Básicamente, con este enfoque, se codificará una fecha de finalización para la prueba, y todos los usuarios que descarguen y usen la aplicación dejarán de poder usar la aplicación al mismo tiempo. He utilizado este enfoque porque es fácil de implementar y, en su mayor parte, simplemente no tenía ganas de pasar por el problema de la tercera técnica. Los usuarios pueden eludir esto cambiando manualmente la fecha en su teléfono, pero la mayoría de los usuarios no se tomarán la molestia de hacer tal cosa.

La tercera técnica es la única forma que he escuchado de poder realmente lograr lo que quieres hacer. Tendrá que configurar un servidor, y luego, cada vez que se inicia su aplicación, su aplicación envía el identificador único del teléfono al servidor. Si el servidor no tiene una entrada para esa identificación de teléfono, entonces crea una nueva y anota la hora. Si el servidor tiene una entrada para la identificación del teléfono, entonces hace una simple verificación para ver si el período de prueba ha expirado. Luego, comunica los resultados de la verificación de vencimiento de la prueba a su aplicación. Este enfoque no debe ser evitable, pero requiere la configuración de un servidor web y demás.

Siempre es una buena práctica realizar estas comprobaciones en onCreate. Si el vencimiento ha finalizado, aparecerá un AlertDialog con un enlace de mercado a la versión completa de la aplicación. Solo incluya un botón "Aceptar" y, una vez que el usuario haga clic en "Aceptar", haga una llamada a "finalizar ()" para finalizar la actividad.

snctln
fuente
2
Fantástica respuesta. Como dices, creo que la segunda opción es posiblemente la mejor. Es una pena que Google no ofrezca algún tipo de sistema de licencias, ya que puede alentar a los desarrolladores de marcas pequeñas y grandes a producir aún más aplicaciones de Android.
Tom
8
Además, no lo comprobaría durante el inicio. Su objetivo es vender la aplicación, no castigar al usuario (eso es solo una bonificación;) Si lo tiene configurado para verificar cada 2 minutos mientras se ejecuta, deja que el usuario comience a hacer algo y luego se dé cuenta de que debe pagar. Si hace que sea realmente fácil pagar y volver al trabajo (no estoy seguro de si puede hacerlo en Android), creo que venderá más que verificar durante onCreate.
Whaledawg
4
@Whaledawg: Necesita ejecutar su propio servidor porque el servidor está almacenando la identificación del teléfono y la hora de la primera ejecución en una base de datos que luego se compara con más tarde. bomba de tiempo codificada en un juego con grandes resultados. Se cargaría toda la aplicación, pero el usuario solo puede interactuar con el diálogo que se ve, hay un botón en ese diálogo que lleva al usuario directamente a la página de compra del juego. A los usuarios no parece importarles AFAIK ya que ese juego ha estado entre las 10 mejores aplicaciones pagas desde la apertura del Android Market.
snctln
11
Para cualquiera que sea reacio a usar la opción 3 debido a la configuración adicional del servidor, eche un vistazo a Parse.com - es una sincronización.
Joel Skrepnek
3
¿Qué se entiende por fecha de finalización de prueba en código rígido? ¿Eso significa que seguirá lanzando nuevas versiones de la aplicación de prueba para siempre con diferentes fechas codificadas en el futuro?
Jasper
21

Desarrollé un SDK de prueba de Android que simplemente puede colocar en su proyecto de Android Studio y se encargará de toda la administración del lado del servidor por usted (incluidos los períodos de gracia fuera de línea).

Para usarlo, simplemente

Agregue la biblioteca a su módulo principal build.gradle

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

Inicialice la biblioteca en el onCreate()método de su actividad principal

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

Agregue un controlador de devolución de llamada:

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

};

Para iniciar una prueba, llame a la mTrialy.startTrial("YOUR_TRIAL_SKU", mTrialyCallback); clave de su aplicación y encontrará el SKU de prueba en el panel de control del desarrollador de Trialy .

Mella
fuente
¿Requiere que los datos estén habilitados?
Sivaram Boina
Trialy no es confiable
Amir Dora
1
@AmirDe Hola Amir, ¿podrías decirme qué no te está funcionando? Me complace ayudar, [email protected] Trialy está funcionando muy bien para más de 1000 usuarios
Nick
@Nick no sabe la razón por la que mi dispositivo está ejecutando Android Lollipop, cuando configuro el día desde el tablero, luego el día se muestra en valor negativo después de unos minutos, dice que la prueba expiró, aunque todavía tengo muchos días en el tablero. También probé en un dispositivo de turrón, parece que funciona bien en naugat. tal vez tenga algunos problemas de compatibilidad de versiones anteriores de Android
Amir Dora
1
Estoy usando este servicio desde 2016, funciona bien todo el tiempo. También utilicé esto en mis proyectos oficiales. Esta debe ser una respuesta aceptada.
Tariq Mahmood
17

Esta es una vieja pregunta, pero de todos modos, tal vez esto ayude a alguien.

En caso de que desee optar por el enfoque más simplista (que fallará si la aplicación se desinstala / reinstala o el usuario cambia la fecha del dispositivo manualmente), así es como podría ser:

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
fuente
Bueno, en realidad, si está utilizando SharedPreferences y Android 2.2 Froyo o superior, siempre que implemente las API de copia de seguridad de datos para la sincronización de datos de Google, el usuario no debería poder escapar de esto en todos los dispositivos o desinstalando, solo yendo a Configuración> Aplicaciones y borrar datos. Además, el método de Date getTimeno lo es getTimeInMillis.
Tom
No estoy de acuerdo, esto también fallará si el usuario cambia la fecha del dispositivo manualmente y también ¿qué pasa si el usuario borra los datos manualmente?
Mohammed Azharuddin Shaikh
@Caner, esto es bueno para verificar con preferencia compartida, pero el usuario borrará la memoria de la configuración-> administrador de aplicaciones y reutilizará, ¿qué hará?
CoronaPintu
@CoronaPintu, de modo que este enfoque también se probará con firebase, esto hará la combinación perfecta, y el período de prueba terminará incluso la aplicación se desinstalará.
Noor Hossain
Combine y agregue el enfoque con la respuesta "tiempos extraños", esto hará que el enfoque sea perfecto.
Noor Hossain
10

Esta pregunta y la respuesta de snctln me inspiraron a trabajar en una solución basada en el método 3 como mi tesis de licenciatura. Sé que el estado actual no es para uso productivo, ¡pero me encantaría saber lo que piensas al respecto! ¿Usarías un sistema así? ¿Le gustaría verlo como un servicio en la nube (sin problemas para configurar un servidor)? ¿Preocupado por problemas de seguridad o razones de estabilidad?

Tan pronto como termine el trámite de licenciatura quiero seguir trabajando en el software. ¡Así que ahora es el momento de que necesito tus comentarios!

El código fuente está alojado en GitHub https://github.com/MaChristmann/mobile-trial

Alguna información sobre el sistema: - El sistema tiene tres partes, una biblioteca de Android, un servidor node.js y un configurador para administrar múltiples aplicaciones de prueba y cuentas de editor / desarrollador.

  • Solo admite pruebas basadas en el tiempo y usa su cuenta (Play Store u otra) en lugar de una identificación de teléfono.

  • Para la biblioteca de Android, se basa en la biblioteca de verificación de licencias de Google Play. Lo modifiqué para conectarme al servidor node.js y, además, la biblioteca intenta reconocer si un usuario cambió la fecha del sistema. También almacena en caché una licencia de prueba recuperada en Preferencias compartidas cifradas con AES. Puede configurar la hora válida de la caché con el configurador. Si un usuario "borra datos", la biblioteca forzará una verificación del lado del servidor.

  • El servidor utiliza https y también firma digitalmente la respuesta de verificación de licencia. También tiene una API para aplicaciones y usuarios de prueba de CRUD (editor y desarrollador). De manera similar a la biblioteca de verificación de licencias, los desarrolladores pueden probar la implementación de su comportamiento en la aplicación de prueba con el resultado de la prueba. Por lo tanto, en el configurador puede establecer explícitamente la respuesta de su licencia en "con licencia", "sin licencia" o "error del servidor".

  • Si actualizas tu aplicación con una nueva característica espectacular, es posible que desees que todos puedan volver a intentarlo. En el configurador, puede renovar la licencia de prueba para usuarios con licencias vencidas configurando un código de versión que debería activarlo. Por ejemplo, el usuario está ejecutando su aplicación en el código de versión 3 y desea que pruebe las funciones del código de versión 4. Si actualiza la aplicación o la reinstala, puede usar el período de prueba completo nuevamente porque el servidor sabe en qué versión la probó por última vez. hora.

  • Todo está bajo la licencia Apache 2.0

Martin Christmann
fuente
2
Me acaba de salvar el día, gracias por su arduo trabajo. Solo pensé en escribir la misma solución de usar la configuración cifrada obtenida solo una vez y mantener la clave pública dentro de la aplicación. Entonces, el gran problema que puedo ver solo es ¿cómo se otorga una licencia? Es posible que aún necesite una identificación única con un teléfono para evitar múltiples concesiones.
user2305886
6

El mas facil y mejor forma de hacerlo es implementar BackupSharedPreferences.

Las preferencias se conservan, incluso si la aplicación se desinstala y se vuelve a instalar.

Simplemente guarde la fecha de instalación como preferencia y estará listo.

Aquí está la teoría: http://developer.android.com/reference/android/app/backup/SharedPreferencesBackupHelper.html

Aquí está el ejemplo: La copia de seguridad de preferencias compartidas de Android no funciona

pstorli
fuente
3
El usuario puede deshabilitar la copia de seguridad en la configuración del sistema.
Patrick
5

Método 4: utilice el tiempo de instalación de la aplicación.

Desde el nivel de API 9 (Android 2.3.2, 2.3.1, Android 2.3, GINGERBREAD) hay firstInstallTime y lastUpdateTime en PackageInfo.

Para leer más: Cómo obtener el tiempo de instalación de la aplicación desde Android

18446744073709551615
fuente
¿Se puede usar el método 1 de la respuesta de snctln con esto de manera confiable, sin ser eludido fácilmente, o tiene el mismo problema o uno similar?
jwinn
He probado este método. El lado bueno es que funciona incluso si se borran los datos. El lado malo es que puede evitarse mediante una desinstalación / reinstalación.
Jean-Philippe Jodoin
puedo confirmar: en mi Pixel, desinstalar y reinstalar la aplicación se restablece la primera vez que se instala. eso hace que sea bastante trivial para los usuarios restablecer la prueba
Fabian Streitel
3

Ahora, en la versión reciente de la suscripción de prueba gratuita de Android que se ha agregado, puede desbloquear todas las funciones de su aplicación solo después de comprar la suscripción dentro de la aplicación para un período de prueba gratuito. Esto permitirá al usuario usar su aplicación durante un período de prueba, si la aplicación aún se desinstala después del período de prueba, se le transferirá el dinero de la suscripción. No lo he intentado, solo comparto una idea.

Aquí está la documentación

Vins
fuente
2
Ojalá esto funcionara para compras individuales. Solo funciona para suscripciones, que pueden ser anuales o mensuales.
jwinn
3

En mi opinión, la mejor manera de hacer esto es simplemente usar Firebase Realtime Database:

1) Agregue soporte de Firebase a su aplicación

2) Seleccione 'Autenticación anónima' para que el usuario no tenga que registrarse o incluso saber lo que está haciendo. Se garantiza que esto se vinculará a la cuenta de usuario actualmente autenticado y, por lo tanto, funcionará en todos los dispositivos.

3) Utilice la API de Realtime Database para establecer un valor para 'installed_date'. En el momento del lanzamiento, simplemente recupere este valor y utilícelo.

Yo he hecho lo mismo y funciona muy bien. Pude probar esto a través de desinstalaciones / reinstalaciones y el valor en la base de datos en tiempo real sigue siendo el mismo. De esta manera, su período de prueba funciona en varios dispositivos de usuario. Incluso puede versionar su install_date para que la aplicación 'restablezca' la fecha de prueba para cada nueva versión principal.

ACTUALIZAR : Después de probar un poco más, parece que Firebase anónimo parece asignar una ID diferente en caso de que tenga diferentes dispositivos y no está garantizado entre reinstalaciones: / La única forma garantizada es usar Firebase pero vincularlo a su google cuenta. Esto debería funcionar, pero requeriría un paso adicional donde el usuario primero necesita iniciar sesión / registrarse.

Hasta ahora, he terminado con un enfoque un poco menos elegante de simplemente verificar las preferencias respaldadas y una fecha almacenada en las preferencias al instalar. Esto funciona para aplicaciones centradas en datos donde no tiene sentido que una persona vuelva a instalar la aplicación y vuelva a ingresar todos los datos agregados anteriormente, pero no funcionaría para un juego simple.

tiempos extraños
fuente
Tengo el mismo requisito para mi aplicación de Android y tengo mi propia base de datos / servidor web. Los usuarios no requieren un inicio de sesión, así que estaba planeando guardar la ID del dispositivo con la fecha de instalación, ¿funcionaría?
user636525
@strangetimes, creo que tu solución funciona mejor, ¿puedes proporcionar más información? gracias
DayDayHappy
Este hilo stackoverflow.com/q/41733137/1396068 sugiere que después de reinstalar la aplicación obtienes una nueva ID de usuario, es decir, un nuevo período de prueba
Fabian Streitel
3

Después de ver todas las opciones en este y otros hilos, estos son mis hallazgos.

Preferencias compartidas, base de datos Puede borrarse en la configuración de Android, perderse después de reinstalar una aplicación. Se puede hacer una copia de seguridad con el mecanismo de copia de seguridad de Android y se restaurará después de una reinstalación. Es posible que la copia de seguridad no siempre esté disponible, aunque debería estar en la mayoría de los dispositivos

Almacenamiento externo (escribir en un archivo) No se ve afectado por un borrado de la configuración o una reinstalación si no escribimos en el directorio privado de la aplicación . Pero: requiere que le pidas permiso al usuario en tiempo de ejecución en las versiones más nuevas de Android, por lo que probablemente solo sea factible si necesitas ese permiso de todos modos. También se puede hacer una copia de seguridad.

PackageInfo.firstInstallTime se restablece después de una reinstalación pero es estable en todas las actualizaciones

Iniciar sesión en alguna cuenta No importa si es su cuenta de Google a través de Firebase o una en su propio servidor: la versión de prueba está vinculada a la cuenta. Crear una nueva cuenta restablecerá la prueba.

Inicio de sesión anónimo en Firebase Puede iniciar sesión con un usuario de forma anónima y almacenar sus datos en Firebase. Pero aparentemente una reinstalación de la aplicación y tal vez otros eventos indocumentados pueden darle al usuario una nueva identificación anónima , restableciendo su tiempo de prueba. (Google mismo no proporciona mucha documentación sobre esto)

ANDROID_ID Puede no estar disponible y puede cambiar bajo ciertas circunstancias , por ejemplo, restablecimiento de fábrica. Las opiniones sobre si es una buena idea usar esto para identificar dispositivos parecen diferir.

Play Advertising ID Puede ser restablecido por el usuario. El usuario puede inhabilitarlo si se excluye del seguimiento de anuncios.

InstanceID Reset en una reinstalación . Reiniciar en caso de un evento de seguridad. Puede ser restablecido por su aplicación.

Qué (combinación de) métodos funcionan para usted depende de su aplicación y de cuánto esfuerzo cree que el John promedio pondrá para obtener otro período de prueba. Recomendaría evitar el uso solamente el anonimato Firebase y el ID de publicidad debido a su inestabilidad. Un enfoque multifactorial parece que dará los mejores resultados. Los factores que están disponibles para usted dependen de su aplicación y sus permisos.

Para mi propia aplicación, encontré que las preferencias compartidas + firstInstallTime + copia de seguridad de las preferencias es el método menos intrusivo pero también lo suficientemente efectivo. Debe asegurarse de solicitar una copia de seguridad solo después de verificar y almacenar la hora de inicio de la prueba en las preferencias compartidas. Los valores de las preferencias compartidas deben tener prioridad sobre firstInstallTime. Luego, el usuario tiene que reinstalar la aplicación, ejecutarla una vez y luego borrar los datos de la aplicación para restablecer la versión de prueba, que es bastante trabajo. Sin embargo, en dispositivos sin un transporte de respaldo, el usuario puede restablecer la versión de prueba simplemente reinstalando.

He hecho que ese enfoque esté disponible como una biblioteca extensible .

Fabián Streitel
fuente
1

Por definición, todas las aplicaciones de pago de Android en el mercado se pueden evaluar durante 24 horas después de la compra.

Hay un botón 'Desinstalar y reembolsar' que cambia a 'Desinstalar' después de 24 horas.

¡Yo diría que este botón es demasiado prominente!

AlexJReid
fuente
17
Tenga en cuenta que el período de reembolso ahora es de solo 15 minutos.
Intricaciones
1

Me encuentro con esta pregunta mientras busco el mismo problema, creo que podemos utilizar la API de fecha gratuita como http://www.timeapi.org/utc/now o alguna otra API de fecha para verificar la caducidad de la aplicación de seguimiento. de esta manera es eficiente si desea entregar la demostración y está preocupado por el pago y requiere una demostración de tenencia fija. :)

encuentra el código a continuación

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

}
}

su solución de trabajo .....

RQube
fuente
seguro que puede usarlo, pero en ese caso solo su actividad principal podría verificar la caducidad.
RQube
0

Así es como hice el mío, creé 2 aplicaciones, una con actividad de prueba y la otra sin,

Cargué el que no tenía actividad de prueba en Play Store como aplicación paga,

y el que tiene actividad de prueba como aplicación gratuita.

La aplicación gratuita en el primer lanzamiento tiene opciones de prueba y compra en la tienda, si el usuario selecciona la compra en la tienda, se redirige a la tienda para que el usuario compre, pero si el usuario hace clic en prueba, lo lleva a la actividad de prueba.

NB: utilicé la opción 3 como @snctln pero con modificaciones

Primero , no dependí de la hora del dispositivo, obtuve mi tiempo del archivo php que hace el registro de prueba en la base de datos,

en segundo lugar , utilicé el número de serie del dispositivo para identificar de forma única cada dispositivo,

Por último , la aplicación depende del valor de tiempo devuelto por la conexión del servidor, no de su propio tiempo, por lo que el sistema solo se puede eludir si se cambia el número de serie del dispositivo, lo cual es bastante estresante para el usuario.

así que aquí va mi código (para la actividad de prueba):

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


}

Mi archivo php se ve así (es una tecnología 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;
        }

    }

luego, en la actividad principal, uso la preferencia compartida (installDate creada en la actividad de prueba) para monitorear la cantidad de días restantes y, si los días terminaron, bloqueo la interfaz de usuario de la actividad principal con un mensaje que los lleva a la tienda para comprar.

El único inconveniente que veo aquí es que si un usuario de Rogue compra la aplicación paga y decide compartirla con aplicaciones como Zender, compartir archivos o incluso alojar el archivo apk directamente en un servidor para que la gente lo descargue gratis. Pero estoy seguro de que pronto editaré esta respuesta con una solución a eso o un enlace a la solución.

Espero que esto salve un alma ... algún día

Codificación feliz ...

El muerto
fuente
0

@snctln opción 3 de puede hacer fácilmente agregando un archivo php a un servidor web con php y mysql instalados como muchos de ellos tienen.

Desde el lado de Android, se pasa un identificador (la ID del dispositivo, la cuenta de Google o lo que quieras) como argumento en la URL usando HttpURLConnection y el php devuelve la fecha de la primera instalación si existe en la tabla o inserta una nueva fila y devuelve la fecha actual.

Funciona bien para mí.

¡Si tengo tiempo, publicaré algún código!

Buena suerte !

Lluis Felisart
fuente
espera, pero ¿pierdes tu identificación única después de reinstalar la aplicación?
Maksim Kniazev
Esta ID identifica el hardware, el teléfono en sí, el usuario no lo ve y no puede cambiarlo. Si reinstala la aplicación, el servicio web php detectará que es el mismo teléfono. Por otro lado, si el usuario cambia de teléfono disfrutará de un nuevo período.
Lluis Felisart