Detect if new install or updated version (Android app) - android

I have an app on the Play Store. I want to put a requirement that if users want to use a certain part of the app, they have to invite a friend before being able to do so. But I only want to impose this restriction to new installs of the app (to be fair to users that have installed the app before the restriction).
Sorry for the long intro, my question is how can I find out if the current device has updated the app or is a new install?

public static boolean isFirstInstall(Context context) {
try {
long firstInstallTime = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).firstInstallTime;
long lastUpdateTime = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).lastUpdateTime;
return firstInstallTime == lastUpdateTime;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return true;
}
}
public static boolean isInstallFromUpdate(Context context) {
try {
long firstInstallTime = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).firstInstallTime;
long lastUpdateTime = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).lastUpdateTime;
return firstInstallTime != lastUpdateTime;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return false;
}
}

The only solution I can see that doesn't involve an entity outside of the device would be to get the PackageInfo for your app and check the values of
versionCode
firstInstallTime
lastUpdateTime
On first install, firstInstallTime and lastUpdateTime will have the same value (at least on my device they were the same); after an update, the values will be different because lastUpdateTime will change. Additionally, you know approximately what date and time you create the version that introduces this new behavior, and you also know which version code it will have.
I would extend Application and implement this checking in onCreate(), and store the result in SharedPreferences:
public class MyApplication extends Application {
// take the date and convert it to a timestamp. this is just an example.
private static final long MIN_FIRST_INSTALL_TIME = 1413267061000L;
// shared preferences key
private static final String PREF_SHARE_REQUIRED = "pref_share_required";
#Override
public void onCreate() {
super.onCreate();
checkAndSaveInstallInfo();
}
private void checkAndSaveInstallInfo() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
if (prefs.contains(PREF_SHARE_REQUIRED)) {
// already have this info, so do nothing
return;
}
PackageInfo info = null;
try {
info = getPackageManager().getPackageInfo(getPackageName(), 0);
} catch (NameNotFoundException e) {
// bad times
Log.e("MyApplication", "couldn't get package info!");
}
if (packageInfo == null) {
// can't do anything
return;
}
boolean shareRequired = true;
if (MIN_FIRST_INSTALL_TIME > info.firstInstallTime
&& info.firstInstallTime != info.lastUpdateTime) {
/*
* install occurred before a version with this behavior was released
* and there was an update, so assume it's a legacy user
*/
shareRequired = false;
}
prefs.edit().putBoolean(PREF_SHARE_REQUIRED, shareRequired).apply();
}
}
This is not foolproof, there are ways to circumvent this if the user really wants to, but I think this is about as good as it gets. If you want to track these things better and avoid tampering by the user, you should start storing user information on a server (assuming you have any sort of backend).

Check if the old version of your app saves some data on disk or preferences. This data must be safe, i.e. it cannot be deleted by the user (I'm not sure it's possible).
When the new version is freshly installed, this data won't exist. If the new version is an upgrade from the old version, this data will exist.
Worst case scenario, an old user will be flagged as a new one and will have a restricted usage.

A concise Kotlin solution, based off the still excellent wudizhuo answer:
val isFreshInstall = with(packageManager.getPackageInfo(packageName, 0)) {
firstInstallTime == lastUpdateTime
}
This can be called directly from within an Activity as an Activity is a context (so can access packageManager etc.)
If you wanted to use this in multiple places/contexts, it could very easily be turned into an extension property:
val Context.isFreshInstall get() = with(packageManager.getPackageInfo(packageName, 0)) {
firstInstallTime == lastUpdateTime
}
This way, you can simply write if (isFreshInstall) in any Activity, or if (requireContext().isFreshInstall) inside any Fragment.

My solution is use SharedPreference
private int getFirstTimeRun() {
SharedPreferences sp = getSharedPreferences("MYAPP", 0);
int result, currentVersionCode = BuildConfig.VERSION_CODE;
int lastVersionCode = sp.getInt("FIRSTTIMERUN", -1);
if (lastVersionCode == -1) result = 0; else
result = (lastVersionCode == currentVersionCode) ? 1 : 2;
sp.edit().putInt("FIRSTTIMERUN", currentVersionCode).apply();
return result;
}
return 3 posibles values:
0: The APP is First Install
1: The APP run once time
2: The APP is Updated

