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

Commit 497ab023 authored by Adrian Roos's avatar Adrian Roos
Browse files

Add prototype of Inline Reply behind debug flag

The flag also extracts wear-only RemoteInput actions
to the normal set of actions under certain
circumstances to make this prototype actually useful.

Change-Id: Ide8bbef4e2ab82e2f152d32b143876ed309a8f21
parent f855b0d2
Loading
Loading
Loading
Loading
+46 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>

<!--
  ~ Copyright (C) 2015 The Android Open Source Project
  ~
  ~ Licensed under the Apache License, Version 2.0 (the "License");
  ~ you may not use this file except in compliance with the License.
  ~ You may obtain a copy of the License at
  ~
  ~      http://www.apache.org/licenses/LICENSE-2.0
  ~
  ~ Unless required by applicable law or agreed to in writing, software
  ~ distributed under the License is distributed on an "AS IS" BASIS,
  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  ~ See the License for the specific language governing permissions and
  ~ limitations under the License
  -->

<!-- FrameLayout -->
<com.android.systemui.statusbar.policy.RemoteInputView
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:theme="@style/systemui_theme_light"
        android:layout_height="match_parent"
        android:layout_width="match_parent"
        android:paddingStart="4dp"
        android:paddingEnd="2dp"
        android:paddingBottom="4dp"
        android:paddingTop="2dp">

    <view class="com.android.systemui.statusbar.policy.RemoteInputView$RemoteEditText"
            android:id="@+id/remote_input_text"
            android:layout_height="wrap_content"
            android:layout_width="match_parent"
            android:singleLine="true"
            android:imeOptions="actionSend" />

    <ProgressBar
            android:id="@+id/remote_input_progress"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom"
            android:visibility="invisible"
            android:indeterminate="true"
            style="?android:attr/progressBarStyleHorizontal" />

</com.android.systemui.statusbar.policy.RemoteInputView>
+5 −0
Original line number Diff line number Diff line
@@ -211,6 +211,11 @@
        <item name="android:colorControlActivated">@color/system_accent_color</item>
    </style>

    <style name="systemui_theme_light" parent="@android:style/Theme.DeviceDefault.Light">
        <item name="android:colorPrimary">@color/system_primary_color</item>
        <item name="android:colorControlActivated">@color/system_accent_color</item>
    </style>

    <style name="Theme.SystemUI.Dialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog">
        <item name="android:colorPrimary">@color/system_primary_color</item>
        <item name="android:colorControlActivated">@color/system_accent_color</item>
+107 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.app.ActivityManagerNative;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.app.TaskStackBuilder;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
@@ -49,6 +50,7 @@ import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
@@ -97,6 +99,7 @@ import com.android.systemui.statusbar.phone.NavigationBarView;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.policy.HeadsUpNotificationView;
import com.android.systemui.statusbar.policy.PreviewInflater;
import com.android.systemui.statusbar.policy.RemoteInputView;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;

import java.util.ArrayList;
@@ -116,6 +119,9 @@ public abstract class BaseStatusBar extends SystemUI implements
    // STOPSHIP disable once we resolve b/18102199
    private static final boolean NOTIFICATION_CLICK_DEBUG = true;

    public static final boolean ENABLE_REMOTE_INPUT =
            Build.IS_DEBUGGABLE && SystemProperties.getBoolean("debug.enable_remote_input", false);

    protected static final int MSG_SHOW_RECENT_APPS = 1019;
    protected static final int MSG_HIDE_RECENT_APPS = 1020;
    protected static final int MSG_TOGGLE_RECENTS_APPS = 1021;
@@ -409,6 +415,7 @@ public abstract class BaseStatusBar extends SystemUI implements
                @Override
                public void run() {
                    for (StatusBarNotification sbn : notifications) {
                        processForRemoteInput(sbn.getNotification());
                        addNotification(sbn, currentRanking);
                    }
                }
@@ -423,6 +430,7 @@ public abstract class BaseStatusBar extends SystemUI implements
                mHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        processForRemoteInput(sbn.getNotification());
                        Notification n = sbn.getNotification();
                        boolean isUpdate = mNotificationData.get(sbn.getKey()) != null
                                || isHeadsUp(sbn.getKey());
