Создание пробного приложения Android, срок действия которого истекает через фиксированный период времени

У меня есть приложение, которое я хочу выпустить на рынок как платное приложение. Я хотел бы иметь другую версию, которая была бы «пробной» версией с ограничением по времени, скажем, 5 дней?

Как я могу это сделать?


person Tom    schedule 15.06.2009    source источник
comment
Google действительно должен поддерживать это в Play Services!   -  person powder366    schedule 26.10.2016
comment
@powder366 на самом деле Google поддерживает это, см. developer.android.com/google/play. /выставление счетов/   -  person Yazazzello    schedule 20.02.2018


Ответы (13)


В настоящее время большинство разработчиков достигают этого, используя один из следующих 3 методов.

Первый подход легко обойти: при первом запуске приложения сохраните дату/время в файл, базу данных или общие настройки, и каждый раз, когда вы запускаете приложение после этого, проверяйте, не закончился ли пробный период. Это легко обойти, потому что удаление и переустановка позволит пользователю получить еще один пробный период.

Второй подход сложнее обойти, но все же можно обойти. Используйте жестко запрограммированную бомбу замедленного действия. По сути, при таком подходе вы жестко задаете дату окончания пробной версии, и все пользователи, загружающие и использующие приложение, перестанут использовать его одновременно. Я использовал этот подход, потому что его легко реализовать, и по большей части мне просто не хотелось разбираться с проблемой третьего метода. Пользователи могут обойти это, вручную изменив дату на своем телефоне, но большинству пользователей это не составит труда.

Третий метод — это единственный способ, о котором я слышал, чтобы действительно достичь того, что вы хотите сделать. Вам нужно будет настроить сервер, а затем всякий раз, когда ваше приложение запускается, ваше приложение отправляет уникальный идентификатор телефона на сервер. Если на сервере нет записи для этого идентификатора телефона, он создает новую и отмечает время. Если на сервере есть запись для идентификатора телефона, он просто проверяет, не истек ли пробный период. Затем он передает результаты проверки срока действия пробной версии вашему приложению. Этот подход нельзя обойти, но он требует настройки веб-сервера и т.п.

Всегда рекомендуется выполнять эти проверки в файле onCreate. Если срок действия истек, откройте всплывающее диалоговое окно AlertDialog с ссылкой на рынок на полную версия приложения. Включите только кнопку «ОК», и как только пользователь нажмет «ОК», сделайте вызов «finish ()», чтобы завершить действие.

