Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 83fe3f55 authored by Dianne Hackborn's avatar Dianne Hackborn
Browse files

Last big work on #1991910: Make swipes work with capacitive keys

This takes care of allowing us to cancel the back button.  The
back button is a bear because it is strewn all over the place --
everywhere you can close something, there is some code looking
for the back button that now needs to deal with being canceled.

The main things changed are activity (of course), dialog,
input method, search dialog.  There are some other misc places
in the framework (and some I missed here that I will get in a
second pass).

To facility all of this, the key dispatching APIs now provide
a lot more support for dealing with looking for cancelled keys,
and incidentally also provide an actual API for catching long
key presses.  This also helped clean up the code in PhoneWindow
where it deals with all of the combinations of key pressed and
releases.  (And also allows people to override
Activity.onKeyLongPress() to provide a different long press
action for a standard key like search.)

And while I was doing this, I reworked how we detect long
presses by having this be part of the key event delivered by
the window manager.  This should greatly reduce (hopefully
outright eliminate) the problems with long presses being
mis-detected when an application is being slow.

Change-Id: Ia19066b8d588d573df3eee6d96e1c90fdc19f57d
parent c2974809
Loading
Loading
Loading
Loading
+317 −1
Original line number Diff line number Diff line
@@ -16089,6 +16089,17 @@
 visibility="public"
>
</method>
<method name="onBackPressed"
 return="void"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
</method>
<method name="onChildTitleChanged"
 return="void"
 abstract="false"
@@ -16318,6 +16329,21 @@
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="onKeyLongPress"
 return="boolean"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="keyCode" type="int">
</parameter>
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="onKeyMultiple"
 return="boolean"
 abstract="false"
@@ -20085,6 +20111,17 @@
 visibility="public"
>
</method>
<method name="onBackPressed"
 return="void"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
</method>
<method name="onContentChanged"
 return="void"
 abstract="false"
@@ -20219,6 +20256,21 @@
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="onKeyLongPress"
 return="boolean"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="keyCode" type="int">
</parameter>
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="onKeyMultiple"
 return="boolean"
 abstract="false"
@@ -69570,6 +69622,17 @@
 visibility="public"
>
</constructor>
<method name="getKeyDispatcherState"
 return="android.view.KeyEvent.DispatcherState"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
</method>
<method name="onBind"
 return="android.os.IBinder"
 abstract="false"
@@ -70315,6 +70378,21 @@
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="onKeyLongPress"
 return="boolean"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="keyCode" type="int">
</parameter>
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="onKeyMultiple"
 return="boolean"
 abstract="false"
@@ -144823,7 +144901,7 @@
 type="android.view.KeyEvent"
 static="false"
 final="false"
 deprecated="not deprecated"
 deprecated="deprecated"
 visibility="public"
>
<parameter name="origEvent" type="android.view.KeyEvent">
@@ -144880,6 +144958,25 @@
<parameter name="newRepeat" type="int">
</parameter>
</method>
<method name="changeTimeRepeat"
 return="android.view.KeyEvent"
 abstract="false"
 native="false"
 synchronized="false"
 static="true"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="event" type="android.view.KeyEvent">
</parameter>
<parameter name="eventTime" type="long">
</parameter>
<parameter name="newRepeat" type="int">
</parameter>
<parameter name="newFlags" type="int">
</parameter>
</method>
<method name="describeContents"
 return="int"
 abstract="false"
@@ -144891,6 +144988,19 @@
 visibility="public"
>
</method>
<method name="dispatch"
 return="boolean"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="true"
 deprecated="deprecated"
 visibility="public"
>
<parameter name="receiver" type="android.view.KeyEvent.Callback">
</parameter>
</method>
<method name="dispatch"
 return="boolean"
 abstract="false"
@@ -144903,6 +145013,10 @@
>
<parameter name="receiver" type="android.view.KeyEvent.Callback">
</parameter>
<parameter name="state" type="android.view.KeyEvent.DispatcherState">
</parameter>
<parameter name="target" type="java.lang.Object">
</parameter>
</method>
<method name="getAction"
 return="int"
