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.
Background
I have an app (here) that, among other features, allows to share APK files.
In order to do so, it reaches the file by accessing the path of packageInfo.applicationInfo.sourceDir (docs link here), and just shares the file (using ContentProvider when needed, as I've used here).
The problem
This works fine in most cases, especially when installing APK files from either the Play Store or from a standalone APK file, but when I install an app using Android-Studio itself, I see multiple APK files on this path, and none of them are valid ones that can be installed and run without any issues.
Here's a screenshot of the content of this folder, after trying out a sample from "Alerter" github repo :
I'm not sure when this issue has started, but it does occur at least on my Nexus 5x with Android 7.1.2. Maybe even before.
What I've found
This seems to be caused only from the fact that instant run is enabled on the IDE, so that it could help updating the app without the need to re-build it all together :
After disabling it, I can see that there is a single APK, just as it used to be in the past:
You can see the difference in file size between the correct APK and the split one.
Also, it seems that there is an API to get the paths to all of the splited APKs:
https://developer.android.com/reference/android/content/pm/ApplicationInfo.html#splitPublicSourceDirs
The question
What should be the easiest way to share an APK that got to be split into multiple ones ?
Is it really needed to somehow merge them?
It seems it is possible according to the docs :
Full paths to zero or more split APKs that, when combined with the
base APK defined in sourceDir, form a complete application.
But what's the correct way to do it, and is there a fast and efficient way to do it? Maybe without really creating a file?
Is there maybe an API to get a merged APK out of all the split ones? Or maybe such an APK already exist anyway in some other path, and there is no need for merging?
EDIT: just noticed that all third party apps that I've tried are supposed to share an installed app's APK fail to do so in this case.
I am the tech lead #Google for the Android Gradle Plugin, let me try to answer your question assuming I understand your use case.
First, some users mentioned you should not share your InstantRun enabled build and they are correct. The Instant Run builds on an application is highly customized for the current device/emulator image you are deploying to. So basically, say you generate an IR enabled build of your app for a particular device running 21, it will fail miserably if you try to use those exact same APKs on say a device running 23. I can go into much deeper explanation if necessary but suffice to say that we generate byte codes customized on the APIs found in android.jar (which is of course version specific).
So I do not think that sharing those APKs make sense, you should either use a IR disabled build or a release build.
Now for some details, each slice APK contains 1+ dex file(s), so in theory, nothing prevents you from unziping all those slice APKs, take all the dex files and stuff them back into the base.apk/rezip/resign and it should just work. However, it will still be an IR enabled application so it will start the small server to listen to IDE requests, etc, etc... I cannot imagine a good reason for doing this.
Hope this helps.
To merge multiple split apks to an single apk might be a little complicated.
Here is a suggestion to share the split apks directly and let the system to handle the merge and installation.
This might not be an answer to the question, since it's a little long, I post here as an 'answer'.
Framework new API PackageInstaller can handle monolithic apk or split apk.
In development environment
for monolithic apk, using adb install single_apk
for split apk, using adb install-multiple a_list_of_apks
You can see these two modes above from android studio Run output depends on your project has Instant run enable or disable.
For the command adb install-multiple, we can see the source code here, it will call the function install_multiple_app.
And then perform the following procedures
pm install-create # create a install session
pm install-write # write a list of apk to session
pm install-commit # perform the merge and install
What the pm actually do is call the framework api PackageInstaller, we can see the source code here
runInstallCreate
runInstallWrite
runInstallCommit
It's not mysterious at all, I just copied some methods or function here.
The following script can be invoked from adb shell environment to install all split apks to device, like adb install-multiple. I think it might work programmatically with Runtime.exec if your device is rooted.
#!/system/bin/sh
# get the total size in byte
total=0
for apk in *.apk
do
o=( $(ls -l $apk) )
let total=$total+${o[3]}
done
echo "pm install-create total size $total"
create=$(pm install-create -S $total)
sid=$(echo $create |grep -E -o '[0-9]+')
echo "pm install-create session id $sid"
for apk in *.apk
do
_ls_out=( $(ls -l $apk) )
echo "write $apk to $sid"
cat $apk | pm install-write -S ${_ls_out[3]} $sid $apk -
done
pm install-commit $sid
I my example, the split apks include (I got the list from android studio Run output)
app/build/output/app-debug.apk
app/build/intermediates/split-apk/debug/dependencies.apk
and all apks under app/build/intermediates/split-apk/debug/slices/slice[0-9].apk
Using adb push all the apks and the script above to a public writable directory, e.g /data/local/tmp/slices, and run the install script, it will install to your device just like adb install-multiple.
The code below is just another variant of the script above, if your app has platform signature or device is rooted, I think it will be ok. I didn't have the environment to test.
private static void installMultipleCmd() {
File[] apks = new File("/data/local/tmp/slices/slices").listFiles(new FileFilter() {
#Override
public boolean accept(File pathname) {
return pathname.getAbsolutePath().endsWith(".apk");
}
});
long total = 0;
for (File apk : apks) {
total += apk.length();
}
Log.d(TAG, "installMultipleCmd: total apk size " + total);
long sessionID = 0;
try {
Process pmInstallCreateProcess = Runtime.getRuntime().exec("/system/bin/sh\n");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(pmInstallCreateProcess.getOutputStream()));
writer.write("pm install-create\n");
writer.flush();
writer.close();
int ret = pmInstallCreateProcess.waitFor();
Log.d(TAG, "installMultipleCmd: pm install-create return " + ret);
BufferedReader pmCreateReader = new BufferedReader(new InputStreamReader(pmInstallCreateProcess.getInputStream()));
String l;
Pattern sessionIDPattern = Pattern.compile(".*(\\[\\d+\\])");
while ((l = pmCreateReader.readLine()) != null) {
Matcher matcher = sessionIDPattern.matcher(l);
if (matcher.matches()) {
sessionID = Long.parseLong(matcher.group(1));
}
}
Log.d(TAG, "installMultipleCmd: pm install-create sessionID " + sessionID);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
StringBuilder pmInstallWriteBuilder = new StringBuilder();
for (File apk : apks) {
pmInstallWriteBuilder.append("cat " + apk.getAbsolutePath() + " | " +
"pm install-write -S " + apk.length() + " " + sessionID + " " + apk.getName() + " -");
pmInstallWriteBuilder.append("\n");
}
Log.d(TAG, "installMultipleCmd: will perform pm install write \n" + pmInstallWriteBuilder.toString());
try {
Process pmInstallWriteProcess = Runtime.getRuntime().exec("/system/bin/sh\n");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(pmInstallWriteProcess.getOutputStream()));
// writer.write("pm\n");
writer.write(pmInstallWriteBuilder.toString());
writer.flush();
writer.close();
int ret = pmInstallWriteProcess.waitFor();
Log.d(TAG, "installMultipleCmd: pm install-write return " + ret);
checkShouldShowError(ret, pmInstallWriteProcess);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
try {
Process pmInstallCommitProcess = Runtime.getRuntime().exec("/system/bin/sh\n");
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(pmInstallCommitProcess.getOutputStream()));
writer.write("pm install-commit " + sessionID);
writer.flush();
writer.close();
int ret = pmInstallCommitProcess.waitFor();
Log.d(TAG, "installMultipleCmd: pm install-commit return " + ret);
checkShouldShowError(ret, pmInstallCommitProcess);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
private static void checkShouldShowError(int ret, Process process) {
if (process != null && ret != 0) {
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String l;
while ((l = reader.readLine()) != null) {
Log.d(TAG, "checkShouldShowError: " + l);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
Meanwhile, the simple way, you can try the framework api. Like the sample code above, it might work if the device is rooted or your app has platform signature, but I didn't get a workable environment to test it.
private static void installMultiple(Context context) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) {
PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller();
PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
try {
final int sessionId = packageInstaller.createSession(sessionParams);
Log.d(TAG, "installMultiple: sessionId " + sessionId);
PackageInstaller.Session session = packageInstaller.openSession(sessionId);
File[] apks = new File("/data/local/tmp/slices/slices").listFiles(new FileFilter() {
#Override
public boolean accept(File pathname) {
return pathname.getAbsolutePath().endsWith(".apk");
}
});
for (File apk : apks) {
InputStream inputStream = new FileInputStream(apk);
OutputStream outputStream = session.openWrite(apk.getName(), 0, apk.length());
byte[] buffer = new byte[65536];
int count;
while ((count = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, count);
}
session.fsync(outputStream);
outputStream.close();
inputStream.close();
Log.d(TAG, "installMultiple: write file to session " + sessionId + " " + apk.length());
}
try {
IIntentSender target = new IIntentSender.Stub() {
#Override
public int send(int i, Intent intent, String s, IIntentReceiver iIntentReceiver, String s1) throws RemoteException {
int status = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE);
Log.d(TAG, "send: status " + status);
return 0;
}
};
session.commit(IntentSender.class.getConstructor(IIntentSender.class).newInstance(target));
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
e.printStackTrace();
}
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
In order to use the hidden api IIntentSender, I add the jar library android-hidden-api as the provided dependency.
Those are called split apks. Which is mainly used in the PlayStore. As you may know, PlayStore only shows apps to the user if it's compatible with the device. Same in this case. The split has files varies from devices. Like if you used Different resources for Different devices which makes app really heavy. By making splits, it saves space for downloading and running for the user by only downloading the usable split apks.
Is it possible to merge them into single apk?
Yes. I used an app called Anti Split which allows that. Plus Apk Editor Ultra has same.
Can we save it into a single file?
Yes you can. As like for Anti Split, you have to first backup the app. Like in this case you have to back it up as apks file or xapk which is called bundled app in Android Studio. I have created a library for doing this. It's working perfectly for me. Am using it to backup apps into xapk which can later be installed using SIA app or XAPK Installer or we can use xapk file to merge it and make apk
For me instant run was a nightmare, 2-5 minute build times, and maddeningly often, recent changes were not included in builds. I highly recommend disabling instant run and adding this line to gradle.properties:
android.enableBuildCache=true
First build often takes some time for large projects (1-2mins), but after it's cached subsequent builds are usually lightnight fast (<10secs).
Got this tip from reddit user /u/QuestionsEverythang which has saved me SO much hassling around with instant run!
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);
}
Android Studio 2.0 Preview 2, Gradle Wrapper 2.8, Mac OS X
-MainProjectWorkspace
-|-build.gradle
-|-settings.gradle
-|-gradle.properties
-|-gradle
-|-MyLibraryDependencies
-|-MyMainModule
--|-build.gradle
--|-build
--|-src
---|-androidTest
---|-main
----|-assets
----|-jniLibs
----|-libs
----|-java
-----|-com
----|-res
----|-AndroidManifest.xml
MyMainModule/build.gradle
//Not a single SourceSets configurations.
productFlavors {
flavor1 {
}
flavor2 {
}
}
buildTypes {
release {
}
debug {
}
}
A genius developer left System.out.println statements, instead of Log statements in several hundreds of Java source-files in 'src/main/java'. Ideally, we do not want Sysout statements getting bundled with either of the applicationVariants, specially flavor1Release and flavor2Release.
Before we make amends to those hundreds of Java source-files and eventually switch the Sysout statements to Log statements, we would need to turn them off urgently.
Two possible solutions -
Execute a simple script that will remove all the Sysout statements in the Java source-files in 'src/main/java'. But about that, variants flavor1Debug and flavor2Debug need the Loggers displaying what's going on in the App.
Execute a simple Gradle task at build-time, copy Java source-files from 'src/main/java' to 'src/release/java', ensuring Sysout statements are omitted.
Solution 2 appears to be quickest, easiest, elegant. Particularly when the Gradle Custom-task is executed independently. Except for that, Gradle-sync in Android-Studio seems to be copying everything from 'src/main' to 'src/release' including assets, jniLibs, libs, res and even the AndroidManifest.xml. Fortunately, the Java source-files are ripped-off the Sysout statements in 'src/release', which is the expected result, and ideally, only 'src/release/java' should remain without any other folders.
task prepareRelease(type: Task) {
Pattern sysoutRegex = Pattern.compile(<Sysout-Pattern>)
try {
File releaseFolder = file("src/release")
File mainFolder = file("src/main")
ByteArrayOutputStream output = new ByteArrayOutputStream()
exec {
commandLine = "sh"
args = ["-c", "find ${mainFolder.canonicalPath} -name '*' -type f -print ",
"| xargs egrep -l '${sysoutRegex.pattern()}'"]
standardOutput = output
}
def fileList = output.toString().split(System.getProperty("line.separator"))
fileList.each { line ->
File srcFile = file(line)
File destFile = file(srcFile.canonicalPath.replace("main", "release"))
def content = srcFile.readLines()
if (!destFile.exists()) {
destFile.parentFile.mkdirs()
destFile.createNewFile()
destFile.writable = true
}
destFile.withWriter { out ->
content.eachWithIndex { contentLine, index ->
if (!sysoutRegex.matcher(contentLine).find()) {
out.println contentLine
}
}
}
} catch (Exception fail) {
throw fail
}
}
There is nothing in the custom Gradle-task that may cause this error of making "src/release" an exact copy of "src/main", which was not intended to be.
Any pointers to prevent this default copy of "src/main" to "src/release" will be greatly appreciated.
Based off RaGe's comment - "How about using *.java as your find pattern instead of * ?"
Kudos. I was not supposed to break the "find | xargs egrep " before the '|' into two separate args in the args[] in the exec task. Indeed, a Genius!!!
I am using Eclipse for Android SDK on Linux, and searching for a way to add the date and starttime of the compilation to one of the xml files. I like to see on the device which build version I am using, without updating this information before every compile step manually.
So far by searching the net I only found hints like "use ant".
I guess I have to use /proc/driver/rtc which is a dynamic "file" provided by the linux kernel that contains real time updated lines with colon separated text named for example "rtc_date" and "rtc_time". Including it and use the app on the device to get the information extracted.
Is there a better way? Like having eclipse either by knowing the time or stripping the information from proc and putting it at compile time in the xml file?
Its my first time using eclipse, so please excuse if I asked something obvious or impossible.
Regards
ct
I am using this code to get application build time. I know this is not outputting to an XML, but if you are trying to get when the app was build, this should work.
private long getAppBuildTime() {
if(cachedAppBuildTime == null) {
try{
ApplicationInfo ai = appContext.getPackageManager().getApplicationInfo(appContext.getPackageName(), 0);
ZipFile zf = new ZipFile(ai.sourceDir);
ZipEntry ze = zf.getEntry("classes.dex");
cachedAppBuildTime = ze.getTime();
log("app build time " + cachedAppBuildTime);
}catch(Throwable t){
return 1;
}
}
return cachedAppBuildTime;
}
The appContext variable in the code is obtained via context.getApplicationContext()
I use the same strategy as yigit 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).
It result in the following code:
private long mAppBuildTime = -1;
public long getAppBuildTime() {
if (mAppBuildTime == -1) {
try{
ApplicationInfo ai = getPackageManager().getApplicationInfo(getPackageName(), 0);
ZipFile zf = new ZipFile(ai.sourceDir);
ZipEntry ze = zf.getEntry("META-INF/MANIFEST.MF");
mAppBuildTime = ze.getTime();
zf.close();
}catch(Exception e){
}
}
return mAppBuildTime;
}