person snctln    schedule 15.06.2009
comment
Фантастический ответ. Как вы сказали, я считаю, что второй вариант, возможно, лучший. Жаль, что сами Google не предлагают какую-то систему лицензирования, поскольку это может побудить как мелких, так и крупных разработчиков брендов создавать еще больше приложений для Android. - person Tom; 15.06.2009
comment
Вам нужно настроить свой собственный сервер? Это похоже на то, что было бы у кого-то другого. Эта веб-страница: nrc-cnrc.gc .ca/eng/services/inms/time-services/, похоже, указывает на то, что у них есть сервис, которым вы могли бы воспользоваться. - person Whaledawg; 16.06.2009
comment
Кроме того, я бы не стал проверять во время запуска. Ваша цель — продать приложение, а не наказать пользователя (это просто бонус;) Если вы настроили проверку каждые 2 минуты во время работы, вы позволяете пользователю начать что-то делать, а затем понимаете, что он должен платить. Если вы действительно упростите оплату и вернетесь к работе (я не уверен, что вы можете это сделать на Android), я думаю, вы продадите больше, чем при проверке во время onCreate. - person Whaledawg; 16.06.2009
comment
@Whaledawg: вам нужно запустить свой собственный сервер, потому что сервер хранит идентификатор телефона и время первого запуска в базе данных, которая затем сравнивается с более поздней. Также, когда вы выполняете проверку, это чисто предпочтение разработчика, я использовал жесткий закодированная бомба замедленного действия в игре с отличными результатами. Все приложение загружается, но пользователь может взаимодействовать только с видимым диалоговым окном, в этом диалоговом окне есть кнопка, которая ведет пользователя прямо на страницу покупки игры. Пользователи, кажется, не возражают против AFAIK, поскольку эта игра вошла в десятку лучших платных приложений с момента открытия Android Market. - person snctln; 16.06.2009
comment
В настоящее время используется первый способ. Похоже, что вариант номер 2 не подходит, если вы хотите, чтобы пользователи продолжали пробовать ваше приложение. 3-е определенно выполнимо, но я проверю статистику использования моего пробного приложения, чтобы увидеть, много ли людей продолжают использовать его дольше, чем 5 дней. - person Peterdk; 21.04.2010
comment
Использование уникального идентификатора телефона не позволит пользователю использовать приложение на новом устройстве. Использование line1Number позволит пользователям взять с собой свои приложения, если они возьмут с собой свой номер. - person andreas; 17.08.2011
comment
Для второго подхода будет ли лучше выполнять проверку onResume(), а не onCreate()? onCreate() будет проверять только дату, когда приложение только что создано, не так ли? - person Declan McKenna; 23.02.2012
comment
Для тех, кто не хочет использовать вариант 3 из-за дополнительной настройки сервера, взгляните на Parse.com - это синхронизация. - person Joel Skrepnek; 21.04.2012
comment
@ snctln-у меня есть небольшое сомнение ... в методе 3, как получить дату, независимую от даты устройства (поскольку она может быть изменена, как указано в методе 2). - person Name is Nilay; 06.02.2013
comment
Я работал над решением с открытым исходным кодом, основанным на методе 3 (как диплом бакалавра). См. мой ответ ниже и текущий статус этого проекта github.com/MaChristmann/mobile-trial - person Martin Christmann; 21.03.2013
comment
@snctln Что, если вы создаете автономное приложение, которому не нужен Интернет, как мы можем принудительно установить пробный период, если мы находимся в такой ситуации, потому что в этом случае пользователь может просто отключить Интернет, и третий подход также не удастся? - person Sheraz Ahmad Khilji; 09.09.2014
comment
Что подразумевается под жесткой датой окончания пробного периода? Означает ли это, что вы будете постоянно выпускать новые версии пробного приложения с другими жестко заданными датами в будущем? - person Jasper; 21.05.2015
comment
На случай, если кто-то не прокрутит вниз достаточно далеко, чтобы увидеть, есть решение подхода 4, используйте установленную дату и время из пакета и жестко закодируйте свой пробный период. Поэтому, если приложение должно работать только в течение 5 дней, проверьте, не истек ли этот срок. Пользователю придется постоянно менять свой день обратно на установленный день, чтобы обойти это, что, как было сказано ранее, никто не хочет делать это для приложения. - person JenniferG; 19.11.2015
comment
Кто-нибудь может написать модуль xposed для победы над третьим способом - person Suici Doga; 06.05.2016
comment
отличный ответ. но посмотрите на такой сценарий. у человека все еще есть мой код после истечения периода следа. я, конечно, могу остановить приложение для запуска после определенного дня. но он/она может реконструировать мое приложение. так как мне просто удалить исходный код. или сделать его зашифрованным, чтобы исходный код нельзя было снова найти путем реверс-инжиниринга. - person Sagar Nayak; 23.08.2016
comment
@snctln В третьем методе предположим, что мое приложение отключено, я никогда не использую свое приложение при подключении к Интернету. Тогда этот способ тоже можно обойти. Что мне делать в этой ситуации? - person Faisal Ahsan; 22.09.2016
comment
Изменяется ли deviceID после каждого обновления ОС? - person user636525; 20.12.2016

Я разработал SDK пробной версии Android, который вы можете просто добавить в свой проект Android Studio, и он позаботится обо всех управление на стороне сервера для вас (включая льготные периоды в автономном режиме).

Чтобы использовать его, просто

Добавьте библиотеку в build.gradle главного модуля.

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

Инициализируйте библиотеку в методе onCreate() основного действия

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

Добавьте обработчик обратного вызова:

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

};

Чтобы начать пробную версию, позвоните по номеру mTrialy.startTrial("YOUR_TRIAL_SKU", mTrialyCallback);. Ваш ключ приложения и номер SKU пробной версии можно найти в панели инструментов разработчика Trialy.

