Створення пробної програми для Android, термін дії якої закінчується через визначений проміжок часу


103

У мене є додаток, яким я хочу потрапити на ринок як платний додаток. Я хотів би мати іншу версію, яка була б "пробною" версією із обмеженням часу, скажімо, 5 днів?

Як я можу робити це?


Google повинен реально підтримувати це в Службах Play!
порошок666

@ pow366 насправді Google підтримує це, дивіться developer.android.com/google/play/billing/…
Yazazzello

Відповіді:


186

В даний час більшість розробників здійснюють це за допомогою однієї з наступних 3 методик.

Перший підхід легко обійти, коли перший раз, коли ви запускаєте додаток, зберігайте дату / час у файлі, базі даних чи спільних налаштуваннях, і кожен раз, коли ви запускаєте додаток після цього, перевірте, чи закінчився пробний період. Це легко обійти, оскільки видалення та перевстановлення дозволить користувачеві мати ще один пробний період.

Другий підхід важче обійти, але все-таки обійти. Використовуйте важко закодовану бомбу. В основному при такому підході вам буде важко визначити дату закінчення випробування, і всі користувачі, які завантажують та використовують додаток, перестануть мати змогу користуватися додатком одночасно. Я використовував такий підхід, тому що його легко здійснити, і здебільшого мені просто не здавалося, що я переживаю проблеми третьої техніки. Користувачі можуть обійти це, вручну змінивши дату на своєму телефоні, але більшість користувачів не зазнають проблем, щоб зробити таке.

Третя техніка - єдиний спосіб, про який я чув, щоб по-справжньому можна було виконати те, що ви хочете зробити. Вам доведеться налаштувати сервер, і тоді, коли програма буде запущена, додаток надсилає на сервер унікальний ідентифікатор телефону . Якщо сервер не має запису цього ідентифікатора телефону, він робить новий і записує час. Якщо сервер має запис для ідентифікатора телефону, він робить просту перевірку, чи закінчився пробний період. Потім він повідомляє про результати перевірки закінчення терміну дії пробної версії до вашої заявки. Цей підхід не повинен бути обхідним, але вимагає створення веб-сервера тощо.

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


2
Фантастична відповідь. Як ви кажете, я вважаю, що другий варіант, можливо, найкращий. Соромно, що самі Google не пропонують ліцензійну систему, оскільки це може заохотити як малих, так і великих розробників брендів виробляти ще більше додатків для Android.
Том

8
Крім того, я б не перевіряв під час запуску. Ваша мета - продати додаток, а не покарати користувача (це лише бонус;) Якщо у вас встановлено, щоб перевіряти кожні 2 хвилини під час роботи, ви дозволяєте користувачеві почати щось робити, а потім зрозумієте, що вони повинні платити. Якщо вам дуже легко платити і повертатися до роботи (я не впевнений, чи можете ви в Android), я думаю, ви продасте більше, ніж перевіряйте під час onCreate.
Whaledawg

4
@Whaledawg: Вам потрібно запустити власний сервер, оскільки сервер зберігає ідентифікатор телефону та час першого запуску в базі даних, який потім порівнюється з пізнішим. Також, коли ви перевіряєте, це виключно уподобання розробника, я використовував жорсткий закодована бомба часу в грі з прекрасними результатами. Весь додаток завантажиться, але користувач може взаємодіяти лише з діалоговим вікном, яке відображається, у цьому діалоговому вікні є кнопка, яка переводить користувача безпосередньо на сторінку придбання гри. Користувачі, мабуть, не проти AFAIK, оскільки ця гра була в топ-10 платних додатків з моменту відкриття Android Market.
snctln

11
Хто не бажає використовувати варіант 3 через додаткові настройки сервера, погляньте на Parse.com - це синхронізація.
Джоель Скрепнек

