简体   繁体   English

创建一个在固定时间段后过期的 Android 试用应用程序

[英]Creating an Android trial application that expires after a fixed time period

I have an application which I want to hit the market as a Paid app.我有一个应用程序,我想将它作为付费应用程序投放市场。 I would like to have other version which would be a "trial" version with a time limit of say, 5 days?我想要其他版本的“试用”版本,时间限制为 5 天?

How can I go about doing this?我该怎么做呢?

Currently most developers accomplish this using one of the following 3 techniques.目前,大多数开发人员使用以下 3 种技术之一来完成此操作。

The first approach is easily circumvented, the first time you run the app save the date/time to a file, database, or shared preferences and every time you run the app after that check to see if the trial period has ended.第一种方法很容易绕过,第一次运行应用程序时将日期/时间保存到文件、数据库或共享首选项中,之后每次运行应用程序时都会检查试用期是否已结束。 This is easy to circumvent because uninstalling and reinstalling will allow the user to have another trial period.这很容易规避,因为卸载和重新安装将允许用户有另一个试用期。

The second approach is harder to circumvent, but still circumventable.第二种方法更难规避,但仍然可以规避。 Use a hard coded time bomb.使用硬编码的定时炸弹。 Basically with this approach you will be hard code an end date for the trial, and all users that download and use the app will stop being able to use the app at the same time.基本上,通过这种方法,您将对试用的结束日期进行硬编码,并且所有下载和使用该应用程序的用户都将无法同时使用该应用程序。 I have used this approach because it is easy to implement and for the most part I just didn't feel like going through the trouble of the third technique.我使用这种方法是因为它很容易实现,而且在大多数情况下我只是不想经历第三种技术的麻烦。 Users can circumvent this by manually changing the date on their phone, but most users won't go through the trouble to do such a thing.用户可以通过手动更改手机上的日期来规避这一点,但大多数用户不会遇到这样做的麻烦。

The third technique is the only way that I have heard about to truly be able to accomplish what you want to do.第三种技术是我听说的真正能够完成你想做的事情的唯一方法。 You will have to set up a server, and then whenever your application is started your app sends the phones unique identifier to the server.您必须设置一个服务器,然后每当您的应用程序启动时,您的应用程序都会将手机唯一标识符发送到服务器。 If the server does not have an entry for that phone id then it makes a new one and notes the time.如果服务器没有该电话 ID 的条目,则它会创建一个新条目并记录时间。 If the server does have an entry for the phone id then it does a simple check to see if the trial period has expired.如果服务器确实有电话 ID 的条目,那么它会做一个简单的检查以查看试用期是否已过期。 It then communicates the results of the trial expiration check back to your application.然后,它会将试用期到期检查的结果返回给您的应用程序。 This approach should not be circumventable, but does require setting up a webserver and such.这种方法不应该是可以规避的,但确实需要设置网络服务器等。

It is always good practice to do these checks in the onCreate.在 onCreate 中进行这些检查总是好的做法。 If the expiration has ended popup an AlertDialog with a market link to the full version of the app.如果到期已结束,则会弹出一个 AlertDialog,其中包含指向该应用程序完整版本的市场链接 Only include an "OK" button, and once the user clicks on "OK" make a call to "finish()" to end the activity.只包含一个“OK”按钮,一旦用户点击“OK”,就调用“finish()”来结束活动。

I've developed a Android Trial SDK which you can simply drop into your Android Studio project and it will take care of all the server-side management for you (including offline grace periods).我开发了一个Android 试用 SDK ,您可以简单地将其放入您的 Android Studio 项目中,它将为您处理所有服务器端管理(包括离线宽限期)。

To use it, simply要使用它,只需

Add the library to your main module's build.gradle将库添加到主模块的build.gradle

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

Initialize the library in your main activity's onCreate() method在主要活动的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);
}

Add a callback handler:添加回调处理程序:

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

};

To start a trial, call mTrialy.startTrial("YOUR_TRIAL_SKU", mTrialyCallback);要开始试用,请调用mTrialy.startTrial("YOUR_TRIAL_SKU", mTrialyCallback); Your app key and trial SKU can be found in your Trialy developer dashboard .您的应用程序密钥和试用版 SKU 可在您的Trialy 开发人员仪表板 中找到

