source que open compatible aosp android compatibility deprecated backwards-compatibility

que - Cómo lidiar con las clases en desuso en Android para mantener la compatibilidad



project android source (4)

Estoy volviendo a trabajar en una aplicación en la que trabajé hace un tiempo, cuando tenía todo construido alrededor de Android 2.2 Froyo.

He actualizado mi SDK para las últimas API y he notado que las funciones de ClipboardManager que estaba usando están en desuso. Actualicé el código para usar el nuevo modelo ClipData y lo probé en mi teléfono Froyo y, por supuesto, obtengo un NoClassDefFoundError en el nuevo código.

He echado un vistazo a SO y no he encontrado ninguna discusión real sobre la estrategia general para mantener la compatibilidad con versiones anteriores.

No estoy completamente seguro de cómo debo manejar esta y otras situaciones en las que la API difiere, porque quiero que los usuarios de todas las versiones puedan usar mi aplicación.

¿Debo hacer un chequeo de la siguiente manera?

if(version == old){ use old API; } else { use new API; }

Si es así, he descartado el código en mi clase y Eclipse tendrá la advertencia allí para siempre.

Por otro lado, podría compilar contra una versión anterior de la API y esperar que las nuevas versiones lo manejen bien. Pero luego corro el riesgo de construir contra buggy o código de bajo rendimiento cuando hay una mejor alternativa disponible.

¿Cuál es la mejor manera de lidiar con esto?


Aquí hay un ejemplo:

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; }

Como puedes ver la sección de código:

if(VERSION.SDK_INT > VERSION_CODES.HONEYCOMB){ Point size = new Point(); display.getSize(size); width = size.x; }

solo está disponible para Android 3.0 y versiones posteriores, si desea que este código esté disponible, al menos para el uso de Jelly Bean (Android 4.1):

if(VERSION.SDK_INT > VERSION_CODES.JELLY_BEAN){ Point size = new Point(); display.getSize(size); width = size.x; }

VERSION.SDK_INT La versión SDK del marco visible por el usuario; sus posibles valores están definidos en Build.VERSION_CODES.

Más información sobre: Build.VERSION

Y puedes ver las constantes VERSION_CODES aquí: Build.VERSION_CODES


Ha identificado correctamente las dos soluciones posibles: decidir en tiempo de ejecución qué API usar, o usar siempre la API antigua.

Si es útil, solo puede pasar un año o más hasta que los dispositivos con la API antigua formen una proporción tan pequeña de la base de instalación que pueda cambiar por completo a la nueva API y no preocuparse por perder demasiados usuarios.


Primero, @Graham Borland tiene razón. Puede elegir usar la API antigua, esto resuelve completamente el problema. Sin embargo, su software no evolucionará y seguirá las mejoras de la API y, en última instancia, coincidirá con una versión de Android que ya no es compatible.

El patrón de diseño que voy a proponer se basa en la introspección, pero proporciona una mejor interfaz de programación que la solución propuesta por @Blundell. Creo que es lo suficientemente poderoso como para inspirar un enfoque estándar para este problema común. Se basa en muchas publicaciones de Stack Over Flow y otros foros.

Primero, debe definir una interfaz para el servicio que desea implementar. Podrá implementar diferentes versiones de este servicio utilizando diferentes versiones de la API que le interesa.

De hecho, como vamos a compartir algunos códigos aquí para cargar nuestras diferentes implementaciones, elegimos usar una clase abstracta. Definirá firmas de métodos públicos, ya que una interfaz bu también ofrecerá un método estático para cargar sus diferentes implementaciones.

/** * 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

Luego, puede proporcionar diferentes implementaciones de esta clase abstracta utilizando diferentes versiones de Android SDK.

Lo que es un tanto inusual con este método es que es un patrón de diseño de fábrica combinado con un patrón de diseño de singleton. Se solicita que todas las subclases sean singleton y proporcionen un método getInstanceMethod estático. El método de fábrica de esta clase abstracta intentará cargar una clase que implementa esta interfaz. Si falla, puede degradar sus requisitos a clases que implementan el servicio y basar un APIS anterior.

Aquí hay un ejemplo de una clase para enviar correos electrónicos y SMS utilizando esta interfaz. Está diseñado para sdk 7 de Android.

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://.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://.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

Y usted podría proporcionar otras alternativas también. Tratando de cargarlos uno tras otro, descendiendo los números de versión de Android.

Usar tu servicio de mensajería es bastante simple:

MessageManager messageManager = MessageManager.getInstance( this );

si es nulo, entonces ningún servicio coincide. Si no es nulo, utilícelo a través de la interfaz definida por MessageManager.

Esta técnica podría extenderse e incluso hacerse más limpia si se incluye el número de versión en que se basa la implementación y se construye un bus pequeño para cargar las clases una tras otra en el orden correcto.

Todos los comentarios son bienvenidos.

Saludos, Stéphane


Puedes hacerlo (verificando la versión de la API).

También puedes usar la reflexión para llamar a las clases más nuevas.

No me preocuparía usar métodos desaprobados, ya que todas las versiones de Android son compatibles con versiones anteriores, diciendo que quieres ver cuándo están las cosas para 3.0 Honeycomb, ya que son un poco diferentes.

Aquí hay una explicación de cómo usar la reflexión: (sí, ha estado en SO antes, así que tal vez busque la reflexión)

http://www.youtube.com/watch?v=zNmohaZYvPw&feature=player_detailpage#t=2087s

Estoy pensando en hacer que el proyecto esté disponible, pero hasta entonces aquí hay algo de código:

(Podría hacer esto en una clase que amplíe la aplicación, es decir, la configuración de una sola vez)

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"); } }

Ahora getExternalFilesDir() solo está disponible en el nivel de API 8 o superior, así que quiero usar esto si lo tienen (Froyo), pero de lo contrario necesito otro método.

Ahora tengo mi prueba para el método que puedo seguir adelante e intento usarla:

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 ;-)) }

Espero que ayude y ataja un montón de googlear :-)

PD: si aún no desea utilizar sus métodos desaprovechados, simplemente agregue la @SuppressWarnings("deprecation") encima de ella. Esto eliminará la advertencia y la ha hecho por las razones correctas, ya que está utilizando la última API cuando sea posible.