3
Що розуміється під терміном завершення випробування на жорсткому коді? Це означає, що ви будете продовжувати випускати нові версії пробного додатку назавжди з різними твердими датами у майбутнє?
Джаспер

21

Я розробив Android Trial SDK, який ви можете просто включити до свого проекту 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 .


Це вимагає включення даних?
Сіварам Боїна

випробування не є надійним
Амір Дора

1
@AmirDe Привіт Аміре, чи не могли б ви повідомити мені, що для вас не працює? Я радий допомогти, support@trialy.io Trialy чудово працює для 1000+ користувачів
Nick

@Nick не знаю, чому на моєму пристрої працює андроїд-льодяник, коли я встановлюю день з панелі інструментів, а потім день показує негативне значення через кілька хвилин, він каже, що термін дії закінчився, хоча у мене ще багато днів на панелі приладів. Я також пройшов тестування на пристрої нуга, здається, добре працює на наугат. можливо, у нього є проблеми з сумісністю старих версій для Android
Амір Дора

1
Я користуюся цією послугою з 2016 року, вона працює нормально кожен раз. Я використовував це і в своїх офіційних проектах. На це слід прийняти відповідь.
Тарік Махмуд

17

Це старе питання, але все одно, можливо, це комусь допоможе.

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

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

    ...
}

Насправді, якщо ви використовуєте SharedPreferences та Android 2.2 Froyo або новіших версій, якщо ви реалізуєте API резервного копіювання даних для синхронізації даних Google, користувач не повинен мати змогу уникнути цього через пристрої або видалити його, лише перейшовши в Налаштування> Програми та очищення даних. Також методу на Дата getTimeнемає getTimeInMillis.
Том

Не погоджуючись, це також не вдасться, якщо користувач змінить дату пристрою вручну, а також що робити, якщо користувач очистить дані вручну?
Мухаммед Ажаруддін Шейх

@Caner це добре обміняти з спільною підтримкою, але користувач видалить очищену пам'ять із seting-> менеджера прикладних програм і повторно використовувати, що робити?
CoronaPintu

@CoronaPintu, так що цей підхід також буде намагатися з firebase, це зробить ідеальну комбінацію, а пробний період буде навіть видалений.
Noor

комбінуйте та доповніть підхід із відповіддю "спокій", це зробить підхід ідеальним.
Noor

10

Це питання та відповідь 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


2
Ти щойно врятував мені день, дякую за важку працю. Я просто думав написати одне і те ж рішення про використання зашифрованої конфігурації, отриманої лише один раз, і збереження відкритого ключа всередині програми. Тож головна проблема, я бачу лише те, як ви надаєте ліцензію? Вам все одно може знадобитися якийсь унікальний ідентифікатор, який можна носити з телефоном, щоб уникнути декількох грантів.
user2305886

6

Найпростіший і найкращий спосіб зробити це реалізація BackupSharedPreferences.

Налаштування зберігаються, навіть якщо додаток видалено та перевстановлено.

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

Ось теорія: http://developer.android.com/reference/android/app/backup/SharedPreferencesBackupHelper.html

Ось приклад: Резервне копіювання Android SharedPreferences не працює


3
Користувач може відключити резервне копіювання в налаштуваннях системи.
Патрік

5

Підхід 4: використовувати час встановлення програми.

Оскільки API рівня 9 (Android 2.3.2, 2.3.1, Android 2.3, GINGERBREAD) існують firstInstallTime та lastUpdateTime в PackageInfo.

Щоб прочитати більше: Як отримати час установки програми з Android


Чи можна використовувати метод 1 з відповіді snctln із цим надійно, не обходячи його легко, чи у нього є однакова чи схожа проблема?
jwinn

Я перевірив цей метод. Хороша сторона полягає в тому, що вона працює, навіть якщо дані очищені. Погана сторона полягає в тому, що її можна обійти шляхом видалення / перевстановлення.
Жан-Філіп Джодін