This is an old question but anyways, maybe this will help someone.这是一个老问题,但无论如何,也许这会对某人有所帮助。

In case you want to go with the most simplistic approach (which will fail if the app is uninstalled/reinstalled or user changes device's date manually), this is how it could be:如果您想采用最简单的方法如果卸载/重新安装应用程序或用户手动更改设备日期,则会失败),可能是这样:

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

    ...
}

This question and the answer of snctln inspired me to work on a solution based on method 3 as my bachelor thesis.这个问题和snctln的回答激发了我基于方法 3 的解决方案作为我的学士论文。 I know the current status is not for productive usage but I would love to hear what you think about it!我知道当前状态不适用于生产用途,但我很想听听您的看法! Would you use such a system?你会使用这样的系统吗? Would you like to see it as a cloud service (not having trouble with configuring a server)?您是否希望将其视为云服务(在配置服务器时没有问题)? Concerned about security issues or stability reasons?担心安全问题或稳定性原因?

A soon as I finished the bachelor procedure I want to continue working on the software.完成学士程序后,我想继续开发该软件。 So now its the time I need your feedback!所以现在是我需要你的反馈的时候了!

Sourcecode is hosted on GitHub https://github.com/MaChristmann/mobile-trial源代码托管在 GitHub https://github.com/MaChristmann/mobile-trial

Some information about the system: - The system has three parts, a Android library, a node.js server and a configurator for managing multiple trial apps and publisher/developer accounts.关于系统的一些信息: - 该系统由三部分组成,一个 Android 库、一个 node.js 服务器和一个用于管理多个试用应用程序和发布者/开发者帐户的配置器。

  • It only supports time-based trials and it uses your (play store or other) account rather than a phone ID.它仅支持基于时间的试用,并且使用您的(Play 商店或其他)帐户而不是手机 ID。

  • For Android library it is based on the Google Play licensing verification library.对于 Android 库,它基于 Google Play 许可验证库。 I modified it to connect to the node.js server and additionally the library tries to recognize if a user changed the system date.我修改了它以连接到 node.js 服务器,此外该库还尝试识别用户是否更改了系统日期。 It also caches a retrieved trial-license in AES encrypted Shared Preferences.它还在 AES 加密的共享首选项中缓存检索到的试用许可证。 You can configure the valid time of the cache with the configurator.您可以使用配置器配置缓存的有效时间。 If a user "clear data" the library will force a server-side check.如果用户“清除数据”,图书馆将强制进行服务器端检查。

  • Server is using https and also digital signing the license-check response.服务器正在使用 https 并对许可证检查响应进行数字签名。 It has also an API for CRUD trial apps and users (publisher and developer).它还具有用于 CRUD 试用应用程序和用户(发布者和开发者)的 API。 Similiar to Licensing Verfication Library developers can test their behaviour implementation in the trial app with test result.类似于许可验证库,开发人员可以使用测试结果在试用应用程序中测试他们的行为实现。 So you in the configurator you can explicit set your license response to "licensed", "not licensed" or "server error".因此,您可以在配置器中将许可响应明确设置为“已许可”、“未许可”或“服务器错误”。

  • If you update your app with an ass-kicking new feature you might want that everyone can try it again.如果您使用令人兴奋的新功能更新您的应用程序,您可能希望每个人都可以再试一次。 In the configurator you can renew the trial license for users with expired licenses by setting a versioncode that should trigger this.在配置器中,您可以通过设置应触发此操作的版本代码来为许可证过期的用户续订试用许可证。 For example user is running your app on versioncode 3 und you want him to try features of versioncode 4. If he updates the app or reinstall it he is able to use full trial period again because the server knows on which version he has tried it last time.例如,用户在版本代码 3 上运行您的应用程序,而您希望他尝试版本代码 4 的功能。如果他更新应用程序或重新安装它,他可以再次使用完整的试用期,因为服务器知道他上次尝试的是哪个版本时间。

  • Everything is under the Apache 2.0 license一切都在 Apache 2.0 许可下

The easiest and best way to do this is the implement BackupSharedPreferences.最简单和最好的方法是实现 BackupSharedPreferences。