@@ -1356,6 +1364,9 @@ public abstract class BaseStatusBar extends SystemUI implements
                (NotificationContentView) row.findViewById(R.id.expandedPublic);

        row.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
        if (ENABLE_REMOTE_INPUT) {
            row.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
        }

        PendingIntent contentIntent = sbn.getNotification().contentIntent;
        if (contentIntent != null) {
@@ -1517,9 +1528,103 @@ public abstract class BaseStatusBar extends SystemUI implements
        }
        row.setUserLocked(userLocked);
        row.setStatusBarNotification(entry.notification);
        applyRemoteInput(entry);
        return true;
    }

    /**
     * Adds RemoteInput actions from the WearableExtender; to be removed once more apps support this
     * via first-class API.
     *
     * TODO: Remove once enough apps specify remote inputs on their own.
     */
    private void processForRemoteInput(Notification n) {
        if (!ENABLE_REMOTE_INPUT) return;

        if (n.extras != null && n.extras.containsKey("android.wearable.EXTENSIONS") &&
                (n.actions == null || n.actions.length == 0)) {
            Notification.Action viableAction = null;
            Notification.WearableExtender we = new Notification.WearableExtender(n);

            List<Notification.Action> actions = we.getActions();
            final int numActions = actions.size();

            for (int i = 0; i < numActions; i++) {
                Notification.Action action = actions.get(i);
                RemoteInput[] remoteInputs = action.getRemoteInputs();
                for (RemoteInput ri : action.getRemoteInputs()) {
                    if (ri.getAllowFreeFormInput()) {
                        viableAction = action;
                        break;
                    }
                }
                if (viableAction != null) {
                    break;
                }
            }

            if (viableAction != null) {
                Notification stripped = n.clone();
                Notification.Builder.stripForDelivery(stripped);
                stripped.actions = new Notification.Action[] { viableAction };
                stripped.extras.putBoolean("android.rebuild.contentView", true);
                stripped.contentView = null;
                stripped.extras.putBoolean("android.rebuild.bigView", true);
                stripped.bigContentView = null;

                // Don't create the HUN input view for now because input doesn't work there yet.
                // TODO: Enable once HUNs can take remote input correctly.
                if (false) {
                    stripped.extras.putBoolean("android.rebuild.hudView", true);
                    stripped.headsUpContentView = null;
                }

                Notification rebuilt = Notification.Builder.rebuild(mContext, stripped);

                n.actions = rebuilt.actions;
                n.bigContentView = rebuilt.bigContentView;
                n.headsUpContentView = rebuilt.headsUpContentView;
                n.publicVersion = rebuilt.publicVersion;
            }
        }
    }

    private void applyRemoteInput(final Entry entry) {
        if (!ENABLE_REMOTE_INPUT) return;

        RemoteInput remoteInput = null;

        // See if the notification has exactly one action and this action allows free-form input
        // TODO: relax restrictions once we support more than one remote input action.
        Notification.Action[] actions = entry.notification.getNotification().actions;
        if (actions != null && actions.length == 1) {
            if (actions[0].getRemoteInputs() != null) {
                for (RemoteInput ri : actions[0].getRemoteInputs()) {
                    if (ri.getAllowFreeFormInput()) {
                        remoteInput = ri;
                        break;
                    }
                }
            }
        }

        // See if we have somewhere to put that remote input
        ViewGroup actionContainer = null;
        if (remoteInput != null && entry.expandedBig != null) {
            View actionContainerCandidate = entry.expandedBig
                    .findViewById(com.android.internal.R.id.actions);
            if (actionContainerCandidate instanceof ViewGroup) {
                actionContainer = (ViewGroup) actionContainerCandidate;
            }
        }

        if (actionContainer != null) {
            actionContainer.removeAllViews();
            actionContainer.addView(
                    RemoteInputView.inflate(mContext, actionContainer, actions[0], remoteInput));
        }
    }

    public NotificationClicker makeClicker(PendingIntent intent, String notificationKey) {
        return new NotificationClicker(intent, notificationKey);
    }
