[android] Comment gérer le changement d'orientation de l'écran lorsque la boîte de dialogue de progression et le fil d'arrière-plan sont actifs?


Answers

Edit: Les ingénieurs de Google ne recommandent pas cette approche, comme décrit par Dianne Hackborn (aka hackbod ) dans ce post StackOverflow . Consultez cet article de blog pour plus d'informations.

Vous devez ajouter ceci à la déclaration d'activité dans le manifeste:

android:configChanges="orientation|screenSize"

donc ça ressemble

<activity android:label="@string/app_name" 
        android:configChanges="orientation|screenSize|keyboardHidden" 
        android:name=".your.package">

Le problème est que le système détruit l'activité lorsqu'un changement de configuration se produit. Voir ConfigurationChanges .

Donc, mettre cela dans le fichier de configuration évite au système de détruire votre activité. Au lieu de cela, il appelle la onConfigurationChanged(Configuration) .

Question

Mon programme effectue une activité réseau dans un thread d'arrière-plan. Avant de commencer, il affiche une boîte de dialogue de progression. La boîte de dialogue est fermée sur le gestionnaire. Tout fonctionne correctement, sauf lorsque l'orientation de l'écran change pendant que la boîte de dialogue est active (et le fil d'arrière-plan est en cours). À ce stade, l'application se bloque, ou blocages, ou entre dans une étape étrange où l'application ne fonctionne pas du tout jusqu'à ce que tous les threads ont été tués.

Comment puis-je gérer le changement d'orientation de l'écran avec élégance?

L'exemple de code ci-dessous correspond grosso modo à ce que fait mon vrai programme:

public class MyAct extends Activity implements Runnable {
    public ProgressDialog mProgress;

    // UI has a button that when pressed calls send

    public void send() {
         mProgress = ProgressDialog.show(this, "Please wait", 
                      "Please wait", 
                      true, true);
        Thread thread = new Thread(this);
        thread.start();
    }

    public void run() {
        Thread.sleep(10000);
        Message msg = new Message();
        mHandler.sendMessage(msg);
    }

    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            mProgress.dismiss();
        }
    };
}

Empiler:

E/WindowManager(  244): Activity MyAct has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager(  244): android.view.WindowLeaked: Activity MyAct has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView@433b7150 that was originally added here
E/WindowManager(  244):     at android.view.ViewRoot.<init>(ViewRoot.java:178)
E/WindowManager(  244):     at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:147)
E/WindowManager(  244):     at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:90)
E/WindowManager(  244):     at android.view.Window$LocalWindowManager.addView(Window.java:393)
E/WindowManager(  244):     at android.app.Dialog.show(Dialog.java:212)
E/WindowManager(  244):     at android.app.ProgressDialog.show(ProgressDialog.java:103)
E/WindowManager(  244):     at android.app.ProgressDialog.show(ProgressDialog.java:91)
E/WindowManager(  244):     at MyAct.send(MyAct.java:294)
E/WindowManager(  244):     at MyAct$4.onClick(MyAct.java:174)
E/WindowManager(  244):     at android.view.View.performClick(View.java:2129)
E/WindowManager(  244):     at android.view.View.onTouchEvent(View.java:3543)
E/WindowManager(  244):     at android.widget.TextView.onTouchEvent(TextView.java:4664)
E/WindowManager(  244):     at android.view.View.dispatchTouchEvent(View.java:3198)

J'ai essayé d'ignorer la boîte de dialogue de progression dans onSaveInstanceState, mais cela évite juste un plantage immédiat. Le thread d'arrière-plan est toujours en cours et l'interface utilisateur est partiellement dessinée. Besoin de tuer l'application entière avant qu'elle ne recommence à fonctionner.




The simplest and most flexible solution is to use an here with a static reference to ProgressBar . This provides an encapsulated and thus reusable solution to orientation change problems. This solution has served me well for varying asyncronous tasks including internet downloads, communicating with Services , and filesystem scans. The solution has been well tested on multiple android versions and phone models. A complete demo can be found here with specific interest in DownloadFile.java