@@ -145149,6 +145263,17 @@
 visibility="public"
>
</method>
<method name="isLongPress"
 return="boolean"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</method>
<method name="isModifierKey"
 return="boolean"
 abstract="false"
@@ -145206,6 +145331,28 @@
 visibility="public"
>
</method>
<method name="isTracking"
 return="boolean"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</method>
<method name="startTracking"
 return="void"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</method>
<method name="writeToParcel"
 return="void"
 abstract="false"
@@ -145275,6 +145422,17 @@
 visibility="public"
>
</field>
<field name="FLAG_CANCELED_LONG_PRESS"
 type="int"
 transient="false"
 volatile="false"
 value="256"
 static="true"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</field>
<field name="FLAG_EDITOR_ACTION"
 type="int"
 transient="false"
@@ -145308,6 +145466,17 @@
 visibility="public"
>
</field>
<field name="FLAG_LONG_PRESS"
 type="int"
 transient="false"
 volatile="false"
 value="128"
 static="true"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</field>
<field name="FLAG_SOFT_KEYBOARD"
 type="int"
 transient="false"
@@ -145319,6 +145488,17 @@
 visibility="public"
>
</field>
<field name="FLAG_TRACKING"
 type="int"
 transient="false"
 volatile="false"
 value="512"
 static="true"
 final="true"
 deprecated="not deprecated"
 visibility="public"
>
</field>
<field name="FLAG_VIRTUAL_HARD_KEY"
 type="int"
 transient="false"
@@ -146464,6 +146644,21 @@
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="onKeyLongPress"
 return="boolean"
 abstract="true"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="keyCode" type="int">
</parameter>
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="onKeyMultiple"
 return="boolean"
 abstract="true"
@@ -146497,6 +146692,101 @@
</parameter>
</method>
</interface>
<class name="KeyEvent.DispatcherState"
 extends="java.lang.Object"
 abstract="false"
 static="true"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<constructor name="KeyEvent.DispatcherState"
 type="android.view.KeyEvent.DispatcherState"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
</constructor>
<method name="handleUpEvent"
 return="void"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="isTracking"
 return="boolean"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="performedLongPress"
 return="void"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="reset"
 return="void"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
</method>
<method name="reset"
 return="void"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="target" type="java.lang.Object">
</parameter>
</method>
<method name="startTracking"
 return="void"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="event" type="android.view.KeyEvent">
</parameter>
<parameter name="target" type="java.lang.Object">
</parameter>
</method>
</class>
<class name="LayoutInflater"
 extends="java.lang.Object"
 abstract="true"
@@ -150871,6 +151161,17 @@
 visibility="public"
>
</method>
<method name="getKeyDispatcherState"
 return="android.view.KeyEvent.DispatcherState"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
</method>
<method name="getLayoutParams"
 return="android.view.ViewGroup.LayoutParams"
 abstract="false"
@@ -151997,6 +152298,21 @@
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="onKeyLongPress"
 return="boolean"
 abstract="false"
 native="false"
 synchronized="false"
 static="false"
 final="false"
 deprecated="not deprecated"
 visibility="public"
>
<parameter name="keyCode" type="int">
</parameter>
<parameter name="event" type="android.view.KeyEvent">
</parameter>
</method>
<method name="onKeyMultiple"
 return="boolean"
 abstract="false"