можу підтвердити: на моєму Pixel видалення та перевстановлення програми скидає час першого встановлення. що робить користувачів досить тривіальним для скидання пробної версії
Fabian Streitel

3

Тепер в останню версію підписки на безкоштовну пробну версію для Android додано, ви можете розблокувати всі функції свого додатка лише після придбання підписки в додатку протягом безкоштовного пробного періоду. Це дозволить користувачеві використовувати ваш додаток протягом пробного періоду, якщо додаток все ще видалено після пробного періоду, тоді гроші на підписку будуть перераховані вам. Я не пробував, а просто ділився ідеєю.

Ось документація


2
Я хотів би, щоб це працювало для разових покупок. Працює лише для передплати, яка може бути щорічною або щомісячною.
jwinn

3

На мою думку, найкращий спосіб зробити це просто використовувати базу даних реального часу Firebase:

1) Додайте підтримку Firebase до свого додатка

2) Виберіть "Анонімне підтвердження автентичності", щоб користувачеві не потрібно було підписуватися або навіть знати, що ви робите. Це гарантовано посилання на обліковий запис користувача, який зареєстровано в даний час, і так буде працювати на всіх пристроях.

3) Використовуйте API бази даних у реальному часі, щоб встановити значення "встановленого_даного". На час запуску просто отримайте це значення і скористайтеся цим.

Я зробив те саме, і це чудово працює. Мені вдалося перевірити це через видалення / перевстановлення, і значення в базі даних в реальному часі залишається тим самим. Таким чином ваш пробний період працює на декількох пристроях користувача. Ви навіть можете встановити свою версію install_date, щоб програма "скинула" дату пробної версії для кожної нової основної версії.

ОНОВЛЕННЯ : Після тестування трохи більше, схоже, анонімний Firebase, схоже, виділяє інший ідентифікатор у випадку, якщо у вас є різні пристрої, і це не гарантується між перевстановленнями: / Єдиним гарантованим способом є використання Firebase, але прив’язання їх до google рахунок. Це повинно працювати, але вимагатиме додаткового кроку, коли спочатку користувачеві потрібно ввійти / зареєструватися.

Я поки що закінчився дещо менш елегантним підходом до простої перевірки резервного копіювання налаштувань та дати, що зберігається в налаштуваннях після встановлення. Це працює для додатків, орієнтованих на дані, де людина не має сенсу перевстановлювати додаток і повторно вводити всі раніше додані дані, але це не працюватиме для простої гри.


У мене однакова вимога до мого додатка для Android, і у мене є своя база даних / веб-сервер. Користувачам не потрібно входити в систему, тому я планував зберегти ідентифікатор пристрою з встановленим_дачею, чи це буде працювати?
користувач636525

@strangasures, я думаю, що ваше рішення працює найкраще, ви можете надати більше інформації? дякую
DayDayHappy

Цей потік stackoverflow.com/q/41733137/1396068 передбачає, що після перевстановлення програми ви отримаєте новий ідентифікатор користувача, тобто новий пробний період
Fabian Streitel

3

Переглянувши всі варіанти в цій та інших темах, це мої висновки

Спільні налаштування, база даних Можна очистити в налаштуваннях для Android, втратити після перевстановлення програми. Можна створити резервну копію за допомогою механізму резервного копіювання Android та буде відновлено після перевстановлення. Резервне копіювання може бути не завжди доступним, хоча має бути на більшості пристроїв

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

PackageInfo.firstInstallTime Скидається після перевстановлення, але стабільний для всіх оновлень

Увійти до якогось облікового запису Не має значення, чи це їхній обліковий запис Google через Firebase або один на власному сервері: пробна версія пов'язана з обліковим записом. Створення нового облікового запису відновить пробну версію.

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

ANDROID_ID Може бути недоступним і може змінюватися за певних обставин , наприклад, скидання заводських налаштувань. Думки про те, чи корисно це використовувати для виявлення пристроїв, схоже, відрізняються.

