/*
* 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 tk.wasdennnoch.androidn_ify.extracted.systemui;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Context;
import android.content.Intent;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import tk.wasdennnoch.androidn_ify.R;
import tk.wasdennnoch.androidn_ify.XposedHook;
import tk.wasdennnoch.androidn_ify.systemui.notifications.NotificationHooks;
import tk.wasdennnoch.androidn_ify.systemui.notifications.stack.NotificationStackScrollLayoutHooks;
import tk.wasdennnoch.androidn_ify.systemui.notifications.views.RemoteInputHelper;
import tk.wasdennnoch.androidn_ify.utils.ResourceUtils;
import static de.robv.android.xposed.XposedHelpers.callMethod;
import static de.robv.android.xposed.XposedHelpers.callStaticMethod;
/**
* Host for the remote input.
*/
public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher {
// A marker object that let's us easily find views of this class.
public static final Object VIEW_TAG = new Object();
private RemoteEditText mEditText;
private ImageButton mSendButton;
private ProgressBar mProgressBar;
private PendingIntent mPendingIntent;
private RemoteInput[] mRemoteInputs;
private RemoteInput mRemoteInput;
private Object mScrollContainer;
private View mScrollContainerChild;
private boolean mRemoved;
private Object headsUpEntry;
public RemoteInputView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public static RemoteInputView inflate(Context context, ViewGroup root) {
LayoutInflater inflater = LayoutInflater.from(context).cloneInContext(ResourceUtils.createOwnContext(context));
inflater.setFactory2(new LayoutInflater.Factory2() {
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
if (name.equals(RemoteInputView.class.getCanonicalName())) {
return new RemoteInputView(context, attrs);
} else if (name.equals(RemoteEditText.class.getCanonicalName())) {
return new RemoteEditText(context, attrs);
} else return null;
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
return onCreateView(name, context, attrs);
}
});
RemoteInputView v = (RemoteInputView) inflater.inflate(ResourceUtils.getInstance(context).getLayout(R.layout.remote_input), root, false);
v.setTag(VIEW_TAG);
return v;
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mProgressBar = (ProgressBar) findViewById(R.id.remote_input_progress);
mSendButton = (ImageButton) findViewById(R.id.remote_input_send);
mSendButton.setOnClickListener(this);
mEditText = (RemoteEditText) getChildAt(0);
mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
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
&& (boolean) callStaticMethod(KeyEvent.class, "isConfirmKey", event.getKeyCode())
&& event.getAction() == KeyEvent.ACTION_DOWN;
if (isSoftImeEvent || isKeyboardEnterKey) {
if (mEditText.length() > 0) {
sendRemoteInput();
}
// Consume action to prevent IME from closing.
return true;
}
return false;
}
});
mEditText.addTextChangedListener(this);
mEditText.setInnerFocusable(false);
mEditText.mRemoteInputView = this;
}
private void sendRemoteInput() {
Bundle results = new Bundle();
results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString());
Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent,
results);
mEditText.setEnabled(false);
mSendButton.setVisibility(INVISIBLE);
mProgressBar.setVisibility(VISIBLE);
mEditText.mShowImeOnInputConnection = false;
try {
mPendingIntent.send(getContext(), 0, fillInIntent);
} catch (PendingIntent.CanceledException e) {
e.printStackTrace();
}
}
@Override
public void onClick(View v) {
if (v == mSendButton) {
sendRemoteInput();
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
super.onTouchEvent(event);
// We never want for a touch to escape to an outer view or one we covered.
return true;
}
public void onDefocus() {
if (headsUpEntry != null)
callMethod(headsUpEntry, "removeAsSoonAsPossible");
RemoteInputHelper.setWindowManagerFocus(false);
// During removal, we get reattached and lose focus. Not hiding in that
// case to prevent flicker.
if (!mRemoved) {
setVisibility(INVISIBLE);
}
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
if (getVisibility() == VISIBLE && mEditText.isFocusable()) {
mEditText.requestFocus();
}
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
RemoteInputHelper.setWindowManagerFocus(false);
}
public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) {
mRemoteInputs = remoteInputs;
mRemoteInput = remoteInput;
mEditText.setHint(mRemoteInput.getLabel());
}
public void focus() {
setVisibility(VISIBLE);
RemoteInputHelper.setWindowManagerFocus(true);
mEditText.setInnerFocusable(true);
mEditText.mShowImeOnInputConnection = true;
mEditText.setSelection(mEditText.getText().length());
// Unblock focus
ViewParent ancestor = getParent();
while (ancestor instanceof ViewGroup) {
final ViewGroup vgAncestor = (ViewGroup) ancestor;
if (vgAncestor.getDescendantFocusability() == FOCUS_BLOCK_DESCENDANTS) {
vgAncestor.setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
break;
} else {
ancestor = vgAncestor.getParent();
}
}
mEditText.requestFocus();
updateSendButton();
}
public void onNotificationUpdateOrReset() {
boolean sending = mProgressBar.getVisibility() == VISIBLE;
if (sending) {
// Update came in after we sent the reply, time to reset.
reset();
}
}
private void reset() {
mEditText.getText().clear();
mEditText.setEnabled(true);
mSendButton.setVisibility(VISIBLE);
mProgressBar.setVisibility(INVISIBLE);
updateSendButton();
onDefocus();
}
private void updateSendButton() {
mSendButton.setEnabled(mEditText.getText().length() != 0);
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
updateSendButton();
}
public void close() {
mEditText.defocusIfNeeded();
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
findScrollContainer();
if (mScrollContainer != null) {
callMethod(mScrollContainer, "removeLongPressCallback");
NotificationStackScrollLayoutHooks stackScrollLayoutHooks = NotificationHooks.mStackScrollLayoutHooks;
if (stackScrollLayoutHooks != null)
stackScrollLayoutHooks.requestDisallowDismiss();
}
}
return super.onInterceptTouchEvent(ev);
}
public boolean requestScrollTo() {
findScrollContainer();
NotificationStackScrollLayoutHooks stackScrollLayoutHooks = NotificationHooks.mStackScrollLayoutHooks;
if (stackScrollLayoutHooks != null)
stackScrollLayoutHooks.lockScrollTo(mScrollContainerChild);
return true;
}
private void findScrollContainer() {
if (mScrollContainer == null) {
mScrollContainerChild = null;
ViewParent p = this;
while (p != null) {
if (mScrollContainerChild == null && p.getClass().getCanonicalName().equals(XposedHook.PACKAGE_SYSTEMUI + ".statusbar.ExpandableView")) {
mScrollContainerChild = (View) p;
}
if (p.getParent().getClass().getCanonicalName().equals(XposedHook.PACKAGE_SYSTEMUI + ".statusbar.stack.NotificationStackScrollLayout")) {
mScrollContainer = p.getParent();
if (mScrollContainerChild == null) {
mScrollContainerChild = (View) p;
}
break;
}
p = p.getParent();
}
}
}
public boolean isActive() {
return mEditText.isFocused();
}
public void stealFocusFrom(RemoteInputView other) {
other.close();
setPendingIntent(other.mPendingIntent);
setRemoteInput(other.mRemoteInputs, other.mRemoteInput);
focus();
}
/**
* Tries to find an action in {@param actions} that matches the current pending intent
* of this view and updates its state to that of the found action
*
* @return true if a matching action was found, false otherwise
*/
public boolean updatePendingIntentFromActions(Notification.Action[] actions) {
boolean found = false;
if (mPendingIntent == null || actions == null) {
return false;
}
Intent current = (Intent) callMethod(mPendingIntent, "getIntent");
if (current == null) {
return false;
}
for (Notification.Action a : actions) {
RemoteInput[] inputs = a.getRemoteInputs();
if (a.actionIntent == null || inputs == null) {
continue;
}
Intent candidate = (Intent) callMethod(a.actionIntent, "getIntent");
if (!current.filterEquals(candidate)) {
continue;
}
RemoteInput input = null;
for (RemoteInput i : inputs) {
if (i.getAllowFreeFormInput()) {
input = i;
}
}
if (input == null) {
continue;
}
setPendingIntent(a.actionIntent);
setRemoteInput(inputs, input);
return true;
}
return false;
}
public PendingIntent getPendingIntent() {
return mPendingIntent;
}
public void setPendingIntent(PendingIntent pendingIntent) {
mPendingIntent = pendingIntent;
}
public void setHeadsUpEntry(Object headsUpEntry) {
this.headsUpEntry = headsUpEntry;
}
public void setRemoved() {
mRemoved = true;
}
/**
* 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;
boolean mShowImeOnInputConnection;
private RemoteInputView mRemoteInputView;
public RemoteEditText(Context context, AttributeSet attrs) {
super(context, attrs);
mBackground = getBackground();
}
private void defocusIfNeeded() {
/*if (mRemoteInputView != null && mRemoteInputView.mEntry.row.isChangingPosition()) {
return;
}*/
if (isFocusable() && isEnabled()) {
setInnerFocusable(false);
if (mRemoteInputView != null) {
mRemoteInputView.onDefocus();
}
mShowImeOnInputConnection = false;
}
}
@Override
protected void onVisibilityChanged(View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if (!isShown()) {
defocusIfNeeded();
}
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
super.onFocusChanged(focused, direction, previouslyFocusedRect);
if (!focused) {
defocusIfNeeded();
}
}
@Override
public void getFocusedRect(Rect r) {
super.getFocusedRect(r);
r.top = getScrollY();
r.bottom = getScrollY() + (getBottom() - getTop());
}
@Override
public boolean requestRectangleOnScreen(Rect rectangle) {
return mRemoteInputView.requestScrollTo();
}
@Override
public boolean onKeyPreIme(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) {
defocusIfNeeded();
final InputMethodManager imm = (InputMethodManager) callStaticMethod(InputMethodManager.class, "getInstance");
imm.hideSoftInputFromWindow(getWindowToken(), 0);
return true;
}
return super.onKeyPreIme(keyCode, event);
}
@Override
public boolean onCheckIsTextEditor() {
// Stop being editable while we're being removed. During removal, we get reattached,
// and editable views get their spellchecking state re-evaluated which is too costly
// during the removal animation.
boolean flyingOut = mRemoteInputView != null && mRemoteInputView.mRemoved;
return !flyingOut && super.onCheckIsTextEditor();
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
final InputConnection inputConnection = super.onCreateInputConnection(outAttrs);
if (mShowImeOnInputConnection && inputConnection != null) {
final InputMethodManager imm = (InputMethodManager) callStaticMethod(InputMethodManager.class, "getInstance");
if (imm != null) {
// onCreateInputConnection is called by InputMethodManager in the middle of
// setting up the connection to the IME; wait with requesting the IME until that
// work has completed.
post(new Runnable() {
@Override
public void run() {
imm.viewClicked(RemoteEditText.this);
imm.showSoftInput(RemoteEditText.this, 0);
}
});
}
}
return inputConnection;
}
@Override
public void onCommitCompletion(CompletionInfo text) {
clearComposingText();
setText(text.getText());
setSelection(getText().length());
}
void setInnerFocusable(boolean focusable) {
setFocusableInTouchMode(focusable);
setFocusable(focusable);
setCursorVisible(focusable);
if (focusable) {
requestFocus();
setBackground(mBackground);
} else {
setBackground(null);
}
}
}
}