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

我有一个应用程序,我想作为付费应用程序打市场。 我想有其他的版本,这将是一个“试用”版本,有5天的时间限制吗?

我怎么能这样做?

目前大多数开发人员使用以下3种技术之一来完成此任务。

第一种方法很容易绕过,第一次运行应用程序时,将date/时间保存到文件,数据库或共享首选项,每次运行应用程序后,检查试用期是否已结束。 这很容易规避,因为卸载和重新安装将允许用户再试用一段时间。

第二种方法很难规避,但仍然可以避免。 使用硬编码的定时炸弹。 基本上用这种方法,你将是试用版的结束date的硬编码,所有下载和使用应用程序的用户将不能同时使用该应用程序。 我已经使用了这种方法,因为它很容易实现,而且在大多数情况下,我并不想经历第三种技术的麻烦。 用户可以通过手动更改手机上的date来规避这种情况,但大多数用户不会经历麻烦来做这样的事情。

第三种技巧是我所听到的唯一真正能够完成你想要做的事情的方法。 您将不得不build立一个服务器,然后每当您的应用程序启动您的应用程序将电话唯一标识符发送到服务器。 如果服务器没有该电话号码的条目,那么它会创build一个新的并logging时间。 如果服务器有一个电话号码的条目,那么它会做一个简单的检查,看看试用期是否已经过期。 然后将试用到期检查的结果传回给您的应用程序。 这种方法不应该是可以避免的,但确实需要build立一个networking服务器等等。

在onCreate中进行这些检查总是一个很好的做法。 如果到期已结束,则会popup一个AlertDialog,并提供市场链接至完整版本的应用程序。 只包含一个“确定”button,一旦用户点击“确定”,就打电话给“完成()”来结束活动。

这是一个老问题,但无论如何,也许这会帮助别人。

如果你想用最简单的方法如果应用程序被卸载/重新安装或用户手动更改设备的date将失败 ),这是如何:

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

我开发了一个Android试用SDK ,您可以简单地将其放入Android Studio项目中,并将负责所有服务器端pipe理(包括离线宽限期)。

简单地使用它

添加库到你的主模块的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); } 

添加一个callback处理程序:

 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开发人员仪表板中find 。

嘿家伙这个问题和snctln的答案激励我工作的基础上方法3作为我的学士论文的解决scheme。 我知道目前的状况不是为了生产的使用,但我很想听听你的想法! 你会使用这样一个系统吗? 你想看到它作为一个云服务(没有configuration服务器的麻烦)? 担心安全问题或稳定性原因? 一旦我完成了学士学位的程序,我想继续在这个软件上工作。 所以,现在是我需要您的反馈的时间了!

源代码托pipe在GitHub上https://github.com/MaChristmann/mobile-trial

有关系统的一些信息: – 系统有三个部分,一个Android库,一个node.js服务器和一个用于pipe理多个试用版应用程序和发行商/开发者账户的configuration器。

  • 它只支持基于时间的试用,它使用您的(玩商店或其他)帐户,而不是电话ID。

  • 对于Android库,它基于Google Play许可证validation库。 我修改它连接到node.js服务器,另外,库试图识别用户是否更改了系统date。 它还可以在AESencryption的共享首选项中caching检索到的试用许可证。 您可以使用configuration程序configurationcaching的有效时间。 如果用户“清除数据”,库将强制进行服务器端检查。

  • 服务器正在使用https,并且还在数字签名许可证检查响应中。 它还有一个用于CRUD试用版应用程序和用户(发布者和开发者)的API。 熟悉授权validation库开发人员可以在testing结果中的testing应用中testing他们的行为实现。 因此,您在configuration程序中可以明确地将您的许可证响应设置为“许可”,“未许可”或“服务器错误”。

  • 如果您更新您的应用程序与屁股踢新function,您可能希望每个人都可以再试一次。 在configuration程序中,您可以通过设置应该触发此操作的版本代码来为已过期的许可证用户续订试用许可证。 例如,用户在版本代码3上运行应用程序,并且您希望他尝试版本代码4的function。如果他更新应用程序或重新安装应用程序,则可以再次使用完整的试用期,因为服务器知道他最后一次尝试了哪个版本时间。

  • 一切都在Apache 2.0许可下

最简单和最好的方法是实现BackupSharedPreferences。