Ідентифікаційний номер у програмі Play може бути скинутий користувачем. Користувач може бути відключений, відмовившись від відстеження реклами.

Скидання InstanceID при перевстановці . Скидання в разі події безпеки. Ваш додаток може бути скинутий.

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

Для мого власного додатка я виявив, що спільні налаштування + firstInstallTime + резервне копіювання налаштувань є найменш нав'язливим, але також досить ефективним методом. Ви повинні переконатися, що ви вимагаєте створити резервну копію лише після перевірки та збереження часу початку проби у спільних налаштуваннях. Значення у спільних префіксах повинні мати перевагу перед першимInstallTime. Потім користувач повинен перевстановити додаток, запустити його один раз, а потім очистити дані програми, щоб відновити пробну версію, що дуже багато роботи. На пристроях без резервного транспорту користувач може скинути пробну версію, просто перевстановивши.

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


1

За визначенням, всі платні програми Android на ринку можна оцінити протягом 24 годин після покупки.

Існує кнопка "Видалити та повернути гроші", яка змінюється на "Видалити" через 24 години.

Я б стверджував, що ця кнопка занадто помітна!


17
Зверніть увагу, що термін повернення коштів зараз становить лише 15 хвилин.
інтриги

1

Я натрапив на це питання під час пошуку тієї ж проблеми, я думаю, що ми можемо використати безкоштовні дати 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

}
}

його робоче рішення .....


Ви впевнені, що можете використовувати, але в такому випадку лише ваша основна діяльність зможе перевірити термін дії.
RQube

0

Ось як я пішов про своє, я створив 2 додатки, одне з пробною діяльністю, інше без,

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

і той, з пробною діяльністю як безкоштовний додаток.

Безкоштовний додаток при першому запуску має варіанти пробної та купівлі магазину, якщо користувач вибирає покупку магазину, він перенаправляє його до магазину, щоб користувач придбав, але якщо користувач натискає пробну версію, відведе їх до пробної діяльності

NB: Я використовував варіант 3, як @snctln, але з модифікаціями

по-перше , я не залежав від часу пристрою, я отримав свій час з файлу php, який робить пробну реєстрацію до db,

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

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

тому ось мій код (для пробної діяльності):

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, створений у пробній діяльності), щоб відстежувати кількість днів, що залишилися, і якщо дні закінчились, я блокую основний інтерфейс активності з повідомленням, яке доставляє їх у магазин для придбання.

Єдине, що я бачу тут, - це те, що якщо користувач Rogue купує платний додаток і вирішує поділитися з такими додатками, як Zender, обмінюватися файлами або навіть розміщувати файл apk безпосередньо на сервері, щоб люди завантажували його безкоштовно. Але я впевнений, що незабаром я відредагую цю відповідь рішенням на це чи посиланням на рішення.

Сподіваюся, це рятує душу ... якийсь день

Щасливе кодування ...


0

@snctln варіант 3 легко виконати, додавши файл php на веб-сервер із встановленими php та mysql, як багато з них.

З боку Android ідентифікатор (ідентифікатор пристрою, обліковий запис google o все, що ви хочете) передається як аргумент в URL-адресі за допомогою HttpURLConnection, і php повертає дату першої установки, якщо вона існує в таблиці або вона вставляє новий рядок і вона повертає поточну дату.

Це прекрасно працює для мене.

Якщо я встигну, опублікую якийсь код!

Щасти !


зачекайте, але ви втратите свій унікальний ідентифікатор після перевстановлення програми? !!
Максим Князєв

Цей ідентифікатор ідентифікує апаратне забезпечення, сам телефон, користувач не бачить його і не може його змінити. Якщо він перевстановить додаток, веб-служба php виявить, що це той самий телефон. З іншого боку, якщо користувач змінить телефон, він буде насолоджуватися новим періодом.
Lluis Felisart
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.