person Nick    schedule 17.04.2017
comment
Это требует, чтобы данные были включены? - person Sivaram Boina; 12.12.2017
comment
триал не надежен - person Amir Dora.; 05.06.2018
comment
@AmirDe Привет, Амир, не могли бы вы сообщить мне, что у вас не работает? Я рад помочь, [email protected] Trialy отлично работает для 1000+ пользователей. - person Nick; 06.06.2018
comment
@Ник не знает, почему на моем устройстве работает Android Lollipop, когда я устанавливаю день на панели инструментов, через несколько минут день отображается с отрицательным значением, в нем говорится, что срок действия пробной версии истек, хотя у меня еще много дней на панели инструментов. Я также тестировал на устройстве нуги, кажется, работает нормально на науге. возможно, у него есть проблемы совместимости со старыми версиями Android. - person Amir Dora.; 06.06.2018
comment
@sivaram Библиотека использует кэширование, чтобы гарантировать, что она отвечает полезным результатом, даже если данные недоступны. Данные должны быть включены только для первого запуска (когда вы регистрируете новую установку в пробной версии). - person Nick; 06.06.2018
comment
@sivaram спасибо за ответ, но я не отключал данные во время тестирования. это не похоже на причину. - person Amir Dora.; 06.06.2018
comment
@Ник, возможно ли определить жестко запрограммированный конец теста, скажем, до конца 2020 года? - person Franzi; 18.03.2019

Это старый вопрос, но в любом случае, может быть, это кому-то поможет.

Если вы хотите использовать наиболее упрощенный подход(который не сработает, если приложение будет удалено/переустановлено или пользователь изменяет дату устройства вручную), вот как это может быть:

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

    ...
}
person Caner    schedule 03.02.2012
comment
Ну, на самом деле, если вы используете SharedPreferences и Android 2.2 Froyo или выше, пока вы реализуете API резервного копирования данных для синхронизации данных Google, пользователь не сможет избежать этого на разных устройствах или путем удаления, только перейдя в Настройки > Приложения и очистка данных. Кроме того, метод Date — это getTime, а не getTimeInMillis. - person Tom; 18.04.2012
comment
Не согласен, это также не удастся, если пользователь изменит дату устройства вручную, а также что, если пользователь очистит данные вручную? - person Mohammed Azharuddin Shaikh; 02.11.2012
comment
@Caner, это хорошо проверить с помощью sharedprefrence, но пользователь очистит память от диспетчера настроек-> приложений и повторно использует, что тогда делать? - person CoronaPintu; 08.06.2013
comment
@CoronaPintu, так что этот подход также можно попробовать с firebase, это будет идеальная комбинация, и пробный период закончится, даже если приложение будет удалено. - person Noor Hossain; 24.09.2019
comment
комбинируйте и добавляйте подход со странным ответом, это сделает подход идеальным. - person Noor Hossain; 24.09.2019

Этот вопрос и ответ snctln вдохновили меня на работу над решением, основанным на методе 3, в качестве дипломной работы бакалавра. Я знаю, что текущий статус не для продуктивного использования, но я хотел бы услышать, что вы думаете об этом! Вы бы воспользовались такой системой? Хотели бы вы видеть его как облачный сервис (без проблем с настройкой сервера)? Беспокоитесь о проблемах безопасности или стабильности?

Как только я закончу процедуру бакалавра, я хочу продолжить работу над программным обеспечением. Итак, настало время, когда мне нужны ваши отзывы!

Исходный код размещен на GitHub https://github.com/MaChristmann/mobile-trial.

