Je me remets au travail sur une application sur laquelle j'ai travaillé il y a quelque temps, quand j'ai tout construit autour de Android 2.2 Froyo.
J'ai mis à jour mon SDK pour les dernières API et j'ai remarqué que les fonctionnalités de ClipboardManager que j'utilisais sont obsolètes. J'ai mis à jour le code pour utiliser le nouveau modèle ClipData et l'ai essayé sur mon téléphone Froyo et, bien sûr, j'obtiens un NoClassDefFoundError dans le nouveau code.
J'ai jeté un coup d'œil à SO et je n'ai trouvé aucune discussion réelle sur la stratégie générale pour maintenir la compatibilité descendante.
Je ne sais pas exactement comment je dois gérer cela et d'autres situations où l'API diffère, car je veux que les utilisateurs de toutes les versions puissent utiliser mon application.
Dois-je faire un contrôle comme suit?
if(version == old){
use old API;
} else {
use new API;
}
Si c'est le cas, j'ai du code obsolète dans ma classe et Eclipse y aura toujours l'avertissement.
D'un autre côté, je pourrais simplement construire contre une ancienne version de l'API et espérer que les nouvelles versions le géreront bien. Mais je risque alors de construire contre du code bogué ou peu performant lorsqu'une meilleure alternative est disponible.
Quelle est la meilleure façon de gérer cela?
Vous pouvez le faire (vérifier la version de l'API).
Vous pouvez également utiliser la réflexion pour appeler les nouvelles classes.
Je ne m'inquiéterais pas d'utiliser des méthodes obsolètes car toutes les versions Android sont rétrocompatibles, disant que vous voulez regarder quand les choses sont pour 3.0 Honeycomb car celles-ci sont un peu différentes.
Voici une explication sur la façon d'utiliser la réflexion: (oui, cela a été sur SO avant, alors peut-être recherchez la réflexion)
http://www.youtube.com/watch?v=zNmohaZYvPw&feature=player_detailpage#t=2087s
Je cherche à rendre le projet disponible, mais d'ici là, voici du code:
(Vous pouvez le faire dans une classe qui étend l'application, c'est-à-dire une configuration unique)
public static Method getExternalFilesDir;
static {
try {
Class<?> partypes[] = new Class[1];
partypes[0] = String.class;
getExternalFilesDir = Context.class.getMethod("getExternalFilesDir", partypes);
} catch (NoSuchMethodException e) {
Log.e(TAG, "getExternalFilesDir isn't available in this devices api");
}
}
Maintenant getExternalFilesDir () est uniquement disponible sur le niveau 8 ou supérieur de l'API, donc je veux l'utiliser si c'est le cas (Froyo), mais sinon j'ai besoin d'une autre méthode.
Maintenant, j'ai mon test pour la méthode, je peux aller de l'avant et essayer de l'utiliser:
if(ClassThatExtendsApplication.getExternalFilesDir != null){
Object arglist[] = new Object[1];
arglist[0] = null;
File path = (File) ClassThatExtendsApplication.getExternalFilesDir.invoke(context, arglist);
// etc etc
} else {
// Not available do something else (like your deprecated methods / or load a different class / or notify they should get a newer version of Android to enhance your app ;-))
}
J'espère que cela aide et raccourcit beaucoup de recherches sur Google :-)
P.S. si dans le reste, vous souhaitez utiliser vos méthodes obsolètes, ajoutez simplement l'annotation @SuppressWarnings("deprecation")
au-dessus, cela supprimera l'avertissement et vous l'avez fait pour les bonnes raisons car vous utilisez les dernières API quand c'est possible.
Voici un exemple:
import Android.os.Build;
public static int getWidth(Context mContext){
int width=0;
WindowManager wm = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
Display display = wm.getDefaultDisplay();
if(VERSION.SDK_INT > VERSION_CODES.HONEYCOMB){
Point size = new Point();
display.getSize(size);
width = size.x;
}
else{
width = display.getWidth(); // deprecated, use only in Android OS<3.0.
}
return width;
}
comme vous pouvez voir la section de code:
if(VERSION.SDK_INT > VERSION_CODES.HONEYCOMB){
Point size = new Point();
display.getSize(size);
width = size.x;
}
est uniquement disponible pour Android 3.0 et versions ultérieures, si vous voulez que ce code soit disponible au moins pour Jelly bean (Android 4.1), utilisez:
if(VERSION.SDK_INT > VERSION_CODES.JELLY_BEAN){
Point size = new Point();
display.getSize(size);
width = size.x;
}
VERSION.SDK_INT La version SDK visible par l'utilisateur du framework; ses valeurs possibles sont définies dans Build.VERSION_CODES.
Plus d'informations sur: Build.VERSION
Et vous pouvez voir les VERSION_CODES constats ici: Build.VERSION_CODES
d'abord, @Graham Borland a raison. Vous pouvez soit choisir d'utiliser l'ancienne API, cela résout complètement le problème. Néanmoins, votre logiciel n'évolue pas et ne suit pas les améliorations de l'API et, finalement, correspondra à une version de Android qui n'est plus prise en charge.
Le modèle de conception que je vais proposer est basé sur l'introspection, mais offre une meilleure interface de programmation que la solution proposée par @Blundell. Je pense que c'est assez puissant pour inspirer une approche standard pour ce problème commun. Il est basé sur de nombreux articles de Stack Over Flow et d'autres forums.
Tout d'abord, vous devez définir une interface pour le service que vous souhaitez implémenter. Vous pourrez implémenter différentes versions de ce service en utilisant différentes versions de l'API qui vous intéresse.
En effet, comme nous allons partager ici du code pour charger nos différentes implémentations, nous choisissons d'utiliser une classe abstraite. Il définira les signatures de méthodes publiques comme une interface mais offrira également une méthode statique pour charger vos différentes implémentations.
/**
* Interface used to interact with the actual instance of MessageManager.
* This inteface allows will be the type of the reference that will point
* to the actual MessageMessenger, which will be loaded dynamically.
* @author steff
*
*/
public abstract class MessageManager {
/** Request code used to identify mail messages.*/
public final static int FOR_MAIL = 0x3689;
/** Request code used to identify SMS messages.*/
public final static int FOR_SMS = 0x3698;
/**
* Start an activity inside the given context. It will allow to pickup a contact
* and will be given an intent code to get contact pick up.
* *@param the request code. Has to be a constant : FOR_MAIL or FOR_SMS
*/
public abstract void pickupContact(int code);//met
/**
* Start an activity inside the given context. It will allow to pickup a contact
* and will be given an intent code to get contact pick up.
* *@param the request code. Has to be a constant : FOR_MAIL or FOR_SMS
*/
public abstract void sendMessage(int code, Intent data, final String body);//met
/**
* Static methode used as in factory design pattern to create an instance
* of messageManager. Here it is combined with the singleton pattern to
* get an instance of an inherited class that is supported by current Android SDK.
* This singleton will be created bu reflexion.
* @param activity the activity that needs messaging capabilities.
* @return an instance of an inherited class that is supported by current Android SDK or null, if not found.
*/
public static MessageManager getInstance( Activity activity )
{
MessageManager instance = null;
try {
Class<? extends MessageManager> messageManagerClass = (Class<? extends MessageManager>) activity.getClassLoader().loadClass( "ca.qc.webalterpraxis.cinedroid.message.MessageManagerSDK7" );
Method singletonMethod = messageManagerClass.getMethod("getInstance", Activity.class );
instance = (MessageManager) singletonMethod.invoke( null , activity);
} catch (Throwable e) {
Log.e( "CinemadroidMain", "Impossible to get an instance of class MessageManagerSDK7",e );
}//met
return instance;
}//met
}//interface
Ensuite, vous pouvez fournir différentes implémentations de cette classe abstraite à l'aide de différentes versions de Android SDK.
Ce qui est quelque peu inhabituel avec cette méthode, c'est qu'il s'agit d'un modèle de conception d'usine combiné à un modèle de conception singleton. Toutes les sous-classes doivent être singleton et fournir un getInstanceMethod statique. La méthode d'usine de cette classe abstraite tentera de charger une classe implémentant cette interface. En cas d'échec, vous pouvez rétrograder vos besoins en classes implémentant le service et basées sur un APIS plus ancien.
Voici un exemple de classe pour envoyer des mails et des sms en utilisant cette interface. Il est conçu pour Android sdk 7.
public class MessageManagerSDK7 extends MessageManager {
/** Used for logcat. */
private static final String LOG_TAG = "MessageManagerSDK7";
/** Singleton instance. */
private static MessageManagerSDK7 instance = null;
/** Activity that will call messaging actions. */
private Activity context;
/** Private constructor for singleton. */
private MessageManagerSDK7( Activity context )
{
if( instance != null )
throw new RuntimeException( "Should not be called twice. Singleton class.");
this.context = context;
}//cons
/**
* Static method that will be called by reflexion;
* @param context the activity that will enclose the call for messaging.
* @return an instance of this class (if class loader allows it).
*/
public static MessageManagerSDK7 getInstance( Activity context )
{
if( instance == null )
instance = new MessageManagerSDK7( context );
instance.context = context;
return instance;
}//met
/* (non-Javadoc)
* @see ca.qc.webalterpraxis.cinedroid.model.MessageManager#pickupContact(int)
*/
@Override
public void pickupContact( int code )
{
if( code != FOR_MAIL && code != FOR_SMS )
throw new RuntimeException( "Wrong request code, has to be either FOR_MAIL or FOR_SMS.");
Intent intentContact = new Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI);
context.startActivityForResult(intentContact, code );
}//met
/* (non-Javadoc)
* @see ca.qc.webalterpraxis.cinedroid.model.MessageManager#sendMessage(int, Android.content.Intent, Java.lang.String)
*/
@Override
public void sendMessage( int code, Intent data, final String body )
{
//System.out.println( "SendMessage");
if( code != FOR_MAIL && code != FOR_SMS )
throw new RuntimeException( "Wrong request code, has to be either FOR_MAIL or FOR_SMS.");
int icon = 0;
int noItemMessage = 0;
int title = 0;
//set the right icon and message for the dialog
if( code == FOR_MAIL )
{
icon=R.drawable.mail;
noItemMessage = R.string.no_email_found;
title = R.string.mail_error;
}//if
else if( code == FOR_SMS )
{
icon=R.drawable.sms;
noItemMessage = R.string.no_number_found;
title = R.string.sms_error;
}//if
//compose email or sms
//pick contact email address
final String[] emailsOrPhoneNumbers = (code == FOR_MAIL ) ? getContactsEmails( data ) : getContactPhoneNumber( data );
if( emailsOrPhoneNumbers == null )
{
new AlertDialog.Builder( context ).setIcon( icon ).setTitle(title).setMessage( noItemMessage ).show();
return;
}//if
//in case there are several addresses, we handle this using a dialog.
//modal dialog would be usefull but it's bad UI practice
//so we use an alert dialog, async ..
//all this is poorly coded but not very interesting, not worth having a dedicated inner class
if( emailsOrPhoneNumbers.length > 1 )
{
selectMultipleAndSend( emailsOrPhoneNumbers, body, code);
return;
}//if
if( code == FOR_MAIL )
sendMail( emailsOrPhoneNumbers, body );
else
sendSMS( emailsOrPhoneNumbers, body );
}//met
private void sendMail( String[] emails, String body )
{
if( body == null )
{
new AlertDialog.Builder( context ).setIcon( R.drawable.mail ).setTitle(R.string.mail_error).setMessage( R.string.impossible_compose_message ).show();
return;
}//if
//prepare email data
try {
Intent i = new Intent(Intent.ACTION_SEND);
i.setType("message/rfc822") ;
i.putExtra(Intent.EXTRA_EMAIL, emails );
//i.putExtra(Intent.EXTRA_EMAIL, emails);
i.putExtra(Intent.EXTRA_SUBJECT, context.getString( R.string.showtimes ) );
i.putExtra(Intent.EXTRA_TEXT,body);
context.startActivity(Intent.createChooser(i, context.getString( R.string.select_application ) ) );
} catch (Throwable e) {
new AlertDialog.Builder( context ).setIcon( R.drawable.mail ).setTitle(R.string.mail_error).setMessage( R.string.no_application_mail ).show();
Log.e( LOG_TAG, "No application found", e);
}//catch
}//met
private void sendSMS( String[] phoneNumbers, String body )
{
try {
Intent sendIntent= new Intent(Intent.ACTION_VIEW);
if( body == null )
{
new AlertDialog.Builder( context ).setIcon( R.drawable.sms ).setTitle(R.string.sms_error).setMessage( R.string.impossible_compose_message ).show();
return;
}//if
sendIntent.putExtra("sms_body", body);
String phones = "";
for( String phoneNumber : phoneNumbers )
phones += ((phones.length() == 0) ? "" : ";") + phoneNumber;
sendIntent.putExtra("address", phones );
sendIntent.setType("vnd.Android-dir/mms-sms");
context.startActivity(sendIntent);
} catch (Throwable e) {
new AlertDialog.Builder( context ).setIcon( R.drawable.sms ).setTitle(R.string.sms_error).setMessage( R.string.no_application_sms ).show();
Log.e( LOG_TAG, "No application found", e);
}//catch
}//met
/**
* @param intent the intent returned by the pick contact activity
* @return the emails of selected people, separated by a comma or null if no emails has been found;
*/
protected String[] getContactsEmails(Intent intent)
{
List<String> resultList = new ArrayList<String>();
//http://stackoverflow.com/questions/866769/how-to-call-Android-contacts-list
Cursor cursor = context.managedQuery(intent.getData(), null, null, null, null);
while (cursor.moveToNext())
{
String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID));
// Find Email Addresses
Cursor emails = context.getContentResolver().query(ContactsContract.CommonDataKinds.Email.CONTENT_URI,null,ContactsContract.CommonDataKinds.Email.CONTACT_ID + " = " + contactId,null, null);
while (emails.moveToNext())
{
resultList.add( emails.getString(emails.getColumnIndex(ContactsContract.CommonDataKinds.Email.DATA)) );
}//while
emails.close();
} //while (cursor.moveToNext())
cursor.close();
if( resultList.size() == 0 )
return null;
else
return resultList.toArray( new String[ resultList.size() ] );
}//met
/**
* @param intent the intent returned by the pick contact activity
* @return the phoneNumber of selected people, separated by a comma or null if no phoneNumber has been found;
*/
protected String[] getContactPhoneNumber(Intent intent)
{
List<String> resultList = new ArrayList<String>();
//http://stackoverflow.com/questions/866769/how-to-call-Android-contacts-list
Cursor cursor = context.managedQuery(intent.getData(), null, null, null, null);
while (cursor.moveToNext())
{
String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID));
String name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME));
String hasPhone = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER));
if ( hasPhone.equalsIgnoreCase("1"))
hasPhone = "true";
else
hasPhone = "false" ;
if (Boolean.parseBoolean(hasPhone))
{
Cursor phones = context.getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null,ContactsContract.CommonDataKinds.Phone.CONTACT_ID +" = "+ contactId,null, null);
while (phones.moveToNext())
{
resultList.add( phones.getString(phones.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)) );
}
phones.close();
}
} //while (cursor.moveToNext())
cursor.close();
if( resultList.size() == 0 )
return null;
else
return resultList.toArray( new String[ resultList.size() ] );
}//met
private void selectMultipleAndSend( final String[] emailsOrPhoneNumbers, final String body, final int code )
{
int icon = 0;
int selectMessage = 0;
//set the right icon and message for the dialog
if( code == FOR_MAIL )
{
icon=R.drawable.mail;
selectMessage = R.string.select_email;
}//if
else if( code == FOR_SMS )
{
icon=R.drawable.sms;
selectMessage = R.string.select_phone;
}//if
final boolean[] selected = new boolean[ emailsOrPhoneNumbers.length ];
Arrays.fill( selected, true );
new AlertDialog.Builder( context ).setIcon( icon ).setTitle( selectMessage ).setMultiChoiceItems(emailsOrPhoneNumbers, selected, new OnMultiChoiceClickListener() {
@Override
public void onClick(DialogInterface dialog, int which, boolean isChecked) {
selected[ which ] = isChecked;
}
}).setPositiveButton( R.string.OK, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
int count = 0;
for( int s=0; s< selected.length; s ++ )
if( selected[s] )
count ++;
String[] selectedEmailsOrPhoneNumbers = new String[ count ];
int index = 0;
for( int s=0; s< selected.length; s ++ )
if( selected[s] )
selectedEmailsOrPhoneNumbers[ index ++ ] = emailsOrPhoneNumbers[ s ];
if( code == FOR_MAIL )
sendMail( selectedEmailsOrPhoneNumbers, body );
else if( code == FOR_SMS )
sendSMS( selectedEmailsOrPhoneNumbers, body );
}
}).setNegativeButton( R.string.cancel , null ).show();
}//met
}//class
Et vous pouvez également proposer d'autres alternatives. Essayer de les charger les uns après les autres, en ordre décroissant Android numéros de version.
Pour utiliser votre service de messagerie est assez simple:
MessageManager messageManager = MessageManager.getInstance( this );
s'il est nul, aucun service ne correspond. S'il n'est pas nul, utilisez-le via l'interface définie par MessageManager.
Cette technique pourrait être étendue et même rendue plus propre en incluant le numéro de version sur lequel une implémentation est basée et en construisant un petit bus pour charger les classes les unes après les autres dans le bon ordre.
Tous les commentaires sont les bienvenus.
Cordialement, Stéphane
Vous avez correctement identifié les deux solutions possibles: décidez au moment de l'exécution quelle API utiliser, ou utilisez toujours l'ancienne API.
Si cela peut vous aider, cela ne peut prendre qu'un an environ jusqu'à ce que les appareils dotés de l'ancienne API forment une si petite proportion de la base d'installation que vous pouvez passer entièrement à la nouvelle API et ne pas craindre de perdre trop d'utilisateurs.