Criação de um aplicativo de teste Android que expira após um período de tempo fixo

103

Eu tenho um aplicativo que desejo chegar ao mercado como um aplicativo pago. Eu gostaria de ter outra versão que seria uma versão de "teste" com um limite de tempo de, digamos, 5 dias?

Como posso fazer isso?

Tom
fonte
O Google deveria realmente apoiar isso no Play Services!
powder366
@ powder366 na verdade, o Google oferece suporte para isso, consulte developer.android.com/google/play/billing/…
Yazazzello

Respostas:

186

Atualmente, a maioria dos desenvolvedores faz isso usando uma das três técnicas a seguir.

A primeira abordagem é facilmente contornada: na primeira vez que você executar o aplicativo, salve a data / hora em um arquivo, banco de dados ou preferências compartilhadas e sempre que executar o aplicativo depois disso, verifique se o período de teste terminou. Isso é fácil de contornar porque desinstalar e reinstalar permitirá que o usuário tenha outro período de teste.

A segunda abordagem é mais difícil de contornar, mas ainda assim contornável. Use uma bomba-relógio codificada. Basicamente, com essa abordagem, você codificará uma data de término para o teste e todos os usuários que fizerem o download e usarem o aplicativo deixarão de poder usá-lo ao mesmo tempo. Usei essa abordagem porque é fácil de implementar e, na maior parte do tempo, simplesmente não estava com vontade de passar pelos problemas da terceira técnica. Os usuários podem contornar isso alterando manualmente a data em seus telefones, mas a maioria dos usuários não terá o trabalho de fazer isso.

A terceira técnica é a única maneira sobre a qual ouvi falar para realmente conseguir realizar o que você deseja. Você terá que configurar um servidor e, em seguida, sempre que seu aplicativo for iniciado, ele enviará o identificador exclusivo do telefone para o servidor. Se o servidor não tiver uma entrada para esse id de telefone, ele cria um novo e anota a hora. Se o servidor não tiver uma entrada para a identificação do telefone, ele fará uma verificação simples para ver se o período de teste expirou. Em seguida, ele comunica os resultados da verificação de expiração do teste ao seu aplicativo. Esta abordagem não deve ser contornável, mas requer a configuração de um servidor web e tal.

É sempre uma boa prática fazer essas verificações no onCreate. Se a expiração terminou, aparecerá um AlertDialog com um link de mercado para a versão completa do aplicativo. Inclua apenas um botão "OK" e, quando o usuário clicar em "OK", faça uma chamada para "terminar ()" para encerrar a atividade.

snctln
fonte
2
Resposta fantástica. Como você disse, acho que a segunda opção é possivelmente a melhor. É uma pena que o próprio Google não ofereça algum tipo de sistema de licenciamento, pois pode encorajar desenvolvedores de marcas pequenas e grandes a produzir ainda mais aplicativos Android.
Tom
8
Além disso, eu não iria verificar durante a inicialização. Seu objetivo é vender o aplicativo, não punir o usuário (isso é apenas um bônus;) Se você configurou para verificar a cada 2 minutos durante a execução, você permite que o usuário comece a fazer algo e então percebe que deve pagar. Se você tornar realmente fácil de pagar e voltar ao trabalho (não tenho certeza se você conseguirá no Android), acho que você venderá mais do que verificar durante onCreate.
Whaledawg de
4
@Whaledawg: Você precisa executar seu próprio servidor porque o servidor está armazenando a id do telefone e a hora da primeira execução em um banco de dados que é então comparado com o posterior. Além disso, quando você faz a verificação é puramente preferência do desenvolvedor, usei o disco rígido bomba-relógio codificada em um jogo com ótimos resultados. Todo o aplicativo carregaria, mas o usuário só pode interagir com a caixa de diálogo que é vista, há um botão nessa caixa de diálogo que leva o usuário diretamente para a página de compra do jogo. Os usuários não parecem se importar com o AFAIK, já que o jogo está entre os 10 principais aplicativos pagos desde a abertura do Android Market.
snctln
11
Para qualquer pessoa relutante em usar a opção 3 por causa da configuração do servidor adicional, dê uma olhada em Parse.com - é uma sincronização.
Joel Skrepnek de
3
O que significa data de término do teste em código fixo? Isso significa que você continuará lançando novas versões do aplicativo de teste para sempre com diferentes datas codificadas no futuro?
Jasper
21