Немного информации о системе: - Система состоит из трех частей: библиотеки Android, сервера node.js и конфигуратора для управления несколькими пробными приложениями и учетными записями издателя/разработчика.

  • Он поддерживает только пробные версии на основе времени и использует вашу учетную запись (игровой магазин или другую), а не идентификатор телефона.

  • Для библиотеки Android она основана на библиотеке проверки лицензий Google Play. Я изменил его для подключения к серверу node.js, и, кроме того, библиотека пытается распознать, изменил ли пользователь системную дату. Он также кэширует полученную пробную лицензию в зашифрованных общих настройках AES. Вы можете настроить время действия кеша с помощью конфигуратора. Если пользователь «удалит данные», библиотека выполнит проверку на стороне сервера.

  • Сервер использует https, а также цифровую подпись ответа на проверку лицензии. Он также имеет API для пробных приложений CRUD и пользователей (издателя и разработчика). Аналогично библиотеке проверки лицензирования, разработчики могут протестировать реализацию своего поведения в пробном приложении с результатами тестирования. Таким образом, вы в конфигураторе можете явно установить свой ответ лицензии на «лицензировано», «не лицензировано» или «ошибка сервера».

  • Если вы обновите свое приложение новой потрясающей функцией, вы можете захотеть, чтобы все могли попробовать ее снова. В конфигураторе вы можете продлить пробную лицензию для пользователей с просроченными лицензиями, установив код версии, который должен активировать это действие. Например, пользователь запускает ваше приложение с кодом версии 3, и вы хотите, чтобы он попробовал функции кода версии 4. Если он обновит приложение или переустановит его, он сможет снова использовать полный пробный период, потому что сервер знает, на какой версии он пробовал его в последний раз. время.

  • Все под лицензией Apache 2.0

person Martin Christmann    schedule 21.03.2013
comment
Вы просто спасли мой день, спасибо за тяжелую работу. Я просто подумал написать такое же решение, используя зашифрованную конфигурацию, полученную только один раз, и сохраняя открытый ключ внутри приложения. Итак, основную проблему я вижу только в том, как вы предоставляете лицензию? Вам все еще может понадобиться какой-то уникальный идентификатор с телефоном, чтобы избежать нескольких грантов. - person user2305886; 28.12.2013

Самый простой и лучший способ сделать это — реализовать BackupSharedPreferences.

Настройки сохраняются, даже если приложение будет удалено и переустановлено.

Просто сохраните дату установки в качестве предпочтения, и все готово.

Вот теория: http://developer.android.com/reference/android/app/backup/SharedPreferencesBackupHelper.html

Вот пример: Резервное копирование общих настроек Android не работает

person pstorli    schedule 25.07.2013
comment
Пользователь может отключить резервное копирование в настройках системы. - person Patrick; 01.08.2014

Подход 4: используйте время установки приложения.

Начиная с уровня API 9 (Android 2.3.2, 2.3.1, Android 2.3, GINGERBREAD) существует firstInstallTime и lastUpdateTime в PackageInfo.

Подробнее: Как получить время установки приложения с Android

person 18446744073709551615    schedule 09.12.2013
comment
Можно ли с этим надежно использовать метод 1 из ответа snctln, не будучи легко обойденным, или у него такая же или аналогичная проблема? - person jwinn; 20.03.2014
comment
Я протестировал этот метод. Хорошая сторона в том, что он работает, даже если данные очищены. Плохая сторона в том, что это можно обойти удалением/переустановкой. - person Jean-Philippe Jodoin; 04.05.2014
comment
могу подтвердить: на моем Pixel удаление и повторная установка приложения сбрасывает время первой установки. что делает сброс пробной версии довольно тривиальным для пользователей - person Fabian Streitel; 18.02.2017

Посмотрев все варианты в этой и других темах, вот мои выводы

Общие настройки, база данных Можно очистить в настройках Android, потерять после переустановки приложения. Можно создать резервную копию с помощью механизма резервного копирования Android, после чего он будет восстановлен. после переустановки. Резервное копирование может быть не всегда доступно, хотя должно быть на большинстве устройств.

Внешнее хранилище (запись в файл) Не влияет на очистку настроек или переустановку, если мы не выполнять запись в личный каталог приложения. Но: требует, чтобы вы запрашивали у пользователя разрешение во время выполнения в более новых версиях Android, так что это, вероятно, возможно только в том случае, если вам все равно нужно это разрешение. Также можно сделать резервную копию.

PackageInfo.firstInstallTime Сбрасывается после переустановки, но стабилен при обновлении

Войдите в какую-либо учетную запись Не имеет значения, будет ли это их учетная запись Google через Firebase или учетная запись на вашем собственном сервере: пробная версия привязана к учетной записи. Создание новой учетной записи приведет к сбросу пробной версии.

Анонимный вход в Firebase. Вы можете анонимно входить в систему и хранить данные для них в Firebase. Но очевидно, переустановка приложения и, возможно, другие недокументированные события могут дать пользователю новый анонимный идентификатор, сбросив пробный период. (Сами Google не предоставляют много документации по этому поводу)