The preferences are preserved, even if the app is uninstalled and reinstalled.即使卸载并重新安装应用程序,首选项也会保留。

Simply save the install date as a preference and you are good to go.只需将安装日期保存为首选项即可。

Here's the theory: http://developer.android.com/reference/android/app/backup/SharedPreferencesBackupHelper.html这是理论: http : //developer.android.com/reference/android/app/backup/SharedPreferencesBackupHelper.html

Here's the example: Android SharedPreferences Backup Not Working示例如下: Android SharedPreferences Backup Not Working

Approach 4: use the application install time.方法四:利用应用安装时间。

Since API level 9 (Android 2.3.2, 2.3.1, Android 2.3, GINGERBREAD) there are firstInstallTime and lastUpdateTime in PackageInfo .从 API 级别 9(Android 2.3.2、2.3.1、Android 2.3、GIGERBREAD)开始, PackageInfofirstInstallTimelastUpdateTime

To read more: How to get app install time from android阅读更多: 如何从安卓获取应用安装时间

After looking at all options in this and other threads, these are my findings查看此线程和其他线程中的所有选项后,这些是我的发现

Shared preferences, database Can be cleared in the android settings, lost after an app reinstall.共享首选项,数据库可以在android设置中清除,重新安装应用程序后丢失。 Can be backed up with android's backup mechanism and will be restored after a reinstall. 可以使用android的备份机制进行备份,重装后即可恢复。 Backup may not always be available, though should be on most devices备份可能并不总是可用,但应该在大多数设备上

External storage (writing to a file) Not affected by a clear from the settings or a reinstall if we don't write to the application's private directory .外部存储(写入文件)如果我们不写入应用程序的私有目录,则不受设置清除或重新安装的影响 But: requires you to ask the user for their permission at runtime in newer android versions, so this is probably only feasible if you need that permission anyways.但是: 要求您在较新的 android 版本中在运行时询问用户的许可,因此这可能仅在您需要该许可时才可行。 Can also be backed up.也可以备份。

PackageInfo.firstInstallTime Is reset after a reinstall but stable across updates PackageInfo.firstInstallTime在重新安装后重置,但在更新中保持稳定

Sign in to some account Doesn't matter if it's their Google account via Firebase or one in your own server: the trial is bound to the account.登录某个帐户无论是他们通过 Firebase 的 Google 帐户还是您自己的服务器中的帐户都没有关系:试用版与该帐户绑定。 Making a new account will reset the trial.创建新帐户将重置试用。

