I'm currently doing an basic math app. The app is very basic, the player is given an easy equation to calculate. For any correct answer scores will be added to the player. If incorrect it will reset to 0. I have set 3 game difficulty-stages: Easy, Medium & Hard. To be able to play Medium-difficulty the player has to finish the Easy stage - example by setting a certain amount of points needed to be reached to open the next level.
Problem: One way I tried was to set a "lock" in the PlayActivity.java in this piece of code:
case R.id.mediumButton:
startActivity(new Intent(this, MediumActivity.class));
break;
this by simply trying to pass data of the Current Score from the EasyActivity.java and then set a limit of ex. 50p by using the if-statement. This would then prevent startActivity(new Intent(this, MediumActivity.class)); from getting executted until the points are achieved. But I couldn't get the codes correct so that I could pass Current Scores from the EasyActivity.java to Playactivity.java.
Since I got nowhere with the above I tried another approach to this problem. I tried instead to use the High Score data stored (check below). Of course this is not the full code, just showing for context to give you guys a hint of what I did. Any ideas?
case R.id.mediumButton:
int score = 50p
if (score > highscore)
startActivity(new Intent(this, MediumActivity.class));
break;
.
SOLUTION:
By using the shared preferences, which stores my highscore in the HighScoreActivity, I retrieved the data and used that with an if-statement to set a "lock" for Medium & Hard stage. This is the code I used (I also added a toast to inform the player about the conditions for the different stages):
case R.id.mediumButton:
if (storedEasyHighScore >= 150) {
startActivity(new Intent(this, MediumActivity.class));
} else {
Toast mediumButtonToast = Toast.makeText(getApplicationContext(), "LEVEL LOCKED!\nEASY-level: Min. 150p required!", Toast.LENGTH_LONG);
TextView toastMessage = (TextView) mediumButtonToast.getView().findViewById(android.R.id.message);
toastMessage.setTextColor(Color.WHITE);
toastMessage.setTextSize(20);
mediumButtonToast.show();
}
break;
case R.id.hardButton:
if ((storedEasyHighScore >= 150) && (storedMediumHighScore >= 300)) {
startActivity(new Intent(this, HardActivity.class));
}else{
Toast mediumButtonToast = Toast.makeText(getApplicationContext(), "LEVEL LOCKED!\nEASY-level: Min. 150p required.\nMEDIUM-level: Min. 300p required.", Toast.LENGTH_LONG);
TextView toastMessage = (TextView) mediumButtonToast.getView().findViewById(android.R.id.message);
toastMessage.setTextColor(Color.WHITE);
toastMessage.setTextSize(20);
mediumButtonToast.show();
}
break;
In easy activity whenever calling playActivity pass the current score.
Intent intent = new Intent(this, playActivity.class);
intent.putExtra("key",value);
startActivity(intent);
In PlayActivity.java get score and initialize it:
Intent intent = getIntent();
int score= intent.getIntExtra("key")
In PlayActivity.java, get highscore from sharedPref, set buttons Enabled(false) and set disabled button's color Color.RED:
#Override
protected void onCreate(Bundle savedInstanceState) {
....
...
SharedPreferences sharedPrefsHighScore = getSharedPreferences("Prefs_HighScore",MODE_PRIVATE);
SharedPreferences.Editor editorScore = sharedPrefsHighScore.edit();
int storedHighScore = sharedPrefsHighScore.getInt("highScore",0);
//fetching button id to work as button click
Button easyButton = (Button) findViewById(R.id.easyButton);
Button mediumButton = (Button) findViewById(R.id.mediumButton);
Button hardButton = (Button) findViewById(R.id.hardButton);
if(storedHighScore > 100) {
mediumButton.setEnabled(true);
hardButton.setEnabled(true);
} else if(storedHighScore > 50) {
mediumButton.setEnabled(true);
hardButton.setEnabled(false);
hardButton.setBackgroundColor(Color.RED);
} else {
mediumButton.setEnabled(false);
mediumButton.setBackgroundColor(Color.RED);
hardButton.setEnabled(false);
hardButton.setBackgroundColor(Color.RED);
}
....
...
.
But I think best way is making Buttons invisible in XML and then check highScore and set visibility of buttons:
if(storedHighScore > 100) {
hardButton.setVisibility(View.VISIBLE);
mediumButton.setVisibility(View.VISIBLE);
} else if(storedHighScore > 50) {
mediumButton.setVisibility(View.VISIBLE);
}
I am trying to determine which app the user selects when clicking on a directions button in my app. I want the "Always" and "Just Once" options of the default chooser but I need to know which app the user is sent to, which is why I find myself creating a custom chooser. I've done research on SO and have come across the following posts:
Custom chooser to allow me to know what app the user selects: https://stackoverflow.com/a/23494967/3957979
ActionProvider (doesn't work in my case since this is not a MenuItem):
https://stackoverflow.com/a/23495696/3957979
Anyone have any idea on how to do this? Please let me know if there is a way to use the default chooser and still determine the app the user selects. Otherwise, please let me know if there is a way to integrate the "Always" and "Just Once" options into a custom chooser.
Below is the code I am implementing:
public View onCreateView(...) {
...
shiftActionButtonDirections.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
Intent intent = directions(mLatitude, mLongitude, location.getFullAddress());
// Check if any apps can handle geo intent
if (intent.resolveActivity(getAppContext().getPackageManager()) != null) {
showChooser(intent, "Get Directions with...");
} else {
Toast.makeText(getAppContext(), R.string.error_maps, Toast.LENGTH_LONG).show();
}
}
});
...
}
public Intent directions(String latitude, String longitude, String address) {
String uri = String.format("geo:%s,%s?q=%s", latitude, longitude, Uri.encode(address));
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(uri));
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
return i;
}
private void showChooser(#NonNull final Intent intent, String title) {
final List<ResolveInfo> activities = getAppContext().getPackageManager().queryIntentActivities(intent, 0);
List<String> appNames = new ArrayList<String>();
for (ResolveInfo info : activities) {
appNames.add(info.loadLabel(getAppContext().getPackageManager()).toString());
}
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setTitle(TextUtils.isEmpty(title) ? "With..." : title);
builder.setItems(appNames.toArray(new CharSequence[appNames.size()]), new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
Map<String, Object> properties = new HashMap<>();
ResolveInfo info = activities.get(item);
if (info.activityInfo.packageName.contains("google")) {
// Google Maps was chosen
} else {
// Another app was chosen
}
// start the selected activity
intent.setPackage(info.activityInfo.packageName);
startActivity(intent);
}
});
AlertDialog alert = builder.create();
alert.show();
}
Please let me know if there is a way to use the default chooser and still determine the app the user selects.
Set your minSdkVersion to 22, then use the three-parameter flavor of createChooser(). Note that this does not allow you to change anything about the user's choice (e.g., substitute a different Intent); it merely gives you a chance to find out the choice.
Or, if you do not want to set your minSdkVersion to 22, use the three-parameter flavor of createChooser() on API Level 22+ devices, and do something else on older ones (custom chooser, live without the choice information, etc.).
Otherwise, please let me know if there is a way to integrate the "Always" and "Just Once" options into a custom chooser.
No, that is not possible.
I am working on an Android application. I want to open a apprater dialog for particular number of launches. So I used the following code.
public static void app_launched(Context mContext) {
SharedPreferences prefs = mContext.getSharedPreferences("apprater", 0);
if (prefs.getBoolean("dontshowagain", false)) { return ; }
SharedPreferences.Editor editor = prefs.edit();
// Increment launch counter
long launch_count = prefs.getLong("launch_count", 0) + 1;
editor.putLong("launch_count", launch_count);
// Get date of first launch
Long date_firstLaunch = prefs.getLong("date_firstlaunch", 0);
if (date_firstLaunch == 0) {
date_firstLaunch = System.currentTimeMillis();
editor.putLong("date_firstlaunch", date_firstLaunch);
}
// Wait at least n days before opening
if (launch_count >= LAUNCHES_UNTIL_PROMPT) {
if (System.currentTimeMillis() >= date_firstLaunch +
(DAYS_UNTIL_PROMPT * 24 * 60 * 60 * 1000)) {
showRateDialog(mContext, editor);
}
}
editor.commit();
}
It is working fine.But my problem is if I give LAUNCHES_UNTIL_PROMPT is 3 then the dialog will appear on 4th launch and user gave his rating, then 5 time again dialog launches..every time the dialog launches. So it is disgusting for users. If the user rated the app then no need to 'rate the app' again until the next version relese
PS: It is helpful to do something to launch apprater whenever a new version of app is released.
Include options in the dialog to "rate now" "rate later" and "no thanks". If the use clicks "rate now" update preferences to not show the dialog again. If they click "rate later" simply reset your counts and prompt them again after another three launches. And if "no thanks" then simply update preferences to indicate the dialog should not be shown again.
There is no way to check if a user has rated your app on Google Play, as developers may use it to incentivise certain things.
I found this answer some time ago and used it on one of my apps and had the same problem as yours.
I would suggest modifying your showRateDialog function to the following:
Button b1 = new Button(mContext);
b1.setText("Rate " + APP_TITLE);
b1.setOnClickListener(new OnClickListener() {
// If Rate <your app> is selected, set dontshowagain field to true
// and you will never see it again
public void onClick(View v) {
mContext.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + APP_PNAME)));
if (editor != null) {
editor.putBoolean("dontshowagain", true);
editor.commit();
}
dialog.dismiss();
}
});
ll.addView(b1);
Button b2 = new Button(mContext);
b2.setText("Remind me later");
b2.setOnClickListener(new OnClickListener() {
// If "Remind me later" is clicked, reset everything until requisite
// number of launches or days since first launch
public void onClick(View v) {
if (editor != null) {
editor.putLong("lLaunchCount", 0);
editor.putLong("lDateFirstLaunch", 0);
editor.commit();
}
dialog.dismiss();
}
});
ll.addView(b2);
Button b3 = new Button(mContext);
b3.setText("No, thanks");
b3.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (editor != null) {
editor.putBoolean("dontshowagain", true);
editor.commit();
}
dialog.dismiss();
}
});
ll.addView(b3);
dialog.setContentView(ll);
dialog.show();
I am trying to show dialog boxes with contents from the database .The fetched data from the database may contain more than 1 data.So I have to show dialog in for loop.But the dialog shows only for the first row in the database.Here is the code
cursor = sqldb.query(Database_Handler.dbdectab, null,"((" + Database_Handler.gendtime + "<='" + after_1hr + "' and " + Database_Handler.gendtime + ">='" + before_1hr + "') and ("
+ Database_Handler.calused + "='Gregorian' or " + Database_Handler.calused + "='both')) ", null, null, null, null);
notif_count = cursor.getCount();
//dec_name_ctr_builder = new StringBuilder("");
if(notif_count>0)
{
dialog1 = new Dialog(this);
cursor.moveToFirst();
do
{
dec_name =cursor.getString(cursor.getColumnIndex(Database_Handler.decname));
dialog1.requestWindowFeature(Window.FEATURE_NO_TITLE);
dialog1.setContentView(R.layout.custom_dialog_alert);
TextView tv_alert = (TextView)dialog1.findViewById(R.id.txt_alert);
tv_alert.setText( dec_name );
Button yes = (Button) dialog1.findViewById(R.id.btn_yes);
Button no = (Button) dialog1.findViewById(R.id.btn_no);
yes.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(donateurl));
startActivity(intent);
dialog1.dismiss();
cursor.close();
sqldb.close();
finish();
}
});
no.setOnClickListener(new OnClickListener()
{
public void onClick(View v)
{
dialog1.dismiss();
cursor.close();
sqldb.close();
finish();
}
});
dialog1.show();
}while(cursor.moveToNext());
Android dialog boxes are not modal (which is to say, they don't block the background processes).Hence your work is done asycnhronuosly and according to :
Romain guy
We've designed Android to prevent developers from writing synchronous dialogs so you don't really have much of a choice.
SO you can not block the while loop execution in between using Dialog.
Karan_Rana is right. The simplest solution for your answer would be storing the results somewhere else and displaying a new dialog each time some data is left and user clicks on a previous dialogs button.
I know this is a bit late, but I had a similar problem once. I used recursion to loop a dialog box. My situation was unique as I needed to detect an internet connection during app startup. So, this approach may not work for you. But I was able to implement the solution without for loops and blocking the main UI thread. Because I was on a splash screen, there was no other UI I needed to contend so I would simple call the dialog box again until the user chooses to "Quit".
I had a splash screen that would only continue to the landing activity of the app if a network connection was detected.
Sample code below to demonstrate the concept. Note that there are custom methods, but you should get the idea to call the prompt again from within onClick method.
protected void onCreate(Bundle savedInstanceState) {
if (isNetworkAvailable()) {
launchApp();
} else {
retryPrompt();
}
}
private void retryPrompt() {
if (isNetworkAvailable()) {
launchApp();
} else {
final ConfirmDialog dialog = new ConfirmDialog.CustomAlertBuilder(this,
"Internet Connection",
"Internet Connection not detected. Please check your connection.",
"Retry").
setNegBtnLabel("Quit").
setCustomCallback(new CustomDialogCallback() {
#Override
public void onClickPositiveBtn(DialogInterface dialogInterface, int which) {
// go around again.
retryPrompt(); // <<== Call method recursively
}
#Override
public void onClickNegativeBtn(DialogInterface dialogInterface, int which) {
// abort app.
finish();
}
}).build();
dialog.show();
}
}
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?
How can I go about doing this?
Currently most developers accomplish this using one of the following 3 techniques.
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. 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.
It is always good practice to do these checks in the onCreate. If the expiration has ended popup an AlertDialog with a market link to 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.
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).
To use it, simply
Add the library to your main module's build.gradle
dependencies {
compile 'io.trialy.library:trialy:1.0.2'
}
Initialize the library in your main activity's onCreate() method
#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.
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. 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
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.
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
The easiest and best way to do this is the implement 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
Here's the example:
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.
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. Can be backed up with android's backup mechanism and will be restored after a reinstall. 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. Can also be backed up.
PackageInfo.firstInstallTime
Is reset after a reinstall but stable across updates
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. 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. 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)
ANDROID_ID
May 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.
Play Advertising ID
May be reset by the user. May be disabled by the user by opting out of ad tracking.
InstanceID
Reset on a reinstall. 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. I would recommend steering clear of using only anonymous 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.
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.
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.
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:
1) Add Firebase support to your app
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.
3) Use the Realtime Database API to set a value for '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.
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.
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.
There's an 'Uninstall and Refund' button which changes to 'Uninstall' after 24 hours.
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. 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,
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
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,
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):
/**
* 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. 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.
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.
It works fine for me.
If I have time I will post some code !
Good Luck !