ANDROID_ID Может быть недоступен и может измениться при определенных обстоятельствах, например при сбросе настроек. Мнения о том, стоит ли использовать это для идентификации устройств, похоже, расходятся.

Идентификатор рекламы в Play Может быть сброшен пользователем. Может быть отключено пользователем путем отказа от отслеживания рекламы.< /а>

InstanceID Сбросить при переустановке. Сброс в случае нарушения безопасности. Может быть сброшено вашим приложением.

Какие (комбинации) методы работают для вас, зависит от вашего приложения и от того, сколько усилий, по вашему мнению, приложит среднестатистический Джон, чтобы получить еще один пробный период. Я бы рекомендовал избегать использования только анонимных Firebase и Advertising ID из-за их нестабильности. Кажется, что многофакторный подход даст наилучшие результаты. Какие факторы вам доступны, зависит от вашего приложения и его разрешений.

Для моего собственного приложения общие настройки + firstInstallTime + резервное копирование настроек были наименее навязчивым, но достаточно эффективным методом. Вы должны убедиться, что запрашиваете резервную копию только после проверки и сохранения времени начала пробной версии в общих настройках. Значения в общих Prefs должны иметь приоритет над firstInstallTime. Затем пользователь должен переустановить приложение, запустить его один раз, а затем очистить данные приложения, чтобы сбросить пробную версию, что довольно много работы. Однако на устройствах без резервного транспорта пользователь может сбросить пробную версию, просто переустановив ее.

Я сделал этот подход доступным в виде расширяемой библиотеки.

person Fabian Streitel    schedule 18.02.2017

Теперь в последней версии Android была добавлена ​​​​бесплатная пробная подписка, вы можете разблокировать все функции вашего приложения только после покупки подписки в приложении на бесплатный пробный период. Это позволит пользователю использовать ваше приложение в течение пробного периода. Если приложение по-прежнему будет удалено после пробного периода, деньги за подписку будут переведены вам. Сам не пробовал, просто поделился идеей.

Документация

person Vins    schedule 05.09.2013
comment
Я хотел бы, чтобы это работало для разовых покупок. Работает только для подписок, которые могут быть годовыми или ежемесячными. - person jwinn; 20.03.2014

На мой взгляд, лучший способ сделать это — просто использовать базу данных Firebase Realtime:

1) Добавьте поддержку Firebase в свое приложение.

2) Выберите «Анонимная аутентификация», чтобы пользователю не нужно было регистрироваться или даже знать, что вы делаете. Это гарантированно связано с текущей аутентифицированной учетной записью пользователя и поэтому будет работать на разных устройствах.

3) Используйте API базы данных реального времени, чтобы установить значение для «установленной_даты». Во время запуска просто получите это значение и используйте его.

Я сделал то же самое, и это прекрасно работает. Я смог проверить это при удалении/переустановке, и значение в базе данных в реальном времени остается прежним. Таким образом, ваш пробный период работает на нескольких пользовательских устройствах. Вы даже можете изменить версию install_date, чтобы приложение «сбрасывало» дату пробной версии для каждого нового основного выпуска.

ОБНОВЛЕНИЕ: после дополнительного тестирования выяснилось, что анонимный Firebase, похоже, выделяет другой идентификатор на случай, если у вас разные устройства, и это не гарантируется между повторными установками: / Единственный гарантированный способ — использовать Firebase, но привяжите его к своей учетной записи Google. Это должно работать, но потребует дополнительного шага, когда пользователю сначала нужно войти/зарегистрироваться.

До сих пор я остановился на несколько менее элегантном подходе, просто проверяя резервные копии настроек и дату, хранящуюся в настройках при установке. Это работает для приложений, ориентированных на данные, где человеку бессмысленно переустанавливать приложение и повторно вводить все ранее добавленные данные, но это не сработает для простой игры.