Eu desenvolvi um Android Trial SDK que você pode simplesmente colocar em seu projeto Android Studio e ele cuidará de todo o gerenciamento do lado do servidor para você (incluindo períodos de carência offline).

Para usá-lo, basta

Adicione a biblioteca ao seu módulo principal build.gradle

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

Inicialize a biblioteca no onCreate()método da sua atividade 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);
}

Adicione um gerenciador de retorno de chamada:

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 um teste, chame mTrialy.startTrial("YOUR_TRIAL_SKU", mTrialyCallback); sua chave de aplicativo e o SKU de teste pode ser encontrado no painel do desenvolvedor Trialy .

usuario
fonte
Requer que os dados sejam ativados?
Sivaram Boina
trialy não é confiável
Amir Dora
1
@AmirDe Oi Amir, você poderia me dizer o que não está funcionando para você? Fico feliz em ajudar, [email protected] Trialy está funcionando muito bem para mais de 1000 usuários
Nick
@Nick não sei o motivo pelo qual meu dispositivo está executando o android lollipop, quando defino o dia no painel, o dia mostra um valor negativo após alguns minutos, diz que o teste expirou, embora eu tenha muitos dias ainda no painel. Eu também testei no dispositivo nougat, parece estar funcionando bem no naugat. talvez tenha alguns problemas de compatibilidade de versão mais antiga do Android
Amir Dora
1
Estou usando este serviço desde 2016, ele sempre funciona bem. Eu usei isso também em meus projetos oficiais. Esta deve ser a resposta aceita.
Tariq Mahmood
17

Essa é uma questão antiga, mas de qualquer forma, talvez isso ajude alguém.

No caso de você querer ir com a abordagem mais simplista (que irá falhar se o aplicativo for desinstalado / reinstalado ou o usuário alterar a data do dispositivo manualmente), pode ser assim:

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
fonte
Bem, na verdade, se você estiver usando SharedPreferences e Android 2.2 Froyo ou superior, contanto que você implemente as APIs de backup de dados para sincronização de dados do Google, o usuário não deve ser capaz de escapar disso em todos os dispositivos ou desinstalando, apenas acessando Configurações> Aplicativos e limpar dados. Além disso, o método em Date getTimenão é getTimeInMillis.
Tom,
Discordar, isso também falhará se o usuário alterar a data do dispositivo manualmente e também se o usuário limpar os dados manualmente?
Mohammed Azharuddin Shaikh
@Caner é bom verificar isso com a preferência compartilhada, mas o usuário limpará a memória do gerenciador de configuração-> aplicação e reutilizará, então o que fará?
CoronaPintu
@CoronaPintu, para que esta abordagem também tente com o firebase, isso fará a combinação perfeita, e o período de teste terminará até mesmo o aplicativo será desinstalado.
Noor Hossain
Combine e adicione a abordagem com a resposta "tempos estranhos", isso tornará a abordagem perfeita.
Noor Hossain
10

Essa pergunta e a resposta do snctln me inspiraram a trabalhar em uma solução baseada no método 3 como minha tese de bacharelado. Sei que o status atual não é para uso produtivo, mas adoraria saber o que você acha disso! Você usaria tal sistema? Você gostaria de vê-lo como um serviço em nuvem (sem problemas para configurar um servidor)? Preocupado com questões de segurança ou motivos de estabilidade?

Assim que terminar o procedimento de bacharelado, quero continuar trabalhando no software. Agora é a hora de eu precisar de seus comentários!

Sourcecode está hospedado no GitHub https://github.com/MaChristmann/mobile-trial

