From ffeef4f160729eefdaec966b75a8baed19c50a63 Mon Sep 17 00:00:00 2001 From: Joshua Quick Date: Thu, 22 Aug 2019 11:41:56 -0700 Subject: [PATCH] fix(android): ProgressIndicator dialog handling (#11143) * [TIMOB-27104] ProgressIndicator logs "WindowLeaked" exception when hiding dialog and closing window at same time as of 8.0.2 * [TIMOB-27308] Determinant ProgressIndicator ignores "value" property before shown * [TIMOB-27309] Cannot re-show ProgressIndicator dialog if auto-closed by previous window * Removed dialog's handleMessage() related code. No longer needed since JS runs on main UI thread. * added unit test for Ti.UI.Android.ProgressIndicator Fixes TIMOB-27104, TIMOB-27308, TIMOB-27309 --- .../ui/widget/TiUIProgressIndicator.java | 139 ++++++------ ....ui.android.progressindicator.addontest.js | 211 ++++++++++++++++++ 2 files changed, 282 insertions(+), 68 deletions(-) create mode 100644 tests/Resources/ti.ui.android.progressindicator.addontest.js diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIProgressIndicator.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIProgressIndicator.java index 22df4607920..a486a808a5e 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIProgressIndicator.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/TiUIProgressIndicator.java @@ -22,26 +22,18 @@ import android.app.Activity; import android.app.ProgressDialog; import android.content.DialogInterface; -import android.os.Handler; -import android.os.Looper; -import android.os.Message; -public class TiUIProgressIndicator extends TiUIView implements Handler.Callback, DialogInterface.OnCancelListener +public class TiUIProgressIndicator + extends TiUIView implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { private static final String TAG = "TiUIProgressDialog"; - private static final int MSG_SHOW = 100; - private static final int MSG_PROGRESS = 101; - private static final int MSG_HIDE = 102; - public static final int INDETERMINANT = 0; public static final int DETERMINANT = 1; public static final int STATUS_BAR = 0; public static final int DIALOG = 1; - protected Handler handler; - protected boolean visible; protected ProgressDialog progressDialog; protected CharSequence statusBarTitle; @@ -55,32 +47,6 @@ public TiUIProgressIndicator(TiViewProxy proxy) { super(proxy); Log.d(TAG, "Creating an progress indicator", Log.DEBUG_MODE); - handler = new Handler(Looper.getMainLooper(), this); - } - - public boolean handleMessage(Message msg) - { - switch (msg.what) { - case MSG_SHOW: { - handleShow(); - return true; - } - case MSG_PROGRESS: { - if (progressDialog != null) { - progressDialog.setProgress(msg.arg1); - } else { - Activity parent = (Activity) this.proxy.getActivity(); - parent.setProgress(msg.arg1); - } - return true; - } - case MSG_HIDE: { - handleHide(); - return true; - } - } - - return false; } @Override @@ -109,10 +75,15 @@ public void propertyChanged(String key, Object oldValue, Object newValue, KrollP } else if (key.equals(TiC.PROPERTY_VALUE)) { if (visible) { - int value = TiConvert.toInt(newValue); - int thePos = (value - min) * incrementFactor; - - handler.obtainMessage(MSG_PROGRESS, thePos, -1).sendToTarget(); + int progressValue = (TiConvert.toInt(newValue, 0) - this.min) * this.incrementFactor; + if (this.progressDialog != null) { + this.progressDialog.setProgress(progressValue); + } else { + Activity activity = (Activity) this.proxy.getActivity(); + if (activity != null) { + activity.setProgress(progressValue); + } + } } } else if (key.equals(TiC.PROPERTY_CANCELABLE)) { @@ -173,6 +144,8 @@ protected void handleShow() type = TiConvert.toInt(proxy.getProperty(TiC.PROPERTY_TYPE)); } + int progressValue = TiConvert.toInt(proxy.getProperty(TiC.PROPERTY_VALUE), 0); + if (location == STATUS_BAR) { incrementFactor = 10000 / (max - min); Activity parent = (Activity) proxy.getActivity(); @@ -189,13 +162,25 @@ protected void handleShow() parent.setProgressBarIndeterminate(false); parent.setProgressBarIndeterminateVisibility(false); parent.setProgressBarVisibility(true); + parent.setProgress((progressValue - this.min) * this.incrementFactor); statusBarTitle = parent.getTitle(); parent.setTitle(message); } else { Log.w(TAG, "Unknown type: " + type); + return; } } else if (location == DIALOG) { incrementFactor = 1; + + // If existing dialog references a destroyed activity, then drop reference to dialog. + if (progressDialog != null) { + Activity activity = progressDialog.getOwnerActivity(); + if ((activity == null) || activity.isFinishing() || activity.isDestroyed()) { + progressDialog = null; + } + } + + // Create progress dialog if not done already. if (progressDialog == null) { Activity a = TiApplication.getInstance().getCurrentActivity(); if ((a == null) || a.isFinishing() || a.isDestroyed()) { @@ -210,81 +195,99 @@ protected void handleShow() progressDialog.setOwnerActivity(a); } progressDialog.setOnCancelListener(this); + progressDialog.setOnDismissListener(this); } + // Set up dialog. + // Note: We must call setCanceledOnTouchOutside() before setCancelable(). progressDialog.setMessage(message); - // setCanceledOnTouchOutside() overrides the value of setCancelable(), so order of execution matters. progressDialog.setCanceledOnTouchOutside( proxy.getProperties().optBoolean(TiC.PROPERTY_CANCELED_ON_TOUCH_OUTSIDE, false)); progressDialog.setCancelable(proxy.getProperties().optBoolean(TiC.PROPERTY_CANCELABLE, false)); - if (type == INDETERMINANT) { progressDialog.setIndeterminate(true); } else if (type == DETERMINANT) { progressDialog.setIndeterminate(false); progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - if (min != 0) { - progressDialog.setMax(max - min); // no min setting so shift - } else { - progressDialog.setMax(max); - } - progressDialog.setProgress(0); + progressDialog.setMax(this.max - this.min); } else { Log.w(TAG, "Unknown type: " + type); } + + // Show the dialog. + // Note: The setProgress() method only works after the dialog is shown. try { progressDialog.show(); + if (type == DETERMINANT) { + progressDialog.setProgress(progressValue - this.min); + } } catch (Exception ex) { Log.e(TAG, "Failed to show progress indicator dialog.", ex); + return; } } else { Log.w(TAG, "Unknown location: " + location); + return; } + + // Flag progress indicator as shown. visible = true; } public void hide(KrollDict options) { - if (!visible) { - return; - } - handler.sendEmptyMessage(MSG_HIDE); + handleHide(); } protected void handleHide() { - if (progressDialog != null) { - Activity ownerActivity = progressDialog.getOwnerActivity(); - if (ownerActivity instanceof TiBaseActivity) { - ((TiBaseActivity) ownerActivity).removeDialog(progressDialog); - if (!ownerActivity.isFinishing() && !ownerActivity.isDestroyed()) { - try { - progressDialog.dismiss(); - } catch (Exception ex) { - Log.e(TAG, "Failed to hide ProgressIndicator dialog.", ex); + if (!this.visible) { + return; + } + + if (this.location == DIALOG) { + if (this.progressDialog != null) { + Activity ownerActivity = this.progressDialog.getOwnerActivity(); + if (ownerActivity instanceof TiBaseActivity) { + ((TiBaseActivity) ownerActivity).removeDialog(progressDialog); + if (!ownerActivity.isFinishing() && !ownerActivity.isDestroyed()) { + try { + this.progressDialog.dismiss(); + } catch (Exception ex) { + Log.e(TAG, "Failed to hide ProgressIndicator dialog.", ex); + } } } + this.progressDialog = null; } - progressDialog = null; - } else { + } else if (this.location == STATUS_BAR) { Activity parent = proxy.getActivity(); if (parent != null) { parent.setProgressBarIndeterminate(false); parent.setProgressBarIndeterminateVisibility(false); parent.setProgressBarVisibility(false); - if (visible) { - parent.setTitle(statusBarTitle); + if (this.visible) { + parent.setTitle(this.statusBarTitle); } } - statusBarTitle = null; + this.statusBarTitle = null; } - visible = false; + + this.visible = false; } @Override public void onCancel(DialogInterface dialog) { - visible = false; + this.visible = false; + this.progressDialog = null; fireEvent(TiC.EVENT_CANCEL, null); } + + @Override + public void onDismiss(DialogInterface dialog) + { + this.visible = false; + this.progressDialog = null; + } } diff --git a/tests/Resources/ti.ui.android.progressindicator.addontest.js b/tests/Resources/ti.ui.android.progressindicator.addontest.js new file mode 100644 index 00000000000..46ad404a857 --- /dev/null +++ b/tests/Resources/ti.ui.android.progressindicator.addontest.js @@ -0,0 +1,211 @@ +/* + * Appcelerator Titanium Mobile + * Copyright (c) 2019 by Axway, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ +/* eslint-env mocha */ +/* global Titanium */ +/* eslint no-unused-expressions: "off" */ +'use strict'; +const should = require('./utilities/assertions'); + +describe.android('Titanium.UI.Android.ProgressIndicator', function () { + this.timeout(5000); + + it('apiName', () => { + const progressIndicator = Ti.UI.Android.createProgressIndicator(); + should(progressIndicator).have.readOnlyProperty('apiName').which.is.a.String; + should(progressIndicator.apiName).be.eql('Ti.UI.Android.ProgressIndicator'); + }); + + it('dialog indeterminant', (finish) => { + const INITIAL_MESSAGE = 'Initial message...'; + const progressIndicator = Ti.UI.Android.createProgressIndicator({ + message: INITIAL_MESSAGE, + location: Ti.UI.Android.PROGRESS_INDICATOR_DIALOG, + type: Ti.UI.Android.PROGRESS_INDICATOR_INDETERMINANT + }); + should(progressIndicator.message).be.eql(INITIAL_MESSAGE); + should(progressIndicator.location).be.eql(Ti.UI.Android.PROGRESS_INDICATOR_DIALOG); + should(progressIndicator.type).be.eql(Ti.UI.Android.PROGRESS_INDICATOR_INDETERMINANT); + progressIndicator.show(); + setTimeout(() => { + const UPDATED_MESSAGE = 'Updated message...'; + progressIndicator.message = UPDATED_MESSAGE; + should(progressIndicator.message).be.eql(UPDATED_MESSAGE); + setTimeout(() => { + progressIndicator.hide(); + finish(); + }, 10); + }, 10); + }); + + it('dialog determinant - min: 0, max: 100', (finish) => { + const INITIAL_MESSAGE = 'Initial message...'; + const progressIndicator = Ti.UI.Android.createProgressIndicator({ + message: INITIAL_MESSAGE, + location: Ti.UI.Android.PROGRESS_INDICATOR_DIALOG, + type: Ti.UI.Android.PROGRESS_INDICATOR_DETERMINANT, + min: 0, + max: 100, + value: 0 + }); + should(progressIndicator.message).be.eql(INITIAL_MESSAGE); + should(progressIndicator.location).be.eql(Ti.UI.Android.PROGRESS_INDICATOR_DIALOG); + should(progressIndicator.type).be.eql(Ti.UI.Android.PROGRESS_INDICATOR_DETERMINANT); + should(progressIndicator.min).be.eql(0); + should(progressIndicator.max).be.eql(100); + should(progressIndicator.value).be.eql(0); + progressIndicator.show(); + setTimeout(() => { + const UPDATED_MESSAGE = 'Updated message...'; + progressIndicator.message = UPDATED_MESSAGE; + progressIndicator.value = 50; + should(progressIndicator.message).be.eql(UPDATED_MESSAGE); + should(progressIndicator.value).be.eql(50); + setTimeout(() => { + progressIndicator.value = 100; + should(progressIndicator.value).be.eql(100); + setTimeout(() => { + progressIndicator.hide(); + finish(); + }, 10); + }, 10); + }, 10); + }); + + it('dialog determinant - min: 50, max: 100', (finish) => { + const progressIndicator = Ti.UI.Android.createProgressIndicator({ + message: 'Progressing...', + location: Ti.UI.Android.PROGRESS_INDICATOR_DIALOG, + type: Ti.UI.Android.PROGRESS_INDICATOR_DETERMINANT, + min: 50, + max: 100, + value: 50 + }); + should(progressIndicator.min).be.eql(50); + should(progressIndicator.max).be.eql(100); + should(progressIndicator.value).be.eql(50); + progressIndicator.show(); + setTimeout(() => { + progressIndicator.value = 75; + should(progressIndicator.value).be.eql(75); + setTimeout(() => { + progressIndicator.value = 100; + should(progressIndicator.value).be.eql(100); + setTimeout(() => { + progressIndicator.hide(); + finish(); + }, 10); + }, 10); + }, 10); + }); + + it('dialog determinant - exceed min/max', (finish) => { + const progressIndicator = Ti.UI.Android.createProgressIndicator({ + message: 'Progressing...', + location: Ti.UI.Android.PROGRESS_INDICATOR_DIALOG, + type: Ti.UI.Android.PROGRESS_INDICATOR_DETERMINANT, + min: 0, + max: 100, + }); + progressIndicator.show(); + setTimeout(() => { + try { + progressIndicator.value = -50; + progressIndicator.value = 150; + progressIndicator.hide(); + finish(); + } catch (err) { + finish(err); + } + }, 10); + }); + + it('dialog indeterminant - show twice', function (finish) { + const progressIndicator = Ti.UI.Android.createProgressIndicator({ + message: 'Progressing...', + location: Ti.UI.Android.PROGRESS_INDICATOR_DIALOG, + type: Ti.UI.Android.PROGRESS_INDICATOR_INDETERMINANT + }); + progressIndicator.show(); + setTimeout(() => { + progressIndicator.hide(); + setTimeout(() => { + const UPDATED_MESSAGE = 'Updated message...'; + progressIndicator.message = UPDATED_MESSAGE; + progressIndicator.show(); + setTimeout(() => { + should(progressIndicator.message).be.eql(UPDATED_MESSAGE); + progressIndicator.hide(); + finish(); + }, 10); + }, 10); + }, 10); + }); + + it('dialog indeterminant - show in different windows', function (finish) { + const progressIndicator = Ti.UI.Android.createProgressIndicator({ + message: 'Progressing...', + location: Ti.UI.Android.PROGRESS_INDICATOR_DIALOG, + type: Ti.UI.Android.PROGRESS_INDICATOR_INDETERMINANT + }); + const window1 = Ti.UI.createWindow(); + window1.addEventListener('open', () => { + progressIndicator.show(); + setTimeout(() => { + progressIndicator.hide(); + window1.close(); + }, 10); + }); + window1.addEventListener('close', () => { + const window2 = Ti.UI.createWindow(); + window2.addEventListener('open', () => { + progressIndicator.show(); + setTimeout(() => { + progressIndicator.hide(); + window2.close(); + }, 10); + }); + window2.addEventListener('close', () => { + finish(); + }); + window2.open(); + }); + window1.open(); + }); + + it('dialog indeterminant - show/hide back-to-back', function () { + const progressIndicator = Ti.UI.Android.createProgressIndicator({ + message: 'Progressing...', + location: Ti.UI.Android.PROGRESS_INDICATOR_DIALOG, + type: Ti.UI.Android.PROGRESS_INDICATOR_INDETERMINANT + }); + progressIndicator.show(); + progressIndicator.hide(); + }); + + it('dialog indeterminant - show while already shown', function () { + const progressIndicator = Ti.UI.Android.createProgressIndicator({ + message: 'Progressing...', + location: Ti.UI.Android.PROGRESS_INDICATOR_DIALOG, + type: Ti.UI.Android.PROGRESS_INDICATOR_INDETERMINANT + }); + progressIndicator.show(); + progressIndicator.show(); // <- Should do nothing. + progressIndicator.hide(); + }); + + it('dialog indeterminant - hide while already hidden', function () { + const progressIndicator = Ti.UI.Android.createProgressIndicator({ + message: 'Progressing...', + location: Ti.UI.Android.PROGRESS_INDICATOR_DIALOG, + type: Ti.UI.Android.PROGRESS_INDICATOR_INDETERMINANT + }); + progressIndicator.hide(); // <- Should do nothing. + progressIndicator.show(); + progressIndicator.hide(); + progressIndicator.hide(); // <- Should do nothing. + }); +});