Firebase anonymous sign in You can sign in a user anonymously and store data for them in Firebase. Firebase 匿名登录您可以匿名登录用户并将他们的数据存储在 Firebase 中。 But apparently a reinstall of the app and maybe other undocumented events may give the user a new anonymous ID , resetting their trial time.显然重新安装应用程序和其他未记录的事件可能会给用户一个新的匿名 ID ,重置他们的试用时间。 (Google themselves don't provide much documentation on this) (谷歌自己没有提供太多关于此的文档)

ANDROID_ID May not be available and may change under certain circumstances , eg factory reset. ANDROID_ID可能不可用并且在某些情况下可能会更改,例如恢复出厂设置。 The opinions on whether it's a good idea to use this to identify devices seem to differ.关于使用它来识别设备是否是个好主意的意见似乎有所不同。

Play Advertising ID May be reset by the user.播放广告 ID可由用户重置。 May be disabled by the user by opting out of ad tracking. 用户可以通过选择退出广告跟踪来禁用。

InstanceID Reset on a reinstall . InstanceID 在重新安装时重置 Reset in case of a security event.发生安全事件时重置。 Can be reset by your app.可以由您的应用程序重置。

Which (combination of) methods work for you depends on your app and on how much effort you think the average John will put into gaining another trial period.哪种(组合)方法适合您取决于您​​的应用程序以及您认为平均 John 会为获得另一个试用期而付出的努力。 I would recommend steering clear of using only anonymous Firebase and Advertising ID due to their instability.由于它们的不稳定性,我建议避免使用匿名 Firebase 和广告 ID。 A multi-factor approach seems like it will yield the best results.多因素方法似乎会产生最好的结果。 Which factors are available to you depends on you app and its permissions.您可以使用哪些因素取决于您的应用及其权限。

For my own app I found shared preferences + firstInstallTime + backup of the preferences to be the least intrusive but also effective enough method.对于我自己的应用程序,我发现共享首选项 + firstInstallTime + 首选项备份是干扰最少但也足够有效的方法。 You have to make sure you only request a backup after checking and storing the trial start time in the shared preferences.您必须确保仅在共享首选项中检查并存储试用开始时间后才请求备份。 Values in the shared Prefs must have precedence over the firstInstallTime.共享首选项中的值必须优先于 firstInstallTime。 Then user has to reinstall the app, run it once and then clear the app's data to reset the trial, which is quite a lot of work.然后用户必须重新安装应用程序,运行一次,然后清除应用程序的数据以重置试用版,这是相当多的工作。 On devices without a backup transport the user can reset the trial by simply reinstalling, though.不过,在没有备份传输的设备上,用户只需重新安装即可重置试用版。

I've made that approach available as an extensible library .我已将该方法作为可扩展库提供

Now in the recent version of android free trial subscription has been added, you can unlock all your app's features only after buying the subscription within app for a free trial period.现在在最新版本的android免费试用订阅中添加了,只有在应用程序内购买订阅免费试用期后,您才能解锁应用程序的所有功能。 This will let the user to use your app for a trial period , if the app is still uninstalled after the trial period then the subscription money will be transferred to you.这将让用户在试用期内使用您的应用程序,如果在试用期后仍卸载该应用程序,则订阅费用将转移给您。 I have not tried , but just sharing an idea.我没有尝试过,只是分享一个想法。

Here's documentation 这是文档

In my opinion, the best way to do this is to simply use the Firebase Realtime Database:在我看来,最好的方法是简单地使用 Firebase 实时数据库:

1) Add Firebase support to your app 1) 为您的应用添加 Firebase 支持

2) Select 'Anonymous authentication' so that the user doesn't have to signup or even know what you're doing. 2) 选择“匿名身份验证”,这样用户就不必注册,甚至不必知道您在做什么。 This is guaranteed to link to the currently authenticated user account and so will work across devices.这保证链接到当前经过身份验证的用户帐户,因此可以跨设备工作。

3) Use the Realtime Database API to set a value for 'installed_date'. 3) 使用实时数据库 API 为“installed_date”设置一个值。 At launch time, simply retrieve this value and use this.在启动时,只需检索此值并使用它。

I've done the same and it works great.我也做了同样的事情,而且效果很好。 I was able to test this across uninstall / re-installs and the value in the realtime database remains the same.我能够在卸载/重新安装时对此进行测试,并且实时数据库中的值保持不变。 This way your trial period works across multiple user devices.这样,您的试用期可以跨多个用户设备运行。 You can even version your install_date so that the app 'resets' the Trial date for each new major release.您甚至可以对 install_date 进行版本控制,以便应用程序“重置”每个新主要版本的试用日期。

UPDATE : After testing a bit more, it seems anonymous Firebase seems to allocate a different ID in case you've got different devices and is not guaranteed between re-installs :/ The only guaranteed way is to use Firebase but tie it to their google account.更新:经过更多测试后,似乎匿名 Firebase 似乎分配了不同的 ID,以防您有不同的设备,并且在重新安装之间无法保证:/ 唯一有保证的方法是使用 Firebase,但将其绑定到他们的谷歌帐户。 This should work, but would require an extra step where the user first needs to login / signup.这应该可以工作,但需要一个额外的步骤,用户首先需要登录/注册。

I've thus far ended up with a slightly less elegant approach of simply checking against backed-up preferences and a date stored in preferences upon install.到目前为止,我最终采用了一种稍微不太优雅的方法,即在安装时简单地检查备份的首选项和存储在首选项中的日期。 This works for data-centric apps where it's pointless for a person to re-install the app and re-enter all the data previously added, but would not work for a simple game.这适用于以数据为中心的应用程序,人们重新安装应用程序并重新输入之前添加的所有数据毫无意义,但不适用于简单的游戏。

By definition, all paid Android apps on the market can be evaluated for 24 hours after purchase.根据定义,市场上所有付费的 Android 应用程序都可以在购买后 24 小时内进行评估。