Algumas informações sobre o sistema: - O sistema tem três partes, uma biblioteca Android, um servidor node.js e um configurador para gerenciar vários aplicativos de teste e contas de editor / desenvolvedor.

  • Ele só oferece suporte a testes baseados em tempo e usa sua conta (da Play Store ou outra) em vez de um ID de telefone.

  • Para a biblioteca Android, é baseado na biblioteca de verificação de licenciamento do Google Play. Eu o modifiquei para se conectar ao servidor node.js e, além disso, a biblioteca tenta reconhecer se um usuário alterou a data do sistema. Ele também armazena em cache uma licença de teste recuperada em Preferências compartilhadas criptografadas AES. Você pode configurar o tempo de validade do cache com o configurador. Se um usuário "limpar os dados", a biblioteca forçará uma verificação do lado do servidor.

  • O servidor está usando https e também assinando digitalmente a resposta de verificação de licença. Ele também tem uma API para aplicativos e usuários de teste CRUD (editor e desenvolvedor). Semelhante ao licenciamento, os desenvolvedores da Verfication Library podem testar a implementação de seu comportamento no aplicativo de teste com o resultado do teste. Assim, no configurador, você pode definir explicitamente a resposta da licença como "licenciada", "não licenciada" ou "erro de servidor".

  • Se você atualizar seu aplicativo com um novo recurso arrasador, convém que todos possam tentar novamente. No configurador, você pode renovar a licença de teste para usuários com licenças expiradas, definindo um código de versão que deve acionar isso. Por exemplo, o usuário está executando seu aplicativo no código de versão 3 e você deseja que ele experimente os recursos do código de versão 4. Se ele atualizar o aplicativo ou reinstalá-lo, ele poderá usar o período de teste completo novamente porque o servidor sabe em qual versão ele tentou pela última vez Tempo.

  • Tudo está sob a licença Apache 2.0

Martin Christmann
fonte
2
Você acabou de salvar meu dia, obrigado pelo trabalho duro. Eu apenas pensei em escrever a mesma solução de usar a configuração criptografada obtida apenas uma vez e manter a chave pública dentro do aplicativo. Então, o problema principal, só consigo ver como você concede uma licença? Você ainda pode precisar de alguma identidade exclusiva carregando um telefone para evitar várias concessões.
user2305886
6

A maneira mais fácil e melhor de fazer isso é implementar BackupSharedPreferences.

As preferências são preservadas, mesmo se o aplicativo for desinstalado e reinstalado.

Basta salvar a data de instalação de preferência e pronto.

Esta é a teoria: http://developer.android.com/reference/android/app/backup/SharedPreferencesBackupHelper.html

Este é o exemplo: Android SharedPreferences Backup Not Working

Pstorli
fonte
3
O usuário pode desabilitar o backup nas configurações do sistema.
Patrick
5

Abordagem 4: use o tempo de instalação do aplicativo.

Desde nível API 9 (Android 2.3.2, 2.3.1, Android 2.3, Gingerbread) existem firstInstallTime e lastUpdateTime no PackageInfo.

Para ler mais: Como obter o tempo de instalação do aplicativo no Android

18446744073709551615
fonte
O método 1 da resposta de snctln pode ser usado com isso de forma confiável, sem ser facilmente contornado, ou tem o mesmo problema ou um problema semelhante?
jwinn
Eu testei esse método. O lado bom é que funciona mesmo se os dados forem apagados. O lado ruim é que ele pode ser contornado por uma desinstalação / reinstalação.
Jean-Philippe Jodoin
pode confirmar: no meu Pixel, desinstalar e reinstalar o aplicativo redefine na primeira instalação. isso torna bastante trivial para os usuários redefinir o teste
Fabian Streitel
3

Agora que a versão recente da assinatura de teste gratuita do Android foi adicionada, você pode desbloquear todos os recursos do seu aplicativo somente após comprar a assinatura dentro do aplicativo por um período de teste gratuito. Isso permitirá que o usuário use seu aplicativo por um período de avaliação. Se o aplicativo ainda for desinstalado após o período de avaliação, o dinheiro da assinatura será transferido para você. Ainda não tentei, mas apenas compartilhando uma ideia.

Aqui está a documentação

Vins
fonte
2
Eu gostaria que isso funcionasse para compras individuais. Só funciona para assinaturas, que podem ser anuais ou mensais.
jwinn
3

Em minha opinião, a melhor maneira de fazer isso é simplesmente usar o Firebase Realtime Database:

1) Adicione suporte do Firebase ao seu aplicativo

2) Selecione 'Autenticação anônima' para que o usuário não precise se inscrever ou mesmo saber o que está fazendo. Isso garante um link para a conta do usuário autenticado no momento e, portanto, funcionará em todos os dispositivos.

3) Use a API Realtime Database para definir um valor para 'installed_date'. No momento da inicialização, basta recuperar esse valor e usá-lo.

