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.
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 want to use android in industry,
I can connect to Profilic and Ftdi USB to Serial chips with slickdevlabs.com library without any problem.
The application has a service and it starts on boot,connect to the usb serial port and do the other things.
my problem is that the host device does not have any interaction with user,
so when the android asks
Allow the app "MyAPP" to access the USB device ?
[checkmark]Use by default for this USB device
Cancel OK
there is no person to click on ok.
even when I check the use by default... checkbox,If I reinsert the USB ,or reboot the host device, it asks again on next boot.
and the answer is mentioned in this link :
bypass android usb host permission confirmation dialog
but the codes are for :
"Note that interfaces of these classes may change depending on the version of Android. In my case the version is 4.0.3. So if you have another version of Android and this code doesn't work you will have to check the source code for your particular version of OS."
so i need the same codes for android 5.1 please
It's been a while since you asked this... but in case it can help someone here is my answer.
The accepted answer in the initial question states:
So if you have another version of Android and this code doesn't work you will have to check the source code for your particular version of OS.
So you should get the files you need directly from the android source code. You can download the source code relative to your version or browse directly from the repo.
The IUsbManager interface you are searching for is normally under:
/frameworks/base/android-branch-name/core/java/android/hardware/usb. As for the Service Manager it can be found under:
/frameworks/base/android-branch-name/core/java/android/os/
I didn't post the code since I suppose you're not searching for it anymore after 2 years+ :)
=== EDIT ===
As asked, here is the code. I made it work for version 6.0.0, but I think functions calls are the same as 5.1. To be verified.
First, here is the android project structure you will get:
Create the interface IUsbManager.java in android.harware.usb:
package android.hardware.usb;
public interface IUsbManager extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements android.hardware.usb.IUsbManager
{
/** Construct the stub at attach it to the interface. */
public Stub()
{
throw new RuntimeException( "Stub!" );
}
/**
* Cast an IBinder object into an android.hardware.usb.IUsbManager interface,
* generating a proxy if needed.
*/
public static android.hardware.usb.IUsbManager asInterface( android.os.IBinder obj )
{
throw new RuntimeException( "Stub!" );
}
public android.os.IBinder asBinder()
{
throw new RuntimeException( "Stub!" );
}
public boolean onTransact( int code, android.os.Parcel data, android.os.Parcel reply, int flags ) throws android.os.RemoteException
{
throw new RuntimeException( "Stub!" );
}
static final int TRANSACTION_getDeviceList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_openDevice = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
static final int TRANSACTION_getCurrentAccessory = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
static final int TRANSACTION_openAccessory = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
static final int TRANSACTION_setDevicePackage = (android.os.IBinder.FIRST_CALL_TRANSACTION + 4);
static final int TRANSACTION_setAccessoryPackage = (android.os.IBinder.FIRST_CALL_TRANSACTION + 5);
static final int TRANSACTION_hasDevicePermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 6);
static final int TRANSACTION_hasAccessoryPermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 7);
static final int TRANSACTION_requestDevicePermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 8);
static final int TRANSACTION_requestAccessoryPermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 9);
static final int TRANSACTION_grantDevicePermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 10);
static final int TRANSACTION_grantAccessoryPermission = (android.os.IBinder.FIRST_CALL_TRANSACTION + 11);
static final int TRANSACTION_hasDefaults = (android.os.IBinder.FIRST_CALL_TRANSACTION + 12);
static final int TRANSACTION_clearDefaults = (android.os.IBinder.FIRST_CALL_TRANSACTION + 13);
static final int TRANSACTION_setCurrentFunction = (android.os.IBinder.FIRST_CALL_TRANSACTION + 14);
static final int TRANSACTION_setMassStorageBackingFile = (android.os.IBinder.FIRST_CALL_TRANSACTION + 15);
}
/* Returns a list of all currently attached USB devices */
public void getDeviceList( android.os.Bundle devices ) throws android.os.RemoteException;
/* Returns a file descriptor for communicating with the USB device.
* The native fd can be passed to usb_device_new() in libusbhost.
*/
public android.os.ParcelFileDescriptor openDevice( java.lang.String deviceName ) throws android.os.RemoteException;
/* Returns the currently attached USB accessory */
public android.hardware.usb.UsbAccessory getCurrentAccessory() throws android.os.RemoteException;
/* Returns a file descriptor for communicating with the USB accessory.
* This file descriptor can be used with standard Java file operations.
*/
public android.os.ParcelFileDescriptor openAccessory( android.hardware.usb.UsbAccessory accessory ) throws android.os.RemoteException;
/* Sets the default package for a USB device
* (or clears it if the package name is null)
*/
public void setDevicePackage(android.hardware.usb.UsbDevice device, java.lang.String packageName, int userId) throws android.os.RemoteException;
/* Sets the default package for a USB accessory
* (or clears it if the package name is null)
*/
public void setAccessoryPackage( android.hardware.usb.UsbAccessory accessory, java.lang.String packageName ) throws android.os.RemoteException;
/* Returns true if the caller has permission to access the device. */
public boolean hasDevicePermission(android.hardware.usb.UsbDevice device) throws android.os.RemoteException;
/* Returns true if the caller has permission to access the accessory. */
public boolean hasAccessoryPermission( android.hardware.usb.UsbAccessory accessory ) throws android.os.RemoteException;
/* Requests permission for the given package to access the device.
* Will display a system dialog to query the user if permission
* had not already been given.
*/
public void requestDevicePermission( android.hardware.usb.UsbDevice device, java.lang.String packageName, android.app.PendingIntent pi ) throws android.os.RemoteException;
/* Requests permission for the given package to access the accessory.
* Will display a system dialog to query the user if permission
* had not already been given. Result is returned via pi.
*/
public void requestAccessoryPermission( android.hardware.usb.UsbAccessory accessory, java.lang.String packageName, android.app.PendingIntent pi ) throws android.os.RemoteException;
/* Grants permission for the given UID to access the device */
public void grantDevicePermission( android.hardware.usb.UsbDevice device, int uid ) throws android.os.RemoteException;
/* Grants permission for the given UID to access the accessory */
public void grantAccessoryPermission( android.hardware.usb.UsbAccessory accessory, int uid ) throws android.os.RemoteException;
/* Returns true if the USB manager has default preferences or permissions for the package */
public boolean hasDefaults( java.lang.String packageName ) throws android.os.RemoteException;
/* Clears default preferences and permissions for the package */
public void clearDefaults( java.lang.String packageName ) throws android.os.RemoteException;
/* Sets the current USB function. */
public void setCurrentFunction( java.lang.String function, boolean makeDefault ) throws android.os.RemoteException;
/* Sets the file path for USB mass storage backing file. */
public void setMassStorageBackingFile( java.lang.String path ) throws android.os.RemoteException;
}
Then create the java class ServiceManager.java in android.os:
package android.os;
import java.util.Map;
public final class ServiceManager
{
public static IBinder getService( String name )
{
throw new RuntimeException( "Stub!" );
}
/**
* Place a new #a service called #a name into the service
* manager.
*
* #param name the name of the new service
* #param service the service object
*/
public static void addService( String name, IBinder service )
{
throw new RuntimeException( "Stub!" );
}
/**
* Retrieve an existing service called #a name from the
* service manager. Non-blocking.
*/
public static IBinder checkService( String name )
{
throw new RuntimeException( "Stub!" );
}
public static String[] listServices() throws RemoteException
{
throw new RuntimeException( "Stub!" );
}
/**
* This is only intended to be called when the process is first being brought
* up and bound by the activity manager. There is only one thread in the process
* at that time, so no locking is done.
*
* #param cache the cache of service references
* #hide
*/
public static void initServiceCache( Map<String, IBinder> cache )
{
throw new RuntimeException( "Stub!" );
}
}
Once this is done, don't forget to add the android.permission.MANAGE_USB in you AndroidManifest.
Then you can use those function calls:
/**
* Verify if the application is a system app and has MANAGE_USB permission
* before granting the USB permission for you specific USB devices
*/
private void manageUSBPermissions() {
if ((this.getApplicationInfo().flags & ApplicationInfo.FLAG_SYSTEM) != 0) {
Log.i(TAG,"This is a system application");
if (getApplicationContext().checkCallingOrSelfPermission("android.permission.MANAGE_USB") == PackageManager.PERMISSION_GRANTED) {
Log.i(TAG,"I have android.permission.MANAGE_USB");
grantUsbPermissions();
} else {
Log.i(TAG,"I do not have android.permission.MANAGE_USB");
}
} else {
Log.i(TAG,"This is not a system application");
}
}
/**
* This is to avoid the android usb host permission confirmation dialog
* The application need to be a system app and have MANAGE_USB permission for it to work
*/
private void grantUsbPermissions() {
try {
PackageManager pm = getPackageManager();
ApplicationInfo ai = pm.getApplicationInfo( "com.your.package", 0 );
if( ai != null ) {
UsbManager manager = (UsbManager) getSystemService( Context.USB_SERVICE );
IBinder b = ServiceManager.getService( Context.USB_SERVICE );
IUsbManager service = IUsbManager.Stub.asInterface( b );
HashMap<String, UsbDevice> deviceList = manager.getDeviceList();
Iterator<UsbDevice> deviceIterator = deviceList.values().iterator();
while( deviceIterator.hasNext() ) {
UsbDevice device = deviceIterator.next();
if ( device.getVendorId() == 0x1234 ) {
service.grantDevicePermission( device, ai.uid );
service.setDevicePackage( device, "com.your.package", ai.uid );
}
}
}
}
catch ( Exception e ) {
Log.e(TAG, "Error granting USB permissions: " + e);
}
}
There's a check on whether your application is a system application and if it has the correct permission, otherwise it would not work.
Also be aware that your vendor id is not in hexadecimal but in decimal.
Have you tried using an intent-filter in the Manifest as described here: https://developer.android.com/guide/topics/connectivity/usb/host.html#using-intents
I had a similar issue - it seems like if you programmatically request a USB permission, it will ignore the ticked checkbox and ask you every time again.
Edit:
If you are having issues because of the service, you might want to read this too: https://stackoverflow.com/a/15151075/3540885
I have a Windows 7 application, which uses Stollmann SDK to successfully bond PC with Android. The bidirectional exchange of Bluetooth MAC address, hash and randomizer is carried out out of band via NFC:
The source code of the Windows application unfortunately can not be shared here. On the Android side no app is needed and the Secure Simple Pairing is performed by the operating system (by HandoverManager?) once an NDEF message with application/vnd.bluetooth.ep.oob is received.
Now I am trying to create an Android app, which would use unidirectional authentication to perform OOB pairing via scanned QR code (instead of NFC).
A custom QR code would be shown at PC screen (generated by ZXing.Net) and contain Bluetooth MAC address, hash and randomizer.
However OOB bonding seems to be not implemented yet in Android -
BluetoothAdapter.java:
/**
* Read the local Out of Band Pairing Data
* <p>Requires {#link android.Manifest.permission#BLUETOOTH}
*
* #return Pair<byte[], byte[]> of Hash and Randomizer
*
* #hide
*/
public Pair<byte[], byte[]> readOutOfBandData() {
if (getState() != STATE_ON) return null;
//TODO(BT
/*
try {
byte[] hash;
byte[] randomizer;
byte[] ret = mService.readOutOfBandData();
if (ret == null || ret.length != 32) return null;
hash = Arrays.copyOfRange(ret, 0, 16);
randomizer = Arrays.copyOfRange(ret, 16, 32);
if (DBG) {
Log.d(TAG, "readOutOfBandData:" + Arrays.toString(hash) +
":" + Arrays.toString(randomizer));
}
return new Pair<byte[], byte[]>(hash, randomizer);
} catch (RemoteException e) {Log.e(TAG, "", e);}*/
return null;
}
BluetoothDevice.java:
/**
* Start the bonding (pairing) process with the remote device using the
* Out Of Band mechanism.
*
* <p>This is an asynchronous call, it will return immediately. Register
* for {#link #ACTION_BOND_STATE_CHANGED} intents to be notified when
* the bonding process completes, and its result.
*
* <p>Android system services will handle the necessary user interactions
* to confirm and complete the bonding process.
*
* <p>Requires {#link android.Manifest.permission#BLUETOOTH_ADMIN}.
*
* #param hash - Simple Secure pairing hash
* #param randomizer - The random key obtained using OOB
* #return false on immediate error, true if bonding will begin
*
* #hide
*/
public boolean createBondOutOfBand(byte[] hash, byte[] randomizer) {
//TODO(BT)
/*
try {
return sService.createBondOutOfBand(this, hash, randomizer);
} catch (RemoteException e) {Log.e(TAG, "", e);}*/
return false;
}
/**
* Set the Out Of Band data for a remote device to be used later
* in the pairing mechanism. Users can obtain this data through other
* trusted channels
*
* <p>Requires {#link android.Manifest.permission#BLUETOOTH_ADMIN}.
*
* #param hash Simple Secure pairing hash
* #param randomizer The random key obtained using OOB
* #return false on error; true otherwise
*
* #hide
*/
public boolean setDeviceOutOfBandData(byte[] hash, byte[] randomizer) {
//TODO(BT)
/*
try {
return sService.setDeviceOutOfBandData(this, hash, randomizer);
} catch (RemoteException e) {Log.e(TAG, "", e);} */
return false;
}
My question:
Since OOB Bluetooth pairing works well over NFC on Android - do you think there is a (hackish) way to do the same via QR code?
Maybe (crazy idea) by feeding HandoverManager with a fake NDEF message?
You can not fake the NFC broadcast which is actually posted by NFC service app when it detects the NFC tag. Since this is a protected broadcast non system apps can not broadcast the intent.
Allright, I have been struggling in understanding this authentification process with this "template" project and just don't get it.
What I am trying to do is a basic sign in, so I can get my authorization token, I've done it before by using SharedPreference (never actually used AccountManager) and accessing it from my rest android client (via a custom SessionManager).
So far, I was able to get the first part done. I am able to get my authorization token.
BoostrapAuthenticatorActivity.java:
/**
* Called when response is received from the server for authentication
* request. See onAuthenticationResult(). Sets the
* AccountAuthenticatorResult which is sent back to the caller. Also sets
* the authToken in AccountManager for this account.
*/
protected void finishLogin() {
final Account account = new Account(email, Constants.Auth.LZGO_ACCOUNT_TYPE);
if (requestNewAccount)
accountManager.addAccountExplicitly(account, password, null);
else
accountManager.setPassword(account, password);
final Intent intent = new Intent();
authToken = token;
userToken = user;
intent.putExtra(KEY_ACCOUNT_NAME, userToken);
intent.putExtra(KEY_ACCOUNT_TYPE, Constants.Auth.LZGO_ACCOUNT_TYPE);
if (authTokenType != null
&& authTokenType.equals(Constants.Auth.AUTHTOKEN_TYPE))
intent.putExtra(KEY_AUTHTOKEN, authToken);
setAccountAuthenticatorResult(intent.getExtras());
setResult(RESULT_OK, intent);
finish();
}
How do I have access to this value ? If you check this class,
BootstrapServiceProvider.java:
#Inject private ApiKeyProvider keyProvider;
#Inject private UserAgentProvider userAgentProvider;
/**
* Get service for configured key provider
* <p>
* This method gets an auth key and so it blocks and shouldn't be called on the main thread.
*
* #return bootstrap service
* #throws IOException
* #throws AccountsException
*/
public BootstrapService getService() throws IOException, AccountsException {
return new BootstrapService(keyProvider.getAuthKey(), userAgentProvider);
}
And finally, the provider:
ApiKeyProvider.java:
#Inject private Activity activity;
#Inject private AccountManager accountManager;
/**
* This call blocks, so shouldn't be called on the UI thread
*
* #return API key to be used for authorization with a {#link com.android.lzgo.core.LzgoService} instance
* #throws AccountsException
* #throws IOException
*/
public String getAuthKey() throws AccountsException, IOException {
AccountManagerFuture<Bundle> accountManagerFuture = accountManager.getAuthTokenByFeatures(Constants.Auth.BOOTSTRAP_ACCOUNT_TYPE,
Constants.Auth.AUTHTOKEN_TYPE, new String[0], activity, null, null, null, null);
Log.d("ApiKeyProvider", "ApiKeyProvider= " + accountManagerFuture.getResult().getString(KEY_AUTHTOKEN));
return accountManagerFuture.getResult().getString(KEY_AUTHTOKEN);
}
But this get me a null value! I'm at lost!
I'm not sure why the authtoken isn't getting applied correctly by setAccountAuthenticatorResult(...), however I've had luck by adding the following accountManager.setAuthToken(...) call in finishLogin()
if (authTokenType != null
&& authTokenType.equals(Constants.Auth.AUTHTOKEN_TYPE)) {
intent.putExtra(KEY_AUTHTOKEN, authToken);
accountManager.setAuthToken(account, authTokenType, authToken);
}
Perhaps this is related to not using Parse.com as our backend?
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;
}
}