web-dev-qa-db-ja.com

Android)で非推奨のクラスを処理して互換性を維持する方法

Android 2.2 Froyo。

SDKを最新のAPIに更新しましたが、使用していたClipboardManager機能が廃止されていることに気付きました。新しいClipDataモデルを使用するようにコードを更新し、Froyo電話で試してみましたが、もちろん、新しいコードでNoClassDefFoundErrorが発生しました。

私はSOを見て回りましたが、後方互換性を維持するための一般的な戦略についての実際の議論は見つかりませんでした。

すべてのバージョンのユーザーが私のアプリを使用できるようにしたいので、APIが異なるこの状況や他の状況をどのように処理すればよいか完全にはわかりません。

次のようにチェックしますか?

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

もしそうなら、私は私のクラスで非推奨のコードを持っています、そしてEclipseはそこに永遠に警告を出します。

一方、私は古いバージョンのAPIに対してビルドするだけで、新しいバージョンがそれをうまく処理できることを願っています。しかし、より良い代替策が利用可能な場合は、バグのあるコードやパフォーマンスの低いコードに対してビルドするリスクを負います。

これに対処する最良の方法は何ですか?

30
HXCaine

あなたはそれを行うことができます(APIバージョンをチェックします)。

リフレクションを使用して、新しいクラスを呼び出すこともできます。

すべてのAndroidバージョンには下位互換性があるため、3.0 Honeycombの場合は少し異なるため、状況を確認する必要があるため、非推奨のメソッドを使用することについて心配する必要はありません。

リフレクションの使用方法の説明は次のとおりです(はいSO以前に使用されているため、リフレクションを検索してください)。

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

私はこれが利用できるプロジェクトを作ることを考えていますが、それまではいくつかのコードがあります:

(これは、アプリケーションを拡張するクラス、つまり1回のセットアップで行うことができます)

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

現在 getExternalFilesDir() はAPIレベル8以上でのみ使用できるため、(Froyo)がある場合はこれを使用しますが、それ以外の場合は別のメソッドが必要です。

これで、先に進んでそれを使用する方法を試すことができるメソッドのテストができました。

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

それが多くのググリングを助けてショートカットになることを願っています:-)

追伸それ以外の場合は、廃止されたメソッドを引き続き使用したい場合は、その上に@SuppressWarnings("deprecation")アノテーションを追加するだけです。これにより警告が取り除かれ、最新のAPIを使用しているため、正しい理由でそれを実行します可能であれば。

17
Blundell

次に例を示します。

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

コードのセクションを見ることができるように:

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

Android 3.0以降のバージョンでのみ利用可能です。このコードを少なくとも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フレームワークのユーザーに表示されるSDKバージョン。その可能な値はBuild.VERSION_CODESで定義されています。

詳細情報: Build.VERSION

そして、ここでVERSION_CODESconstatsを見ることができます: Build.VERSION_CODES

4
Jorgesys

まず、@ Graham Borlandが正しいです。古いAPIを使用することもできます。これにより問題が完全に解決します。それにもかかわらず、ソフトウェアは進化せず、APIの改善に追従せず、最終的には、Androidのサポートされなくなったバージョンに一致します。

私が提案しようとしている設計パターンは内省に基づいていますが、@ Blundellによって提案されたソリューションよりも優れたプログラミングインターフェイスを提供します。この共通の問題に対する標準的なアプローチを刺激するのに十分強力だと思います。 Stack Over Flowや他のフォーラムからの多くの投稿に基づいています。

まず、実装するサービスのインターフェースを定義する必要があります。目的のAPIの異なるバージョンを使用して、このサービスの異なるバージョンを実装できます。

実際、さまざまな実装をロードするためにここでいくつかのコードを共有するので、抽象クラスを使用することを選択します。インターフェースbuは異なる実装をロードするための静的メソッドも提供するため、パブリックメソッドシグネチャを定義します。

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

次に、Android SDKの異なるバージョンを使用して、この抽象クラスの異なる実装を提供できます。

この方法で少し変わっているのは、シングルトン設計パターンと組み合わせた工場設計パターンであることです。すべてのサブクラスはシングルトンであり、静的なgetInstanceMethodを提供するように要求されます。この抽象クラスのファクトリメソッドは、このインターフェイスを実装するクラスをロードしようとします。失敗した場合は、サービスを実装し、古いAPIに基づくクラスに要件をダウングレードできます。

このインターフェイスを使用してメールとSMSを送信するクラスの例を次に示します。 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

また、他の選択肢を提供することもできます。 Androidバージョン番号の降順で、次々にそれらをロードしようとしています。

メッセンジャーサービスを使用するのは非常に簡単です。

MessageManager messageManager = MessageManager.getInstance( this );

nullの場合、一致するサービスはありません。 nullでない場合は、MessageManagerによって定義されたインターフェースを介して使用します。

この手法は、実装のベースになっているバージョン番号を含め、クラスを正しい順序で次々にロードする小さなバスを構築することにより、拡張してさらにきれいにすることができます。

すべてのフィードバックを歓迎します。

よろしく、ステファン

4
Snicolas

2つの可能な解決策を正しく識別しました。使用するAPIを実行時に決定するか、常に古いAPIを使用します。

効果がある場合は、古いAPIを備えたデバイスがインストールベースのごく一部を形成し、完全に新しいAPIに切り替えて、多くのユーザーを失う心配をしないようになるまで、1年ほどしかかかりません。

2
Graham Borland