Eu fiz o mesmo e funciona muito bem. Consegui testar isso na desinstalação / reinstalação e o valor no banco de dados em tempo real permanece o mesmo. Desta forma, seu período de teste funciona em vários dispositivos de usuário. Você pode até criar uma versão de install_date para que o aplicativo 'redefina' a data de teste para cada novo lançamento principal.

ATUALIZAÇÃO : depois de testar um pouco mais, parece que o Firebase anônimo aloca um ID diferente no caso de você ter dispositivos diferentes e não é garantido entre as reinstalações: / A única maneira garantida é usar o Firebase, mas vinculá-lo ao Google conta. Isso deve funcionar, mas exigiria uma etapa extra em que o usuário primeiro precisa fazer o login / inscrição.

Até agora, acabei com uma abordagem um pouco menos elegante de simplesmente verificar as preferências de backup e uma data armazenada nas preferências durante a instalação. Isso funciona para aplicativos centrados em dados, onde é inútil para uma pessoa reinstalar o aplicativo e inserir novamente todos os dados adicionados anteriormente, mas não funcionaria para um jogo simples.

tempos estranhos
fonte
Eu tenho o mesmo requisito para meu aplicativo Android e tenho meu próprio banco de dados / servidor da web. Os usuários não requerem um login, então eu estava planejando salvar o ID do dispositivo com a data_instalada, isso funcionaria?
user636525
@strangetimes, acho que sua solução funciona melhor. Você pode fornecer mais informações? obrigado
DayDayHappy
Este thread stackoverflow.com/q/41733137/1396068 sugere que depois de reinstalar o aplicativo você obtenha um novo ID de usuário, ou seja, um novo período de teste
Fabian Streitel
3

Depois de examinar todas as opções neste e em outros tópicos, estas são minhas conclusões

Preferências compartilhadas, banco de dados Podem ser apagadas nas configurações do Android, perdidas após a reinstalação de um aplicativo. Pode ser feito backup com o mecanismo de backup do Android e será restaurado após a reinstalação. O backup pode nem sempre estar disponível, embora deva estar na maioria dos dispositivos

Armazenamento externo (gravação em um arquivo) Não afetado por uma limpeza das configurações ou uma reinstalação se não gravarmos no diretório privado do aplicativo . Mas: requer que você peça permissão ao usuário no tempo de execução nas versões mais recentes do Android, então isso provavelmente só é viável se você precisar dessa permissão de qualquer maneira. Também pode ser feito backup.

PackageInfo.firstInstallTime é redefinido após a reinstalação, mas estável entre as atualizações

Faça login em alguma conta Não importa se é sua conta do Google via Firebase ou uma em seu próprio servidor: o teste está vinculado à conta. Criar uma nova conta reiniciará o teste.

Login anônimo do Firebase Você pode fazer login de um usuário anonimamente e armazenar dados para ele no Firebase. Mas, aparentemente, uma reinstalação do aplicativo e talvez outros eventos não documentados podem dar ao usuário uma nova ID anônima , reiniciando seu tempo de teste. (O próprio Google não fornece muita documentação sobre isso)

ANDROID_ID Pode não estar disponível e pode mudar sob certas circunstâncias , por exemplo, redefinição de fábrica. As opiniões sobre se é uma boa ideia usar isso para identificar dispositivos parecem divergentes.

O ID de publicidade do Google Play pode ser redefinido pelo usuário. Pode ser desativado pelo usuário optando por sair do rastreamento de anúncios.

InstanceID Redefinir em uma reinstalação . Reinicialize no caso de um evento de segurança. Pode ser redefinido pelo seu aplicativo.

Quais (combinações de) métodos funcionam para você depende do seu aplicativo e de quanto esforço você acha que o John médio fará para obter outro período de teste. Eu recomendaria evitar o uso de apenas Firebase anônimo e ID de publicidade devido à instabilidade deles. Uma abordagem multifatorial parece produzir os melhores resultados. Os fatores disponíveis para você dependem de seu aplicativo e de suas permissões.