@@ -2036,6 +2141,8 @@ public abstract class BaseStatusBar extends SystemUI implements
        entry.row.setStatusBarNotification(notification);
        entry.row.notifyContentUpdated();
        entry.row.resetHeight();

        applyRemoteInput(entry);
    }

    protected void notifyHeadsUpScreenOn(boolean screenOn) {
+2 −1
Original line number Diff line number Diff line
@@ -115,7 +115,8 @@ public class StatusBarWindowManager {

    private void applyFocusableFlag(State state) {
        if (state.isKeyguardShowingAndNotOccluded() && state.keyguardNeedsInput
                && state.bouncerShowing) {
                && state.bouncerShowing
                || BaseStatusBar.ENABLE_REMOTE_INPUT && state.statusBarExpanded) {
            mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            mLpChanged.flags &= ~WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
        } else if (state.isKeyguardShowingAndNotOccluded() || state.statusBarFocusable) {
+191 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License
 */

package com.android.systemui.statusbar.policy;

import com.android.systemui.R;

import android.annotation.NonNull;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ProgressBar;
import android.widget.TextView;

/**
 * Host for the remote input.
 */
public class RemoteInputView extends FrameLayout implements View.OnClickListener {

    private static final String TAG = "RemoteInput";

    private RemoteEditText mEditText;
    private ProgressBar mProgressBar;
    private PendingIntent mPendingIntent;
    private RemoteInput mRemoteInput;
    private Notification.Action mAction;

    public RemoteInputView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        mProgressBar = (ProgressBar) findViewById(R.id.remote_input_progress);

        mEditText = (RemoteEditText) getChildAt(0);
        mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {

                // Check if this was the result of hitting the enter key
                final boolean isSoftImeEvent = event == null
                        && (actionId == EditorInfo.IME_ACTION_DONE
                        || actionId == EditorInfo.IME_ACTION_NEXT
                        || actionId == EditorInfo.IME_ACTION_SEND);
                final boolean isKeyboardEnterKey = event != null
                        && KeyEvent.isConfirmKey(event.getKeyCode())
                        && event.getAction() == KeyEvent.ACTION_DOWN;

                if (isSoftImeEvent || isKeyboardEnterKey) {
                    sendRemoteInput();
                    return true;
                }
                return false;
            }
        });
        mEditText.setOnClickListener(this);
        mEditText.setInnerFocusable(false);
    }

    private void sendRemoteInput() {
        Bundle results = new Bundle();
        results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString());
        Intent fillInIntent = new Intent();
        RemoteInput.addResultsToIntent(mAction.getRemoteInputs(), fillInIntent,
                results);

        mEditText.setEnabled(false);
        mProgressBar.setVisibility(VISIBLE);

        try {
            mPendingIntent.send(mContext, 0, fillInIntent);
        } catch (PendingIntent.CanceledException e) {
            Log.i(TAG, "Unable to send remote input result", e);
        }
    }

    public static RemoteInputView inflate(Context context, ViewGroup root,
            Notification.Action action, RemoteInput remoteInput) {
        RemoteInputView v = (RemoteInputView)
                LayoutInflater.from(context).inflate(R.layout.remote_input, root, false);

        v.mEditText.setHint(action.title);
        v.mPendingIntent = action.actionIntent;
        v.mRemoteInput = remoteInput;
        v.mAction = action;

        return v;
    }

    @Override
    public void onClick(View v) {
        if (v == mEditText) {
            if (!mEditText.isFocusable()) {
                mEditText.setInnerFocusable(true);
                InputMethodManager imm = InputMethodManager.getInstance();
                if (imm != null) {
                    imm.viewClicked(mEditText);
                    imm.showSoftInput(mEditText, 0);
                }
            }
        }
    }

    /**
     * An EditText that changes appearance based on whether it's focusable and becomes
     * un-focusable whenever the user navigates away from it or it becomes invisible.
     */
    public static class RemoteEditText extends EditText {

        private final Drawable mBackground;

        public RemoteEditText(Context context, AttributeSet attrs) {
            super(context, attrs);
            mBackground = getBackground();
        }

        private void defocusIfNeeded() {
            if (isFocusable() && isEnabled()) {
                setInnerFocusable(false);
            }
        }

        @Override
        protected void onVisibilityChanged(View changedView, int visibility) {
            super.onVisibilityChanged(changedView, visibility);

            if (!isShown()) {
                defocusIfNeeded();
            }
        }

        @Override
        protected void onFocusLost() {
            super.onFocusLost();
            defocusIfNeeded();
        }

        @Override
        public boolean onKeyPreIme(int keyCode, KeyEvent event) {
            if (keyCode == KeyEvent.KEYCODE_BACK) {
                defocusIfNeeded();
            }
            return super.onKeyPreIme(keyCode, event);
        }


        void setInnerFocusable(boolean focusable) {
            setFocusableInTouchMode(focusable);
            setFocusable(focusable);
            setCursorVisible(focusable);

            if (focusable) {
                requestFocus();
                setBackground(mBackground);
            } else {
                setBackground(null);
            }

        }
    }
}