Android应用开发中,mvp模式是目前比较流行的设计模式。
三层依赖关系如下图:
本文记录一下通话界面的对于MVP设计模式的使用。
1.V层即view层的接口定义
(1)接口Ui定义
package com.android.incallui.baseui;
/** Base class for all presenter ui. */
public interface Ui {}
(2)BaseFragment定义,这也是在V层,是所有使用Presenter 和Ui的父亲fragment
package com.android.incallui.baseui;
import android.os.Bundle;
import android.support.v4.app.Fragment;
/** Parent for all fragments that use Presenters and Ui design. */
public abstract class BaseFragment, U extends Ui> extends Fragment {
private static final String KEY_FRAGMENT_HIDDEN = "key_fragment_hidden";
private T presenter;
protected BaseFragment() {
presenter = createPresenter();
}
public abstract T createPresenter();
public abstract U getUi();
/**
* Presenter will be available after onActivityCreated().
*
* @return The presenter associated with this fragment.
*/
public T getPresenter() {
return presenter;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
presenter.onUiReady(getUi());
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (savedInstanceState != null) {
presenter.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState.getBoolean(KEY_FRAGMENT_HIDDEN)) {
getFragmentManager().beginTransaction().hide(this).commit();
}
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
presenter.onUiDestroy(getUi());
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
presenter.onSaveInstanceState(outState);
outState.putBoolean(KEY_FRAGMENT_HIDDEN, isHidden());
}
}
(3)V层接口实现类定义
DialpadFragment 类定义
package com.android.incallui;
import android.content.Context;
import android.os.Bundle;
import android.telephony.PhoneNumberUtils;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnKeyListener;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.dialer.common.LogUtil;
import com.android.dialer.dialpadview.DialpadKeyButton;
import com.android.dialer.dialpadview.DialpadKeyButton.OnPressedListener;
import com.android.dialer.dialpadview.DialpadView;
import com.android.dialer.logging.DialerImpression;
import com.android.dialer.logging.Logger;
import com.android.incallui.DialpadPresenter.DialpadUi;
import com.android.incallui.baseui.BaseFragment;
import java.util.Map;
/** Fragment for call control buttons */
public class DialpadFragment extends BaseFragment
implements DialpadUi, OnKeyListener, OnClickListener, OnPressedListener {
/** Hash Map to map a view id to a character */
private static final Map displayMap = new ArrayMap<>();
/** Set up the static maps */
static {
// Map the buttons to the display characters
displayMap.put(R.id.one, '1');
displayMap.put(R.id.two, '2');
displayMap.put(R.id.three, '3');
displayMap.put(R.id.four, '4');
displayMap.put(R.id.five, '5');
displayMap.put(R.id.six, '6');
displayMap.put(R.id.seven, '7');
displayMap.put(R.id.eight, '8');
displayMap.put(R.id.nine, '9');
displayMap.put(R.id.zero, '0');
displayMap.put(R.id.pound, '#');
displayMap.put(R.id.star, '*');
}
private final int[] buttonIds =
new int[] {
R.id.zero,
R.id.one,
R.id.two,
R.id.three,
R.id.four,
R.id.five,
R.id.six,
R.id.seven,
R.id.eight,
R.id.nine,
R.id.star,
R.id.pound
};
private EditText dtmfDialerField;
// KeyListener used with the "dialpad digits" EditText widget.
private DtmfKeyListener dtmfKeyListener;
private DialpadView dialpadView;
private int currentTextColor;
@Override
public void onClick(View v) {
if (v.getId() == R.id.dialpad_back) {
Logger.get(getContext())
.logImpression(DialerImpression.Type.IN_CALL_DIALPAD_CLOSE_BUTTON_PRESSED);
getActivity().onBackPressed();
}
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
Log.d(this, "onKey: keyCode " + keyCode + ", view " + v);
if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER || keyCode == KeyEvent.KEYCODE_ENTER) {
int viewId = v.getId();
if (displayMap.containsKey(viewId)) {
switch (event.getAction()) {
case KeyEvent.ACTION_DOWN:
if (event.getRepeatCount() == 0) {
getPresenter().processDtmf(displayMap.get(viewId));
}
break;
case KeyEvent.ACTION_UP:
getPresenter().stopDtmf();
break;
default: // fall out
}
// do not return true [handled] here, since we want the
// press / click animation to be handled by the framework.
}
}
return false;
}
@Override
public DialpadPresenter createPresenter() {
return new DialpadPresenter();
}
@Override
public DialpadPresenter.DialpadUi getUi() {
return this;
}
// TODO(klp) Adds hardware keyboard listener
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View parent = inflater.inflate(R.layout.incall_dialpad_fragment, container, false);
dialpadView = (DialpadView) parent.findViewById(R.id.dialpad_view);
dialpadView.setCanDigitsBeEdited(false);
dialpadView.setBackgroundResource(R.color.incall_dialpad_background);
dtmfDialerField = (EditText) parent.findViewById(R.id.digits);
if (dtmfDialerField != null) {
LogUtil.i("DialpadFragment.onCreateView", "creating dtmfKeyListener");
dtmfKeyListener = new DtmfKeyListener(getPresenter());
dtmfDialerField.setKeyListener(dtmfKeyListener);
// remove the long-press context menus that support
// the edit (copy / paste / select) functions.
dtmfDialerField.setLongClickable(false);
dtmfDialerField.setElegantTextHeight(false);
configureKeypadListeners();
}
View backButton = dialpadView.findViewById(R.id.dialpad_back);
backButton.setVisibility(View.VISIBLE);
backButton.setOnClickListener(this);
return parent;
}
@Override
public void onResume() {
super.onResume();
updateColors();
}
public void updateColors() {
int textColor = InCallPresenter.getInstance().getThemeColorManager().getPrimaryColor();
if (currentTextColor == textColor) {
return;
}
DialpadKeyButton dialpadKey;
for (int i = 0; i < buttonIds.length; i++) {
dialpadKey = (DialpadKeyButton) dialpadView.findViewById(buttonIds[i]);
((TextView) dialpadKey.findViewById(R.id.dialpad_key_number)).setTextColor(textColor);
}
currentTextColor = textColor;
}
@Override
public void onDestroyView() {
dtmfKeyListener = null;
super.onDestroyView();
}
/**
* Getter for Dialpad text.
*
* @return String containing current Dialpad EditText text.
*/
public String getDtmfText() {
return dtmfDialerField.getText().toString();
}
/**
* Sets the Dialpad text field with some text.
*
* @param text Text to set Dialpad EditText to.
*/
public void setDtmfText(String text) {
dtmfDialerField.setText(PhoneNumberUtils.createTtsSpannable(text));
/// M: ALPS03175530 set the focus to the end of the text
dtmfDialerField.setSelection(dtmfDialerField.length());
}
/** Starts the slide up animation for the Dialpad keys when the Dialpad is revealed. */
public void animateShowDialpad() {
final DialpadView dialpadView = (DialpadView) getView().findViewById(R.id.dialpad_view);
dialpadView.animateShow();
}
@Override
public void appendDigitsToField(char digit) {
if (dtmfDialerField != null) {
// TODO: maybe *don't* manually append this digit if
// mDialpadDigits is focused and this key came from the HW
// keyboard, since in that case the EditText field will
// get the key event directly and automatically appends
// whetever the user types.
// (Or, a cleaner fix would be to just make mDialpadDigits
// *not* handle HW key presses. That seems to be more
// complicated than just setting focusable="false" on it,
// though.)
dtmfDialerField.getText().append(digit);
}
}
/** Called externally (from InCallScreen) to play a DTMF Tone. */
/* package */ boolean onDialerKeyDown(KeyEvent event) {
Log.d(this, "Notifying dtmf key down.");
if (dtmfKeyListener != null) {
return dtmfKeyListener.onKeyDown(event);
} else {
return false;
}
}
/** Called externally (from InCallScreen) to cancel the last DTMF Tone played. */
public boolean onDialerKeyUp(KeyEvent event) {
Log.d(this, "Notifying dtmf key up.");
if (dtmfKeyListener != null) {
return dtmfKeyListener.onKeyUp(event);
} else {
return false;
}
}
private void configureKeypadListeners() {
DialpadKeyButton dialpadKey;
for (int i = 0; i < buttonIds.length; i++) {
dialpadKey = (DialpadKeyButton) dialpadView.findViewById(buttonIds[i]);
dialpadKey.setOnKeyListener(this);
dialpadKey.setOnClickListener(this);
dialpadKey.setOnPressedListener(this);
}
}
@Override
public void onPressed(View view, boolean pressed) {
if (pressed && displayMap.containsKey(view.getId())) {
Logger.get(getContext())
.logImpression(DialerImpression.Type.IN_CALL_DIALPAD_NUMBER_BUTTON_PRESSED);
Log.d(this, "onPressed: " + pressed + " " + displayMap.get(view.getId()));
getPresenter().processDtmf(displayMap.get(view.getId()));
}
if (!pressed) {
Log.d(this, "onPressed: " + pressed);
getPresenter().stopDtmf();
}
}
/**
* LinearLayout with getter and setter methods for the translationY property using floats, for
* animation purposes.
*/
public static class DialpadSlidingLinearLayout extends LinearLayout {
public DialpadSlidingLinearLayout(Context context) {
super(context);
}
public DialpadSlidingLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
public DialpadSlidingLinearLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public float getYFraction() {
final int height = getHeight();
if (height == 0) {
return 0;
}
return getTranslationY() / height;
}
public void setYFraction(float yFraction) {
setTranslationY(yFraction * getHeight());
}
}
/// M: ------------------------------- MediaTek feature ---------------------------
/**
* M: Used for making the fragment's state is latest.
* @param hidden Dialpad fragment is hidden or show
*/
@Override
public void onHiddenChanged(boolean hidden) {
super.onHiddenChanged(hidden);
if (!hidden) {
updateColors();
}
}
}
2.P层
(1)Presenter抽象类定义
package com.android.incallui.baseui;
import android.os.Bundle;
/** Base class for Presenters. */
public abstract class Presenter {
private U ui;
/**
* Called after the UI view has been created. That is when fragment.onViewCreated() is called.
*
* @param ui The Ui implementation that is now ready to be used.
*/
public void onUiReady(U ui) {
this.ui = ui;
}
/** Called when the UI view is destroyed in Fragment.onDestroyView(). */
public final void onUiDestroy(U ui) {
onUiUnready(ui);
this.ui = null;
}
/**
* To be overriden by Presenter implementations. Called when the fragment is being destroyed but
* before ui is set to null.
*/
public void onUiUnready(U ui) {}
public void onSaveInstanceState(Bundle outState) {}
public void onRestoreInstanceState(Bundle savedInstanceState) {}
public U getUi() {
return ui;
}
}
(2)Presenter类的接口实现类DialpadPresenter定义
package com.android.incallui;
import android.telephony.PhoneNumberUtils;
import com.android.incallui.DialpadPresenter.DialpadUi;
import com.android.incallui.baseui.Presenter;
import com.android.incallui.baseui.Ui;
import com.android.incallui.call.CallList;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.TelecomAdapter;
/** Logic for call buttons. */
public class DialpadPresenter extends Presenter
implements InCallPresenter.InCallStateListener {
private DialerCall call;
@Override
public void onUiReady(DialpadUi ui) {
super.onUiReady(ui);
InCallPresenter.getInstance().addListener(this);
call = CallList.getInstance().getOutgoingOrActive();
}
@Override
public void onUiUnready(DialpadUi ui) {
super.onUiUnready(ui);
InCallPresenter.getInstance().removeListener(this);
}
@Override
public void onStateChange(
InCallPresenter.InCallState oldState,
InCallPresenter.InCallState newState,
CallList callList) {
call = callList.getOutgoingOrActive();
Log.d(this, "DialpadPresenter mCall = " + call);
}
/**
* Processes the specified digit as a DTMF key, by playing the appropriate DTMF tone, and
* appending the digit to the EditText field that displays the DTMF digits sent so far.
*/
public final void processDtmf(char c) {
Log.d(this, "Processing dtmf key " + c);
// if it is a valid key, then update the display and send the dtmf tone.
if (PhoneNumberUtils.is12Key(c) && call != null) {
Log.d(this, "updating display and sending dtmf tone for '" + c + "'");
// Append this key to the "digits" widget.
DialpadUi dialpadUi = getUi();
if (dialpadUi != null) {
dialpadUi.appendDigitsToField(c);
}
// Plays the tone through Telecom.
TelecomAdapter.getInstance().playDtmfTone(call.getId(), c);
} else {
Log.d(this, "ignoring dtmf request for '" + c + "'");
}
}
/** Stops the local tone based on the phone type. */
public void stopDtmf() {
if (call != null) {
Log.d(this, "stopping remote tone");
TelecomAdapter.getInstance().stopDtmfTone(call.getId());
}
}
public interface DialpadUi extends Ui {
void appendDigitsToField(char digit);
}
}
3.M层
TelecomAdapter类就是model层的实现
package com.android.incallui.call;
import android.app.Notification;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.os.Looper;
import android.support.annotation.MainThread;
import android.support.annotation.VisibleForTesting;
import android.telecom.InCallService;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import java.util.List;
/** Wrapper around Telecom APIs. */
public class TelecomAdapter implements InCallServiceListener {
private static final String ADD_CALL_MODE_KEY = "add_call_mode";
private static TelecomAdapter instance;
private InCallService inCallService;
private TelecomAdapter() {}
@MainThread
public static TelecomAdapter getInstance() {
if (!Looper.getMainLooper().isCurrentThread()) {
throw new IllegalStateException();
}
if (instance == null) {
instance = new TelecomAdapter();
}
return instance;
}
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
public static void setInstanceForTesting(TelecomAdapter telecomAdapter) {
instance = telecomAdapter;
}
@Override
public void setInCallService(InCallService inCallService) {
this.inCallService = inCallService;
}
@Override
public void clearInCallService() {
inCallService = null;
}
private android.telecom.Call getTelecomCallById(String callId) {
DialerCall call = CallList.getInstance().getCallById(callId);
return call == null ? null : call.getTelecomCall();
}
public void mute(boolean shouldMute) {
if (inCallService != null) {
inCallService.setMuted(shouldMute);
} else {
LogUtil.e("TelecomAdapter.mute", "mInCallService is null");
}
}
public void setAudioRoute(int route) {
if (inCallService != null) {
inCallService.setAudioRoute(route);
} else {
LogUtil.e("TelecomAdapter.setAudioRoute", "mInCallService is null");
}
}
public void merge(String callId) {
android.telecom.Call call = getTelecomCallById(callId);
if (call != null) {
List conferenceable = call.getConferenceableCalls();
if (!conferenceable.isEmpty()) {
call.conference(conferenceable.get(0));
// It's safe to clear restrict count for merge action.
DialerCall.clearRestrictedCount();
} else {
if (call.getDetails().can(android.telecom.Call.Details.CAPABILITY_MERGE_CONFERENCE)) {
call.mergeConference();
// It's safe to clear restrict count for merge action.
DialerCall.clearRestrictedCount();
}
}
} else {
LogUtil.e("TelecomAdapter.merge", "call not in call list " + callId);
}
}
public void swap(String callId) {
android.telecom.Call call = getTelecomCallById(callId);
if (call != null) {
if (call.getDetails().can(android.telecom.Call.Details.CAPABILITY_SWAP_CONFERENCE)) {
call.swapConference();
}
} else {
LogUtil.e("TelecomAdapter.swap", "call not in call list " + callId);
}
}
public void addCall() {
if (inCallService != null) {
Intent intent = new Intent(Intent.ACTION_DIAL);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// when we request the dialer come up, we also want to inform
// it that we're going through the "add call" option from the
// InCallScreen / PhoneUtils.
intent.putExtra(ADD_CALL_MODE_KEY, true);
try {
LogUtil.d("TelecomAdapter.addCall", "Sending the add DialerCall intent");
inCallService.startActivity(intent);
} catch (ActivityNotFoundException e) {
// This is rather rare but possible.
// Note: this method is used even when the phone is encrypted. At that moment
// the system may not find any Activity which can accept this Intent.
LogUtil.e("TelecomAdapter.addCall", "Activity for adding calls isn't found.", e);
}
}
}
public void playDtmfTone(String callId, char digit) {
android.telecom.Call call = getTelecomCallById(callId);
if (call != null) {
call.playDtmfTone(digit);
} else {
LogUtil.e("TelecomAdapter.playDtmfTone", "call not in call list " + callId);
}
}
public void stopDtmfTone(String callId) {
android.telecom.Call call = getTelecomCallById(callId);
if (call != null) {
call.stopDtmfTone();
} else {
LogUtil.e("TelecomAdapter.stopDtmfTone", "call not in call list " + callId);
}
}
public void postDialContinue(String callId, boolean proceed) {
android.telecom.Call call = getTelecomCallById(callId);
if (call != null) {
call.postDialContinue(proceed);
} else {
LogUtil.e("TelecomAdapter.postDialContinue", "call not in call list " + callId);
}
}
public boolean canAddCall() {
if (inCallService != null) {
return inCallService.canAddCall();
}
return false;
}
/**
* Start a foreground notification. Calling it multiple times with the same id only updates the
* existing notification. Whoever called this function are responsible for calling {@link
* #stopForegroundNotification()} to remove the notification.
*/
public void startForegroundNotification(int id, Notification notification) {
Assert.isNotNull(
inCallService, "No inCallService available for starting foreground notification");
inCallService.startForeground(id, notification);
}
/**
* Stop a started foreground notification. This does not stop {@code mInCallService} from running.
*/
public void stopForegroundNotification() {
if (inCallService != null) {
inCallService.stopForeground(true /*removeNotification*/);
} else {
LogUtil.e(
"TelecomAdapter.stopForegroundNotification",
"no inCallService available for stopping foreground notification");
}
}
}
model层就是完全的数据逻辑层。