If someone tampers with an installed android app (apk file), are there any checks done at the time of launching to ensure integrity of an app is not compromised? As I understand there are no checks performed at launch time and I am trying to do the following:
I am trying to compute SHA-1 digests of the installed applications (apk file). I am aware that an apk file is like a zip file. It consists of other files. However, I am treating it as any other file (just a stream of bytes) and trying to compute SHA-1 digests of all the apk files. Are there any problems with this approach? The following code kept on giving null exception:
private static byte[] getSHA1FromFileContent(String filename)
{
try
{
MessageDigest digest = MessageDigest.getInstance("SHA-1");
//byte[] buffer = new byte[65536]; //created at start.
final FileInputStream fis = new FileInputStream(filename);
int n = 0;
byte[] buffer = null;
while (n != -1)
{
n = fis.read(buffer);
if (n > 0)
{
digest.update(buffer, 0, n);
}
}
byte[] digestResult = digest.digest();
return digestResult;
}
catch (Exception e)
{
return null;
}
}
As an alternative when I attempted to retrieve the files from the apk file and save the individual files as follows, I again kept on null exception
public void unzip()
{
try
{
FileInputStream fin = new FileInputStream(_zipFile);
ZipInputStream zin = new ZipInputStream(fin);
ZipEntry ze = null;
while ((ze = zin.getNextEntry()) != null)
{
Log.v("Decompress", "Unzipping " + ze.getName());
if(ze.isDirectory()) {
_dirChecker(ze.getName());
} else {
File dstfile = new File(_location + ze.getName());
dstfile.createNewFile();
FileOutputStream fout = new FileOutputStream(dstfile.getPath());
//OutputStream out = openFileOutput(_location + ze.getName(), Context.MODE_PRIVATE);
for (int c = zin.read(); c != -1; c = zin.read()) {
fout.write(c);
}
zin.closeEntry();
fout.close();
}
}
zin.close();
}
catch(Exception e)
{
Log.e("Decompress", "unzip", e);
}
}
I am also verifying the application configuration by way of - PreferencesManager.getDefaultSharedPreferences call providing the package name of the application as input parameter
In order to verify the integrity of an installed application is the above check enough?
If someone tampers with an installed android app (apk file), are there any checks done at the time of launching to ensure integrity of an app is not compromised?
There is no such concept as "compromised" from the OS standpoint, other than having an invalid digital signature. If somebody tampers with your app and signs it, that is indistinguishable to the OS from your original app, or the app after Amazon "tampers" with it for their store, etc.
Are there any problems with this approach? The following code kept on giving null exception
First, you are handling exceptions and doing no logging. You will find that debugging is much simpler when you log your exceptions. Then, you can use the stack trace (e.g., from DDMS) to find the line on which you are crashing, and fix your bug, whatever it is. If you want help with that, you will need to include in your question details on where the NullPointerException is occurring.
Second, whoever tampers with your app will simply remove all of this code, if they can find it.
Third, it may be fairly slow, making it easier for them to find it.
I am also verifying the application configuration by way of - PreferencesManager.getDefaultSharedPreferences call providing the package name of the application as input parameter
I have no idea why you think that this will be some form of verification.
In order to verify the integrity of an installed application is the above check enough?
IMHO, the above check is largely useless. If you obfuscate your code (e.g., with ProGuard), call it from several places, and use the other techniques outlined in this blog post, perhaps it will be worthwhile, but it may be too slow.
Related
In the last few days, my Android app is suddenly failing to download files from a web server to store in the app. This is the same for all users I have contacted. It was previously working in Android 11, so it's something that has only just changed. It's a (free) niche app for UK glider pilots to process NOTAMS, and has relatively large number of users who I don't want to let down.
The published app uses getExternalFilesDir(null) to return the directory in which to store the downloaded files, with android:requestLegacyExternalStorage set to "true" in the manifest.
I changed getExternalFilesDir(null) to getFilesDir() in Android Studio since that's what I understand should now be used for internal app data files. This returns /data/user/0/(my package name)/files. I'm running the Pixel 2 API 30 emulator for debugging, and the File Explorer shows that /data/data/(my package name)/files directory has been created. Everything I've read on here says that this is what is supposed to happen and it should all work. However no file was created when I attempted the download.
I changed android:requestLegacyExternalStorage to "false", and this time a file was created as expected. However it was empty and the download thread was giving an exception "unexpected end of stream on com.android.okhttp.Address#89599f3f".
This is the relevant code in my DownloadFile class which runs as a separate thread (comments removed for compactness):
public class DownloadFile implements Runnable
{
private String mUrlString;
private String mFileName;
private CountDownLatch mLatch;
public DownloadFile(String urlString, String fileName, CountDownLatch latch)
{
mUrlString = urlString;
mFileName = fileName;
mLatch = latch;
}
public void run()
{
HttpURLConnection urlConnection = null;
// Note for StackOverflow: following is a public static variable defined in the main activity
Spine.mDownloadStatus = false;
try
{
URL url = new URL(mUrlString);
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.setDoOutput(true);
urlConnection.setUseCaches(false);
urlConnection.connect();
File file = new File(Spine.dataDir, mFileName);
FileOutputStream fileOutput = new FileOutputStream(file);
InputStream inputStream = urlConnection.getInputStream();
byte[] buffer = new byte[1024];
int bufferLength = 0; // used to store a temporary size of the buffer
while ((bufferLength = inputStream.read(buffer)) > 0)
{
fileOutput.write(buffer, 0, bufferLength);
}
fileOutput.close();
Spine.mDownloadStatus = true;
}
// catch some possible errors...
catch (IOException e)
{
Spine.mErrorString = e.getMessage();
}
if (urlConnection != null)
urlConnection.disconnect();
// Signal completion
mLatch.countDown();
}
}
I now believe the problem lies with the URL connection, rather than the changes to local file storage access which is what I first thought. Incidentally, if I enter the full URL into my web browser the complete text file is displayed OK, so it's not a problem with the server.
The problem has been narrowed down to changes to the functionality of the website that hosts the data files to be downloaded. It's been made https secure and they are currently working on further changes.
I temporarily moved the hosting to my own website in Android Studio and everything worked so it's down to those website changes and nothing to do with my code (at least it may need changing later to support the upgrade to the main hosting site).
Thanks to all for responding.
Background
We want to let the user choose a video from any app, and then trim a video to be of max of 5 seconds.
The problem
For getting a Uri to be selected, we got it working fine (solution available here) .
As for the trimming itself, we couldn't find any good library that has permissive license, except for one called "k4l-video-trimmer" . The library "FFmpeg", for example, is considered not permission as it uses GPLv3, which requires the app that uses it to also be open sourced. Besides, as I've read, it takes quite a lot (about 9MB).
Sadly, this library (k4l-video-trimmer) is very old and wasn't updated in years, so I had to fork it (here) in order to handle it nicely. It uses a open sourced library called "mp4parser" to do the trimming.
Problem is, this library seems to be able to handle files only, and not a Uri or InputStream, so even the sample can crash when selecting items that aren't reachable like a normal file, or even have paths that it can't handle. I know that in many cases it is possible to get a path of a file, but in many other cases, it's not, and I also know it's possible to just copy the file (here), but this isn't a good solution, as the file could be large and take a lot of space even though it's already accessible.
What I've tried
There are 2 places that the library uses a file:
In "K4LVideoTrimmer" file, in the "setVideoURI" function, which just gets the file size to be shown. Here the solution is quite easy, based on Google's documentation:
public void setVideoURI(final Uri videoURI) {
mSrc = videoURI;
if (mOriginSizeFile == 0) {
final Cursor cursor = getContext().getContentResolver().query(videoURI, null, null, null, null);
if (cursor != null) {
int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
cursor.moveToFirst();
mOriginSizeFile = cursor.getLong(sizeIndex);
cursor.close();
mTextSize.setText(Formatter.formatShortFileSize(getContext(), mOriginSizeFile));
}
}
...
In "TrimVideoUtils" file, in "startTrim" which calls "genVideoUsingMp4Parser" function. There, it calls the "mp4parser" library using :
Movie movie = MovieCreator.build(new FileDataSourceViaHeapImpl(src.getAbsolutePath()));
It says that they use FileDataSourceViaHeapImpl (from "mp4parser" library) to avoid OOM on Android, so I decided to stay with it.
Thing is, there are 4 CTORS for it, all expect some variation of a file: File, filePath, FileChannel , FileChannel+fileName .
The questions
Is there a way to overcome this?
Maybe implement FileChannel and simulate a real file, by using ContentResolver and Uri ? I guess it might be possible, even if it means re-opening the InputStream when needed...
In order to see what I got working, you can clone the project here. Just know that it doesn't do any trimming, as the code for it in "K4LVideoTrimmer" file is commented:
//TODO handle trimming using Uri
//TrimVideoUtils.startTrim(file, getDestinationPath(), mStartPosition, mEndPosition, mOnTrimVideoListener);
Is there perhaps a better alternative to this trimming library, which is also permissive (meaning of Apache2/MIT licences , for example) ? One that don't have this issue? Or maybe even something of Android framework itself? I think MediaMuxer class could help (as written here), but I think it might need API 26, while we need to handle API 21 and above...
EDIT:
I thought I've found a solution by using a different solution for trimming itself, and wrote about it here, but sadly it can't handle some input videos, while mp4parser library can handle them.
Please let me know if it's possible to modify mp4parser to handle such input videos even if it's from Uri and not a File (without a workaround of just copying to a video file).
First of all a caveat: I am not familiar with the mp4parser library but your question looked interesting so I took a look.
I think its worth you looking at one of the classes the code comments say is "mainly for testing". InMemRandomAccessSourceImpl. To create a Movie from any URI, the code would be as follows:
try {
InputStream inputStream = getContentResolver().openInputStream(uri);
Log.e("InputStream Size","Size " + inputStream);
int bytesAvailable = inputStream.available();
int bufferSize = Math.min(bytesAvailable, MAX_BUFFER_SIZE);
final byte[] buffer = new byte[bufferSize];
int read = 0;
int total = 0;
while ((read = inputStream.read(buffer)) !=-1 ) {
total += read;
}
if( total < bytesAvailable ){
Log.e(TAG, "Read from input stream failed")
return;
}
//or try inputStream.readAllBytes() if using Java 9
inputStream.close();
ByteBuffer bb = ByteBuffer.wrap(buffer);
Movie m2 = MovieCreator.build(new ByteBufferByteChannel(bb),
new InMemRandomAccessSourceImpl(bb), "inmem");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
But I would say, there looks to be somewhat of a conflict between what you want to achieve and the approach the parser takes. It is depending on local files to avoid large memory overheads, and random access to bytes can only be done if the entire set of data is available, which differs from a streaming approach.
It will require buffering at least the amount of data required for your clip in one go before the parser is given the buffer. That might be workable for you if you are looking to grab short sections and the buffering is not too cumbersome. You may be subject to IO exceptions and the like if the read from the InputStream has issues, especially if it is remote content, whereas you really aren't expecting that with a file on a modern system.
There is also MemoryFile to consider which provides an ashmem backed file-like object. I think somehow that could be worked in.
Next a snipped shows how to open a MediaStore Uri with IsoFile from Mp4Parser. So, you can see how to get a FileChannel from a Uri.
public void test(#NonNull final Context context, #NonNull final Uri uri) throws IOException
{
ParcelFileDescriptor fileDescriptor = null;
try
{
final ContentResolver resolver = context.getContentResolver();
fileDescriptor = resolver.openFileDescriptor(uri, "rw");
if (fileDescriptor == null)
{
throw new IOException("Failed to open Uri.");
}
final FileDescriptor fd = fileDescriptor.getFileDescriptor();
final FileInputStream inputStream = new FileInputStream(fd);
final FileChannel fileChannel = inputStream.getChannel();
final DataSource channel = new FileDataSourceImpl(fileChannel);
final IsoFile isoFile = new IsoFile(channel);
... do what you need ....
}
finally
{
if (fileDescriptor != null)
{
fileDescriptor.close();
}
}
}
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 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;
}
There's an exporting feature in my application. It's just a copy operation since all my settings are store in shared preference.
I just copy the xml file from /data/data/package.name/shared_prefs/settings.xml to SD card. It works fine on my HTC desire. However, it might not work on Samsung devices, and i got the following error while I try to copy the file.
I/System.out( 3166): /data/data/package.name/shared_prefs/settings.xml (No such file or directory)
in the directory.
Anyone know how to fix it, or is there another simple way to store the shared preference ?
Thanks.
Never never never never never never never never never hardwire paths.
Unfortunately, there's no getSharedPreferenceDir() anywhere that I can think of. The best solution I can think of will be:
new File(getFilesDir(), "../shared_prefs")
This way if a device manufacturer elects to change partition names, you are covered.
Try this and see if it helps.
CommonsWare's suggestion would a be clever hack, but unfortunately it won't work.
Samsung does not always put the shared_prefs directory in the same parent directory as the getFilesDir().
I'd recommend testing for the existence of (hardcode it, except for package name):
/dbdata/databases/<package_name>/shared_prefs/package.name_preferences.xml and if it exists use it, otherwise fall back to either CommonsWare's suggestion of new File(getFilesDir(), "../shared_prefs") or just /data/data/<package_name>/shared_prefs/package.name_preferences.xml.
A warning though that this method could potentially have problems if a user switched from a Samsung rom to a custom rom without wiping, as the /dbdata/databases file might be unused but still exist.
More details
On some Samsung devices, such as the Galaxy S series running froyo, the setup is this:
/data/data/<package_name>/(lib|files|databases)
Sometimes there's a shared_prefs there too, but it's just Samsung's attempt to confuse you! Don't trust it! (I think it can happen as a left over from a 2.1 upgrade to 2.2, but it might be a left over from users switching roms. I don't really know, I just have both included in my app's bug report interface and sometimes see both files).
And:
/dbdata/databases/<package_name>/shared_prefs
That's the real shared_prefs directory.
However on the Galaxy Tab on Froyo, it's weird. Generally you have: /data/data/<package_name>/(lib|shared_prefs|files|databases)
With no /dbdata/databases/<package_name> directory, but it seems the system apps do have:
/dbdata/databases/<package_name>/yourdatabase.db
And added bonus is that /dbdata/databases/<package_name> is not removed when your app is uninstalled. Good luck using SharedPreferences if the user ever reinstalls your app!
Try using
context.getFilesDir().getParentFile().getAbsolutePath()
Best way to get valid path on all devices - run method Context.getSharedPrefsFile defined as:
/**
* {#hide}
* Return the full path to the shared prefs file for the given prefs group name.
*
* <p>Note: this is not generally useful for applications, since they should
* not be directly accessing the file system.
*/
public abstract File getSharedPrefsFile(String name);
Because of it hidden need use reflection and use fallback on fail:
private File getSharedPrefsFile(String name) {
Context context = ...;
File file = null;
try {
if (Build.VERSION.SDK_INT >= 24) {
try {
Method m = context.getClass().getMethod("getSharedPreferencesPath", new Class[] {String.class});
file = (File)m.invoke(context, new Object[]{name});
} catch (Throwable e) {
Log.w("App TAG", "Failed call getSharedPreferencesPath", e);
}
}
if (file == null) {
Method m = context.getClass().getMethod("getSharedPrefsFile", new Class[] {String.class});
file = (File)m.invoke(context, new Object[]{name});
}
} catch (Throwable e) {
Log.w("App TAG", "Failed call getSharedPrefsFile", e);
file = new File(context.getFilesDir(), "../shared_prefs/" + name + ".xml");
}
return file;
}
On some Samsungs implements like this:
public File getSharedPrefsFile(String paramString) {
return makeFilename(getPreferencesDir(), paramString + ".xml");
}
private File getPreferencesDir() {
synchronized (this.mSync) {
if (this.mPreferencesDir == null) {
this.mPreferencesDir = new File("/dbdata/databases/" + getPackageName() + "/", "shared_prefs");
}
File localFile = this.mPreferencesDir;
return localFile;
}
}
On other Android like this:
public File getSharedPrefsFile(String name) {
return makeFilename(getPreferencesDir(), name + ".xml");
}
private File getPreferencesDir() {
synchronized (mSync) {
if (mPreferencesDir == null) {
mPreferencesDir = new File(getDataDirFile(), "shared_prefs");
}
return mPreferencesDir;
}
}
private File getDataDirFile() {
if (mPackageInfo != null) {
return mPackageInfo.getDataDirFile();
}
throw new RuntimeException("Not supported in system context");
}
After while Google change API for level 24 and later:
https://android.googlesource.com/platform/frameworks/base/+/6a6cdafaec56fcd793214678c7fcc52f0b860cfc%5E%21/core/java/android/app/ContextImpl.java
I've tested in Samsung P1010 with:
//I'm in a IntentService class
File file = this.getDir("shared_prefs", MODE_PRIVATE);
I got:
"/data/data/package.name/app_shared_prefs"
It works fine to me. I can run ffmpeg in this folder.
Look:
Context.getDir
You have to create the shared_prefs directory:
try{
String dir="/data/data/package.name/shared_prefs";
// Create one directory
boolean success = (new File(dir)).mkdirs();
if (success) {
// now copy the file
}
}catch (Exception e){//Catch exception if any
System.err.println("Error: " + e.getMessage());
}
Also... the package of your app is package.name? Make sure you are referring to the right package.