I present the following as a concept example

public class SimpleAsync extends AsyncTask<String, Integer, String> {
    private static ProgressDialog mProgressDialog = null;
    private final Context mContext;

    public SimpleAsync(Context context) {
        mContext = context;
        if ( mProgressDialog != null ) {
            onPreExecute();
        }
    }

    @Override
    protected void onPreExecute() {
        mProgressDialog = new ProgressDialog( mContext );
        mProgressDialog.show();
    }

    @Override
    protected void onPostExecute(String result) {
        if ( mProgressDialog != null ) {
            mProgressDialog.dismiss();
            mProgressDialog = null;
        }
    }

    @Override
    protected void onProgressUpdate(Integer... progress) {
        mProgressDialog.setProgress( progress[0] );
    }

    @Override
    protected String doInBackground(String... sUrl) {
        // Do some work here
        publishProgress(1);
        return null;
    }

    public void dismiss() {
        if ( mProgressDialog != null ) {
            mProgressDialog.dismiss();
        }
    }
}

Usage in an Android Activity is simple

public class MainActivity extends Activity {
    DemoServiceClient mClient = null;
    DownloadFile mDownloadFile = null;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate( savedInstanceState );
        setContentView( R.layout.main );
        mDownloadFile = new DownloadFile( this );

        Button downloadButton = (Button) findViewById( R.id.download_file_button );
        downloadButton.setOnClickListener( new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                mDownloadFile.execute( "http://www.textfiles.com/food/bakebred.txt");
            }
        });
    }

    @Override
    public void onPause() {
        super.onPause();
        mDownloadFile.dismiss();
    }
}



J'ai rencontré le même problème. Mon activité doit analyser certaines données d'une URL et c'est lent. Donc, je crée un fil pour le faire, puis montrer un progressdialog. Je laisse le fil poster un message au fil de l'interface utilisateur via Handler quand il est fini. Dans Handler.handleMessage, j'obtiens l'objet de données (prêt maintenant) du fil et le remplis à l'interface utilisateur. Donc, c'est très similaire à votre exemple.

Après beaucoup d'essais et d'erreurs, il semble que j'ai trouvé une solution. Au moins maintenant je peux faire pivoter l'écran à tout moment, avant ou après que le fil soit terminé. Dans tous les tests, le dialogue est correctement fermé et tous les comportements sont comme prévu.

Ce que j'ai fait est montré ci-dessous. Le but est de remplir mon modèle de données (mDataObject), puis de le remplir en interface utilisateur. Devrait permettre la rotation de l'écran à tout moment sans surprise.

class MyActivity {

private MyDataObject mDataObject = null;
private static MyThread mParserThread = null; // static, or make it singleton

OnCreate() {
        ...
        Object retained = this.getLastNonConfigurationInstance();
        if(retained != null) {
            // data is already completely obtained before config change
            // by my previous self.
            // no need to create thread or show dialog at all
            mDataObject = (MyDataObject) retained;
            populateUI();
        } else if(mParserThread != null && mParserThread.isAlive()){
            // note: mParserThread is a static member or singleton object.
            // config changed during parsing in previous instance. swap handler
            // then wait for it to finish.
            mParserThread.setHandler(new MyHandler());
        } else {
            // no data and no thread. likely initial run
            // create thread, show dialog
            mParserThread = new MyThread(..., new MyHandler());
            mParserThread.start();
            showDialog(DIALOG_PROGRESS);
        }
}

// http://android-developers.blogspot.com/2009/02/faster-screen-orientation-change.html
public Object onRetainNonConfigurationInstance() {
        // my future self can get this without re-downloading
        // if it's already ready.
        return mDataObject;
}

// use Activity.showDialog instead of ProgressDialog.show
// so the dialog can be automatically managed across config change
@Override
protected Dialog onCreateDialog(int id) {
    // show progress dialog here
}

// inner class of MyActivity
private class MyHandler extends Handler {
    public void handleMessage(msg) {
        mDataObject = mParserThread.getDataObject();
        populateUI();
        dismissDialog(DIALOG_PROGRESS);
    }
}
}