Para meu próprio aplicativo, descobri que as preferências compartilhadas + firstInstallTime + backup das preferências são o método menos intrusivo, mas também eficaz o suficiente. Você deve certificar-se de solicitar um backup apenas após verificar e armazenar a hora de início do teste nas preferências compartilhadas. Os valores nos Prefs compartilhados devem ter precedência sobre o firstInstallTime. Em seguida, o usuário tem que reinstalar o aplicativo, executá-lo uma vez e, em seguida, limpar os dados do aplicativo para redefinir o teste, o que é muito trabalhoso. Em dispositivos sem transporte de backup, o usuário pode reiniciar o teste simplesmente reinstalando.

Disponibilizei essa abordagem como uma biblioteca extensível .

Fabian Streitel
fonte
1

Por definição, todos os aplicativos Android pagos no mercado podem ser avaliados por 24 horas após a compra.

Existe um botão 'Desinstalar e Reembolsar' que muda para 'Desinstalar' após 24 horas.

Eu diria que esse botão é proeminente demais!

AlexJReid
fonte
17
Observe que o período de reembolso agora é de apenas 15 minutos.
Intricações em
1

Eu me deparei com essa pergunta enquanto procurava o mesmo problema, acho que podemos utilizar a API de data gratuita como http://www.timeapi.org/utc/now ou alguma outra API de data para verificar a validade do aplicativo de trilha. dessa forma é eficiente se você deseja entregar a demonstração e está preocupado com o pagamento e requer uma demonstração de posse fixa. :)

encontre o código abaixo

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

}
}

sua solução de trabalho .....

RQube
fonte
claro que você pode usar, mas nesse caso apenas sua atividade principal seria capaz de verificar a validade.
RQube
0

Aqui está como eu fiz o meu, criei 2 aplicativos, um com atividade de teste e o outro sem,

carreguei aquele sem atividade de teste para a Play Store como um aplicativo pago

e aquele com atividade de teste como aplicativo gratuito.

O aplicativo gratuito no primeiro lançamento tem opções de teste e compra na loja, se o usuário selecionar a compra na loja, ele redireciona para a loja para o usuário comprar, mas se o usuário clicar em teste, ele o levará para a atividade de teste

NB: usei a opção 3 como @snctln, mas com modificações

primeiro , eu não dependia do tempo do dispositivo, obtive meu tempo do arquivo php que faz o registro de teste no banco de dados,

em segundo lugar , usei o número de série do dispositivo para identificar exclusivamente cada dispositivo,

por último , o aplicativo depende do valor de tempo retornado da conexão do servidor e não de seu próprio tempo, então o sistema só pode ser contornado se o número de série do dispositivo for alterado, o que é bastante estressante para o usuário.

então aqui vai meu código (para a atividade de teste):

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


}

Meu arquivo php se parece com isto (é uma 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;
        }

    }

depois, na atividade principal, uso a preferência compartilhada (installDate criado na atividade de teste) para monitorar o número de dias restantes e, se os dias se esgotarem, bloqueio a IU da atividade principal com uma mensagem que os leva à loja para fazer a compra.

A única desvantagem que vejo aqui é que, se um usuário Rogue comprar o aplicativo pago e decidir compartilhar com aplicativos como o Zender, compartilhe arquivos ou mesmo hospede o arquivo apk diretamente em um servidor para as pessoas baixarem gratuitamente. Mas tenho certeza que em breve editarei esta resposta com uma solução para isso ou um link para a solução.

Espero que isso salve uma alma ... algum dia

Boa codificação ...

O cara morto
fonte
0

@snctln opção 3 pode ser facilmente feita adicionando um arquivo php a um servidor web com php e mysql instalados como muitos deles.

Do lado do Android, um identificador (o ID do dispositivo, conta do Google ou o que você quiser) é passado como argumento no URL usando HttpURLConnection e o php retorna a data da primeira instalação se existir na tabela ou insere uma nova linha e ele retorna a data atual.

Isso funciona bem para mim.

Se eu tiver tempo, postarei algum código!

Boa sorte !

Lluis Felisart
fonte
espere, mas você perde sua id exclusiva após a reinstalação do aplicativo? !!
Maksim Kniazev
Este ID identifica o hardware, o próprio telefone, o usuário não o vê e não pode alterá-lo. Se ele reinstalar o aplicativo, o serviço web php detectará que é o mesmo telefone. Por outro lado, se o usuário trocar de telefone, terá um novo período.
Lluis Felisart de