即使应用程序已卸载并重新安装,首选项也会保留。

只需将安装date保存为首选项即可。

这里的理论: http : //developer.android.com/reference/android/app/backup/SharedPreferencesBackupHelper.html

以下是示例: Android SharedPreferences备份不起作用

方法4:使用应用程序安装时间。

由于API级别9(Android 2.3.2,2.3.1,Android 2.3,GINGERBREAD)在PackageInfo有firstInstallTime和lastUpdateTime 。

阅读更多: 如何从android获得应用程序安装时间

现在在最新版本的Android免费试用版中已经添加了,只有在购买了应用内的订阅免费试用期后,才能解锁所有应用的function。 这将让用户使用您的应用程序的试用期,如果应用程序仍然试用期后卸载,那么订阅资金将转移给您。 我没有尝试过,只是分享一个想法。

这里是文档

在我看来,最好的方法是简单地使用Firebase实时数据库:

1)将Firebase支持添加到您的应用

2)select“匿名身份validation”,以便用户不必注册,甚至不知道自己在做什么。 这是保证链接到当前authentication的用户帐户,因此将跨设备工作。

3)使用Realtime Database API为“installed_date”设置一个值。 在启动时,只需检索这个值并使用它。

我也这样做了,效果很好。 我能够通过卸载/重新安装来testing这一点,并且实时数据库中的值保持不变。 这样您的试用期就可以在多个用户设备上运行。 你甚至可以对你的install_date进行版本化,以便应用程序可以重置每个新版本的试用date。

更新 :经过多次testing后,似乎匿名Firebase似乎分配了一个不同的ID,以防您有不同的设备,并且不能保证在重新安装之间:/唯一保证的方法是使用Firebase,但将其绑定到他们的谷歌帐户。 这应该工作,但需要额外的步骤,用户首先需要login/注册。

到目前为止,我已经结束了一个简单的检查备份偏好和安装时存储在首选项中的date稍微不太优雅的方法。 这适用于以数据为中心的应用程序,一个人重新安装应用程序并重新input之前添加的所有数据是毫无意义的,但对于简单的游戏则无效。

根据定义, 所有付费的Android应用程序可以在购买后24小时评估。

有一个“卸载和退款”button,在24小时后更改为“卸载”。

我认为这个button太突出了!

我遇到这个问题,同时寻找同样的问题,我认为我们可以利用自由dateapi,如http://www.timeapi.org/utc/now或其他dateapi来检查足迹应用程序到期。; 这种方式是有效的,如果你想提供演示,并担心付款,并要求修复权属演示。 🙂

find下面的代码

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

其工作解决scheme…..

看了这个和其他线程的所有选项后,这些都是我的发现

共享首选项,数据库可以在Android设置中清除,重新安装应用程序后丢失。 可以使用android的备份机制进行备份,并在重新安装后恢复。 备份可能不总是可用的,但应该在大多数设备上

外部存储(写入文件)不受设置中的清除影响,或者如果我们不写入应用程序的专用目录,则重新安装。 但是: 需要你在新的android版本中在运行时询问用户的权限 ,所以这可能只有在你需要权限时才可行。 也可以备份。

PackageInfo.firstInstallTime在重新安装后重置,但在更新期间保持稳定

login到某个帐户无论是通过Firebase提供的Google帐户,还是您自己的服务器上的Google帐户:试用都绑定到该帐户。 创build新帐户将重置试用版。

Firebase匿名login您可以匿名login用户,并将其数据存储在Firebase中。 但显然重新安装应用程序,也许其他未公开的事件可能会给用户一个新的匿名ID ,重置他们的试用时间。 (Google本身并没有提供太多的文档)

ANDROID_ID 可能无法使用,并且在某些情况下可能会更改 ,例如出厂重置。 关于用这个来识别设备是个好主意的看法似乎有所不同。

播放广告ID可能会被用户重置。 可能会被用户禁用,停用广告跟踪。

InstanceID 在重新安装时重置 。 在发生安全事件的情况下重置。 可以由您的应用程序重置。

哪种(组合)方法适合您,取决于您的应用程序以及您认为约翰将平均花费多less时间来获得另一个试用期。 我build议您不要使用匿名Firebase和广告ID,因为它们的不稳定性。 多因素的方法似乎会产生最好的结果。 哪些因素可用取决于您的应用程序及其权限。