class MyThread extends Thread {
    Handler mHandler;
    MyDataObject mDataObject;

    public MyHandler(..., Handler h) {...; mHandler = h;} // constructor with handler param
    public void setHandler(Handler h) { mHandler = h; } // for handler swapping after config change
    public MyDataObject getDataObject() { return mDataObject; } // return data object (completed) to caller
    public void run() {
        mDataObject = new MyDataObject();
        // do the lengthy task to fill mDataObject with data
        lengthyTask(mDataObject);
        // done. notify activity
        mHandler.sendEmptyMessage(0); // tell activity: i'm ready. come pick up the data.
    }
}

C'est ce qui fonctionne pour moi. Je ne sais pas si c'est la méthode "correcte" telle que conçue par Android - ils prétendent que "détruire / recréer l'activité pendant la rotation de l'écran" rend les choses plus faciles, donc je suppose que ça ne devrait pas être trop compliqué.

Faites-moi savoir si vous voyez un problème dans mon code. Comme dit plus haut, je ne sais pas vraiment s'il y a un effet secondaire.




Tried to implement jfelectron 's solution because it is a " rock-solid solution to these issues that conforms with the 'Android Way' of things " but it took some time to look up and put together all the elements mentioned. Ended up with this slightly different, and I think more elegant, solution posted here in it's entirety.

Uses an IntentService fired from an activity to perform the long running task on a separate thread. The service fires back sticky Broadcast Intents to the activity which update the dialog. The Activity uses showDialog(), onCreateDialog() and onPrepareDialog() to eliminate the need to have persistent data passed in the application object or the savedInstanceState bundle. This should work no matter how your application is interrupted.

Activity Class:

public class TesterActivity extends Activity {
private ProgressDialog mProgressDialog;
private static final int PROGRESS_DIALOG = 0;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    Button b = (Button) this.findViewById(R.id.test_button);
    b.setOnClickListener(new OnClickListener() {
        public void onClick(View v) {
            buttonClick();
        }
    });
}

private void buttonClick(){
    clearPriorBroadcast();
    showDialog(PROGRESS_DIALOG);
    Intent svc = new Intent(this, MyService.class);
    startService(svc);
}

protected Dialog onCreateDialog(int id) {
    switch(id) {
    case PROGRESS_DIALOG:
        mProgressDialog = new ProgressDialog(TesterActivity.this);
        mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
        mProgressDialog.setMax(MyService.MAX_COUNTER);
        mProgressDialog.setMessage("Processing...");
        return mProgressDialog;
    default:
        return null;
    }
}

@Override
protected void onPrepareDialog(int id, Dialog dialog) {
    switch(id) {
    case PROGRESS_DIALOG:
        // setup a broadcast receiver to receive update events from the long running process
        IntentFilter filter = new IntentFilter();
        filter.addAction(MyService.BG_PROCESS_INTENT);
        registerReceiver(new MyBroadcastReceiver(), filter);
        break;
    }
}

public class MyBroadcastReceiver extends BroadcastReceiver{
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.hasExtra(MyService.KEY_COUNTER)){
            int count = intent.getIntExtra(MyService.KEY_COUNTER, 0);
            mProgressDialog.setProgress(count);
            if (count >= MyService.MAX_COUNTER){
                dismissDialog(PROGRESS_DIALOG);
            }
        }
    }
}

/*
 * Sticky broadcasts persist and any prior broadcast will trigger in the 
 * broadcast receiver as soon as it is registered.
 * To clear any prior broadcast this code sends a blank broadcast to clear 
 * the last sticky broadcast.
 * This broadcast has no extras it will be ignored in the broadcast receiver 
 * setup in onPrepareDialog()
 */
private void clearPriorBroadcast(){
    Intent broadcastIntent = new Intent();
    broadcastIntent.setAction(MyService.BG_PROCESS_INTENT);
    sendStickyBroadcast(broadcastIntent);
}}

IntentService Class:

public class MyService extends IntentService {

public static final String BG_PROCESS_INTENT = "com.mindspiker.Tester.MyService.TEST";
public static final String KEY_COUNTER = "counter";
public static final int MAX_COUNTER = 100;

public MyService() {
  super("");
}

@Override
protected void onHandleIntent(Intent intent) {
    for (int i = 0; i <= MAX_COUNTER; i++) {
        Log.e("Service Example", " " + i);
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        Intent broadcastIntent = new Intent();
        broadcastIntent.setAction(BG_PROCESS_INTENT);
        broadcastIntent.putExtra(KEY_COUNTER, i);
        sendStickyBroadcast(broadcastIntent);
    }
}}

Manifest file entries:

before application section:

uses-permission android:name="com.mindspiker.Tester.MyService.TEST"
uses-permission android:name="android.permission.BROADCAST_STICKY"

inside application section

service android:name=".MyService"



If you're struggling with detecting orientation change events of a dialog INDEPENDENT OF AN ACTIVITY REFERENCE , this method works excitingly well. I use this because I have my own dialog class that can be shown in multiple different Activities so I don't always know which Activity it's being shown in. With this method you don't need to change the AndroidManifest, worry about Activity references, and you don't need a custom dialog (as I have). You do need, however, a custom content view so you can detect the orientation changes using that particular view. Voici mon exemple:

Installer

public class MyContentView extends View{
    public MyContentView(Context context){
        super(context);
    }

    @Override
    public void onConfigurationChanged(Configuration newConfig){
        super.onConfigurationChanged(newConfig);

        //DO SOMETHING HERE!! :D
    }
}

Implementation 1 - Dialog

Dialog dialog = new Dialog(context);
//set up dialog
dialog.setContentView(new MyContentView(context));
dialog.show();

Implementation 2 - AlertDialog.Builder

AlertDialog.Builder builder = new AlertDialog.Builder(context);
//set up dialog builder
builder.setView(new MyContentView(context));        //Can use this method
builder.setCustomTitle(new MycontentView(context)); // or this method
builder.build().show();

Implementation 3 - ProgressDialog / AlertDialog

ProgressDialog progress = new ProgressDialog(context);
//set up progress dialog
progress.setView(new MyContentView(context));        //Can use this method
progress.setCustomTitle(new MyContentView(context)); // or this method
progress.show();



I faced the same situation. What I did was get only one instance for my progress dialog in the entire application.

First, I created a DialogSingleton class to get only one instance (Singleton pattern)

public class DialogSingleton
{
    private static Dialog dialog;

    private static final Object mLock = new Object();
    private static DialogSingleton instance;

    private DialogSingleton()
    {

    }

    public static DialogSingleton GetInstance()
    {
        synchronized (mLock)
        {
            if(instance == null)
            {
                instance = new DialogSingleton();
            }

            return instance;
        }
    }

    public void DialogShow(Context context, String title)
    {
        if(!((Activity)context).isFinishing())
        {
            dialog = new ProgressDialog(context, 2);

            dialog.setCanceledOnTouchOutside(false);

            dialog.setTitle(title);

            dialog.show();
        }
    }

    public void DialogDismiss(Context context)
    {
        if(!((Activity)context).isFinishing() && dialog.isShowing())
        {
            dialog.dismiss();
        }
    }
}

As I show in this class, I have the progress dialog as attribute. Every time I need to show a progress dialog, I get the unique instance and create a new ProgressDialog.

DialogSingleton.GetInstance().DialogShow(this, "My title here!");

When I am done with the background task, I call again the unique instance and dismiss its dialog.

DialogSingleton.GetInstance().DialogDismiss(this);

I save the background task status in my shared preferences. When I rotate the screen, I ask if I have a task running for this activity: (onCreate)

if(Boolean.parseBoolean(preference.GetValue(IS_TASK_NAME_EXECUTED_KEY, "boolean").toString()))
{
    DialogSingleton.GetInstance().DialogShow(this, "Checking credentials!");
} // preference object gets the info from shared preferences (my own implementation to get and put data to shared preferences) and IS_TASK_NAME_EXECUTED_KEY is the key to save this flag (flag to know if this activity has a background task already running).

