Gradle Play publisher lets you override the version name before publishing to the play store.
play {
// ...
resolutionStrategy = "auto"
outputProcessor { // this: ApkVariantOutput
versionNameOverride = "$versionNameOverride.$versionCode"
}
}
Is it possible to use the value of versionNameOverride in Java Code? We display the version name in the about page of the app using the versionName attribute. GPP updates the versionNameOverride value so the play store listing shows the correct version number but the app's about page keeps showing a different version number that's based on versionName.
Using the versionNameOverride could be done like this:
outputProcessor { output ->
output.versionNameOverride = output.versionNameOverride + "." + output.versionCode
def versionPropertyFile = file "version.properties"
def versionProperties = new Properties()
versionProperties.setProperty('versionCode', "$output.versionCode")
versionProperties.setProperty('versionName', output.versionNameOverride)
versionPropertyFile.withWriter { versionProperties.store(it, "Generated by the outputProcessor for the play plugin during publishing.") }
}
But if you want to show the versionName in your app it would be easier to use
try {
PackageInfo pInfo =
context.getPackageManager().getPackageInfo(getPackageName(), 0);
String version = pInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
The reason why your app did show the wrong values might be that your app was using BuildConfig. But the versionCode and versionName there do not reflect what has been set through versionNameOverride, but those values from your build gradle.
Related
digging into fastlane for the first time on a react native project, and in the process of getting a demo version up for internal testing on both the google play store, and testflight.
After following some tutorials for getting the android bundle up successfully, I ended up with some lanes that do increment both the versionCode and versionName, then bundle and push to the google play store via the supply command.
Now I'm moving onto some iOS lanes, and I'm wondering the best way to sync these up. Ideally, I'd like to use the package.json version as the one source of truth for this, but I'm lost a little on the best way to tie it all in, so I'm asking for some advice/workflows that others have found successful with fastlane and building/versioning their ios/android apps. Thanks!
Here are my current android lanes.
desc "Build a version of the app, that allows params for task and type"
lane :build do |options|
build_task = options.fetch(:build_task, "bundle")
build_type = options.fetch(:build_type, "Release")
gradle(task: "clean")
gradle_params = {
task: build_task,
build_type: build_type,
}
gradle(gradle_params)
end
##### ---------------------------------------------
desc "Build and push a new internal build to the Play Store"
lane :internal do
build()
supply_params = {
track: "internal",
release_status: "draft",
}
supply(supply_params)
end
##### ---------------------------------------------
desc "Increment build number and push to repository"
lane :inc_build_number do |options|
params = {
:app_project_dir => 'app'
}
# Specify a custom build number to be passed in
if options[:build_number]
params[:version_code] = options[:build_number].to_i
end
android_increment_version_code(params)
new_version_code = Actions.lane_context[Actions::SharedValues::ANDROID_VERSION_CODE]
UI.important("Incremented android version code to #{new_version_code}")
end
##### ---------------------------------------------
desc "Increment version number and push to repository"
lane :inc_version_number do |options|
should_commit = options.fetch(:should_commit, true)
commit_message = options.fetch(:commit_message, "android: bump version code & number[skip ci]")
should_push = options.fetch(:should_push, true)
ensure_git_status_clean if should_commit
# increment build number first
inc_build_number
increment_type = options.fetch(:increment_type, "patch")
new_version_params = options[:version]
params = {
app_project_dir: 'app',
increment_type: increment_type,
}
unless new_version_params.nil?()
params[:version_name] = new_version_params
end
android_increment_version_name(params)
new_version_name = Actions.lane_context[Actions::SharedValues::ANDROID_VERSION_NAME]
UI.important("Incremented android version name to #{new_version_name}")
if should_commit
path = "android/app/build.gradle"
git_add(path: path)
git_commit(path: path, message: commit_message)
push_to_git_remote if should_push
end
end
I think you should not use a single source for Android and iOS while versioning. Because versioning presents how your app changes in time.
Anyway, creating a good Fastlane configuration not an easy job, so I decide to share what I have got.
For Android, I created a file called version.properties, I increment version number in this file before submitting a new version to Google Play Store and a Gradle script automatically generates build number in build time. I'm just increment version number (following Semver) and the script handles the rest. When I use Fastlane I'm doing same thing, Fastlane ask me new version, I give a version number to it then it changes the version.properties file all after then it compiles the app.
Follow Below Steps:
Create a file named version.properties under android/version folder
Write your app current version in it like: VERSION=7.0.5
Create a file named versioning.gradle under android folder and write below code in this file:
ext {
buildVersionCode = {
def versionName = buildVersionName()
def (major, minor, patch) = versionName.toLowerCase().tokenize('.')
(major, minor, patch) = [major, minor, patch].collect { it.toInteger() }
(major * 10000) + (minor * 100) + patch
}
buildVersionName = {
def props = new Properties()
file("../version/version.properties").withInputStream { props.load(it) }
return props.getProperty("VERSION")
}
}
Add below marked lines in your android/app/build.gradle file
// ...
apply from: "../../node_modules/react-native/react.gradle"
apply from: '../versioning.gradle' // <- add this line
// ...
// ...
// ...
android {
// ...
defaultConfig {
// ...
versionCode buildVersionCode() // <- add this line
versionName buildVersionName() // <- add this line
// ...
}
// ...
}
// ...
You can change the version from android/version/version.properties file now. Version code will automatically create while building. For an example, if you typed version like 7.3.5 your build number will be 70305.
Now let's make Fastlane integration,
Install property_file_read plugin to ability read property files: fastlane add_plugin property_file_read
You can use below Fastlane configuration for Android:
# CONSTANTS
NOTIFICATION_TITLE = "FOO APP FINISHED!"
platform :android do
desc "Choose release name"
private_lane :determine_release_name do |options|
versions = google_play_track_release_names(track: options[:track])
if versions.empty?
UI.user_error!("Whoops, current version not found!")
else
current_version = versions[0]
end
parts = current_version.split(".")
major = parts[0]
minor = parts[1]
patch = parts[2]
target_major = (major.to_i + 1).to_s + ".0.0"
target_minor = major + "." + (minor.to_i + 1).to_s + ".0"
target_patch = major + "." + minor + "." + (patch.to_i + 1).to_s
properties = property_file_read(file: "android/version/version.properties")
file_version = properties["VERSION"]
target_version_label = UI.select("What version do you want to use?", [
"Bump patch (#{target_patch})",
"Bump minor (#{target_minor})",
"Bump major (#{target_major})",
"KEEP EXISTING (#{file_version})",
"CUSTOM",
])
next target_major if target_version_label.match(/major/)
next target_minor if target_version_label.match(/minor/)
next target_patch if target_version_label.match(/patch/)
next file_version if target_version_label.match(/FILE/)
custom_version = prompt(text: "\nEnter New Version Number:")
custom_version
end
desc "Build and Deploy to Google Play Internal App Sharing"
lane :beta do
newVersion = determine_release_name(track: "internal")
# update version
File.open("../android/version/version.properties", "w") do |file|
file.write("VERSION=#{newVersion}")
end
# gradle(task: "clean", project_dir: "./android/")
gradle(task: "bundle", build_type: "Release", project_dir: "android")
upload_to_play_store(track: "internal", aab: "android/app/build/outputs/bundle/release/app-release.aab")
notification(title: NOTIFICATION_TITLE, subtitle: "Google Play - Internal App Sharing", message: "Finished!!")
end
end
I think it is the best system you can find for Fastlane integration on Android, I use these settings myself.
In iOS, I'm asking new version number then I compile and send app to the TestFlight, there is nothing special. You can check my iOS configuration below:
# CONSTANTS
NOTIFICATION_TITLE = "FOO APP FINISHED!"
IOS_XCWORKSPACE = "ios/Foo.xcworkspace"
IOS_XCODEPROJ = "ios/Foo.xcodeproj"
IOS_SCHEME = "Foo"
IOS_TARGET = "Foo"
APP_STORE_KEYFILE = "fastlane/app-store-auth-key.p8"
APP_STORE_KEY_ID = "xxxxxxxxxx"
APP_STORE_ISSUER_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
platform :ios do
desc "Choose release name"
private_lane :determine_release_name do
current_build_number = latest_testflight_build_number(api_key: lane_context[SharedValues::APP_STORE_CONNECT_API_KEY])
current_version = lane_context[SharedValues::LATEST_TESTFLIGHT_VERSION]
parts = current_version.split(".")
major = parts[0]
minor = parts[1]
patch = parts[2]
target_major = (major.to_i + 1).to_s + ".0.0"
target_minor = major + "." + (minor.to_i + 1).to_s + ".0"
target_patch = major + "." + minor + "." + (patch.to_i + 1).to_s
selected_version_ok = false
while selected_version_ok === false
target_version_label = UI.select("What version do you want to use?", [
"KEEP EXISTING (#{current_version})",
"Bump patch (#{target_patch})",
"Bump minor (#{target_minor})",
"Bump major (#{target_major})",
"CUSTOM",
])
if target_version_label.match(/CUSTOM/)
custom_version = prompt(text: "\nEnter New Version Number:")
if custom_version < current_version
UI.important "Wahaha, version (#{custom_version}) can't lower than the current version (#{current_version})"
else
selected_version_ok = true
end
else
selected_version_ok = true
end
end
next { version: target_major, type: "major" } if target_version_label.match(/major/)
next { version: target_minor, type: "minor" } if target_version_label.match(/minor/)
next { version: target_patch, type: "patch" } if target_version_label.match(/patch/)
next { version: current_version, type: "current" } if target_version_label.match(/KEEP/)
{ version: custom_version, type: "custom" }
end
desc "Push a new beta build to TestFlight"
lane :beta do
api_key = app_store_connect_api_key(
key_id: APP_STORE_KEY_ID,
issuer_id: APP_STORE_ISSUER_ID,
key_filepath: APP_STORE_KEYFILE,
duration: 1200, # optional (maximum 1200)
in_house: false, # optional but may be required if using match/sigh
)
new_version = determine_release_name
if new_version[:type] === "current"
# get latest build number from App Store
build_num = app_store_build_number(
initial_build_number: 1,
live: false,
version: get_version_number(xcodeproj: IOS_XCODEPROJ, target: IOS_TARGET),
api_key: api_key,
)
increment_build_number(build_number: build_num + 1, xcodeproj: IOS_XCODEPROJ)
else
increment_version_number(version_number: new_version[:version], xcodeproj: IOS_XCODEPROJ)
end
# 🏗️ Build app, this method has other options few we will explore in next section
build_app(
silent: true,
workspace: IOS_XCWORKSPACE,
scheme: IOS_SCHEME,
)
# ⏫ Its time to upload
upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: true)
# 🧽 Clear artifacts
clean_build_artifacts
sh "rm -rf \"#{lane_context[SharedValues::XCODEBUILD_ARCHIVE]}\""
notification(title: NOTIFICATION_TITLE, subtitle: "Testflight", message: "Finished!!")
end
end
Let me what you think in comments! (Btw sorry for my poor England :p)
I am trying to change version code, version name and app icon. Below is my code
variant.outputs.all { output ->
def newApkName
if (output.zipAlign) {
newApkName = "Application.apk"
}
output.processManifest.doLast{
// Stores the path to the maifest.
String manifestPath = "$manifestOutputDirectory/AndroidManifest.xml"
// Stores the contents of the manifest.
def manifestContent = file(manifestPath).getText()
// Changes the version code in the stored text.
manifestContent = manifestContent.replace('android:versionName="dev_build"',
String.format('android:versionName="%s"', variant.versionName))
// Overwrites the manifest with the new text.
file(manifestPath).write(manifestContent)
}
output.processManifest.doLast{
// Stores the path to the maifest.
String manifestPath = "$manifestOutputDirectory/AndroidManifest.xml"
// Stores the contents of the manifest.
def manifestContent = file(manifestPath).getText()
// Changes the version code in the stored text.
manifestContent = manifestContent.replace('android:icon="#drawable/app_icon_main"',
String.format('android:icon="#drawable/%s"', getVersionIconName()))
// Overwrites the manifest with the new text.
file(manifestPath).write(manifestContent)
}
outputFileName = "Application.apk"
}
Getting bellow warning:
WARNING: API 'variantOutput.getProcessManifest()' is obsolete and has been replaced with 'variantOutput.getProcessManifestProvider()'.
It will be removed at the end of 2019.
For more information, see https://d.android.com/r/tools/task-configuration-avoidance.
To determine what is calling variantOutput.getProcessManifest(), use -Pandroid.debug.obsoleteApi=true on the command line to display a stack trace.
Affected Modules: Application
Environment Details:
Android Studio : 3.3.1
Gradle Version: 4.10.0
Build Tool Version 28.0.2
Change the following
output.processManifest.doLast {
//your code
}
with
output.getProcessManifestProvider().get().doLast {
//your code
}
I have a scenario where based on the build variant i have to run a task.
application is using the productFlavour,so its generating debug and release version for the two product flavors.
All i want is to store the current selected build type of the product flavour in a variable in gradle itself.
i have tried the folowing code
task getflavour(){
android.applicationVariants.all { com.android.build.gradle.api.ApplicationVariant variant ->
println "values of variant ${variant.productFlavors[0].name}"
println "assemble ${variant.flavorName}"
}
}
but its giving all the build variant ie 2 debug and 2 release version name. I just want to have the currently selected build variant.
I would really appreciate any inputs in it. thanks in advance
You can use in the variants loop: gradle.startParameter.taskNames[0]
in gradle:
ext.vMajor = 1
ext.vMinor = 9
def computeVersionName() {
return String.format('%d.%d', vMajor, vMinor)
}
and in your code:
PackageInfo pInfo = null;
try {
pInfo = YamsaferApplication.getContext().getPackageManager().getPackageInfo(YamsaferApplication.getContext().getPackageName(), 0);
return pInfo.versionName;
} catch (PackageManager.NameNotFoundException e) {
Log.e("getAppVersion", "Error getting App version", e);
}
I'm integrating New Relic in my project (with Android Studio & Gradle) which has 2 variants. Each variant has its own generated token, which I store in each variant's string.xml file.
In the New Relic documentation, it states the following:
In your project’s root directory (projectname/app), add a newrelic.properties file with the following line:
com.newrelic.application_token=generated_token
The problem is, if I do this, how can make the correct token appear for the correct variant? If this file must appear in the project root, I can't create one per variant, and so I'm forced to use the same token for both variants, which doesn't work for my requirements.
Any insight would be appreciated.
Okay, so after contacting the support team at New Relic, there is apparently no direct solution for this as of today, although they said they've opened a feature request, and so this problem might be solved soon.
From what I managed to understand, the reason this file is needed is so that the New Relic system can display an un-obfuscated error log when an exception occurs on a production version which has been obfuscated with ProGuard.
The New Relic system, with the help of this file, will upload the ProGuard mapping.txt file to the New Relic servers and associate it with your app according to the specified token. With this, New Relic can un-obfuscate stack traces and display a descriptive stack trace with actual class & method names, rather a, b, c, etc.
As a workaround, I was told that I can forego this file all together, if I upload the mapping file manually.
The mapping file can be found at:
build/outputs/proguard/release/mapping.txt
In order to manually upload the file, perform the following via command line:
curl -v -F proguard=#"<path_to_mapping.txt>" -H "X-APP-LICENSE-KEY:<APPLICATION_TOKEN>" https://mobile-symbol-upload.newrelic.com/symbol
This must be done for each variant which is being obfuscated with ProGuard (classically, release builds).
Source
Hope this helps someone else.
I solved creating some Gradle tasks. Please, take a look at https://discuss.newrelic.com/t/how-to-set-up-newrelic-properties-file-for-project-with-multiple-build-variants/46176/5
I following a code that worked pretty well for me.
Add the New Relic application token on a string resource file. i.e.: api.xml.
Create a new Gradle file. i.e: newrelic-util.gradle.
Add the following content on the newly created Gradle file:
apply plugin: 'com.android.application'
android.applicationVariants.all { variant ->
//<editor-fold desc="Setup New Relic property file">
def variantName = variant.name.capitalize()
def newRelicTasksGroup = "newrelic"
def projectDirPath = project.getProjectDir().absolutePath
def newRelicPropertyFileName = "newrelic.properties"
def newRelicPropertyFilePath = "${projectDirPath}/${newRelicPropertyFileName}"
// Cleanup task for New Relic property file creation process.
def deleteNewRelicPropertyFile = "deleteNewRelicPropertyFile"
def taskDeleteNewRelicPropertyFile = project.tasks.findByName(deleteNewRelicPropertyFile)
if (!taskDeleteNewRelicPropertyFile) {
taskDeleteNewRelicPropertyFile = tasks.create(name: deleteNewRelicPropertyFile) {
group = newRelicTasksGroup
description = "Delete the newrelic.properties file on project dir."
doLast {
new File("${newRelicPropertyFilePath}").with {
if (exists()) {
logger.lifecycle("Deleting file ${absolutePath}.")
delete()
} else {
logger.lifecycle("Nothing to do. File ${absolutePath} not found.")
}
}
}
}
}
/*
* Fix for warning message reported by task "newRelicMapUploadVariantName"
* Message:
* [newrelic] newrelic.properties was not found! Mapping file for variant [variantName] not uploaded.
* New Relic discussion:
* https://discuss.newrelic.com/t/how-to-set-up-newrelic-properties-file-for-project-with-multiple-build-variants/46176
*/
def requiredTaskName = "assemble${variantName}"
def taskAssembleByVariant = project.tasks.findByName(requiredTaskName)
def createNewRelicPropertyFileVariantName = "createNewRelicPropertyFile${variantName}"
// 0. Searching task candidate to be dependent.
if (taskAssembleByVariant) {
logger.debug("Candidate task to be dependent found: ${taskAssembleByVariant.name}")
// 1. Task creation
def taskCreateNewRelicPropertyFile = tasks.create(name: createNewRelicPropertyFileVariantName) {
group = newRelicTasksGroup
description = "Generate the newrelic.properties file on project dir.\nA key/value propety " +
"will be written in file to be consumed by newRelicMapUploadVariantName task."
logger.debug("Creating task: ${name}")
doLast {
def newRelicPropertyKey = "com.newrelic.application_token"
def newRelicStringResourceKey = "new_relic_key"
def targetResourceFileName = "api.xml"
def variantXmlResourceFilePath = "${projectDirPath}/src/${variant.name}/res/values/${targetResourceFileName}"
def mainXmlResourceFilePath = "${projectDirPath}/src/main/res/values/${targetResourceFileName}"
def xmlResourceFilesPaths = [variantXmlResourceFilePath, mainXmlResourceFilePath]
xmlResourceFilesPaths.any { xmlResourceFilePath ->
// 1.1. Searching xml resource file.
def xmlResourceFile = new File(xmlResourceFilePath)
if (xmlResourceFile.exists()) {
logger.lifecycle("Reading property from xml resource file: ${xmlResourceFilePath}.")
// 1.2. Searching for string name new_relic_key api.xml resource file.
def nodeResources = new XmlParser().parse(xmlResourceFile)
def nodeString = nodeResources.find {
Node nodeString -> nodeString.'#name'.toString() == newRelicStringResourceKey
}
// 1.3. Checking if string name new_relic_key was found.
if (nodeString != null) {
def newRelicApplicationToken = "${nodeString.value()[0]}"
logger.lifecycle("name:${nodeString.'#name'.toString()};" +
"value:${newRelicApplicationToken}")
// 1.4 Checking the content of newRelicApplicationToken
if (newRelicApplicationToken == 'null' || newRelicApplicationToken.allWhitespace) {
logger.warn("Invalid value for key ${newRelicStringResourceKey}. " +
"Please, consider configuring a value for key ${newRelicStringResourceKey}" +
" on ${xmlResourceFile.name} resource file for buildVariant: ${variantName}. " +
"The ${newRelicPropertyFileName} will be not created.")
return true // break the loop
}
// 1.5. File creation.
File fileProperties = new File(newRelicPropertyFilePath)
fileProperties.createNewFile()
logger.lifecycle("File ${fileProperties.absolutePath} created.")
// 1.6. Writing content on properties file.
def fileComments = "File generated dynamically by gradle task ${createNewRelicPropertyFileVariantName}.\n" +
"Don't change it manually.\n" +
"Don't track it on VCS."
new Properties().with {
load(fileProperties.newDataInputStream())
setProperty(newRelicPropertyKey, newRelicApplicationToken.toString())
store(fileProperties.newWriter(), fileComments)
}
logger.lifecycle("Properties saved on file ${fileProperties.absolutePath}.")
return true // break the loop
} else {
logger.warn("The key ${newRelicStringResourceKey} was not found on ${xmlResourceFile.absolutePath}.\n" +
"Please, consider configuring a key/value on ${xmlResourceFile.name} resource file for buildVariant: ${variantName}.")
return // continue to next xmlResourceFilePath
}
} else {
logger.error("Resource file not found: ${xmlResourceFile.absolutePath}")
return // continue next xmlResourceFilePath
}
}
}
}
// 2. Task dependency setup
// To check the task dependencies, use:
// logger.lifecycle("Task ${name} now depends on tasks:")
// dependsOn.forEach { dep -> logger.lifecycle("\tTask: ${dep}") }
tasks['clean'].dependsOn taskDeleteNewRelicPropertyFile
taskCreateNewRelicPropertyFile.dependsOn taskDeleteNewRelicPropertyFile
taskAssembleByVariant.dependsOn taskCreateNewRelicPropertyFile
} else {
logger.error("Required task ${requiredTaskName} was not found. " +
"The task ${createNewRelicPropertyFileVariantName} will be not created.")
}
//</editor-fold>
}
On app/build.gradle file, apply the Gradle file.
apply from: './newrelic-util.gradle'
That’s it. I created a file named newrelic-util.gradle on project app dir. If you execute the task assembleAnyVariantName, the task createNewRelicPropertyFileAnyVarianteName will be performed first. Tip: don’t track the generated file newrelic.properties file. Ignore it on your VCS.
Additionally, the task deleteNewRelicPropertyFile will be performed right before the tasks ‘clean’ and ‘createNewRelicPropertyFileAnyVarianteName’ in order to avoid a file with a wrong New Relic application token.
Making some changes in Android Contacts package
Using mm (make) command to build this application
Because I have to change and build this app again and again, so I want to add a build time stamp in the Contacts.apk to check the build time when we runn it in the handset.
As we know, when we run mm command, the Android.mk (makefile) in Contacts package will be called.
And now, we can get the build time using date-macro.
But how we can write this build time stamp into a file that our application can read at runtime?
Any suggestions?
If you use Gradle, you can add buildConfigField with timestamp updated at build time.
android {
defaultConfig {
buildConfigField "long", "TIMESTAMP", System.currentTimeMillis() + "L"
}
}
Then read it at runtime.
Date buildDate = new Date(BuildConfig.TIMESTAMP);
Method which checks date of last modification of classes.dex, this means last time when your app's code was built:
try{
ApplicationInfo ai = getPackageManager().getApplicationInfo(getPackageName(), 0);
ZipFile zf = new ZipFile(ai.sourceDir);
ZipEntry ze = zf.getEntry("classes.dex");
long time = ze.getTime();
String s = SimpleDateFormat.getInstance().format(new java.util.Date(time));
zf.close();
}catch(Exception e){
}
Tested, and works fine, even if app is installed on SD card.
Since API version 9 there's:
PackageInfo.lastUpdateTime
The time at which the app was last updated.
try {
PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), 0);
//TODO use packageInfo.lastUpdateTime
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
On lower API versions you must make build time yourself. For example putting a file into assets folder containing the date. Or using __ DATE__ macro in native code. Or checking date when your classes.dex was built (date of file in your APK).
Edit: My answer does not work anymore since option keepTimestampsInApk was removed.
Working in 2020 is https://stackoverflow.com/a/26372474/6937282 (also https://stackoverflow.com/a/22649533/6937282 for more details)
Original answer:
A hint for solution "last modification time of classes.dex file" an newer AndroidStudio versions:
In default config the timestamp is not written anymore to files in apk file. Timestamp is always "Nov 30 1979".
You can change this behavior by adding this line to file
%userdir%/.gradle/gradle.properties (create if not existing)
android.keepTimestampsInApk = true
See Issue 220039
(Must be in userdir, gradle.properties in project build dir seems not to work)
Install time : packageInfo.lastUpdateTime
build time : zf.getEntry("classes.dex").getTime()
Both are differnet time.
You can check with the code below.
public class BuildInfoActivity extends Activity {
private static final String TAG = BuildInfoActivity.class.getSimpleName();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
try {
PackageManager pm = getPackageManager();
PackageInfo packageInfo = null;
try {
packageInfo = pm.getPackageInfo(getPackageName(), 0);
} catch (NameNotFoundException e) {
e.printStackTrace();
}
// install datetime
String appInstallDate = DateUtils.getDate(
"yyyy/MM/dd hh:mm:ss.SSS", packageInfo.lastUpdateTime);
// build datetime
String appBuildDate = DateUtils.getDate("yyyy/MM/dd hh:mm:ss.SSS",
DateUtils.getBuildDate(this));
Log.i(TAG, "appBuildDate = " + appBuildDate);
Log.i(TAG, "appInstallDate = " + appInstallDate);
} catch (Exception e) {
}
}
static class DateUtils {
public static String getDate(String dateFormat) {
Calendar calendar = Calendar.getInstance();
return new SimpleDateFormat(dateFormat, Locale.getDefault())
.format(calendar.getTime());
}
public static String getDate(String dateFormat, long currenttimemillis) {
return new SimpleDateFormat(dateFormat, Locale.getDefault())
.format(currenttimemillis);
}
public static long getBuildDate(Context context) {
try {
ApplicationInfo ai = context.getPackageManager()
.getApplicationInfo(context.getPackageName(), 0);
ZipFile zf = new ZipFile(ai.sourceDir);
ZipEntry ze = zf.getEntry("classes.dex");
long time = ze.getTime();
return time;
} catch (Exception e) {
}
return 0l;
}
}
}
in your build.gradle:
android {
defaultConfig {
buildConfigField 'String', 'BUILD_TIME', 'new java.text.SimpleDateFormat("MM.dd.yy HH:mm", java.util.Locale.getDefault()).format(new java.util.Date(' + System.currentTimeMillis() +'L))'
}
}
I use the same strategy as Pointer Null except I prefer the MANIFEST.MF file.
This one is regenerated even if a layout is modified (which is not the case for classes.dex).
I also force the date to be formated in GMT to avoid confusion between terminal and server TZs (if a comparison has to be made, ex: check latest version).
It result in the following code:
try{
ApplicationInfo ai = getPackageManager().getApplicationInfo(getPackageName(), 0);
ZipFile zf = new ZipFile(ai.sourceDir);
ZipEntry ze = zf.getEntry("META-INF/MANIFEST.MF");
long time = ze.getTime();
SimpleDateFormat formatter = (SimpleDateFormat) SimpleDateFormat.getInstance();
formatter.setTimeZone(TimeZone.getTimeZone("gmt"));
String s = formatter.format(new java.util.Date(time));
zf.close();
}catch(Exception e){
}
So Android Developer - Android Studio User Guide - Gradle Tips and Recipes - Simplify App Development actually documents what to add in order to have a release timestamp available to your app:
android {
...
buildTypes {
release {
// These values are defined only for the release build, which
// is typically used for full builds and continuous builds.
buildConfigField("String", "BUILD_TIME", "\"${minutesSinceEpoch}\"")
resValue("string", "build_time", "${minutesSinceEpoch}")
...
}
debug {
// Use static values for incremental builds to ensure that
// resource files and BuildConfig aren't rebuilt with each run.
// If they were dynamic, they would prevent certain benefits of
// Instant Run as well as Gradle UP-TO-DATE checks.
buildConfigField("String", "BUILD_TIME", "\"0\"")
resValue("string", "build_time", "0")
}
}
}
...
In your app code, you can access the properties as follows:
...
Log.i(TAG, BuildConfig.BUILD_TIME);
Log.i(TAG, getString(R.string.build_time));
I'm including this here since all of the other solutions appear to be from before the official example.
I know this is really old, but here's how I did it using ant within eclipse:
build.xml in project root
<project name="set_strings_application_build_date" default="set_build_date" basedir=".">
<description>
This ant script updates strings.xml application_build_date to the current date
</description>
<!-- set global properties for this build -->
<property name="strings.xml" location="./res/values/strings.xml"/>
<target name="init">
<!-- Create the time stamp -->
<tstamp/>
</target>
<target name="set_build_date" depends="init" description="sets the build date" >
<replaceregexp file="${strings.xml}"
match="(<string name="application_build_date">)\d+(</string>)"
replace="<string name="application_build_date">${DSTAMP}</string>" />
</target>
</project>
Then add an application_build_date string to your strings.xml
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
<string name="app_name">your app name</string>
<string name="application_build_date">20140101</string>
...
</resources>
Ensure the ant script is executed as a pre-build activity and you will always have a valid build date available to you within R.string.application_build_date.
For time stamping and versioning, build.gradle/android/defaultConfig:
def buildDateStamp = new Date().format("yyyyMMdd").toInteger()
versionCode buildDateStamp
versionName "$buildDateStamp"
buildConfigField "String", "BUILD_DATE_STAMP", "\"$buildDateStamp\""
Usage in code: BuildConfig.BUILD_DATE_STAMP
resValue "string", "build_date_stamp", "$buildDateStamp"
Usage in xml: "#string/build_date_stamp"
Caveat: adding HHmm will cause errors (probably integer overflow)