Как справиться с изменением ориентации экрана при активном диалоге прогресса и фоновом потоке? - PullRequest
505 голосов
/ 11 июля 2009

Моя программа выполняет сетевую активность в фоновом потоке. Перед началом работы появляется диалоговое окно прогресса. Диалог закрывается на обработчике. Это все работает нормально, за исключением случаев, когда ориентация экрана изменяется, когда диалоговое окно открыто (и фоновая нить идет). В этот момент приложение либо вылетает, либо блокируется, либо попадает в странную стадию, когда приложение вообще не работает, пока все потоки не будут уничтожены.

Как можно изящно справиться с изменением ориентации экрана?

Пример кода ниже примерно соответствует тому, что делает моя настоящая программа:

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

Stack:

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)

Я пытался закрыть диалоговое окно прогресса в onSaveInstanceState, но это только предотвращает немедленный сбой. Фоновый поток все еще продолжается, а пользовательский интерфейс находится в частично отрисованном состоянии. Необходимо убить все приложение, прежде чем оно снова заработает.

Ответы [ 26 ]

4 голосов
/ 24 марта 2012

Я сделал это так:

    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 {

                }
            }
        }

    }

Вы также можете попробовать сообщить мне, работает это у вас или нет

4 голосов
/ 11 июля 2009

Переместить длинное задание в отдельный класс. Реализуйте его как шаблон субъекта-наблюдателя. Каждый раз, когда создается действие, регистрируйтесь, а при закрытии отменяйте регистрацию в классе задач. Класс задач может использовать AsyncTask.

2 голосов
/ 29 апреля 2011

У меня есть реализация, которая позволяет уничтожить действие при изменении ориентации экрана, но все еще успешно уничтожает диалог в воссозданном действии. Я использую ...NonConfigurationInstance, чтобы прикрепить фоновое задание к воссозданному действию. Обычная платформа Android обрабатывает воссоздание самого диалога, там ничего не меняется.

Я вложил в подкласс AsyncTask, добавив поле для «собственного» действия и метод для обновления этого владельца.

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

  ...
}

В своем классе деятельности я добавил поле backgroundTask, относящееся к фоновой задаче «в собственности», и обновляю это поле, используя onRetainNonConfigurationInstance и 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;
  }
  ...
}

Предложения по дальнейшему улучшению:

  • Очистите ссылку backgroundTask в действии после завершения задачи, чтобы освободить любую память или другие ресурсы, связанные с ней.
  • Очистите ссылку ownerActivity в фоновой задаче перед тем, как действие будет уничтожено, если оно не будет воссоздано немедленно.
  • Создайте BackgroundTask интерфейс и / или коллекцию, позволяющую запускать разные типы задач из одного и того же владельца.
2 голосов
/ 26 декабря 2010

Если вы создаете фон Service, который выполняет всю тяжелую работу (запросы / ответы tcp, демаршаллинг), View и Activity могут быть уничтожены и воссозданы без утечки окна или потерять данные. Это позволяет поведение, рекомендованное Android, которое заключается в уничтожении активности при каждом изменении конфигурации (например, при каждом изменении ориентации).

Это немного сложнее, но это лучший способ для вызова запроса к серверу, предварительной / пост-обработки данных и т. Д.

Вы даже можете использовать свой Service для постановки каждого запроса в очередь на сервер, поэтому с ним легко и эффективно справляться.

В руководстве разработчика есть полная глава , посвященная Services.

2 голосов
/ 10 ноября 2011

Пытался внедрить решение jfelectron , потому что это « надежное решение этих проблем, которое соответствует« способу Android »вещей », но заняло некоторое время посмотреть и собрать все упомянутые элементы. Закончилось этим немного по-другому, и я думаю, что более элегантное решение выложено здесь полностью.

Использует IntentService, запускаемый из действия, для выполнения длительной задачи в отдельном потоке. Служба запускает липкие Broadcast Intents для действия, которое обновляет диалог. Упражнение использует showDialog (), onCreateDialog () и onPrepareDialog (), чтобы исключить необходимость передачи постоянных данных в объект приложения или в комплект сохраненного файла. Это должно работать независимо от того, как ваше приложение прерывается.