Update
(thanks for the comments below my answer for prodding for a more specific/complete response).
Because you can't really retroactively change the code for previous versions of your app, I think the easiest is to allow for all current installs to be grandfathered in.
So to keep track of that, one way would be to find a piece of information that points to a specific version of your app. Be that a timestamped file, or a SharedPreferences, or even the versionCode (as suggested by #DaudArfin in his answer) from the last version of the app you want to allow users to not have this restriction. Then you need to change this. That change then becomes your reference point for all the previous installs. For those users mark their "has_shared" flag to true. They become grandfathered in. Then, going forward, you can set the "has_shared" default to true
(Original, partial answer below)
Use a SharedPrefence (or similar)
Use something like SharedPreferences.
This way you can put a simple value like has_shared = true and SharedPreferences will persist through app updates.
Something like this when they have signed someone up / shared your app
SharedPreferences prefs = getActivity().getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("has_shared", true)
editor.commit();
Then you can only bug people when the pref returns true
SharedPreferences prefs = getActivity().getPreferences(Context.MODE_PRIVATE);
boolean defaultValue = false;
boolean hasShared= prefs.gettBoolean("has_shared", defaultValue);
if (!hasShared) {
askUserToShare();
}
Docs for SharedPreference:
http://developer.android.com/training/basics/data-storage/shared-preferences.html

You can get the version code and version name using below code snippet
String versionName = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
int versionCode = getPackageManager().getPackageInfo(getPackageName(), 0).versionCode;
Now you can check for the latest version and restrict as per your requirement.

We can use broadcast receiver to listen app update.
Receiver
class AppUpgradeReceiver : BroadcastReceiver() {
#SuppressLint("UnsafeProtectedBroadcastReceiver")
override fun onReceive(context: Context?, intent: Intent?) {
if (context == null) {
return
}
Toast.makeText(context, "Updated to version #${BuildConfig.VERSION_CODE}!", Toast.LENGTH_LONG).show()
}
}
Manifest
<receiver android:name=".AppUpgradeReceiver">
<intent-filter>
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
It doesn't work while debug. So you have to install to manually.
Increase the versionCode in your app-level build.gradle (so it counts as an update).
Click Build -> Build Bundle(s) / APK(s) -> Build APK(s), and select a debug APK.
Run following command in the terminal of Android Studio:
adb install -r C:\Repositories\updatelistener\app\build\outputs\apk\debug\app-debug.apk

If you want to perform any operation only once per update then follow below code snippet
private void performOperationIfInstallFromUpdate(){
try {
SharedPreferences prefs = getActivity().getPreferences(Context.MODE_PRIVATE);
String versionName = prefs.getString(versionName, "1.0");
String currVersionName = getApplicationContext().getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
if(!versionName.equals(currVersionName)){
//Perform Operation which want execute only once per update
//Modify pref
SharedPreferences.Editor editor = prefs.edit();
editor.putString(versionName, currVersionName);
editor.commit();
}
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return BASE_VERSION;
}
}

Related

DocumentsUI shows "Anonymous" application when requesting access to directory

One user reported that my app fails to request directory access when selecting a folder via the ACTION_OPEN_DOCUMENT_TREE intent.
For some reason it does not show my application, instead "Anonymous":
Translated: "Allow Anonymous to access files in Camera. This will let Anonymous access current and future content stored in Camera".
The user has a MIUI 12 with Android 11 on a Mi Note 10 lite.
I have the same just with a Mi Note 10, no issues ofc.
Checked the Android source code:
https://android.googlesource.com/platform/packages/apps/DocumentsUI/+/refs/heads/master/src/com/android/documentsui/picker/ConfirmFragment.java#82
case TYPE_OEPN_TREE:
final Uri treeUri = mTarget.getTreeDocumentUri();
final BaseActivity activity = (BaseActivity) getActivity();
final String target = activity.getCurrentTitle();
final String text = getString(R.string.open_tree_dialog_title,
**getCallingAppName**(getActivity()), target);
message = getString(R.string.open_tree_dialog_message,
**getCallingAppName**(getActivity()), target);
builder.setTitle(text);
builder.setMessage(message);
builder.setPositiveButton(
R.string.allow,
(DialogInterface dialog, int id) -> {
pickResult.increaseActionCount();
mActions.finishPicking(treeUri);
});
break;
#NonNull
public static String getCallingAppName(Activity activity) {
final String anonymous = activity.getString(R.string.anonymous_application);
final String packageName = getCallingPackageName(activity);
if (TextUtils.isEmpty(packageName)) {
return anonymous;
}
final PackageManager pm = activity.getPackageManager();
ApplicationInfo ai;
try {
ai = pm.getApplicationInfo(packageName, 0);
} catch (final PackageManager.NameNotFoundException e) {
return anonymous;
}
CharSequence result = pm.getApplicationLabel(ai);
return TextUtils.isEmpty(result) ? anonymous : result.toString();
}
public static String getCallingPackageName(Activity activity) {
String callingPackage = activity.getCallingPackage();
// System apps can set the calling package name using an extra.
try {
ApplicationInfo info =
activity.getPackageManager().getApplicationInfo(callingPackage, 0);
if (isSystemApp(info) || isUpdatedSystemApp(info)) {
final String extra = activity.getIntent().getStringExtra(
Intent.EXTRA_PACKAGE_NAME);
if (extra != null && !TextUtils.isEmpty(extra)) {
callingPackage = extra;
}
}
} catch (NameNotFoundException e) {
// Couldn't lookup calling package info. This isn't really
// gonna happen, given that we're getting the name of the
// calling package from trusty old Activity.getCallingPackage.
// For that reason, we ignore this exception.
}
return callingPackage;
}
...and it seems that for whatever reason my packagename isn't found. How can can happen?
Asked him to install one of my other apps, and it happens there as well.
Asked him then to install another app from the playstore (FX File Explorer) and there it does not happen.
So it is specific to his device and my app.
So it turned out that this user having that issue turned off the MIUI Optimizations in the developer settings.
Bug report: συσκευη, εκδοση miui, Play store install (alpha 1021). It was impossible to specify a b i o s file or specify a game image directory in when MIUI optimizations are off. Turning them back on fixed the issue and directories are scanned normally. Also on the popup to allow folder access the app displays as "Anonymous" instead of AetherSX2 on my system. Some developer was talking about having the same issue here.

Sharing shared preference with other app is giving old/cached values

I have two apps ,lets say A and B. App A has a shared pref which is created as world readable so that app B can access it.
When i try to access the App A's shared pref value from app B for the very first time, it gives correct result. But the problem come when i change the shared pref value of app A and then open app B to check the updated shared pref it gives the same old value. And surprisingly when i force close the app B from settings--> apps and reopen the app B it gives the correct updated value of app A's shared pref.
What is the problem with accessing shared pref a application when it is WORLD_READABLE.
Below is the source code of app B when i am accessing the Shared pref of app A.
private boolean isDAEnabled() throws SecurityException {
Context context = null;
try {
context = createPackageContext(APP_A_PACKAGE_NAME, Context.CONTEXT_IGNORE_SECURITY);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
if (context == null) {
throw new SecurityException("can not read shared pref of old DAE");
}
SharedPreferences oldDaPrefs = context.getSharedPreferences
(A_SHAREDPREF_FILE_NAME, Context.MODE_WORLD_READABLE);
int what = oldDaPrefs.getInt(A_PREF_ENGINE_STATE, 4);
Log.d(TAG, "What is value "+ what);
return what == ENABLE_ENGINE; // ENABLE_ENGINE IS == 0
}
And Below is the code where i am changing the shared prefs of App A
private void setEnginePreference(boolean engineStatus) {
mPreferenceEditor = mPreference.edit();
if(engineStatus){
mPreferenceEditor.putInt(Constants.PREF_ENGINE_STATE, ENABLE_ENGINE);
} else {
mPreferenceEditor.putInt(Constants.PREF_ENGINE_STATE, DISABLE_ENGINE);
}
mPreferenceEditor.commit();
}
Change this :
private void setEnginePreference(boolean engineStatus) {
mPreferenceEditor = mPreference.edit();
if(engineStatus){
mPreferenceEditor.putInt(Constants.PREF_ENGINE_STATE, ENABLE_ENGINE);
} else {
mPreferenceEditor.putInt(Constants.PREF_ENGINE_STATE, DISABLE_ENGINE);
}
mPreferenceEditor.commit();
}
To :
private void setEnginePreference(boolean engineStatus) {
mPreferenceEditor = mPreference.edit();
if(engineStatus){
mPreferenceEditor.putInt(Constants.PREF_ENGINE_STATE, ENABLE_ENGINE);
} else {
mPreferenceEditor.putInt(Constants.PREF_ENGINE_STATE, DISABLE_ENGINE);
}
mPreferenceEditor.apply();
}
Answering my own question as i have figured out the solution.
I found in the blog that i can use MODE_MULTI_PROCESS while accessing the shared prefs of the different app(i.e. app A in my case). However, as of API Level 23 (OS version 6.0), Context.MODE_MULTI_PROCESS is deprecated. But my requirement was only to support API level 23 so it was good for me.
Here is the change i had to make
private boolean isDAEnabled() throws SecurityException {
Context context = null;
try {
context = createPackageContext(APP_A_PACKAGE_NAME, Context.CONTEXT_IGNORE_SECURITY);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
if (context == null) {
throw new SecurityException("can not read shared pref of old DAE");
}
SharedPreferences oldDaPrefs = context.getSharedPreferences
(A_SHAREDPREF_FILE_NAME, Context.MODE_MULTI_PROCESS);
int what = oldDaPrefs.getInt(A_PREF_ENGINE_STATE, 4);
Log.d(TAG, "What is value "+ what);
return what == ENABLE_ENGINE; // ENABLE_ENGINE IS == 0}
I hope that it will help others.

Preventing an android app being cloned by an app cloner

Created an app that used the device's uniqueID which is fetched by the following code snippet
String deviceId = Settings.Secure.getString(getContentResolver(),
Settings.Secure.ANDROID_ID);
When the user tries to clone the app by app cloner, then it creates a different deviceID and the app is not allowed to work
Is there any way to make our app non clonable
or
Any possible way to have the same deviceId even if the app instance is cloned?
Is there any way to find out whether the app is running in a cloned instance?
Applications like Cloner usually change your application's package name so you can retrieve package name and check if it is changed or not.
if (!context.getPackageName().equals("your.package.name")){
// close the app or do whatever
}
Also they usually sign cloned apk so the signature might be different from yours, you can check if signature is changed or not. I usually use this function:
#SuppressLint("PackageManagerGetSignatures")
public static int getCertificateValue(Context ctx){
try {
Signature[] signatures = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
try {
signatures = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), PackageManager.GET_SIGNING_CERTIFICATES).signingInfo.getApkContentsSigners();
}catch (Throwable ignored){}
}
if (signatures == null){
signatures = ctx.getPackageManager().getPackageInfo(ctx.getPackageName(), PackageManager.GET_SIGNATURES).signatures;
}
int value = 1;
for (Signature signature : signatures) {
value *= signature.hashCode();
}
return value;
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
public static boolean checkCertificate(Context ctx, int trustedValue){
return getCertificateValue(ctx) == trustedValue;
}
Before releasing your app call getCertificateValue(context) and write down the value and alongside with package name, check if that value matches the value that you get in runtime.
PS: as #vladyslav-matviienko said hackers will always find a way so try to make cloning harder by running some obfuscations on hardcoded package name and that value. Also try to tangle and spread these kind of logics all around the source code.
I found a story in proandroiddev by Siddhant Panhalkar and with some minor changes it's work perfectly in Mi device I did checked in Mi phones default Dual apps and some third party apps from playstore and it prevents from cloning (means not working properly after clone).
private const val APP_PACKAGE_DOT_COUNT = 3 // number of dots present in package name
private const val DUAL_APP_ID_999 = "999"
private const val DOT = '.'
fun CheckAppCloning(activity: Activity) {
val path: String = activity.filesDir.getPath()
if (path.contains(DUAL_APP_ID_999)) {
killProcess(activity)
} else {
val count: Int = getDotCount(path)
if (count > APP_PACKAGE_DOT_COUNT) {
killProcess(activity)
}
}
}
private fun getDotCount(path: String): Int {
var count = 0
for (element in path) {
if (count > APP_PACKAGE_DOT_COUNT) {
break
}
if (element == DOT) {
count++
}
}
return count
}
private fun killProcess(context: Activity) {
context.finish()
android.os.Process.killProcess( android.os.Process.myPid())
}

How to detect Android App has been upgraded from version x to y?

I have a problem with my android app for own educational reason.
Is there a ready solution to detect that the app has now been updated from version x to y? Because I want to migrate some data once only if the app was installed in a special version before.
Thanks.
The easiest way is to store the last known version of your application in SharedPreferences, and check that when the app is launched.
You can get the current version code of your application with BuildConfig.VERSION_CODE.
For example:
#Override
public void onStart() {
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
int lastKnownVersion = prefs.getInt("lastKnownAppVersion", 0);
if (lastKnownVersion < BuildConfig.VERSION_CODE) {
// Your app has been updated
prefs.edit().putInt("lastKnownAppVersion", BuildConfig.VERSION_CODE);
}
}
Registering a BroadcastReceiver to listen for the ACTION_MY_PACKAGE_REPLACED intent will tell you when your package is replaced (i.e. updated), but that won't tell you what version was previously installed.
Here is a simple way that can solve your problem. In your code you can easily get the app versionCode and versionName. In your home activity you check isAppUpdated(). if current APP VERSION_NAME is not matched with stored VERSION_NAME then app is updated.
Here is sample code:
Save VersionName in preferences:
public static void saveVersionNameAndCode(Context context){
try{
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0);
int versionCode = packageInfo.versionCode;
String versionName=packageInfo.versionName;
CommonTasks.showLog("Saving APP VersionCode and Version Name");
// SAVE YOUR DATA
}catch(Exception e){
}
}
Check App is Upgraded:
public static boolean isAppUpdated(Context context){
boolean result=false;
try{
PackageInfo packageInfo = context.getPackageManager()
.getPackageInfo(context.getPackageName(), 0);
int versionCode = packageInfo.versionCode;
String versionName=packageInfo.versionName;
String prevVersionName= GET STORED VERSION_NAME;
if(prevVersionName!=null && !prevVersionName.equals("") &&
!prevVersionName.equals(versionName)){
showLog("App Updated");
result=true;
}else{
showLog("App Not updated");
}
}catch(Exception e){
}
return result;
}
here, if isAppUpdated() return true means your app is updated.
Note, After checking upgrade issue, you must have to update the sharedPreferences. :)

What is android:sharedUserLabel and what added value does it add on top of android:sharedUserID?

The documentation (http://developer.android.com/guide/topics/manifest/manifest-element.html#uid) only states I can't use raw strings and the API level it was added, but doesn't explain why I would want to use it.
If I already set android:sharedUserID to "com.foo.bar" what value should I put in the string referenced by android:sharedUserLabel, and most importantly why!?
Thank you
As far as I understand from the AOSP actually you can use this label just to display a pretty name to a user (if you have several processes in the same uid). For instance, here is a part of code in the RunningState.java file:
// If we couldn't get information about the overall
// process, try to find something about the uid.
String[] pkgs = pm.getPackagesForUid(mUid);
// If there is one package with this uid, that is what we want.
if (pkgs.length == 1) {
try {
ApplicationInfo ai = pm.getApplicationInfo(pkgs[0], 0);
mDisplayLabel = ai.loadLabel(pm);
mLabel = mDisplayLabel.toString();
mPackageInfo = ai;
return;
} catch (PackageManager.NameNotFoundException e) {
}
}
// If there are multiple, see if one gives us the official name
// for this uid.
for (String name : pkgs) {
try {
PackageInfo pi = pm.getPackageInfo(name, 0);
if (pi.sharedUserLabel != 0) {
CharSequence nm = pm.getText(name,
pi.sharedUserLabel, pi.applicationInfo);
if (nm != null) {
mDisplayLabel = nm;
mLabel = nm.toString();
mPackageInfo = pi.applicationInfo;
return;
}
}
} catch (PackageManager.NameNotFoundException e) {
}
}
Basically, it does the following things. At first, it tries to get information about the overall process. If it has not find, it tries to get information using UID of the application as a parameter (this is a part of code that I've given here). If there is only one package with this UID the information about the process is got from this package. But if there are several packages (using shareUserId) then it iterates and tries to find official (pretty) name.
As a confirmation to my words I found the following string in MediaProvider:
<!-- Label to show to user for all apps using this UID. -->
<string name="uid_label">Media</string>
Thus, all process that uses android:sharedUserId="android.media" will have name Media.
I do not think that this feature will be used a lot by ordinary developers and is useful for them.

Categories

Resources