person strangetimes    schedule 05.10.2016
comment
У меня есть такое же требование для моего приложения для Android, и у меня есть собственная база данных/веб-сервер. Пользователям не требуется вход в систему, поэтому я планировал сохранить идентификатор устройства с установленной_датой, это сработает? - person user636525; 17.01.2017
comment
@strangetimes, я думаю, что ваше решение работает лучше всего, можете ли вы предоставить больше информации? Благодарность - person DayDayHappy; 13.02.2017
comment
Этот поток stackoverflow.com/q/41733137/1396068 предполагает, что после переустановки приложения вы получите новый идентификатор пользователя, т.е. новый испытательный срок - person Fabian Streitel; 15.02.2017

По определению, все платные приложения для Android на рынке можно оценить в течение 24 часов после покупки.

Есть кнопка «Удалить и вернуть деньги», которая меняется на «Удалить» через 24 часа.

Я бы сказал, что эта кнопка слишком заметна!

person AlexJReid    schedule 04.12.2009
comment
Обратите внимание, что период возврата теперь составляет всего 15 минут. - person Intrications; 14.02.2011

Я столкнулся с этим вопросом при поиске той же проблемы, я думаю, что мы можем использовать бесплатный API для дат, например http://www.timeapi.org/utc/now или какой-либо другой API даты, чтобы проверить срок действия следа приложения. этот способ эффективен, если вы хотите предоставить демо-версию и беспокоитесь об оплате и хотите исправить демо-версию. :)

найдите код ниже

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

}
}

его рабочее решение.....

person RQube    schedule 18.02.2014
comment
конечно, вы можете использовать, но в этом случае только ваша основная деятельность сможет проверить срок действия. - person RQube; 28.04.2015

Вот как я поступил со своим: я создал 2 приложения, одно с пробной активностью, другое без,

я загрузил приложение без пробной версии в магазин игр как платное приложение,

и тот, у которого есть пробная версия бесплатного приложения.

Бесплатное приложение при первом запуске имеет варианты пробной версии и покупки в магазине, если пользователь выбирает покупку в магазине, оно перенаправляет пользователя в магазин для покупки, но если пользователь нажимает пробную версию, он переводит их в пробную активность.

NB: я использовал вариант 3, например @snctln, но с изменениями.

во-первых, я не зависел от времени устройства, я получил свое время из файла php, который выполняет пробную регистрацию в базе данных,

во-вторых, я использовал серийный номер устройства для уникальной идентификации каждого устройства,

наконец, приложение зависит от значения времени, возвращаемого при подключении к серверу, а не от собственного времени, поэтому систему можно обойти только в случае изменения серийного номера устройства, что является довольно напряженным для пользователя.

Итак, вот мой код (для пробной активности):

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


}

Мой файл php выглядит так (это технология 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;
        }

    }

затем в основном действии я использую общий параметр (installDate, созданный в пробном действии), чтобы отслеживать количество оставшихся дней, и если дни закончились, я блокирую пользовательский интерфейс основного действия с сообщением, которое отправляет их в магазин для покупки.

Единственным недостатком, который я здесь вижу, является то, что если пользователь-мошенник покупает платное приложение и решает поделиться им с такими приложениями, как Zender, общий доступ к файлам или даже разместить файл apk непосредственно на сервере, чтобы люди могли загрузить его для бесплатно. Но я уверен, что скоро отредактирую этот ответ с решением или ссылкой на решение.

Надеюсь, это спасет душу ... когда-нибудь

Удачного кодирования...

person The Billionaire Guy    schedule 10.03.2017

@snctln вариант 3 можно легко выполнить, добавив php-файл на веб-сервер с установленными php и mysql, как и многие из них.

Со стороны Android идентификатор (идентификатор устройства, учетная запись Google или все, что вы хотите) передается в качестве аргумента в URL-адресе с использованием HttpURLConnection, и php возвращает дату первой установки, если она существует в таблице, или вставляет новую строку и он возвращает текущую дату.

Он отлично работает для меня.

Если у меня будет время, я опубликую код!

Удачи !

person Lluis Felisart    schedule 24.06.2017
comment
подождите, но вы теряете свой уникальный идентификатор после переустановки приложения?!! - person Yaroslav Dukal; 15.01.2019
comment
Этот ID идентифицирует железо, сам телефон, пользователь его не видит и не может изменить. Если он переустановит приложение, веб-служба php обнаружит, что это тот же телефон. С другой стороны, если пользователь сменит телефон, он будет наслаждаться новым периодом. - person Lluis Felisart; 15.01.2019