对于我自己的应用程序,我发现共享首选项+ firstInstallTime +首选项的备份是最不干扰,但也是有效的方法。 您必须确保在检查并将试用开始时间存储在共享首选项中后才请求备份。 共享Prefs中的值必须优先于firstInstallTime。 然后用户必须重新安装应用程序,运行一次,然后清除应用程序的数据重置试用,这是相当多的工作。 在没有备份传输的设备上,用户可以通过重新安装来重置试用版。

我已经把这个方法作为一个可扩展的库来使用 。

这里是我如何去了解我的,我创build了2个应用程序之一与审判活动另一个没有,

我上传了一个没有试用活动的商店作为付费应用程序,

和免费应用程序的试用活动。

首次推出的免费应用程序有试用和商店购买的选项,如果用户select商店购买,它redirect到商店供用户购买,但如果用户点击试用,则将其带到试用活动

注意:我使用了像@snctln这样的选项3,但是进行了修改

首先 ,我不依赖于设备的时间,我得到了从试用注册到DB的PHP文件的时间,

其次 ,我使用设备序列号来唯一标识每个设备,

最后 ,应用程序依赖于从服务器连接返回的时间值,而不是自己的时间,所以如果设备序列号发生变化,系统只能被回避,这对用户来说是非常有压力的。