When I start running a background task:

preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, true, "boolean");

DialogSingleton.GetInstance().DialogShow(this, "My title here!");

When I finish running a background task:

preference.AddValue(IS_TASK_NAME_EXECUTED_KEY, false, "boolean");

DialogSingleton.GetInstance().DialogDismiss(ActivityName.this);

J'espère que ça aide.




I have an implementation which allows the activity to be destroyed on a screen orientation change, but still destroys the dialog in the recreated activity successfully. I use ...NonConfigurationInstance to attach the background task to the recreated activity. The normal Android framework handles recreating the dialog itself, nothing is changed there.

I subclassed AsyncTask adding a field for the 'owning' activity, and a method to update this owner.

class MyBackgroundTask extends AsyncTask<...> {
  MyBackgroundTask (Activity a, ...) {
    super();
    this.ownerActivity = a;
  }

  public void attach(Activity a) {
    ownerActivity = a;
  }

  protected void onPostExecute(Integer result) {
    super.onPostExecute(result);
    ownerActivity.dismissDialog(DIALOG_PROGRESS);
  }

  ...
}

In my activity class I added a field backgroundTask referring to the 'owned' backgroundtask, and I update this field using onRetainNonConfigurationInstance and getLastNonConfigurationInstance .

class MyActivity extends Activity {
  public void onCreate(Bundle savedInstanceState) {
    ...
    if (getLastNonConfigurationInstance() != null) {
      backgroundTask = (MyBackgroundTask) getLastNonConfigurationInstance();
      backgroundTask.attach(this);
    }
  }

  void startBackgroundTask() {
    backgroundTask = new MyBackgroundTask(this, ...);
    showDialog(DIALOG_PROGRESS);
    backgroundTask.execute(...);
  }

  public Object onRetainNonConfigurationInstance() {
    if (backgroundTask != null && backgroundTask.getStatus() != Status.FINISHED)
      return backgroundTask;
    return null;
  }
  ...
}

Suggestions for further improvement:

  • Clear the backgroundTask reference in the activity after the task is finished to release any memory or other resources associated with it.
  • Clear the ownerActivity reference in the backgroundtask before the activity is destroyed in case it will not be recreated immediately.
  • Create a BackgroundTask interface and/or collection to allow different types of tasks to run from the same owning activity.



J'ai découvert une solution à cela que je n'ai pas encore vue ailleurs. Vous pouvez utiliser un objet d'application personnalisé qui sait si vous avez des tâches en arrière-plan, au lieu d'essayer de le faire dans l'activité qui est détruite et recréée lors du changement d'orientation. J'ai blogué à ce sujet here .




Déplacez la tâche longue vers une classe séparée. Implémentez-le comme un modèle sujet-observateur. Chaque fois que l'activité est créée, enregistrez-la et en fermant la désinscription avec la classe de tâches. La classe de tâche peut utiliser AsyncTask.




I've tried EVERYTHING. Spent days experimenting. I didn't want to block the activity from rotating. My scenario was:

  1. A progress dialog showing dynamic information to the user. Eg: "Connecting to server...", "Downloading data...", etc.
  2. A thread doing the heavy stuff and updating the dialog
  3. Updating the UI with the results at the end.

The problem was, when rotating the screen, every solution on the book failed. Even with the AsyncTask class, which is the correct Android way of dealing with this situations. When rotating the screen, the current Context that the starting thread is working with, is gone, and that messes up with the dialog that is showing. The problem was always the Dialog, no matter how many tricks I added to the code (passing new contexts to running threads, retaining thread states through rotations, etc...). The code complexity at the end was always huge and there was always something that could go wrong.

The only solution that worked for me was the Activity/Dialog trick. It's simple and genius and it's all rotation proof:

  1. Instead of creating a Dialog and ask to show it, create an Activity that has been set in the manifest with android:theme="@android:style/Theme.Dialog". So, it just looks like a dialog.

  2. Replace showDialog(DIALOG_ID) with startActivityForResult(yourActivityDialog, yourCode);

  3. Use onActivityResult in the calling Activity to get the results from the executing thread (even the errors) and update the UI.

  4. On your 'ActivityDialog', use threads or AsyncTask to execute long tasks and onRetainNonConfigurationInstance to save "dialog" state when rotating the screen.