+43 −11
Original line number Diff line number Diff line
@@ -1752,8 +1752,9 @@ public class Activity extends ContextThemeWrapper
     * 
     * <p>If the focused view didn't want this event, this method is called.
     *
     * <p>The default implementation handles KEYCODE_BACK to stop the activity
     * and go back, and other default key handling if configured with {@link #setDefaultKeyMode}.
     * <p>The default implementation sets up state to call
     * {@link #onKeyLongPress}, and does other default key handling
     * if configured with {@link #setDefaultKeyMode}.
     * 
     * @return Return <code>true</code> to prevent this event from being propagated
     * further, or <code>false</code> to indicate that you have not handled 
@@ -1762,16 +1763,19 @@ public class Activity extends ContextThemeWrapper
     * @see android.view.KeyEvent
     */
    public boolean onKeyDown(int keyCode, KeyEvent event)  {
        if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
            finish();
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            event.startTracking();
            return true;
        }
        
        if (mDefaultKeyMode == DEFAULT_KEYS_DISABLE) {
            return false;
        } else if (mDefaultKeyMode == DEFAULT_KEYS_SHORTCUT) {
            return getWindow().performPanelShortcut(Window.FEATURE_OPTIONS_PANEL, 
                                                    keyCode, event, Menu.FLAG_ALWAYS_PERFORM_CLOSE);
            if (getWindow().performPanelShortcut(Window.FEATURE_OPTIONS_PANEL, 
                    keyCode, event, Menu.FLAG_ALWAYS_PERFORM_CLOSE)) {
                return true;
            }
            return false;
        } else {
            // Common code for DEFAULT_KEYS_DIALER & DEFAULT_KEYS_SEARCH_*
            boolean clearSpannable = false;
@@ -1780,8 +1784,8 @@ public class Activity extends ContextThemeWrapper
                clearSpannable = true;
                handled = false;
            } else {
                handled = TextKeyListener.getInstance().onKeyDown(null, mDefaultKeySsb, 
                                                                  keyCode, event);
                handled = TextKeyListener.getInstance().onKeyDown(
                        null, mDefaultKeySsb, keyCode, event);
                if (handled && mDefaultKeySsb.length() > 0) {
                    // something useable has been typed - dispatch it now.

@@ -1812,12 +1816,24 @@ public class Activity extends ContextThemeWrapper
        }
    }

    /**
     * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent)
     * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle
     * the event).
     */
    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
        return false;
    }

    /**
     * Called when a key was released and not handled by any of the views
     * inside of the activity. So, for example, key presses while the cursor 
     * is inside a TextView will not trigger the event (unless it is a navigation
     * to another object) because TextView handles its own key presses.
     * 
     * <p>The default implementation handles KEYCODE_BACK to stop the activity
     * and go back.
     * 
     * @return Return <code>true</code> to prevent this event from being propagated
     * further, or <code>false</code> to indicate that you have not handled 
     * this event and it should continue to be propagated. 
@@ -1825,6 +1841,11 @@ public class Activity extends ContextThemeWrapper
     * @see KeyEvent
     */
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
                && !event.isCanceled()) {
            onBackPressed();
            return true;
        }
        return false;
    }

@@ -1837,6 +1858,15 @@ public class Activity extends ContextThemeWrapper
        return false;
    }
    
    /**
     * Called when the activity has detected the user's press of the back
     * key.  The default implementation simply finishes the current activity,
     * but you can override this to do whatever you want.
     */
    public void onBackPressed() {
        finish();
    }
    
    /**
     * Called when a touch screen event was not handled by any of the views
     * under it.  This is most useful to process touch events that happen
@@ -1909,9 +1939,10 @@ public class Activity extends ContextThemeWrapper
    /**
     * Called when the current {@link Window} of the activity gains or loses
     * focus.  This is the best indicator of whether this activity is visible
     * to the user.
     * to the user.  The default implementation clears the key tracking
     * state, so should always be called.
     * 
     * <p>Note that this provides information what global focus state, which
     * <p>Note that this provides information about global focus state, which
     * is managed independently of activity lifecycles.  As such, while focus
     * changes will generally have some relation to lifecycle changes (an
     * activity that is stopped will not generally get window focus), you
@@ -1988,7 +2019,8 @@ public class Activity extends ContextThemeWrapper
        if (getWindow().superDispatchKeyEvent(event)) {
            return true;
        }
        return event.dispatch(this);
        return event.dispatch(this, mDecor != null
                ? mDecor.getKeyDispatcherState() : null, this);
    }

    /**
+33 −6
Original line number Diff line number Diff line
@@ -481,30 +481,45 @@ public class Dialog implements DialogInterface, Window.Callback,
     * 
     * <p>If the focused view didn't want this event, this method is called.
     *
     * <p>The default implementation handles KEYCODE_BACK to close the
     * dialog.
     * <p>The default implementation consumed the KEYCODE_BACK to later
     * handle it in {@link #onKeyUp}.
     *
     * @see #onKeyUp
     * @see android.view.KeyEvent
     */
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            if (mCancelable) {
                cancel();
            }
            event.startTracking();
            return true;
        }

        return false;
    }

    /**
     * Default implementation of {@link KeyEvent.Callback#onKeyLongPress(int, KeyEvent)
     * KeyEvent.Callback.onKeyLongPress()}: always returns false (doesn't handle
     * the event).
     */
    public boolean onKeyLongPress(int keyCode, KeyEvent event) {
        return false;
    }

    /**
     * A key was released.
     * 
     * <p>The default implementation handles KEYCODE_BACK to close the
     * dialog.
     *
     * @see #onKeyDown
     * @see KeyEvent
     */
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking()
                && !event.isCanceled()) {
            onBackPressed();
            return true;
        }
        return false;
    }