Класс деятельности:

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:

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

Записи файла манифеста:

перед разделом приложения:

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

внутри раздела приложения

service android:name=".MyService"
2 голосов
/ 03 августа 2011

Если вы поддерживаете два макета, весь поток пользовательского интерфейса должен быть прерван.

Если вы используете AsynTask, то вы можете легко вызвать .cancel() метод внутри onDestroy() метода текущей активности.

@Override
protected void onDestroy (){
    removeDialog(DIALOG_LOGIN_ID); // remove loading dialog
    if (loginTask != null){
        if (loginTask.getStatus() != AsyncTask.Status.FINISHED)
            loginTask.cancel(true); //cancel AsyncTask
    }
    super.onDestroy();
}

Для AsyncTask читайте больше в разделе «Отмена задачи» на здесь .

Обновление: Добавлено условие для проверки статуса, так как его можно отменить, только если он находится в рабочем состоянии. Также обратите внимание, что AsyncTask может быть выполнен только один раз.

2 голосов
/ 26 июня 2014

Это мое предлагаемое решение:

  • Переместите AsyncTask или Thread в оставшийся фрагмент, как объяснено здесь . Я считаю хорошей практикой переводить все сетевые вызовы во фрагменты. Если вы уже используете фрагменты, один из них может быть привлечен к ответственности за звонки. В противном случае вы можете создать фрагмент только для выполнения запроса, как предполагает связанная статья.
  • Фрагмент будет использовать интерфейс слушателя, чтобы сигнализировать о завершении / сбое задачи. Вам не нужно беспокоиться об изменении ориентации там. Фрагмент всегда будет иметь правильную ссылку на текущее действие, и диалог прогресса можно будет безопасно возобновить.
  • Сделайте ваше диалоговое окно прогресса членом вашего класса. На самом деле вы должны сделать это для всех диалогов. В методе onPause вы должны отклонить их, в противном случае у вас появится окно с изменением конфигурации. Состояние занятости должно сохраняться фрагментом. Когда фрагмент прикреплен к действию, вы можете снова вызвать диалоговое окно прогресса, если вызов все еще выполняется. Для этой цели в интерфейс прослушивателя фрагмент-активности можно добавить метод void showProgressDialog().
1 голос
/ 04 июля 2016

Это очень старый вопрос, который почему-то возник на боковой панели.

Если фоновой задаче необходимо выжить, пока активность находится на переднем плане, «новым» решением является размещение фонового потока (или, предпочтительно, AsyncTask) в сохраненном фрагменте , как описано в этом руководстве разработчика и многочисленных вопросах и ответах .

Сохраненный фрагмент выживает, если действие уничтожается при изменении конфигурации, но не , когда действие уничтожается в фоновом или заднем стеке. Следовательно, фоновая задача все еще должна быть прервана, если isChangingConfigurations() имеет значение false в onPause().

1 голос
/ 29 декабря 2015

Я столкнулся с такой же ситуацией. Я получил только один экземпляр моего диалогового окна прогресса во всем приложении.

Сначала я создал класс DialogSingleton, чтобы получить только один экземпляр (шаблон Singleton)

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

Как я покажу в этом классе, у меня есть диалог прогресса как атрибут. Каждый раз, когда мне нужно показать диалог прогресса, я получаю уникальный экземпляр и создаю новый ProgressDialog.

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

Когда я закончу с фоновой задачей, я снова вызываю уникальный экземпляр и закрываю его диалоговое окно.

DialogSingleton.GetInstance().DialogDismiss(this);

Я сохраняю статус фоновой задачи в общих настройках. Когда я поворачиваю экран, я спрашиваю, запущено ли у меня задание для этого действия: (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).

Когда я запускаю фоновое задание:

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

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

Когда я закончу запуск фоновой задачи:

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

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

Надеюсь, это поможет.

1 голос
/ 08 июля 2015

Я более свежий в Android, и я попробовал это, и это сработало.

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);
        }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...