diff options
author | Adrian Roos <roosa@google.com> | 2015-02-10 20:49:33 +0100 |
---|---|---|
committer | Adrian Roos <roosa@google.com> | 2015-03-09 18:31:58 +0100 |
commit | 497ab023f9ed121664a210c380b43746b13e9038 (patch) | |
tree | d88a47dde8266c32c530e3f2943abe2fd1d2e3d6 /packages | |
parent | f855b0d2d0c0c157d657668fcb98097e2aca097e (diff) | |
download | frameworks_base-497ab023f9ed121664a210c380b43746b13e9038.zip frameworks_base-497ab023f9ed121664a210c380b43746b13e9038.tar.gz frameworks_base-497ab023f9ed121664a210c380b43746b13e9038.tar.bz2 |
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
Diffstat (limited to 'packages')
5 files changed, 351 insertions, 1 deletions
diff --git a/packages/SystemUI/res/layout/remote_input.xml b/packages/SystemUI/res/layout/remote_input.xml new file mode 100644 index 0000000..8ca5634 --- /dev/null +++ b/packages/SystemUI/res/layout/remote_input.xml @@ -0,0 +1,46 @@ +<?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> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 94f77c6..07fcb82 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -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> diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java index 3a812cc..2fb4bde 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BaseStatusBar.java @@ -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) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java index eba7d9f..63bbf97 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowManager.java @@ -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) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java new file mode 100644 index 0000000..7d721c2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -0,0 +1,191 @@ +/* + * 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); + } + + } + } +} |