@@ -517,6 +532,17 @@ public class Dialog implements DialogInterface, Window.Callback,
        return false;
    }
    
    /**
     * Called when the dialog has detected the user's press of the back
     * key.  The default implementation simply cancels the dialog (only if
     * it is cancelable), but you can override this to do whatever you want.
     */
    public void onBackPressed() {
        if (mCancelable) {
            cancel();
        }
    }
    
    /**
     * Called when a touch screen event was not handled by any of the views
     * under it. This is most useful to process touch events that happen outside
@@ -599,7 +625,8 @@ public class Dialog implements DialogInterface, Window.Callback,
        if (mWindow.superDispatchKeyEvent(event)) {
            return true;
        }
        return event.dispatch(this);
        return event.dispatch(this, mDecor != null
                ? mDecor.getKeyDispatcherState() : null, this);
    }

    /**
+84 −35
Original line number Diff line number Diff line
@@ -745,11 +745,9 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
            return true;
        }
        
        if (keyCode == KeyEvent.KEYCODE_SEARCH && event.getRepeatCount() < 1) {
            // If the search key is pressed, toggle between global and in-app search. If we are
            // currently doing global search and there is no in-app search context to toggle to,
            // just don't do anything.
            return toggleGlobalSearch();
        if (keyCode == KeyEvent.KEYCODE_SEARCH) {
            // Consume search key for later use.
            return true;
        }

        // if it's an action specified by the searchable activity, launch the
@@ -763,6 +761,29 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
        return false;
    }
    
    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (DBG) Log.d(LOG_TAG, "onKeyUp(" + keyCode + "," + event + ")");
        if (mSearchable == null) {
            return false;
        }

        // handle back key to go back to previous searchable, etc.
        if (handleBackKey(keyCode, event)) {
            return true;
        }
        
        if (keyCode == KeyEvent.KEYCODE_SEARCH && event.isTracking()
                && !event.isCanceled()) {
            // If the search key is pressed, toggle between global and in-app search. If we are
            // currently doing global search and there is no in-app search context to toggle to,
            // just don't do anything.
            return toggleGlobalSearch();
        }
        
        return false;
    }
    
    /**
     * Callback to watch the textedit field for empty/non-empty
     */
