for my app i want to implement a changelog, but dont know how (which concept).
I want, that the changelog pops up once a time after new version of my app installed.
Sounds easy, but i have no clue. :/
Dialog to show my Changelog exists already, i just wanna know how to show it one after an update.
Thanks for your hints.
Prexx
one option is to use Android Change Log.
With Android Change Log you can easily create, show and maintain an
Android change log dialog.
Features
display only what's new or show the whole change log
display on first start of newly installed app or on new app version
write the change log in a simplified language but also use HTML and
CSS if needed
You can store a value in SharedPreferences which version you showed the changelog last time.
E.g.: 'lastChangelogVersion' : '1.1.0'
When your MainActivity starts it compares this value with the current version of your software and if it differs the changelog popup appears (and sets the new value).
This value will not be overridden when a new version of your application is being installed.
UPDATE:
Also, you might encounter that the user cleared your application's data. In this case you can't decide whether the changelog was displayed before or not so you can show it again. Android Market works the same way: if you clear it's app data you will be facing with the Licence Agreement again when launching Market.
I found the following options for adding a changelog to your Android app. Using any of these libraries would definitely save time over implementing this yourself. They all follow the general approach that #papaiatis mentions in his answer.
changeloglib
ckChangeLog
paperboy
changelog
android-change-log
Appnouncements (Disclaimer: I'm the author of this one)
I found Michael Flisar's change log (https://github.com/MFlisar/changelog) extremely easy to use.
After an app update I show a "What's New" dialog by:
ChangelogBuilder builder = new ChangelogBuilder()
.withTitle("What\'s New")
.withUseBulletList(true)
.withManagedShowOnStart(true)
.buildAndShowDialog(activity, false);
And I can show an activity with the entire change log via:
ChangelogBuilder builder = new ChangelogBuilder()
.withTitle("Change Log")
.withUseBulletList(true)
.buildAndStartActivity(context, true);
Easy peasy.
/**
* Copyright (C) 2011-2013, Karsten Priegnitz
*
* Permission to use, copy, modify, and distribute this piece of software
* for any purpose with or without fee is hereby granted, provided that
* the above copyright notice and this permission notice appear in the
* source code of all copies.
*
* It would be appreciated if you mention the author in your change log,
* contributors list or the like.
*
* #author: Karsten Priegnitz
* #see: http://code.google.com/p/android-change-log/
*/
package sheetrock.panda.changelog;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.content.pm.PackageManager.NameNotFoundException;
import android.graphics.Color;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.webkit.WebView;
public class ChangeLog {
private final Context context;
private String lastVersion, thisVersion;
// this is the key for storing the version name in SharedPreferences
private static final String VERSION_KEY = "PREFS_VERSION_KEY";
private static final String NO_VERSION = "";
/**
* Constructor
*
* Retrieves the version names and stores the new version name in SharedPreferences
*
* #param context
*/
public ChangeLog(Context context) {
this(context, PreferenceManager.getDefaultSharedPreferences(context));
}
/**
* Constructor
*
* Retrieves the version names and stores the new version name in SharedPreferences
*
* #param context
* #param sp
* the shared preferences to store the last version name into
*/
public ChangeLog(Context context, SharedPreferences sp) {
this.context = context;
// get version numbers
this.lastVersion = sp.getString(VERSION_KEY, NO_VERSION);
Log.d(TAG, "lastVersion: " + lastVersion);
try {
this.thisVersion = context.getPackageManager().getPackageInfo(context.getPackageName(),
0).versionName;
} catch (NameNotFoundException e) {
this.thisVersion = NO_VERSION;
Log.e(TAG, "could not get version name from manifest!");
e.printStackTrace();
}
Log.d(TAG, "appVersion: " + this.thisVersion);
}
/**
* #return The version name of the last installation of this app (as described in the former
* manifest). This will be the same as returned by <code>getThisVersion()</code> the
* second time this version of the app is launched (more precisely: the second time
* ChangeLog is instantiated).
* #see AndroidManifest.xml#android:versionName
*/
public String getLastVersion() {
return this.lastVersion;
}
/**
* #return The version name of this app as described in the manifest.
* #see AndroidManifest.xml#android:versionName
*/
public String getThisVersion() {
return this.thisVersion;
}
/**
* #return <code>true</code> if this version of your app is started the first time
*/
public boolean firstRun() {
return !this.lastVersion.equals(this.thisVersion);
}
/**
* #return <code>true</code> if your app including ChangeLog is started the first time ever.
* Also <code>true</code> if your app was deinstalled and installed again.
*/
public boolean firstRunEver() {
return NO_VERSION.equals(this.lastVersion);
}
/**
* #return An AlertDialog displaying the changes since the previous installed version of your
* app (what's new). But when this is the first run of your app including ChangeLog then
* the full log dialog is show.
*/
public AlertDialog getLogDialog() {
return this.getDialog(this.firstRunEver());
}
/**
* #return an AlertDialog with a full change log displayed
*/
public AlertDialog getFullLogDialog() {
return this.getDialog(true);
}
protected AlertDialog getDialog(boolean full) {
WebView wv = new WebView(this.context);
wv.setBackgroundColor(Color.parseColor(context.getResources().getString(
R.string.background_color)));
wv.loadDataWithBaseURL(null, this.getLog(full), "text/html", "UTF-8", null);
AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(this.context,
android.R.style.Theme_Dialog));
builder.setTitle(
context.getResources().getString(
full ? R.string.changelog_full_title : R.string.changelog_title))
.setView(wv)
.setCancelable(false)
// OK button
.setPositiveButton(context.getResources().getString(R.string.changelog_ok_button),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
updateVersionInPreferences();
}
});
if (!full) {
// "more ..." button
builder.setNegativeButton(R.string.changelog_show_full,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
getFullLogDialog().show();
}
});
}
return builder.create();
}
protected void updateVersionInPreferences() {
// save new version number to preferences
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
SharedPreferences.Editor editor = sp.edit();
editor.putString(VERSION_KEY, thisVersion);
// // on SDK-Versions > 9 you should use this:
// if(Build.VERSION.SDK_INT < Build.VERSION_CODES.GINGERBREAD) {
// editor.commit();
// } else {
// editor.apply();
// }
editor.commit();
}
/**
* #return HTML displaying the changes since the previous installed version of your app (what's
* new)
*/
public String getLog() {
return this.getLog(false);
}
/**
* #return HTML which displays full change log
*/
public String getFullLog() {
return this.getLog(true);
}
/** modes for HTML-Lists (bullet, numbered) */
private enum Listmode {
NONE, ORDERED, UNORDERED,
};
private Listmode listMode = Listmode.NONE;
private StringBuffer sb = null;
private static final String EOCL = "END_OF_CHANGE_LOG";
protected String getLog(boolean full) {
// read changelog.txt file
sb = new StringBuffer();
try {
InputStream ins = context.getResources().openRawResource(R.raw.changelog);
BufferedReader br = new BufferedReader(new InputStreamReader(ins));
String line = null;
boolean advanceToEOVS = false; // if true: ignore further version
// sections
while ((line = br.readLine()) != null) {
line = line.trim();
char marker = line.length() > 0 ? line.charAt(0) : 0;
if (marker == '$') {
// begin of a version section
this.closeList();
String version = line.substring(1).trim();
// stop output?
if (!full) {
if (this.lastVersion.equals(version)) {
advanceToEOVS = true;
} else if (version.equals(EOCL)) {
advanceToEOVS = false;
}
}
} else if (!advanceToEOVS) {
switch (marker) {
case '%':
// line contains version title
this.closeList();
sb.append("<div class='title'>" + line.substring(1).trim() + "</div>\n");
break;
case '_':
// line contains version title
this.closeList();
sb.append("<div class='subtitle'>" + line.substring(1).trim() + "</div>\n");
break;
case '!':
// line contains free text
this.closeList();
sb.append("<div class='freetext'>" + line.substring(1).trim() + "</div>\n");
break;
case '#':
// line contains numbered list item
this.openList(Listmode.ORDERED);
sb.append("<li>" + line.substring(1).trim() + "</li>\n");
break;
case '*':
// line contains bullet list item
this.openList(Listmode.UNORDERED);
sb.append("<li>" + line.substring(1).trim() + "</li>\n");
break;
default:
// no special character: just use line as is
this.closeList();
sb.append(line + "\n");
}
}
}
this.closeList();
br.close();
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
protected void openList(Listmode listMode) {
if (this.listMode != listMode) {
closeList();
if (listMode == Listmode.ORDERED) {
sb.append("<div class='list'><ol>\n");
} else if (listMode == Listmode.UNORDERED) {
sb.append("<div class='list'><ul>\n");
}
this.listMode = listMode;
}
}
protected void closeList() {
if (this.listMode == Listmode.ORDERED) {
sb.append("</ol></div>\n");
} else if (this.listMode == Listmode.UNORDERED) {
sb.append("</ul></div>\n");
}
this.listMode = Listmode.NONE;
}
private static final String TAG = "ChangeLog";
/**
* manually set the last version name - for testing purposes only
*
* #param lastVersion
*/
public void dontuseSetLastVersion(String lastVersion) {
this.lastVersion = lastVersion;
}
}
Related
I'm using dnsjava in my android project "android studio" I have imported the library. But when I run the code I'm getting "no records found" does anyone face this? I found a post here that talks about similar problem but the solution didn't work. as I change the type of query to ANY or A or NS. It just doesn't work. Anyone know if this is due to malfunction of the library code? any suggestions welcome.
package org.pctechtips.netdroid.async;
import android.os.AsyncTask;
import android.util.Log;
import org.pctechtips.netdroid.response.DnsAsyncResponse;
import org.xbill.DNS.Lookup;
import org.xbill.DNS.Record;
import org.xbill.DNS.TextParseException;
import org.xbill.DNS.Type;
import java.lang.ref.WeakReference;
public class DnsLookupAsyncTask extends AsyncTask<String, Void, String> {
private final WeakReference<DnsAsyncResponse> delegate;
/**
* Constructor to set the delegate
*
* #param delegate Called when the DNS lookup finishes
*/
public DnsLookupAsyncTask(DnsAsyncResponse delegate) {
this.delegate = new WeakReference<>(delegate);
}
/**
* Performs the appropriate lookup for specified record type
*
* #param params
* #return DNS answer
*/
#Override
protected String doInBackground(String... params) {
String domain = params[0];
int recordType = Integer.parseInt(params[1]);
Record[] records;
try {
records = new Lookup("google.com", Type.ANY).run();
Log.d("DNS", records.toString());
if (records == null) {
return "No records found.";
}
StringBuilder answer = new StringBuilder();
for (Record record : records) {
String rClass = this.parseRecordClass(record.getDClass());
answer.append(String.format("%s\t\t\t\t%s\t\t\t\t%s\t\t\t\t%s%n%n", record.getName(), record.getTTL(), rClass, record.rdataToString()));
}
return answer.toString();
} catch (TextParseException e) {
return "Error performing lookup!";
}
}
/**
* Determines the string representation of the DNS record class
*
* #param recordClass Numeric record class
* #return Human readable record class
*/
private String parseRecordClass(int recordClass) {
switch (recordClass) {
case 1:
return "IN";
case 2:
return "CS";
case 3:
return "CH";
case 4:
return "HS";
default:
return "IN";
}
}
/**
* Calls the delegate when the DNS lookup has finished
*
* #param result DNS answer
*/
#Override
protected void onPostExecute(String result) {
DnsAsyncResponse activity = delegate.get();
if (activity != null) {
activity.processFinish(result);
}
}
}
Android Oreo made a change that prevents looking up dns servers. https://developer.android.com/about/versions/oreo/android-8.0-changes.html#o-pri
See: https://stackoverflow.com/a/48973823/5420880 for more details and workarounds.
As a specific workaround for dnsjava, you can supply a comma-separated list of dns servers via the dns.server system property.
I just complete a android app based on a website tutorial. This app is to send and receive data to google datastore. I have created a appengine backend. This works well locally on localhost:8888. I could see the data transformation. But after I deploy it to google app engine. It can not show the data. I could access the datastore by myapp.appspot.com/_ah/api/explorer. But I can not access it with phone while I can access local data with phone emulator. I just followed this gentleman's guide https://github.com/sachinkariyattin/Cloudendpoints
Any one can help me? Thanks in advance.
The Below is the CloudEndpointUtils class
package com.iot1;
import com.google.api.client.googleapis.json.GoogleJsonError;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.client.googleapis.services.AbstractGoogleClient;
import com.google.api.client.googleapis.services.AbstractGoogleClientRequest;
import com.google.api.client.googleapis.services.GoogleClientRequestInitializer;
import android.app.Activity;
import android.util.Log;
import android.widget.Toast;
import java.io.IOException;
/**
* Common utilities for working with Cloud Endpoints.
*
* If you'd like to test using a locally-running version of your App Engine
* backend (i.e. running on the Development App Server), you need to set
* LOCAL_ANDROID_RUN to 'true'.
*
* See the documentation at
* http://developers.google.com/eclipse/docs/cloud_endpoints for more
* information.
*/
public class CloudEndpointUtils {
/*
* TODO: Need to change this to 'true' if you're running your backend locally using
* the DevAppServer. See
* http://developers.google.com/eclipse/docs/cloud_endpoints for more
* information.
*/
protected static final boolean LOCAL_ANDROID_RUN = true;
/*
* The root URL of where your DevAppServer is running (if you're running the
* DevAppServer locally).
*/
protected static final String LOCAL_APP_ENGINE_SERVER_URL = "http://localhost:8888/";
/*
* The root URL of where your DevAppServer is running when it's being
* accessed via the Android emulator (if you're running the DevAppServer
* locally). In this case, you're running behind Android's virtual router.
* See
* http://developer.android.com/tools/devices/emulator.html#networkaddresses
* for more information.
*/
protected static final String LOCAL_APP_ENGINE_SERVER_URL_FOR_ANDROID = "http://10.0.2.2:8888";
/**
* Updates the Google client builder to connect the appropriate server based
* on whether LOCAL_ANDROID_RUN is true or false.
*
* #param builder
* Google client builder
* #return same Google client builder
*/
public static <B extends AbstractGoogleClient.Builder> B updateBuilder(
B builder) {
if (LOCAL_ANDROID_RUN) {
builder.setRootUrl(LOCAL_APP_ENGINE_SERVER_URL_FOR_ANDROID
+ "/_ah/api/");
}
// only enable GZip when connecting to remote server
final boolean enableGZip = builder.getRootUrl().startsWith("https:");
builder.setGoogleClientRequestInitializer(new GoogleClientRequestInitializer() {
#Override
public void initialize(AbstractGoogleClientRequest<?> request)
throws IOException {
if (!enableGZip) {
request.setDisableGZipContent(true);
}
}
});
return builder;
}
/**
* Logs the given message and shows an error alert dialog with it.
*
* #param activity
* activity
* #param tag
* log tag to use
* #param message
* message to log and show or {#code null} for none
*/
public static void logAndShow(Activity activity, String tag, String message) {
Log.e(tag, message);
showError(activity, message);
}
/**
* Logs the given throwable and shows an error alert dialog with its
* message.
*
* #param activity
* activity
* #param tag
* log tag to use
* #param t
* throwable to log and show
*/
public static void logAndShow(Activity activity, String tag, Throwable t) {
Log.e(tag, "Error", t);
String message = t.getMessage();
// Exceptions that occur in your Cloud Endpoint implementation classes
// are wrapped as GoogleJsonResponseExceptions
if (t instanceof GoogleJsonResponseException) {
GoogleJsonError details = ((GoogleJsonResponseException) t)
.getDetails();
if (details != null) {
message = details.getMessage();
}
}
showError(activity, message);
}
/**
* Shows an error alert dialog with the given message.
*
* #param activity
* activity
* #param message
* message to show or {#code null} for none
*/
public static void showError(final Activity activity, String message) {
final String errorMessage = message == null ? "Error" : "[Error ] "
+ message;
activity.runOnUiThread(new Runnable() {
#Override
public void run() {
Toast.makeText(activity, errorMessage, Toast.LENGTH_LONG)
.show();
}
});
}
}
I guess you did not create your own account at Google Cloud and/or make the necessary changes in appengine-web.xml file. For a thorough example you may follow: http://rominirani.com/2014/08/20/gradle-tutorial-part-7-android-studio-app-engine-gradle/
I found it that I should update the created google client library after I changed the appengine-web.xml. And it all works now.
I've got an Android app which scans for all Apps installed on the device and then reports this to a server (it's an MDM agent). Any suggestions on how to get the Category of the App? Everyone has a different list of Categories, but basically something like Game, Entertainment, Tools/Utilities, etc.
From what I can tell there is nothing related to Category stored on the device itself. I was thinking of using the android market API to search for the application in the market and use the Category value returned by the search. Not sure how successful this will be finding a match. Any suggestions on how best to do this?
Any suggestions on a different approach?
Thanks in advance.
mike
I know that this is an old post, but for anyone still looking for this, API level 26 (O) has added categories to android.content.pm.ApplicationInfo.
From the docs https://developer.android.com/reference/android/content/pm/ApplicationInfo#category:
public int category
The category of this app. Categories are used to cluster multiple apps together into meaningful groups, such as when summarizing battery, network, or disk usage. Apps should only define this value when they fit well into one of the specific categories.
Set from the R.attr.appCategory attribute in the manifest. If the manifest doesn't define a category, this value may have been provided by the installer via PackageManager#setApplicationCategoryHint(String, int).
Value is CATEGORY_UNDEFINED, CATEGORY_GAME, CATEGORY_AUDIO, CATEGORY_VIDEO, CATEGORY_IMAGE, CATEGORY_SOCIAL, CATEGORY_NEWS, CATEGORY_MAPS, or CATEGORY_PRODUCTIVITY
One can now do something like:
PackageManager pm = context.getPackageManager();
ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, 0);
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
int appCategory = applicationInfo.category;
String categoryTitle = (String) ApplicationInfo.getCategoryTitle(context, appCategory)
// ...
}
if you get for each application its package name, you could ask directly to play store which category an app belongs, parsing html response page with this library:
org.jsoup.jsoup1.8.3
Here's a snippet to solve your problem:
public class MainActivity extends AppCompatActivity {
public final static String GOOGLE_URL = "https://play.google.com/store/apps/details?id=";
public static final String ERROR = "error";
...
private class FetchCategoryTask extends AsyncTask<Void, Void, Void> {
private final String TAG = FetchCategoryTask.class.getSimpleName();
private PackageManager pm;
private ActivityUtil mActivityUtil;
#Override
protected Void doInBackground(Void... errors) {
String category;
pm = getPackageManager();
List<ApplicationInfo> packages = pm.getInstalledApplications(PackageManager.GET_META_DATA);
Iterator<ApplicationInfo> iterator = packages.iterator();
while (iterator.hasNext()) {
ApplicationInfo packageInfo = iterator.next();
String query_url = GOOGLE_URL + packageInfo.packageName;
Log.i(TAG, query_url);
category = getCategory(query_url);
// store category or do something else
}
return null;
}
private String getCategory(String query_url) {
boolean network = mActivityUtil.isNetworkAvailable();
if (!network) {
//manage connectivity lost
return ERROR;
} else {
try {
Document doc = Jsoup.connect(query_url).get();
Element link = doc.select("span[itemprop=genre]").first();
return link.text();
} catch (Exception e) {
return ERROR;
}
}
}
}
}
You could make these queries in an AsyncTask, or in a service. Hope that you find it helpful.
I also faced the same issue. The solution for the above query is stated below.
Firstly, download the Jsoup library or download the jar file.
or
Add this to your build.gradle(Module: app) implementation 'org.jsoup:jsoup:1.11.3'
private class FetchCategoryTask extends AsyncTask<Void, Void, Void> {
private final String TAG = FetchCategoryTask.class.getSimpleName();
private PackageManager pm;
//private ActivityUtil mActivityUtil;
#Override
protected Void doInBackground(Void... errors) {
String category;
pm = getPackageManager();
List<ApplicationInfo> packages = pm.getInstalledApplications(PackageManager.GET_META_DATA);
Iterator<ApplicationInfo> iterator = packages.iterator();
// while (iterator.hasNext()) {
// ApplicationInfo packageInfo = iterator.next();
String query_url = "https://play.google.com/store/apps/details?id=com.imo.android.imoim"; //GOOGLE_URL + packageInfo.packageName;
Log.i(TAG, query_url);
category = getCategory(query_url);
Log.e("CATEGORY", category);
// store category or do something else
//}
return null;
}
private String getCategory(String query_url) {
try {
Document doc = Jsoup.connect(query_url).get();
Elements link = doc.select("a[class=\"hrTbp R8zArc\"]");
return link.text();
} catch (Exception e) {
Log.e("DOc", e.toString());
}
}
}
In return, you will get Application Company Name and category of the application
I made a Kotlin solution based on the answer from #Ankit Kumar Singh.
This solution maps the category to an enum, in case you want to do other things than just show it.
import kotlinx.coroutines.*
import org.jsoup.Jsoup
import javax.inject.Inject
import javax.inject.Singleton
class AppCategoryService {
companion object {
private const val APP_URL = "https://play.google.com/store/apps/details?id="
private const val CAT_SIZE = 9
private const val CATEGORY_STRING = "category/"
}
suspend fun fetchCategory(packageName: String): AppCategory {
val url = "$APP_URL$packageName&hl=en" //https://play.google.com/store/apps/details?id=com.example.app&hl=en
val categoryRaw = parseAndExtractCategory(url) ?: return AppCategory.OTHER
return AppCategory.fromCategoryName(categoryRaw)
}
#Suppress("BlockingMethodInNonBlockingContext")
private suspend fun parseAndExtractCategory(url: String): String? = withContext(Dispatchers.IO) {
return#withContext try {
val text = Jsoup.connect(url).get()?.select("a[itemprop=genre]") ?: return#withContext null
val href = text.attr("abs:href")
if (href != null && href.length > 4 && href.contains(CATEGORY_STRING)) {
getCategoryTypeByHref(href)
} else {
null
}
} catch (e: Throwable) {
null
}
}
private fun getCategoryTypeByHref(href: String) = href.substring(href.indexOf(CATEGORY_STRING) + CAT_SIZE, href.length)
}
And here is the enum with all the possible values at of this moment in time:
// Note: Enum name matches API value and should not be changed
enum class AppCategory {
OTHER,
ART_AND_DESIGN,
AUTO_AND_VEHICLES,
BEAUTY,
BOOKS_AND_REFERENCE,
BUSINESS,
COMICS,
COMMUNICATION,
DATING,
EDUCATION,
ENTERTAINMENT,
EVENTS,
FINANCE,
FOOD_AND_DRINK,
HEALTH_AND_FITNESS,
HOUSE_AND_HOME,
LIBRARIES_AND_DEMO,
LIFESTYLE,
MAPS_AND_NAVIGATION,
MEDICAL,
MUSIC_AND_AUDIO,
NEWS_AND_MAGAZINES,
PARENTING,
PERSONALIZATION,
PHOTOGRAPHY,
PRODUCTIVITY,
SHOPPING,
SOCIAL,
SPORTS,
TOOLS,
TRAVEL_AND_LOCAL,
VIDEO_PLAYERS,
WEATHER,
GAMES;
companion object {
private val map = values().associateBy(AppCategory::name)
private const val CATEGORY_GAME_STRING = "GAME_" // All games start with this prefix
fun fromCategoryName(name: String): AppCategory {
if (name.contains(CATEGORY_GAME_STRING)) return GAMES
return map[name.toUpperCase(Locale.ROOT)] ?: OTHER
}
}
}
private fun getCategory(){
val GOOGLE_URL = "https://play.google.com/store/apps/details?id=com.google.android.deskclock"
lifecycleScope.launch(Dispatchers.IO) {
val doc: Document = Jsoup.connect(GOOGLE_URL).get()
val index = doc.body().data().indexOf("applicationCategory")
val simpleString = doc.body().data().subSequence(index,index+100)
val data = simpleString.split(":")[1].split(",")[0]
Log.e("DATA-->",data.toString())
}
}
You can use below AsyncTask for extract Android app category from playStore by using app package id.
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.AsyncTask;
import android.util.Log;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.IOException;
public class GetAppCategory extends AsyncTask<String, Void, String> {
//Main URL for each app on Play Store
public static final String APP_URL = "https://play.google.com/store/apps/details?id=";
//Use below String if extracting 'CATEGORY' from href tag.
private final String CATEGORY_STRING = "category/";
private final int cat_size = 9;
/*Use below String for identified 'GAME' apps, which must start with this prefix.
Here I only display 'Games' as category for all Games app instead of showing their subCategory also*/
private final String CATEGORY_GAME_STRING = "GAME_";
//Name of the app package for which you want to get category.
private String packageName = null;
private PackageManager pm = null;
//Activity or Application context as per requirement.
private Context appContext;
/* You can add default system app OR non play store app package name here as comma seprated for ignore them
and set their category directly 'Others' OR anythings you wish. */
private final String extractionApps = "com.android.providers.downloads.ui, com.android.contacts," +
" com.android.gallery3d, com.android.vending, com.android.calculator2, com.android.calculator," +
" com.android.deskclock, com.android.messaging, com.android.settings, com.android.stk";
//Class level TAG, use for Logging.
private final String TAG = "GetAppCategory";
/**
* #param packageName: package name of the app, you want to extract category.
* #param appContext: Activity/Application level Context ap per requirement.
*/
public GetAppCategory(String packageName, Context appContext) {
this.packageName = packageName;
this.appContext = appContext;
}
#Override
protected String doInBackground(String... params) {
try {
pm = appContext.getPackageManager();
if (packageName != null && packageName.length() > 1) {
if (packageName.contains("package:")) {
packageName = packageName.replace("package:", "");
}
/**
* Mathod used for parse play store html page and extract category from their.
*/
String appCategoryType = parseAndExtractCategory(packageName);
Log.i(TAG, "package :" + packageName);
Log.i(TAG, "APP_CATEGORY: " + appCategoryType);
}
} catch (Exception e) {
//TODO:: Handle Exception
e.printStackTrace();
} finally {
//TODO::
}
return null;
}
#Override
protected void onPostExecute(String result) {
}
/**
* #param packageName
* #return
*/
private String parseAndExtractCategory(String packageName) {
//You can pass hl={language_code} for get category in some other langauage also other than English.
//String url = APP_URL + packageName + "&hl=" + appContext.getString(R.string.app_lang);
String url = APP_URL + packageName + "&hl=en"; //{https://play.google.com/store/apps/details?id=com.example.app&hl=en}
String appCategoryType = null;
String appName = null;
try {
if (!extractionApps.contains(packageName)) {
Document doc = null;
try {
doc = Jsoup.connect(url).get();
if (doc != null) {
//TODO: START_METHOD_1
//Extract category String from a <anchor> tag value directly.
//NOTE: its return sub category text, for apps with multiple sub category.
//Comment this method {METHOD_1}, if you wish to extract category by href value.
Element CATEGORY_SUB_CATEGORY = doc.select("a[itemprop=genre]").first();
if (CATEGORY_SUB_CATEGORY != null) {
appCategoryType = CATEGORY_SUB_CATEGORY.text();
}
//TODO: END_METHOD_1
//TODO: START_METHOD_2
// Use below code only if you wist to extract category by href value.
//Its return parent or Main Category Text for all app.
//Comment this method {METHOD_2}, If you wihs to extract category from a<anchor> value.
if (appCategoryType == null || appCategoryType.length() < 1) {
Elements text = doc.select("a[itemprop=genre]");
if (text != null) {
if (appCategoryType == null || appCategoryType.length() < 2) {
String href = text.attr("abs:href");
if (href != null && href.length() > 4 && href.contains(CATEGORY_STRING)) {
appCategoryType = getCategoryTypeByHref(href);
}
}
}
}
//TODO: END_METHOD_2
if (appCategoryType != null && appCategoryType.length() > 1) {
/**
* Ger formatted category String by removing special character.
*/
appCategoryType = replaceSpecialCharacter(appCategoryType);
}
}
} catch (IOException e) {
//appCategoryType = appContext.getString(R.string.category_others);
appCategoryType = "OTHERS";
//TODO:: Handle Exception
e.printStackTrace();
}
} else {
//appCategoryType = appContext.getString(R.string.category_others);
appCategoryType = "OTHERS";
}
} catch (Exception e) {
//TODO:: Handle Exception
e.printStackTrace();
}
return appCategoryType;
}
/**
* #param href
* #return
*/
private String getCategoryTypeByHref(String href) {
String appCategoryType = null;
try {
appCategoryType = href.substring((href.indexOf(CATEGORY_STRING) + cat_size), href.length());
if (appCategoryType != null && appCategoryType.length() > 1) {
if (appCategoryType.contains(CATEGORY_GAME_STRING)) {
//appCategoryType = appContext.getString(R.string.games);
appCategoryType = "GAMES";
}
}
} catch (Exception e) {
//TODO:: Handle Exception
e.printStackTrace();
}
return appCategoryType;
}
/**
* #param appCategoryType
* #return: formatted String
*/
private String replaceSpecialCharacter(String appCategoryType) {
try {
//Find and Replace '&' with '&' in category Text
if (appCategoryType.contains("&")) {
appCategoryType = appCategoryType.replace("&", " & ");
}
//Find and Replace '_AND_' with ' & ' in category Text
if (appCategoryType.contains("_AND_")) {
appCategoryType = appCategoryType.replace("_AND_", " & ");
}
//Find and Replace '_' with ' ' <space> in category Text
if (appCategoryType.contains("_")) {
appCategoryType = appCategoryType.replace("_", " ");
}
} catch (Exception e) {
//TODO:: Handle Exception
e.printStackTrace();
}
return appCategoryType;
}
}
It's requires jsoup library for parsing the html page. you can find it here org.jsoup.jsoup1.11.1
Probably a bit late, but the problem is still here.
The OP has the advantage because of sending those results to the API (here I assume that the API is managed by the OP or his API colleagues at least).
So, for anyone with the similar problem I'd suggest following:
Collect all the package names you're interested in from device.
Send that data to the your API
API should extract package names and try to read results from its cache / db...
For those packages that do not exist in cache / db make "market API" call and extract category - save it to the db / cache for reuse in this iteration.
When all requests (to cache / db and market API) are completed do whatever you like with the results.
Things to consider:
When multiple users try to query your API for a same package name and you don't have a category for that package in your cache / db...
Do 1 request to "market API" for packagex and update packagex in your cache / db to "waiting for results" state - next request should either get a "waiting for results" or a result that "market API" returned.
One should also consider a fallback for possible "market API" fails (market API not working, not a google play app, or something similar). This decision is basically tied to your domain and the business trend that you're trying to catch will force a decision about this for you. If you're really into getting this category stuff sorted out you could pipeline this fallback to human decision and update your API db / cache for packagex accordingly.
put up a nice API that would handle these and similar scenarios gracefully then one could probably even commercialize it up to a certain extent and "market API endpoint" - AKA play store package details page. That page would lose a big part of it's fake users :)
I am getting a list of ApplicationInfo Objects with packageManager.getInstalledApplications(0) and attempting to categorize them by whether or not they are a system application.
For a while I have been using the technique described here, however after seeing that in my application, some of the apps were not in the non-system apps list (such as Facebook, which when available asks the system to install itself on the SD card). After next reading the actual documentation for ApplicationInfo.FLAG_SYSTEM, and understanding that it doesn't actually filter system apps, I am now looking for a new approach.
My guess is that there is a large gap between UIDs of System and non-system apps that I can gather to make this distinction, but as of yet I have not found an answer. I also looked into other flags, such as ApplicationInfo.FLAG_EXTERNAL_STORAGE, however I am supporting API 1.5.
Does anyone have a real solution to this (not involving FLAG_SYSTEM)?
PackageManager pm = mcontext.getPackageManager();
List<PackageInfo> list = pm.getInstalledPackages(0);
for(PackageInfo pi : list) {
ApplicationInfo ai = pm.getApplicationInfo(pi.packageName, 0);
System.out.println(">>>>>>packages is<<<<<<<<" + ai.publicSourceDir);
if ((ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
System.out.println(">>>>>>packages is system package"+pi.packageName);
}
}
I was under the impression that all apps in the system image are system apps (and normally installed in /system/app).
If FLAG_SYSTEM is only set to system applications, this will work even for apps in external storage:
boolean isUserApp(ApplicationInfo ai) {
int mask = ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
return (ai.flags & mask) == 0;
}
An alternative is to use the pm command-line program in your phone.
Syntax:
pm list packages [-f] [-d] [-e] [-s] [-3] [-i] [-u] [--user USER_ID] [FILTER]
pm list packages: prints all packages, optionally only
those whose package name contains the text in FILTER. Options:
-f: see their associated file.
-d: filter to only show disbled packages.
-e: filter to only show enabled packages.
-s: filter to only show system packages.
-3: filter to only show third party packages.
-i: see the installer for the packages.
-u: also include uninstalled packages.
Code:
ProcessBuilder builder = new ProcessBuilder("pm", "list", "packages", "-s");
Process process = builder.start();
InputStream in = process.getInputStream();
Scanner scanner = new Scanner(in);
Pattern pattern = Pattern.compile("^package:.+");
int skip = "package:".length();
Set<String> systemApps = new HashSet<String>();
while (scanner.hasNext(pattern)) {
String pckg = scanner.next().substring(skip);
systemApps.add(pckg);
}
scanner.close();
process.destroy();
Then:
boolean isUserApp(String pckg) {
return !mSystemApps.contains(pckg);
}
You can check the signature of application which it signed with system. Like below
/**
* Match signature of application to identify that if it is signed by system
* or not.
*
* #param packageName
* package of application. Can not be blank.
* #return <code>true</code> if application is signed by system certificate,
* otherwise <code>false</code>
*/
public boolean isSystemApp(String packageName) {
try {
// Get packageinfo for target application
PackageInfo targetPkgInfo = mPackageManager.getPackageInfo(
packageName, PackageManager.GET_SIGNATURES);
// Get packageinfo for system package
PackageInfo sys = mPackageManager.getPackageInfo(
"android", PackageManager.GET_SIGNATURES);
// Match both packageinfo for there signatures
return (targetPkgInfo != null && targetPkgInfo.signatures != null && sys.signatures[0]
.equals(targetPkgInfo.signatures[0]));
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
You can get more code on my blog How to check if application is system app or not (By signed signature)
There are 2 type of Non - system applications :
Apps downloaded from Google Play Store
Preloaded apps by device manufacturer
This code will return a list of all above applications:
ArrayList<ApplicationInfo> mAllApp =
mPackageManager.getInstalledApplications(PackageManager.GET_META_DATA);
for(int i = 0; i < mAllApp.size(); i++) {
if((mAllApp.get(i).flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
// 1. Applications downloaded from Google Play Store
mAllApp1.add(mAllApp.get(i));
}
if((mAllApp.get(i).flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0) {
// 2. Applications preloaded in device by manufecturer
mAllApp1.add(mAllApp.get(i));
}
}
Well, it's a sloppy solution in my opinion (what if /data/app isn't the apps directory on all devices?), but after a thorough search, this is what I have come up with:
for (ApplicationInfo ai : appInfo) {
if (ai.sourceDir.startsWith("/data/app/")) {
//Non-system app
}
else {
//System app
}
}
There is a little bit of misunderstanding here. For Android the notion of a "system app" is one that is install on the system image, it says nothing about what developer it came from. So, if an OEM decides to preload Facebook on to the system image, it is a system app and will continue to be so, regardless of where updates to the app get installed. They won't get installed on the system image, for sure, because it is read-only.
So ApplicationInfo.FLAG_SYSTEM is correct, but that doesn't seem to be the question you are asking. I think you're asking if a package is signed with the system certificate. Which is not necessarily a good indicator of anything, this may vary from device to device and some surprising components on vanilla Android are not signed with the system certificate, even though you might expect them to be.
In newer versions of Android there is a new path, /system/priv-app/ that attempts to be the install location for "real" system apps. Apps that are just pre-loaded on the system image then end up in /system/app/. See AOSP Privileged vs System app
If an Application is a non-system application it must have a launch Intent by which it can be launched. If the launch intent is null then its a system App.
Example of System Apps: "com.android.browser.provider", "com.google.android.voicesearch".
For the above apps you will get NULL when you query for launch Intent.
PackageManager pm = getPackageManager();
List<ApplicationInfo> packages = pm.getInstalledApplications(PackageManager.GET_META_DATA);
for(ApplicationInfo packageInfo:packages){
if( pm.getLaunchIntentForPackage(packageInfo.packageName) != null ){
String currAppName = pm.getApplicationLabel(packageInfo).toString();
//This app is a non-system app
}
}
This is a simplified and more efficient version of other responses listed here. It is more efficient if you just iterate directly over the ApplicationInfos.
List<ApplicationInfo> applications = context.getPackageManager()
.getInstalledApplications(PackageManager.GET_META_DATA);
for(ApplicationInfo appInfo : applications){
if((appInfo.flags & ApplicationInfo.FLAG_SYSTEM) == 0){
// Not a system app
}
}
Here are different possible ways to see if the app is a system app by its package name (used some of the codes in this post)
package com.test.util;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Scanner;
import java.util.Set;
import java.util.regex.Pattern;
import timber.log.Timber;
public class SystemAppChecker {
private PackageManager packageManager = null;
public SystemAppChecker(Context context) {
packageManager = context.getPackageManager();
}
/**
* Check if system app by 'pm' command-line program
*
* #param packageName
* package name of application. Cannot be null.
* #return <code>true</code> if package is a system app.
*/
public boolean isSystemAppByPM(String packageName) {
if (packageName == null) {
throw new IllegalArgumentException("Package name cannot be null");
}
ProcessBuilder builder = new ProcessBuilder("pm", "list", "packages", "-s");
Process process = null;
try {
process = builder.start();
} catch (IOException e) {
Timber.e(e);
return false;
}
InputStream in = process.getInputStream();
Scanner scanner = new Scanner(in);
Pattern pattern = Pattern.compile("^package:.+");
int skip = "package:".length();
Set<String> systemApps = new HashSet<String>();
while (scanner.hasNext(pattern)) {
String pckg = scanner.next().substring(skip);
systemApps.add(pckg);
}
scanner.close();
process.destroy();
if (systemApps.contains(packageName)) {
return true;
}
return false;
}
/**
* Check if application is preloaded.
*
* #param packageName
* package name of application. Cannot be null.
* #return <code>true</code> if package is preloaded.
*/
public boolean isSystemPreloaded(String packageName) {
if (packageName == null) {
throw new IllegalArgumentException("Package name cannot be null");
}
try {
ApplicationInfo ai = packageManager.getApplicationInfo(
packageName, 0);
if (ai.sourceDir.startsWith("/system/app/") || ai.sourceDir.startsWith("/system/priv-app/")) {
return true;
}
} catch (NameNotFoundException e) {
Timber.e(e);
}
return false;
}
/**
* Check if the app is system signed or not
*
* #param packageName
* package of application. Cannot be blank.
* #return <code>true</code> if application is signed by system certificate,
* otherwise <code>false</code>
*/
public boolean isSystemSigned(String packageName) {
if (packageName == null) {
throw new IllegalArgumentException("Package name cannot be null");
}
try {
// Get packageinfo for target application
PackageInfo targetPkgInfo = packageManager.getPackageInfo(
packageName, PackageManager.GET_SIGNATURES);
// Get packageinfo for system package
PackageInfo sys = packageManager.getPackageInfo(
"android", PackageManager.GET_SIGNATURES);
// Match both packageinfo for there signatures
return (targetPkgInfo != null && targetPkgInfo.signatures != null && sys.signatures[0]
.equals(targetPkgInfo.signatures[0]));
} catch (PackageManager.NameNotFoundException e) {
Timber.e(e);
}
return false;
}
/**
* Check if application is installed in the device's system image
*
* #param packageName
* package name of application. Cannot be null.
* #return <code>true</code> if package is a system app.
*/
public boolean isSystemAppByFLAG(String packageName) {
if (packageName == null) {
throw new IllegalArgumentException("Package name cannot be null");
}
try {
ApplicationInfo ai = packageManager.getApplicationInfo(
packageName, 0);
// Check if FLAG_SYSTEM or FLAG_UPDATED_SYSTEM_APP are set.
if (ai != null
&& (ai.flags & (ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP)) != 0) {
return true;
}
} catch (NameNotFoundException e) {
Timber.e(e);
}
return false;
}
}
if (!packageInfo.sourceDir.toLowerCase().startsWith("/system/"))
If having an APK file and want to check is it System app or User installed
a Simple logic:-
System app Files are not writable
private boolean isSystemApkFile(File file){
return !file.canWrite();
}
So I'd like to put here an utility class I made with the knowledge of this thread and a few others. But before I continue, an explanation of some terms, if I got them all right, copied from that class, which are used on it.
Below KitKat 4.4, all apps in /system/app were given privileged
permissions. Even the Calculator app had them. That could be a
security breach. So they were separated between ordinary and
privileged system apps and ordinary ones don't have privileged
permissions above KitKat 4.4. So these utilities have that in mind.
They also have in mind the following designations:
Platform-signed app: any app that is signed with the platform/system key (so they have system signature permissions), whether it is
installed on the system partitions or not.
System app: any app that is installed on the system partitions.
Updated system app: any system app that was updated (meaning now it is also installed on /data/app).
Privileged system app: below KitKat 4.4, any app installed on /system/app; from KitKat 4.4 onwards, only the apps installed on
/system/priv-app (I really mean only /system). These apps have
privileged permissions.
Ordinary system app: only as of KitKat 4.4, those without privileged permissions, even though they're still system apps. Below KitKat 4.4,
they're non-existent.
System partition notes: until Oreo 8.1, there
was only one: /system. As of Pie (9), there is also /vendor and
/product.
So with that in mind, here are 2 functions:
/**
* <p>Checks if an app is installed on the system partitions and was updated.</p>
*
* #param applicationInfo an instance of {#link ApplicationInfo} for the package to be checked
*
* #return true if it is, false otherwise
*/
private static boolean isUpdatedSystemApp(#NonNull final ApplicationInfo applicationInfo) {
return (applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0;
}
/**
* <p>Checks if an app is installed in the system partitions (ordinary app or privileged app, doesn't matter).</p>
*
* #param applicationInfo an instance of {#link ApplicationInfo} for the package to be checked
*
* #return true if it is, false otherwise
*/
private static boolean isSystemApp(#NonNull final ApplicationInfo applicationInfo) {
// Below Android Pie (9), all system apps were in /system. As of Pie, they can ALSO be in /vendor and /product.
boolean ret_value = (applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
// FLAG_SYSTEM checks if it's on the system image, which means /system. So to check for /vendor and
// /product, here are 2 special flags.
ret_value = ret_value || (applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0;
ret_value = ret_value || (applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRODUCT) != 0;
}
return ret_value;
}
To check if an app is a privileged system app or is ordinary system app, and/or is signed with the platform/system key, I'll leave 3 functions below. I believe it's off-topic to the question, but I'll put it in case anyone like me needed it.
/**
* <p>Checks if an app is an ordinary system app (installed on the system partitions, but no privileged or signature
* permissions granted to it).</p>
* <p>Note: will return false for any app on KitKat 4.4 and below.</p>
*
* #param applicationInfo an instance of {#link ApplicationInfo} for the package to be checked
*
* #return true if it is, false otherwise
*/
private static boolean isOrdinarySystemApp(#NonNull final ApplicationInfo applicationInfo) {
// It's an ordinary system app if it doesn't have any special permission privileges (it's not a Privileged app
// nor is it signed with the system key).
boolean ret_value = isSystemApp(applicationInfo) && !hasPrivilegedPermissions(applicationInfo);
final boolean signed_system_key = hasSystemSignaturePermissions(applicationInfo);
ret_value = ret_value && signed_system_key;
return ret_value;
}
/**
* <p>Checks if an app has signature permissions - checks if it's signed with the platform/system certificate by
* comparing it to the "android" package.</p>
* <br>
* <p>ATTENTION: if the chosen app was signed multiple times and the system is running below Android Pie, this check
* may return false wrongly, since it checks if ALL the signatures from the "android" package and the chosen
* application match. If at least one doesn't match in both, this will return false. So use with caution in case of
* multiple signers. With only one signer, it's all right.</p>
*
* #param applicationInfo an instance of {#link ApplicationInfo} for the package to be checked
* #return true if it is, false otherwise
*/
private static boolean hasSystemSignaturePermissions(#NonNull final ApplicationInfo applicationInfo) {
// If on Pie or above, check with a private flag (appeared on Pie only).
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
return (applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_SIGNED_WITH_PLATFORM_KEY) != 0;
}
// Else, check by comparing signatures of a platform-signed app and the chosen app.
return UtilsGeneral.getContext().getPackageManager().checkSignatures(applicationInfo.packageName, "android")
== PackageManager.SIGNATURE_MATCH;
}
/**
* <p>"Value for {#link ApplicationInfo#flags}: set to {#code true} if the application
* is permitted to hold privileged permissions.</p>
*
* {#hide}"
* <p>NOTE: Only on API 19 through API 22.</p>
*/
private static final int FLAG_PRIVILEGED = 1 << 30;
/**
* <p>Checks if an app is a Privileged App.</p>
* <p>Note: will return true for any system app below KitKat 4.4.</p>
*
* #param applicationInfo an instance of {#link ApplicationInfo} for the package to be checked
*
* #return true if it is, false otherwise
*/
private static boolean hasPrivilegedPermissions(#NonNull final ApplicationInfo applicationInfo) {
// Check if it's an app installed in the system partitions. If it is, check with methods that apply only to
// apps installed on the system partitions.
if (isSystemApp(applicationInfo)) {
// If it's below KitKat 4.4 and it's a system app, it's a privileged one automatically.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return true;
}
// If on Marshmallow or above, check with a private flag.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
return true;
}
}
// If between KitKat 4.4 and Lollipop 5.1, use a deleted flag.
if ((applicationInfo.flags & FLAG_PRIVILEGED) != 0) {
return true;
}
}
// In case none returned true above, the app may still be signed with the platform/system's key, which will
// grant it exactly all permissions there are (which includes privileged permissions - ALL permissions).
return hasSystemSignaturePermissions(applicationInfo);
}
If you want, you can join this last one to the ones above, but I don't really recommend it. It will only work work as long as the system app hasn't been updated.
/**
* <p>Gets a list of folders a system app might be installed in, depending on the device's Android version.</p>
* <p>Note that an updated system app will report as being installed in /data/app. For these locations to be
* checked, the app must not have been updated. If it has, it's not possible to tell using the directory, I think.</p>
*
* #param privileged_app true if it's to return a list for privileged apps, false if it's for ordinary system apps,
* null if it's to return a list for both types
*
* #return a list of folders its APK might be in
*/
#NonNull
private static String[] getAppPossibleFolders(#Nullable final Boolean privileged_app) {
final Collection<String> ret_folders = new ArrayList<>(5);
final String PRIV_APP_FOLDER = "/system/priv-app";
final String ORD_APP_SYSTEM_FOLDER = "/system/app";
final String ORD_APP_VENDOR_FOLDER = "/vendor/app";
final String ORD_APP_PRODUCT_FOLDER = "/product/app";
if (privileged_app == null) {
ret_folders.add(PRIV_APP_FOLDER);
ret_folders.add(ORD_APP_SYSTEM_FOLDER);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ret_folders.add(ORD_APP_VENDOR_FOLDER);
ret_folders.add(ORD_APP_PRODUCT_FOLDER);
}
} else if (privileged_app) {
ret_folders.add(PRIV_APP_FOLDER);
} else {
ret_folders.add(ORD_APP_SYSTEM_FOLDER);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
ret_folders.add(ORD_APP_VENDOR_FOLDER);
ret_folders.add(ORD_APP_PRODUCT_FOLDER);
}
}
// Leave it in 0 size allocation. Or null values will appear, and I don't want to need to be careful about it.
return ret_folders.toArray(new String[0]);
/*
Use with:
// If it's an updated system app, its APK will be said to be in /data/app, and the one on the system partitions
// will become unused. But if it's not updated, it's all fine and the APK path can be used to check if it's
// a privileged app or not.
if (!isUpdatedSystemApp(applicationInfo)) {
for (final String folder : getAppPossibleFolders(false)) {
if (applicationInfo.sourceDir.startsWith(folder)) {
return true;
}
}
}
*/
}
Here is an AppUtil I wrote for that purpose.
Usage example:
new AppsUtil(this).printInstalledAppPackages(AppsUtil.AppType.USER);
AppsUtil.java
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.util.Log;
public class AppsUtil
{
public static final String TAG = "PackagesInfo";
private Context _context;
private ArrayList<PckgInfo> _PckgInfoList;
public enum AppType
{
ALL {
#Override
public String toString() {
return "ALL";
}
},
USER {
#Override
public String toString() {
return "USER";
}
},
SYSTEM {
#Override
public String toString() {
return "SYSTEM";
}
}
}
class PckgInfo
{
private AppType appType;
private String appName = "";
private String packageName = "";
private String versionName = "";
private int versionCode = 0;
private void prettyPrint()
{
Log.i(TAG, appName + "\n AppType: " + appType.toString() + "\n Package: " + packageName + "\n VersionName: " + versionName + "\n VersionCode: " + versionCode);
}
}
public AppsUtil(Context context)
{
super();
this._context = context;
this._PckgInfoList = new ArrayList<PckgInfo>();
}
public void printInstalledAppPackages(AppType appType)
{
retrieveInstalledAppsPackages();
Log.i(TAG, "");
for (int i = 0; i < _PckgInfoList.size(); i++)
{
if (AppType.ALL == appType)
{
_PckgInfoList.get(i).prettyPrint();
}
else
{
if (_PckgInfoList.get(i).appType == appType)
_PckgInfoList.get(i).prettyPrint();
}
}
}
public ArrayList<PckgInfo> getInstalledAppPackages(AppType appType)
{
retrieveInstalledAppsPackages();
ArrayList<PckgInfo> resultPInfoList = new ArrayList<PckgInfo>();
if (AppType.ALL == appType)
{
return _PckgInfoList;
}
else
{
for (int i = 0; i < _PckgInfoList.size(); i++)
{
if (_PckgInfoList.get(i).appType == appType)
resultPInfoList.add(_PckgInfoList.get(i));
}
return resultPInfoList;
}
}
private void retrieveInstalledAppsPackages()
{
PackageManager pm = _context.getPackageManager();
List<PackageInfo> packs = pm.getInstalledPackages(0);
for (PackageInfo pi : packs)
{
try
{
PckgInfo newInfo = new PckgInfo();
ApplicationInfo ai = pm.getApplicationInfo(pi.packageName, 0);
newInfo.appType = getAppType(ai);
newInfo.appName = pi.applicationInfo.loadLabel(pm).toString();
newInfo.packageName = pi.packageName;
newInfo.versionName = pi.versionName;
newInfo.versionCode = pi.versionCode;
_PckgInfoList.add(newInfo);
}
catch (NameNotFoundException e)
{
e.printStackTrace();
}
}
}
AppType getAppType(ApplicationInfo ai)
{
AppType resultType ;
if (isUserApp(ai))
resultType = AppType.USER;
else
resultType = AppType.SYSTEM;
return resultType;
}
boolean isUserApp(ApplicationInfo ai)
{
int mask = ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP;
return (ai.flags & mask) == 0;
}
}
You can use checkSignatures to determine if an app is a system app or not.
All system apps are signed with the same key.
https://developer.android.com/reference/android/content/pm/PackageManager#checkSignatures(java.lang.String,%20java.lang.String)
And signed with the system key is the "android" package.
val checkPackage: String = "com.package.to.check"
val systemPackageName = "android"
if (packageManager.checkSignatures(systemPackageName, checkPackage) == PackageManager.SIGNATURE_MATCH) {
Log.d("TUT", "System app")
} else {
Log.d("TUT", "Non-System app")
}
What is the best way and how do I set up a configuration file for a application?
I want the application to be able to look into a text file on the SD card and pick out certain information that it requires.
If your application is going to be released to the public, and if you have sensitive data in your config, such as API keys or passwords, I would suggest to use secure-preferences instead of SharedPreferences since, ultimately, SharedPreferences are stored in an XML in clear text, and on a rooted phone, it is very easy for an application to access another's shared preferences.
By default it's not bullet proof security (in fact it's more like
obfuscation of the preferences) but it's a quick win for incrementally
making your android app more secure. For instance it'll stop users on
rooted devices easily modifying your app's shared prefs. (link)
I would suggest a few other methods:
*Method 1: Use a .properties file with Properties
Pros:
Easy to edit from whatever IDE you are using
More secure: since it is compiled with your app
Can easily be overridden if you use Build variants/Flavors
You can also write in the config
Cons:
You need a context
You can also write in the config (yes, it can also be a con)
(anything else?)
First, create a config file: res/raw/config.properties and add some values:
api_url=http://url.to.api/v1/
api_key=123456
You can then easily access the values with something like this:
package some.package.name.app;
import android.content.Context;
import android.content.res.Resources;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
public final class Helper {
private static final String TAG = "Helper";
public static String getConfigValue(Context context, String name) {
Resources resources = context.getResources();
try {
InputStream rawResource = resources.openRawResource(R.raw.config);
Properties properties = new Properties();
properties.load(rawResource);
return properties.getProperty(name);
} catch (Resources.NotFoundException e) {
Log.e(TAG, "Unable to find the config file: " + e.getMessage());
} catch (IOException e) {
Log.e(TAG, "Failed to open config file.");
}
return null;
}
}
Usage:
String apiUrl = Helper.getConfigValue(this, "api_url");
String apiKey = Helper.getConfigValue(this, "api_key");
Of course, this could be optimized to read the config file once and get all values.
Method 2: Use AndroidManifest.xml meta-data element:
Personally, I've never used this method because it doesn't seem very flexible.
In your AndroidManifest.xml, add something like:
...
<application ...>
...
<meta-data android:name="api_url" android:value="http://url.to.api/v1/"/>
<meta-data android:name="api_key" android:value="123456"/>
</application>
Now a function to retrieve the values:
public static String getMetaData(Context context, String name) {
try {
ApplicationInfo ai = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA);
Bundle bundle = ai.metaData;
return bundle.getString(name);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "Unable to load meta-data: " + e.getMessage());
}
return null;
}
Usage:
String apiUrl = Helper.getMetaData(this, "api_url");
String apiKey = Helper.getMetaData(this, "api_key");
Method 3: Use buildConfigField in your Flavor:
I didn't find this in the official Android documentation/training, but this blog article is very useful.
Basically setting up a project Flavor (for example prod) and then in your app's build.gradle have something like:
productFlavors {
prod {
buildConfigField 'String', 'API_URL', '"http://url.to.api/v1/"'
buildConfigField 'String', 'API_KEY', '"123456"'
}
}
Usage:
String apiUrl = BuildConfig.API_URL;
String apiKey = BuildConfig.API_KEY;
You can achieve this using shared preferences
There is a very detailed guide on how to use Shared Preferences on the Google Android page
https://developer.android.com/guide/topics/data/data-storage.html#pref
If you want to store the preferences of your application, Android provides SharedPreferences for this.
Here is the link to official training resource.
I met such requirement recently, noting down here, how I did it.
the application to be able to look into a text file on the sd card and
pick out certain information that it requires
Requirement:
Configuration value(score_threshold) has to be available at the sdcard. So somebody can change the values after releasing the apk.
The config file must be available at the "/sdcard/config.txt" of the android hardware.
The config.txt file contents are,
score_threshold=60
Create a utility class Config.java, for reading and writing text file.
import android.util.Log;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Properties;
public final class Config {
private static final String TAG = Config.class.getSimpleName();
private static final String FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() + "/config.txt";
private static Config sInstance = null;
/**
* Gets instance.
*
* #return the instance
*/
public static Config getInstance() {
if (sInstance == null) {
synchronized (Config.class) {
if (sInstance == null) {
sInstance = new Config();
}
}
}
return sInstance;
}
/**
* Write configurations values boolean.
*
* #return the boolean
*/
public boolean writeConfigurationsValues() {
try (OutputStream output = new FileOutputStream(FILE_PATH)) {
Properties prop = new Properties();
// set the properties value
prop.setProperty("score_threshold", "60");
// save properties
prop.store(output, null);
Log.i(TAG, "Configuration stored properties: " + prop);
return true;
} catch (IOException io) {
io.printStackTrace();
return false;
}
}
/**
* Get configuration value string.
*
* #param key the key
* #return the string
*/
public String getConfigurationValue(String key){
String value = "";
try (InputStream input = new FileInputStream(FILE_PATH)) {
Properties prop = new Properties();
// load a properties file
prop.load(input);
value = prop.getProperty(key);
Log.i(TAG, "Configuration stored properties value: " + value);
} catch (IOException ex) {
ex.printStackTrace();
}
return value;
}
}
Create another utility class to write the configuration file for the first time execution of the application,
Note: SD card read/write permission has to be set for the application.
public class ApplicationUtils {
/**
* Sets the boolean preference value
*
* #param context the current context
* #param key the preference key
* #param value the value to be set
*/
public static void setBooleanPreferenceValue(Context context, String key, boolean value) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
sp.edit().putBoolean(key, value).commit();
}
/**
* Get the boolean preference value from the SharedPreference
*
* #param context the current context
* #param key the preference key
* #return the the preference value
*/
public static boolean getBooleanPreferenceValue(Context context, String key) {
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context);
return sp.getBoolean(key, false);
}
}
At your Main Activity, onCreate()
if(!ApplicationUtils.getBooleanPreferenceValue(this,"isFirstTimeExecution")){
Log.d(TAG, "First time Execution");
ApplicationUtils.setBooleanPreferenceValue(this,"isFirstTimeExecution",true);
Config.getInstance().writeConfigurationsValues();
}
// get the configuration value from the sdcard.
String thresholdScore = Config.getInstance().getConfigurationValue("score_threshold");
Log.d(TAG, "thresholdScore from config file is : "+thresholdScore );