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 );
Related
I'm able to successfully add my images to google drive, using the API provided by Google Drive.
Can I now retrieve it back to my App?
If yes, Can anyone help with how to do that?
Use this to retrieve files from the Google Drive API
https://www.googleapis.com/drive/v3/files/[FILEID]?key=[YOUR_API_KEY]'
If you want the entirety of code required to retrieve a file, here it is:
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpResponse;
import com.google.api.services.drive.Drive;
import com.google.api.services.drive.model.File;
import java.io.IOException;
import java.io.InputStream;
// ...
public class MyClass {
// ...
/**
* Print a file's metadata.
*
* #param service Drive API service instance.
* #param fileId ID of the file to print metadata for.
*/
private static void printFile(Drive service, String fileId) {
try {
File file = service.files().get(fileId).execute();
System.out.println("Title: " + file.getTitle());
System.out.println("Description: " + file.getDescription());
System.out.println("MIME type: " + file.getMimeType());
} catch (IOException e) {
System.out.println("An error occurred: " + e);
}
}
/**
* Download a file's content.
*
* #param service Drive API service instance.
* #param file Drive File instance.
* #return InputStream containing the file's content if successful,
* {#code null} otherwise.
*/
private static InputStream downloadFile(Drive service, File file) {
if (file.getDownloadUrl() != null && file.getDownloadUrl().length() > 0) {
try {
HttpResponse resp =
service.getRequestFactory().buildGetRequest(new GenericUrl(file.getDownloadUrl()))
.execute();
return resp.getContent();
} catch (IOException e) {
// An error occurred.
e.printStackTrace();
return null;
}
} else {
// The file doesn't have any content stored on Drive.
return null;
}
}
// ...
}
Is there to a way to check the systems theme at run-time? (see image below)
EDIT/NOTE: Okay, I did some more research, as far as I know this is only a global setting on the OnePlus 6 (OxygenOs), did android P not get a dark mode after all?
So I figured out a hacky way to get this setting:
System settings are stored at: content://settings/system
On my device (OxygenOs 6.x.x) value for theme is stored at oem_black_mode.
To fetch this result we can execute the following command via adb:
adb shell content query --uri content://settings/system/oem_black_mode
this does not require the phone to be rooted.
I have created a simple wrapper for a content resolver (feel free to use/modify):
import android.content.ContentResolver;
import android.content.Context;
import android.provider.Settings;
public class SystemSettingsResolver {
public static final String OEM_BLACK_MODE = "oem_black_mode";
public static final String OEM_BLACK_MODE_ACCENT_COLOR = "oem_black_mode_accent_color";
public static final String OEM_BLACK_MODE_ACCENT_COLOR_INDEX = "oem_black_mode_accent_color_index";
private Context context;
public SystemSettingsResolver(Context context) {
this.context = context;
}
public int getInt(String setting) {
ContentResolver resolver = context.getContentResolver();
try {
return Settings.System.getInt(resolver, setting);
} catch (Settings.SettingNotFoundException e) {
e.printStackTrace();
}
return -1;
}
public String getString(String setting) {
ContentResolver resolver = context.getContentResolver();
return Settings.System.getString(resolver, setting);
}
// extend with getFloat etc whatever is required for your app.
}
And here is how to use this wrapper:
public int systemTheme() {
/**
* In my testing:
* 0 = light
* 1 = dark
* 2 = default
*/
SystemSettingsResolver resolver = SystemSettingsResolver(this); //pass context
return resolver.getInt(SystemSettingsResolver.OEM_BLACK_MODE)
}
I'm trying to implement offline DRM support for ExoPlayer 2 but I have some problems.
I found this conversation. There is some implementation for ExoPlayer 1.x and some steps how to work that implementation with ExoPlayer 2.x.
I have I problem with OfflineDRMSessionManager whitch implements DrmSessionManager. In that example is DrmSessionManager imported from ExoPlayer 1.x. If I import it from ExoPlayer 2 then I have a problems to compile it. I have a problem with #Override methods (open(), close(), ..) which are NOT in that new DrmSessionManager and there are some new methods: acquireSession(), ... .
With the latest release of ExoPlayer 2.2.0 , it provides this facility inbuilt in ExoPlayer. ExoPlayer has a helper class to download and refresh offline license keys. It should be the preferred way to do this.
OfflineLicenseHelper.java
/**
* Helper class to download, renew and release offline licenses. It utilizes {#link
* DefaultDrmSessionManager}.
*/
public final class OfflineLicenseHelper<T extends ExoMediaCrypto> {
You can access the latest code from the ExoPlayer repo
I created a sample application for Offline playback of DRM content.You can access it from here
As #TheJango explained, with the latest release of ExoPlayer 2.2.0 , it provides this facility inbuilt in ExoPlayer.
However, the OfflineLicenseHelper class was designed with some VOD use case in mind. Buy a movie, save the license (download method), download the movie, load the license in a DefaultDrmSessionManager and then setMode for playback.
Another use case could be that you want to make an online streaming system where different content is using the same license (e.g. Television) for quite some time (e.g. 24hours) more intelligent. So that it never downloads a license which it already has (Suppose your DRM system charges you per license request and there will be a lot of requests for the same license otherwise), the following approach can be used with ExoPlayer 2.2.0. It took me some time to get a working solution without modifying anything to the ExoPlayer source. I don't quite like the approach they've taken with the setMode() method which can only be called once. Previously DrmSessionManagers would work for multiple sessions (audio, video) and now they no longer work if licenses differ or come from different methods (DOWNLOAD, PLAYBACK, ...). Anyway, I introduced a new class CachingDefaultDrmSessionManager to replace the DefaultDrmSessionManager you are probably using. Internally it delegates to a DefaultDrmSessionManager.
package com.google.android.exoplayer2.drm;
import android.content.Context;
import android.content.SharedPreferences;
import java.util.concurrent.atomic.AtomicBoolean;
import android.os.Handler;
import android.os.Looper;
import android.util.Base64;
import android.util.Log;
import com.google.android.exoplayer2.C;
import com.google.android.exoplayer2.extractor.mp4.PsshAtomUtil;
import com.google.android.exoplayer2.util.Util;
import java.util.Arrays;
import java.util.HashMap;
import java.util.UUID;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_DOWNLOAD;
import static com.google.android.exoplayer2.drm.DefaultDrmSessionManager.MODE_QUERY;
public class CachingDefaultDrmSessionManager<T extends ExoMediaCrypto> implements DrmSessionManager<T> {
private final SharedPreferences drmkeys;
public static final String TAG="CachingDRM";
private final DefaultDrmSessionManager<T> delegateDefaultDrmSessionManager;
private final UUID uuid;
private final AtomicBoolean pending = new AtomicBoolean(false);
private byte[] schemeInitD;
public interface EventListener {
void onDrmKeysLoaded();
void onDrmSessionManagerError(Exception e);
void onDrmKeysRestored();
void onDrmKeysRemoved();
}
public CachingDefaultDrmSessionManager(Context context, UUID uuid, ExoMediaDrm<T> mediaDrm, MediaDrmCallback callback, HashMap<String, String> optionalKeyRequestParameters, final Handler eventHandler, final EventListener eventListener) {
this.uuid = uuid;
DefaultDrmSessionManager.EventListener eventListenerInternal = new DefaultDrmSessionManager.EventListener() {
#Override
public void onDrmKeysLoaded() {
saveDrmKeys();
pending.set(false);
if (eventListener!=null) eventListener.onDrmKeysLoaded();
}
#Override
public void onDrmSessionManagerError(Exception e) {
pending.set(false);
if (eventListener!=null) eventListener.onDrmSessionManagerError(e);
}
#Override
public void onDrmKeysRestored() {
saveDrmKeys();
pending.set(false);
if (eventListener!=null) eventListener.onDrmKeysRestored();
}
#Override
public void onDrmKeysRemoved() {
pending.set(false);
if (eventListener!=null) eventListener.onDrmKeysRemoved();
}
};
delegateDefaultDrmSessionManager = new DefaultDrmSessionManager<T>(uuid, mediaDrm, callback, optionalKeyRequestParameters, eventHandler, eventListenerInternal);
drmkeys = context.getSharedPreferences("drmkeys", Context.MODE_PRIVATE);
}
final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public void saveDrmKeys() {
byte[] offlineLicenseKeySetId = delegateDefaultDrmSessionManager.getOfflineLicenseKeySetId();
if (offlineLicenseKeySetId==null) {
Log.i(TAG,"Failed to download offline license key");
} else {
Log.i(TAG,"Storing downloaded offline license key for "+bytesToHex(schemeInitD)+": "+bytesToHex(offlineLicenseKeySetId));
storeKeySetId(schemeInitD, offlineLicenseKeySetId);
}
}
#Override
public DrmSession<T> acquireSession(Looper playbackLooper, DrmInitData drmInitData) {
if (pending.getAndSet(true)) {
return delegateDefaultDrmSessionManager.acquireSession(playbackLooper, drmInitData);
}
// First check if we already have this license in local storage and if it's still valid.
DrmInitData.SchemeData schemeData = drmInitData.get(uuid);
schemeInitD = schemeData.data;
Log.i(TAG,"Request for key for init data "+bytesToHex(schemeInitD));
if (Util.SDK_INT < 21) {
// Prior to L the Widevine CDM required data to be extracted from the PSSH atom.
byte[] psshData = PsshAtomUtil.parseSchemeSpecificData(schemeInitD, C.WIDEVINE_UUID);
if (psshData == null) {
// Extraction failed. schemeData isn't a Widevine PSSH atom, so leave it unchanged.
} else {
schemeInitD = psshData;
}
}
byte[] cachedKeySetId=loadKeySetId(schemeInitD);
if (cachedKeySetId!=null) {
//Load successful.
Log.i(TAG,"Cached key set found "+bytesToHex(cachedKeySetId));
if (!Arrays.equals(delegateDefaultDrmSessionManager.getOfflineLicenseKeySetId(), cachedKeySetId))
{
delegateDefaultDrmSessionManager.setMode(MODE_QUERY, cachedKeySetId);
}
} else {
Log.i(TAG,"No cached key set found ");
delegateDefaultDrmSessionManager.setMode(MODE_DOWNLOAD,null);
}
DrmSession<T> tDrmSession = delegateDefaultDrmSessionManager.acquireSession(playbackLooper, drmInitData);
return tDrmSession;
}
#Override
public void releaseSession(DrmSession<T> drmSession) {
pending.set(false);
delegateDefaultDrmSessionManager.releaseSession(drmSession);
}
public void storeKeySetId(byte[] initData, byte[] keySetId) {
String encodedInitData = Base64.encodeToString(initData, Base64.NO_WRAP);
String encodedKeySetId = Base64.encodeToString(keySetId, Base64.NO_WRAP);
drmkeys.edit()
.putString(encodedInitData, encodedKeySetId)
.apply();
}
public byte[] loadKeySetId(byte[] initData) {
String encodedInitData = Base64.encodeToString(initData, Base64.NO_WRAP);
String encodedKeySetId = drmkeys.getString(encodedInitData, null);
if (encodedKeySetId == null) return null;
return Base64.decode(encodedKeySetId, 0);
}
}
Here keys are persisted as Base64 encoded strings in local storage. Because for a typical DASH stream both audio and video renderers will request a license from the DrmSessionManager, possibly at the same time, the AtomicBoolean is used. If audio and or video would use different keys, I think this approach would fail.
Also I am not yet checking for expired keys here. Have a look at OfflineLicenseHelper to see how to deal with those.
#Pepa Zapletal, proceed with below changes to play in offline.
You can also see the updated answer here.
Changes are as follows :
Changed signature of the method private void onKeyResponse(Object response) to private void onKeyResponse(Object response, boolean offline)
Rather than sending the file manifest URI send stored file path to PlayerActivity.java.
Change MediaDrm.KEY_TYPE_STREAMING to MediaDrm.KEY_TYPE_OFFLINE in getKeyRequest().
In postKeyRequest() first check whether the key is stored or not, if key found then directly call onKeyResponse(key, true).
In onKeyResponse(), call restoreKeys() rather than calling provideKeyResponse().
The rest everything is same, now your file will be playing.
Major role : Here provideKeyResponse() and restoreKeys() are native methods which acts major role in getting the key and restoring the key.
provideKeyResponse() method which will return us the main License key in byte array if and only if the keyType is MediaDrm.KEY_TYPE_OFFLINE else this method will return us the empty byte array with which we can do nothing with that array.
restoreKeys() method will expect the key which is to be restored for the current session, so feed the key which we have already stored in local to this method and it will take care of it.
Note : First you have to somehow download the license key and store it somewhere in local device securely.
In my case first im playing the file online, so exoplayer will fetch the key that key i have stored in local. From second time onwards first it will check whether the key is stored or not, if key found it will skip the License key request and will the play the file.
Replace the methods and inner classes of StreamingDrmSessionManager.java with these things.
private void postKeyRequest() {
KeyRequest keyRequest;
try {
// check is key exist in local or not, if exist no need to
// make a request License server for the key.
byte[] keyFromLocal = Util.getKeyFromLocal();
if(keyFromLocal != null) {
onKeyResponse(keyFromLocal, true);
return;
}
keyRequest = mediaDrm.getKeyRequest(sessionId, schemeData.data, schemeData.mimeType, MediaDrm.KEY_TYPE_OFFLINE, optionalKeyRequestParameters);
postRequestHandler.obtainMessage(MSG_KEYS, keyRequest).sendToTarget();
} catch (NotProvisionedException e) {
onKeysError(e);
}
}
private void onKeyResponse(Object response, boolean offline) {
if (state != STATE_OPENED && state != STATE_OPENED_WITH_KEYS) {
// This event is stale.
return;
}
if (response instanceof Exception) {
onKeysError((Exception) response);
return;
}
try {
// if we have a key and we want to play offline then call
// 'restoreKeys()' with the key which we have already stored.
// Here 'response' is the stored key.
if(offline) {
mediaDrm.restoreKeys(sessionId, (byte[]) response);
} else {
// Don't have any key in local, so calling 'provideKeyResponse()' to
// get the main License key and store the returned key in local.
byte[] bytes = mediaDrm.provideKeyResponse(sessionId, (byte[]) response);
Util.storeKeyInLocal(bytes);
}
state = STATE_OPENED_WITH_KEYS;
if (eventHandler != null && eventListener != null) {
eventHandler.post(new Runnable() {
#Override
public void run() {
eventListener.onDrmKeysLoaded();
}
});
}
} catch (Exception e) {
onKeysError(e);
}
}
#SuppressLint("HandlerLeak")
private class PostResponseHandler extends Handler {
public PostResponseHandler(Looper looper) {
super(looper);
}
#Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_PROVISION:
onProvisionResponse(msg.obj);
break;
case MSG_KEYS:
// We don't have key in local so calling 'onKeyResponse()' with offline to 'false'.
onKeyResponse(msg.obj, false);
break;
}
}
}
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 :)
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;
}
}