There's an 'Uninstall and Refund' button which changes to 'Uninstall' after 24 hours.有一个“卸载和退款”按钮,24 小时后会变成“卸载”。

I'd argue this button is way too prominent!我认为这个按钮太突出了!

I come across this question while searching for the same problem, i think we can utilize free date api like http://www.timeapi.org/utc/now or some other date api to check for expiry of trail app.我在搜索同样的问题时遇到了这个问题,我认为我们可以使用免费的日期 api 像http://www.timeapi.org/utc/now或其他一些日期 api 来检查跟踪应用程序是否到期。 this way is efficient if you wish to deliver the demo and worried about payment and require fix tenure demo.如果您希望交付演示并担心付款并需要修复任期演示,这种方式是有效的。 :) :)

find the code below找到下面的代码

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

}
}

its working solution.....它的工作解决方案......

Here is how i went about mine, I created 2 apps one with trial activity the other without,这是我如何进行我的,我创建了 2 个应用程序,一个带有试用活动,另一个没有,

i uploaded the one without trial activity to play store as paid app,我上传了一个没有试用活动的商店作为付费应用程序,

and the one with trial activity as free app.以及作为免费应用程序的试用活动。

The free app on first launch has options for trial and store purchase, if the user select store purchase it redirects to the store for the user to purchase but if the user clicks trial it take them to the trial activity首次发布的免费应用程序有试用和商店购买选项,如果用户选择商店购买,它会重定向到商店供用户购买,但如果用户点击试用,则会将他们带到试用活动

NB: I used option 3 like @snctln but with modifications注意:我使用了像@snctln 这样的选项 3 但做了修改

first , i did not depend on the device time, i got my time from the php file that does the trial registration to the db,首先,我不依赖于设备时间,我从进行试用注册到数据库的 php 文件中获得了时间,

secondly , i used the device serial number to uniquely identify each device,其次,我使用设备序列号来唯一标识每个设备,

lastly , the app depends on the time value returned from the server connection not its own time so the system can only be circumvented if the device serial number is changed, which is quite stressful for a user.最后,应用程序依赖于从服务器连接返回的时间值而不是它自己的时间,因此只有更改设备序列号才能绕过系统,这对用户来说是相当有压力的。

so here goes my code (for the Trial activity):所以这是我的代码(用于试用活动):

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


}

My php file looks like this (its a REST-slim technology):我的 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;
        }

    }

then on the main activity i use the shared preference (installDate created in trial activity) to monitor the number of days remaining and if the days are over i block the main activity UI with a message that takes them to the store to purchase.然后在主要活动上,我使用共享首选项(在试用活动中创建的安装日期)来监控剩余天数,如果天数结束,我会用一条消息阻止主要活动用户界面,将他们带到商店购买。

The only down side i see here is that if a Rogue user buys the paid app and decides to share with apps like Zender, file share or even host the apk file directly on a server for people to download for free.我在这里看到的唯一缺点是,如果Rogue 用户购买付费应用程序并决定与 Zender 等应用程序共享,文件共享甚至直接在服务器上托管 apk 文件供人们免费下载。 But am sure i will soon edit this answer with a solution to that or a link to the solution.但我相信我很快就会用解决方案或解决方案的链接来编辑这个答案。

Hope this saves a soul...some day希望这能拯救一个灵魂......有一天

Happy Coding...快乐编码...

@snctln option 3 can be easily done adding a php file to a web server with php and mysql installed as many of them have. @snctln选项 3 可以很容易地完成,将一个 php 文件添加到一个安装了 php 和 mysql 的 web 服务器。

From the Android side an identifier (the device ID, google account o whatever you want) is passed as argument in the URL using HttpURLConnection and the php returns the date of the first install if it exist in the table or it inserts a new row and it return the current date.从 Android 端,一个标识符(设备 ID、谷歌帐户或任何你想要的)作为参数在 URL 中使用 HttpURLConnection 传递,并且 php 返回第一次安装的日期,如果它存在于表中或者它插入一个新行和它返回当前日期。

It works fine for me.这对我来说可以。

If I have time I will post some code !如果我有时间,我会发布一些代码!

Good Luck !祝你好运 !

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM