创建一个在固定时间段后过期的 Android 试用应用程序
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/995719/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Creating an Android trial application that expires after a fixed time period
提问by Tom
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?
我该怎么做呢?
回答by snctln
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 identifierto the server. If the server does not have an entry for that phone id then it makes a new one and notes the time. 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. 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.
第三种技术是我听说的真正能够完成你想做的事情的唯一方法。您必须设置一个服务器,然后每当您的应用程序启动时,您的应用程序都会将手机唯一标识符发送到服务器。如果服务器没有该电话 ID 的条目,则它会创建一个新条目并记录时间。如果服务器确实有电话 ID 的条目,那么它会做一个简单的检查以查看试用期是否已过期。然后,它会将试用期到期检查的结果返回给您的应用程序。这种方法不应该是可以规避的,但确实需要设置网络服务器等。
It is always good practice to do these checks in the onCreate. If the expiration has ended popup an AlertDialog with a market linkto the full version of the app. Only include an "OK" button, and once the user clicks on "OK" make a call to "finish()" to end the activity.
在 onCreate 中进行这些检查总是好的做法。如果到期已结束,则会弹出一个 AlertDialog,其中包含指向该应用程序完整版本的市场链接。只包含一个“OK”按钮,一旦用户点击“OK”,就调用“finish()”来结束活动。
回答by Nick
I've developed a Android Trial SDKwhich 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);
Your app key and trial SKU can be found in your Trialy developer dashboard.
要开始试用,请调用mTrialy.startTrial("YOUR_TRIAL_SKU", mTrialyCallback);
您的应用程序密钥和试用版 SKU,可在您的Trialy 开发人员仪表板 中找到。
回答by Caner
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 ifthe 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 !!!
}
}
...
}
回答by Martin Christmann
This question and the answer of snctlninspired me to work on a solution based on method 3 as my bachelor thesis. 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?
这个问题和snctln的回答激发了我基于方法 3 的解决方案作为我的学士论文。我知道当前状态不适用于生产用途,但我很想听听您的看法!你会使用这样的系统吗?您是否希望将其视为云服务(在配置服务器时没有问题)?担心安全问题或稳定性原因?
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.
For Android library it is based on the Google Play licensing verification library. I modified it to connect to the node.js server and additionally the library tries to recognize if a user changed the system date. It also caches a retrieved trial-license in AES encrypted Shared Preferences. 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. It has also an API for CRUD trial apps and users (publisher and developer). 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.
Everything is under the Apache 2.0 license
它仅支持基于时间的试用,并且使用您的(Play 商店或其他)帐户而不是手机 ID。
对于 Android 库,它基于 Google Play 许可验证库。我修改了它以连接到 node.js 服务器,此外该库还尝试识别用户是否更改了系统日期。它还在 AES 加密的共享首选项中缓存检索到的试用许可证。您可以使用配置器配置缓存的有效时间。如果用户“清除数据”,图书馆将强制进行服务器端检查。
服务器正在使用 https 并对许可证检查响应进行数字签名。它还具有用于 CRUD 试用应用程序和用户(发布者和开发者)的 API。类似于许可验证库,开发人员可以使用测试结果在试用应用程序中测试他们的行为实现。因此,您可以在配置器中将许可响应明确设置为“已许可”、“未许可”或“服务器错误”。
如果您使用令人兴奋的新功能更新您的应用程序,您可能希望每个人都可以再试一次。在配置器中,您可以通过设置应触发此操作的版本代码来为许可证过期的用户续订试用许可证。例如,用户在版本代码 3 上运行您的应用程序,而您希望他尝试版本代码 4 的功能。如果他更新应用程序或重新安装它,他可以再次使用完整的试用期,因为服务器知道他上次尝试的是哪个版本时间。
一切都在 Apache 2.0 许可下
回答by pstorli
The easiest and bestway 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
回答by 18446744073709551615
Approach 4: use the application install time.
方法四:利用应用安装时间。
Since API level 9 (Android 2.3.2, 2.3.1, Android 2.3, GINGERBREAD) there are firstInstallTimeand lastUpdateTimein PackageInfo
.
由于API级9(2.3.2的Android,2.3.1,机器人2.3,姜饼)有firstInstallTime和lastUpdateTime在PackageInfo
。
To read more: How to get app install time from android
阅读更多: 如何从安卓获取应用安装时间
回答by Vins
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. 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.
现在在最新版本的android免费试用订阅中添加了,只有在应用程序内购买订阅免费试用期后,您才能解锁应用程序的所有功能。这将让用户在试用期内使用您的应用程序,如果在试用期后仍卸载该应用程序,则订阅费用将转移给您。我没有尝试过,只是分享一个想法。
回答by strangetimes
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. This is guaranteed to link to the currently authenticated user account and so will work across devices.
2) 选择“匿名身份验证”,这样用户就不必注册,甚至不必知道您在做什么。这保证链接到当前经过身份验证的用户帐户,因此可以跨设备工作。
3) Use the Realtime Database API to set a value for 'installed_date'. At launch time, simply retrieve this value and use this.
3) 使用实时数据库 API 为“installed_date”设置一个值。在启动时,只需检索此值并使用它。
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. This should work, but would require an extra step where the user first needs to login / signup.
更新:经过更多测试后,似乎匿名 Firebase 似乎分配了不同的 ID,以防您有不同的设备,并且在重新安装之间无法保证:/ 唯一有保证的方法是使用 Firebase,但将其绑定到他们的谷歌帐户。这应该可以工作,但需要一个额外的步骤,用户首先需要登录/注册。
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 Fabian Streitel
After looking at all options in this and other threads, these are my findings
查看此线程和其他线程中的所有选项后,这些是我的发现
Shared preferences, databaseCan be cleared in the android settings, lost after an app reinstall. Can be backed up with android's backup mechanismand will be restored after a reinstall. Backup may not always be available, though should be on most devices
共享首选项,数据库可以在android设置中清除,重新安装应用程序后丢失。可以使用android的备份机制进行备份,重装后即可恢复。备份可能并不总是可用,但应该在大多数设备上
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 runtimein newer android versions, so this is probably only feasible if you need that permission anyways. Can also be backed up.
外部存储(写入文件)如果我们不写入应用程序的私有目录,则不受设置清除或重新安装的影响。但是:要求您在较新的 android 版本中在运行时询问用户的许可,因此这可能仅在您需要该许可时才可行。也可以备份。
PackageInfo.firstInstallTimeIs reset after a reinstall but stable across updates
PackageInfo.firstInstallTime在重新安装后重置,但在更新中保持稳定
Sign in to some accountDoesn't matter if it's their Google account via Firebase or one in your own server: the trial is bound to the account. Making a new account will reset the trial.
登录某个帐户无论是他们通过 Firebase 的 Google 帐户还是您自己的服务器中的帐户都没有关系:试用版与该帐户绑定。创建新帐户将重置试用。
Firebase anonymous sign inYou can sign in a user anonymously and store data for them in 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. (Google themselves don't provide much documentation on this)
Firebase 匿名登录您可以匿名登录用户并将他们的数据存储在 Firebase 中。但显然重新安装应用程序和其他未记录的事件可能会给用户一个新的匿名 ID,重置他们的试用时间。(谷歌自己没有提供太多关于此的文档)
ANDROID_IDMay not be available and may change under certain circumstances, e.g factory reset. The opinions on whether it's a good idea to use this to identify devices seem to differ.
ANDROID_ID可能不可用并且在某些情况下可能会更改,例如恢复出厂设置。关于使用它来识别设备是否是个好主意的意见似乎有所不同。
Play Advertising IDMay be reset by the user. May be disabled by the user by opting out of ad tracking.
播放广告 ID可由用户重置。用户可以通过选择退出广告跟踪来禁用。
InstanceIDReset on a reinstall. Reset in case of a security event. Can be reset by your app.
InstanceID在重新安装时重置。发生安全事件时重置。可以由您的应用程序重置。
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. I would recommend steering clear of using onlyanonymous Firebase and Advertising ID due to their instability. 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.
哪种(组合)方法适合您取决于您的应用程序以及您认为平均 John 会为获得另一个试用期而付出的努力。由于它们的不稳定性,我建议避免仅使用匿名 Firebase 和广告 ID。多因素方法似乎会产生最好的结果。您可以使用哪些因素取决于您的应用及其权限。
For my own app I found shared preferences + firstInstallTime + backup of the preferences to be the least intrusive but also effective enough method. 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. 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.
对于我自己的应用程序,我发现共享首选项 + firstInstallTime + 首选项备份是干扰最少但也足够有效的方法。您必须确保仅在共享首选项中检查并存储试用开始时间后才请求备份。共享首选项中的值必须优先于 firstInstallTime。然后用户必须重新安装应用程序,运行一次,然后清除应用程序的数据以重置试用版,这是相当多的工作。不过,在没有备份传输的设备上,用户只需重新安装即可重置试用版。
I've made that approach available as an extensible library.
我已将该方法作为可扩展库提供。
回答by RQube
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/nowor some other date api to check for expiry of trail app. this way is efficient if you wish to deliver the demo and worried about payment and require fix tenure demo. :)
我在搜索同样的问题时遇到了这个问题,我认为我们可以使用免费的日期 api 像http://www.timeapi.org/utc/now或其他一些日期 api 来检查跟踪应用程序是否到期。如果您希望交付演示并担心付款并需要修复任期演示,这种方式是有效的。:)
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.....
它的工作解决方案......