This is fast and works fine. I still use dialogs for other tasks and the AsyncTask for something that doesn't require a constant dialog on screen. But with this scenario, I always go for the Activity/Dialog pattern.

And, I didn't try it, but it's even possible to block that Activity/Dialog from rotating, when the thread is running, speeding things up, while allowing the calling Activity to rotate.




Le problème perçu original était que le code ne survivrait pas à un changement d'orientation de l'écran. Apparemment, cela a été "résolu" en faisant en sorte que le programme gère le changement d'orientation de l'écran lui-même, au lieu de laisser le framework UI le faire (en appelant onDestroy)).

Je soumets que si le problème sous-jacent est que le programme ne survivra pas onDestroy (), alors la solution acceptée est juste une solution de contournement qui laisse le programme avec d'autres problèmes sérieux et des vulnérabilités. Rappelez-vous que le cadre Android précise que votre activité risque d'être détruite presque à tout moment en raison de circonstances indépendantes de votre volonté. Par conséquent, votre activité doit pouvoir survivre onDestroy () et subséquente onCreate () pour une raison quelconque, et pas seulement un changement d'orientation de l'écran.

Si vous acceptez de manipuler vous-même l'orientation de l'écran pour résoudre le problème de l'OP, vous devez vérifier que les autres causes de onDestroy () ne provoquent pas la même erreur. Es-tu capable de faire ça? Sinon, je me demande si la réponse «acceptée» est vraiment très bonne.




Je l'ai fait comme ceci:

    package com.palewar;
    import android.app.Activity;
    import android.app.ProgressDialog;
    import android.os.Bundle;
    import android.os.Handler;
    import android.os.Message;

    public class ThreadActivity extends Activity {


        static ProgressDialog dialog;
        private Thread downloadThread;
        final static Handler handler = new Handler() {

            @Override
            public void handleMessage(Message msg) {

                super.handleMessage(msg);

                dialog.dismiss();

            }

        };

        protected void onDestroy() {
    super.onDestroy();
            if (dialog != null && dialog.isShowing()) {
                dialog.dismiss();
                dialog = null;
            }

        }

        /** Called when the activity is first created. */
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);

            downloadThread = (Thread) getLastNonConfigurationInstance();
            if (downloadThread != null && downloadThread.isAlive()) {
                dialog = ProgressDialog.show(ThreadActivity.this, "",
                        "Signing in...", false);
            }

            dialog = ProgressDialog.show(ThreadActivity.this, "",
                    "Signing in ...", false);

            downloadThread = new MyThread();
            downloadThread.start();
            // processThread();
        }

        // Save the thread
        @Override
        public Object onRetainNonConfigurationInstance() {
            return downloadThread;
        }


        static public class MyThread extends Thread {
            @Override
            public void run() {

                try {
                    // Simulate a slow network
                    try {
                        new Thread().sleep(5000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    handler.sendEmptyMessage(0);

                } finally {

                }
            }
        }

    }

Vous pouvez également essayer et laissez-moi savoir que cela fonctionne pour vous ou non




I am a fresher in android and I tried this and it's worked.

public class loadTotalMemberByBranch extends AsyncTask<Void, Void,Void> {
        ProgressDialog progressDialog = new ProgressDialog(Login.this);
        int ranSucess=0;
        @Override
        protected void onPreExecute() {
            // TODO Auto-generated method stub
            super.onPreExecute();
            progressDialog.setTitle("");    
            progressDialog.isIndeterminate();
            progressDialog.setCancelable(false);
            progressDialog.show();
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);

        }
        @Override
        protected Void doInBackground(Void... params) {
            // TODO Auto-generated method stub

            return null;
        }
        @Override
        protected void onPostExecute(Void result) {
            // TODO Auto-generated method stub
            super.onPostExecute(result);
            progressDialog.dismiss();
            setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
        }
}



Related