@@ -1500,11 +1521,13 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
     * 
     * @return <code>true</code> if there was a previous component that we could go back to.
     */
    private boolean backToPreviousComponent() {
    private boolean backToPreviousComponent(boolean doIt) {
        ComponentName previous = popPreviousComponent();
        if (previous == null) {
            return false;
        }
        
        if (doIt) {
            if (!show(previous, mAppSearchData, false)) {
                Log.w(LOG_TAG, "Failed to switch to source " + previous);
                return false;
@@ -1515,6 +1538,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
            // the source that we are now going back to?
            String query = mSearchAutoComplete.getText().toString();
            setUserQuery(query);
        }
        
        return true;
    }
@@ -1660,6 +1684,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
    public static class SearchAutoComplete extends AutoCompleteTextView {

        private int mThreshold;
        private int mLastKeyDown;
        private SearchDialog mSearchDialog;
        
        public SearchAutoComplete(Context context) {
@@ -1740,11 +1765,27 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
         */
        @Override
        public boolean onKeyPreIme(int keyCode, KeyEvent event) {
            mLastKeyDown = keyCode;
            if (mSearchDialog.mSearchable == null) {
                return false;
            }
            if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
                if (mSearchDialog.backToPreviousComponent()) {
            if (keyCode == KeyEvent.KEYCODE_BACK) {
                if (event.getAction() == KeyEvent.ACTION_DOWN
                        && event.getRepeatCount() == 0) {
                    // We releae the back key, might we want to do
                    // something before the IME?
                    if (mSearchDialog.backToPreviousComponent(false)) {
                        return true;
                    }
                    if (isInputMethodNotNeeded() ||
                            (isEmpty() && getDropDownChildCount() >= getAdapterCount())) {
                        return true;
                    }
                    mLastKeyDown = 0;
                    return false; // will dismiss soft keyboard if necessary
                } else if (event.getAction() == KeyEvent.ACTION_UP
                        && mLastKeyDown == keyCode && !event.isCanceled()) {
                    if (mSearchDialog.backToPreviousComponent(true)) {
                        return true;
                    }
                    // If the drop-down obscures the keyboard, the user wouldn't see anything
@@ -1762,6 +1803,7 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
                    }
                    return false; // will dismiss soft keyboard if necessary
                }
            }
            return false;
        }

@@ -1772,11 +1814,18 @@ public class SearchDialog extends Dialog implements OnItemClickListener, OnItemS
    }
    
    protected boolean handleBackKey(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
            if (backToPreviousComponent()) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            if (event.getAction() == KeyEvent.ACTION_DOWN) {
                // Consume the event, to get an up at which point we execute.
                return true;
            }
            if (event.getAction() == KeyEvent.ACTION_UP && event.isTracking()
                    && !event.isCanceled()) {
                if (backToPreviousComponent(true)) {
                    return true;
                }
                cancel();
            }
            return true;
        }
        return false;
+15 −1
Original line number Diff line number Diff line
@@ -45,6 +45,9 @@ public abstract class AbstractInputMethodService extends Service
        implements KeyEvent.Callback {
    private InputMethod mInputMethod;
    
    final KeyEvent.DispatcherState mDispatcherState
            = new KeyEvent.DispatcherState();

    /**
     * Base class for derived classes to implement their {@link InputMethod}
     * interface.  This takes care of basic maintenance of the input method,
@@ -129,7 +132,8 @@ public abstract class AbstractInputMethodService extends Service
         * callbacks on the service, and tell the client when this is done.
         */
        public void dispatchKeyEvent(int seq, KeyEvent event, EventCallback callback) {
            boolean handled = event.dispatch(AbstractInputMethodService.this);
            boolean handled = event.dispatch(AbstractInputMethodService.this,
                    mDispatcherState, this);
            if (callback != null) {
                callback.finishedEvent(seq, handled);
            }
@@ -147,6 +151,16 @@ public abstract class AbstractInputMethodService extends Service
        }
    }
    
    /**
     * Return the global {@link KeyEvent.DispatcherState KeyEvent.DispatcherState}
     * for used for processing events from the target application.
     * Normally you will not need to use this directly, but
     * just use the standard high-level event callbacks like {@link #onKeyDown}.
     */
    public KeyEvent.DispatcherState getKeyDispatcherState() {
        return mDispatcherState;
    }
    
    /**
     * Called by the framework during initialization, when the InputMethod
     * interface for this service needs to be created.
Loading