所以这里是我的代码(试用活动):

 package com.example.mypackage.my_app.Start_Activity.activity; import android.Manifest; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Color; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.v4.app.ActivityCompat; import android.support.v7.app.AppCompatActivity; import android.telephony.TelephonyManager; import android.view.KeyEvent; import android.widget.TextView; import com.android.volley.Request; import com.android.volley.RequestQueue; import com.android.volley.Response; import com.android.volley.VolleyError; import com.android.volley.toolbox.JsonObjectRequest; import com.android.volley.toolbox.Volley; import com.example.onlinewisdom.cbn_app.R; import com.example.mypackage.my_app.Start_Activity.app.Config; import com.example.mypackage.my_app.Start_Activity.data.TrialData; import com.example.mypackage.my_app.Start_Activity.helper.connection.Connection; import com.google.gson.Gson; import org.json.JSONObject; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Map; import cn.pedant.SweetAlert.SweetAlertDialog; public class Trial extends AppCompatActivity { Connection check; SweetAlertDialog pDialog; TextView tvPleaseWait; private static final int MY_PERMISSIONS_REQUEST_READ_PHONE_STATE = 0; String BASE_URL = Config.BASE_URL; String BASE_URL2 = BASE_URL+ "/register_trial/"; //http://ur link to ur API //KEY public static final String KEY_IMEI = "IMEINumber"; private final SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd"); private final long ONE_DAY = 24 * 60 * 60 * 1000; SharedPreferences preferences; String installDate; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_trial); preferences = getPreferences(MODE_PRIVATE); installDate = preferences.getString("InstallDate", null); pDialog = new SweetAlertDialog(this, SweetAlertDialog.PROGRESS_TYPE); pDialog.getProgressHelper().setBarColor(Color.parseColor("#008753")); pDialog.setTitleText("Loading..."); pDialog.setCancelable(false); tvPleaseWait = (TextView) findViewById(R.id.tvPleaseWait); tvPleaseWait.setText(""); if(installDate == null) { //register app for trial animateLoader(true); CheckConnection(); } else { //go to main activity and verify there if trial period is over Intent i = new Intent(Trial.this, MainActivity.class); startActivity(i); // close this activity finish(); } } public void CheckConnection() { check = new Connection(this); if (check.isConnected()) { //trigger 'loadIMEI' loadIMEI(); } else { errorAlert("Check Connection", "Network is not detected"); tvPleaseWait.setText("Network is not detected"); animateLoader(false); } } public boolean onKeyDown(int keyCode, KeyEvent event) { //Changes 'back' button action if (keyCode == KeyEvent.KEYCODE_BACK) { finish(); } return true; } public void animateLoader(boolean visibility) { if (visibility) pDialog.show(); else pDialog.hide(); } public void errorAlert(String title, String msg) { new SweetAlertDialog(this, SweetAlertDialog.ERROR_TYPE) .setTitleText(title) .setContentText(msg) .show(); } /** * Called when the 'loadIMEI' function is triggered. */ public void loadIMEI() { // Check if the READ_PHONE_STATE permission is already available. if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) { // READ_PHONE_STATE permission has not been granted. requestReadPhoneStatePermission(); } else { // READ_PHONE_STATE permission is already been granted. doPermissionGrantedStuffs(); } } /** * Requests the READ_PHONE_STATE permission. * If the permission has been denied previously, a dialog will prompt the user to grant the * permission, otherwise it is requested directly. */ private void requestReadPhoneStatePermission() { if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.READ_PHONE_STATE)) { // Provide an additional rationale to the user if the permission was not granted // and the user would benefit from additional context for the use of the permission. // For example if the user has previously denied the permission. new AlertDialog.Builder(Trial.this) .setTitle("Permission Request") .setMessage(getString(R.string.permission_read_phone_state_rationale)) .setCancelable(false) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { //re-request ActivityCompat.requestPermissions(Trial.this, new String[]{Manifest.permission.READ_PHONE_STATE}, MY_PERMISSIONS_REQUEST_READ_PHONE_STATE); } }) .setIcon(R.drawable.warning_sigh) .show(); } else { // READ_PHONE_STATE permission has not been granted yet. Request it directly. ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_PHONE_STATE}, MY_PERMISSIONS_REQUEST_READ_PHONE_STATE); } } /** * Callback received when a permissions request has been completed. */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == MY_PERMISSIONS_REQUEST_READ_PHONE_STATE) { // Received permission result for READ_PHONE_STATE permission.est."); // Check if the only required permission has been granted if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { // READ_PHONE_STATE permission has been granted, proceed with displaying IMEI Number //alertAlert(getString(R.string.permision_available_read_phone_state)); doPermissionGrantedStuffs(); } else { alertAlert(getString(R.string.permissions_not_granted_read_phone_state)); } } } private void alertAlert(String msg) { new AlertDialog.Builder(Trial.this) .setTitle("Permission Request") .setMessage(msg) .setCancelable(false) .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { // do somthing here } }) .setIcon(R.drawable.warning_sigh) .show(); } private void successAlert(String msg) { new SweetAlertDialog(this, SweetAlertDialog.SUCCESS_TYPE) .setTitleText("Success") .setContentText(msg) .setConfirmText("Ok") .setConfirmClickListener(new SweetAlertDialog.OnSweetClickListener() { @Override public void onClick(SweetAlertDialog sDialog) { sDialog.dismissWithAnimation(); // Prepare intent which is to be triggered //Intent i = new Intent(Trial.this, MainActivity.class); //startActivity(i); } }) .show(); } public void doPermissionGrantedStuffs() { //Have an object of TelephonyManager TelephonyManager tm =(TelephonyManager)getSystemService(Context.TELEPHONY_SERVICE); //Get IMEI Number of Phone //////////////// for this example i only need the IMEI String IMEINumber = tm.getDeviceId(); /************************************************ * ********************************************** * This is just an icing on the cake * the following are other children of TELEPHONY_SERVICE * //Get Subscriber ID String subscriberID=tm.getDeviceId(); //Get SIM Serial Number String SIMSerialNumber=tm.getSimSerialNumber(); //Get Network Country ISO Code String networkCountryISO=tm.getNetworkCountryIso(); //Get SIM Country ISO Code String SIMCountryISO=tm.getSimCountryIso(); //Get the device software version String softwareVersion=tm.getDeviceSoftwareVersion() //Get the Voice mail number String voiceMailNumber=tm.getVoiceMailNumber(); //Get the Phone Type CDMA/GSM/NONE int phoneType=tm.getPhoneType(); switch (phoneType) { case (TelephonyManager.PHONE_TYPE_CDMA): // your code break; case (TelephonyManager.PHONE_TYPE_GSM) // your code break; case (TelephonyManager.PHONE_TYPE_NONE): // your code break; } //Find whether the Phone is in Roaming, returns true if in roaming boolean isRoaming=tm.isNetworkRoaming(); if(isRoaming) phoneDetails+="\nIs In Roaming : "+"YES"; else phoneDetails+="\nIs In Roaming : "+"NO"; //Get the SIM state int SIMState=tm.getSimState(); switch(SIMState) { case TelephonyManager.SIM_STATE_ABSENT : // your code break; case TelephonyManager.SIM_STATE_NETWORK_LOCKED : // your code break; case TelephonyManager.SIM_STATE_PIN_REQUIRED : // your code break; case TelephonyManager.SIM_STATE_PUK_REQUIRED : // your code break; case TelephonyManager.SIM_STATE_READY : // your code break; case TelephonyManager.SIM_STATE_UNKNOWN : // your code break; } */ // Now read the desired content to a textview. //tvPleaseWait.setText(IMEINumber); UserTrialRegistrationTask(IMEINumber); } /** * Represents an asynchronous login task used to authenticate * the user. */ private void UserTrialRegistrationTask(final String IMEINumber) { JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(Request.Method.GET, BASE_URL2+IMEINumber, null, new Response.Listener<JSONObject>() { @Override public void onResponse(JSONObject response) { Gson gson = new Gson(); TrialData result = gson.fromJson(String.valueOf(response), TrialData.class); animateLoader(false); if ("true".equals(result.getError())) { errorAlert("Error", result.getResult()); tvPleaseWait.setText("Unknown Error"); } else if ("false".equals(result.getError())) { //already created install/trial_start date using the server // so just getting the date called back Date before = null; try { before = (Date)formatter.parse(result.getResult()); } catch (ParseException e) { e.printStackTrace(); } Date now = new Date(); assert before != null; long diff = now.getTime() - before.getTime(); long days = diff / ONE_DAY; // save the date received SharedPreferences.Editor editor = preferences.edit(); editor.putString("InstallDate", String.valueOf(days)); // Commit the edits! editor.apply(); //go to main activity and verify there if trial period is over Intent i = new Intent(Trial.this, MainActivity.class); startActivity(i); // close this activity finish(); //successAlert(String.valueOf(days)); //if(days > 5) { // More than 5 days? // Expired !!! //} } } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { animateLoader(false); //errorAlert(error.toString()); errorAlert("Check Connection", "Could not establish a network connection."); tvPleaseWait.setText("Network is not detected"); } }) { protected Map<String, String> getParams() { Map<String, String> params = new HashMap<String, String>(); params.put(KEY_IMEI, IMEINumber); return params; } }; RequestQueue requestQueue = Volley.newRequestQueue(this); requestQueue.add(jsonObjectRequest); } } 

我的php文件看起来像这样(它是一个REST-slim技术):

 /** * registerTrial */ public function registerTrial($IMEINumber) { //check if $IMEINumber already exist // Instantiate DBH $DBH = new PDO_Wrapper(); $DBH->query("SELECT date_reg FROM trials WHERE device_id = :IMEINumber"); $DBH->bind(':IMEINumber', $IMEINumber); // DETERMINE HOW MANY ROWS OF RESULTS WE GOT $totalRows_registered = $DBH->rowCount(); // DETERMINE HOW MANY ROWS OF RESULTS WE GOT $results = $DBH->resultset(); if (!$IMEINumber) { return 'Device serial number could not be determined.'; } else if ($totalRows_registered > 0) { $results = $results[0]; $results = $results['date_reg']; return $results; } else { // Instantiate variables $trial_unique_id = es_generate_guid(60); $time_reg = date('H:i:s'); $date_reg = date('Ym-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; } } 

然后在主要活动中,我使用共享首选项(在试用活动中创build的installDate)来监控剩余天数,如果天数已经结束,则使用将消息带到商店购买的消息来阻止主要活动UI。

唯一不利的一面是,如果一个stream氓用户购买付费应用程序,并决定与Zender等应用程序共享文件共享,甚至直接在服务器上托pipeapk文件,供用户免费下载。 但是,我确定我很快就会编辑这个答案,并提供解决scheme的解决scheme。

希望这能拯救一个灵魂……有一天

快乐编码

@snctln选项3可以很容易地完成添加一个PHP文件到一个Web服务器与PHP和MySQL的安装,因为他们有很多。

在Android端,使用HttpURLConnection作为parameter passing标识符(设备ID,Google帐户,任何你想要的)作为参数,如果PHP存在于表中,则返回第一次安装的date,或者插入一个新行它返回当前date。

它对我来说工作正常。

如果我有时间,我会张贴一些代码!

祝你好运 !