Blanket copy of PhoneApp to services/Telephony.
First phase of splitting out InCallUI from PhoneApp.
Change-Id: I237341c4ff00e96c677caa4580b251ef3432931b
diff --git a/src/com/android/phone/ADNList.java b/src/com/android/phone/ADNList.java
new file mode 100644
index 0000000..b4e8ac7
--- /dev/null
+++ b/src/com/android/phone/ADNList.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2007 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.phone;
+
+import static android.view.Window.PROGRESS_VISIBILITY_OFF;
+import static android.view.Window.PROGRESS_VISIBILITY_ON;
+
+import android.app.ListActivity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.Window;
+import android.widget.CursorAdapter;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+/**
+ * ADN List activity for the Phone app.
+ */
+public class ADNList extends ListActivity {
+ protected static final String TAG = "ADNList";
+ protected static final boolean DBG = false;
+
+ private static final String[] COLUMN_NAMES = new String[] {
+ "name",
+ "number",
+ "emails"
+ };
+
+ protected static final int NAME_COLUMN = 0;
+ protected static final int NUMBER_COLUMN = 1;
+ protected static final int EMAILS_COLUMN = 2;
+
+ private static final int[] VIEW_NAMES = new int[] {
+ android.R.id.text1,
+ android.R.id.text2
+ };
+
+ protected static final int QUERY_TOKEN = 0;
+ protected static final int INSERT_TOKEN = 1;
+ protected static final int UPDATE_TOKEN = 2;
+ protected static final int DELETE_TOKEN = 3;
+
+
+ protected QueryHandler mQueryHandler;
+ protected CursorAdapter mCursorAdapter;
+ protected Cursor mCursor = null;
+
+ private TextView mEmptyText;
+
+ protected int mInitialSelection = -1;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ setContentView(R.layout.adn_list);
+ mEmptyText = (TextView) findViewById(android.R.id.empty);
+ mQueryHandler = new QueryHandler(getContentResolver());
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ query();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mCursor != null) {
+ mCursor.deactivate();
+ }
+ }
+
+ protected Uri resolveIntent() {
+ Intent intent = getIntent();
+ if (intent.getData() == null) {
+ intent.setData(Uri.parse("content://icc/adn"));
+ }
+
+ return intent.getData();
+ }
+
+ private void query() {
+ Uri uri = resolveIntent();
+ if (DBG) log("query: starting an async query");
+ mQueryHandler.startQuery(QUERY_TOKEN, null, uri, COLUMN_NAMES,
+ null, null, null);
+ displayProgress(true);
+ }
+
+ private void reQuery() {
+ query();
+ }
+
+ private void setAdapter() {
+ // NOTE:
+ // As it it written, the positioning code below is NOT working.
+ // However, this current non-working state is in compliance with
+ // the UI paradigm, so we can't really do much to change it.
+
+ // In the future, if we wish to get this "positioning" correct,
+ // we'll need to do the following:
+ // 1. Change the layout to in the cursor adapter to:
+ // android.R.layout.simple_list_item_checked
+ // 2. replace the selection / focus code with:
+ // getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ // getListView().setItemChecked(mInitialSelection, true);
+
+ // Since the positioning is really only useful for the dialer's
+ // SpecialCharSequence case (dialing '2#' to get to the 2nd
+ // contact for instance), it doesn't make sense to mess with
+ // the usability of the activity just for this case.
+
+ // These artifacts include:
+ // 1. UI artifacts (checkbox and highlight at the same time)
+ // 2. Allowing the user to edit / create new SIM contacts when
+ // the user is simply trying to retrieve a number into the d
+ // dialer.
+
+ if (mCursorAdapter == null) {
+ mCursorAdapter = newAdapter();
+
+ setListAdapter(mCursorAdapter);
+ } else {
+ mCursorAdapter.changeCursor(mCursor);
+ }
+
+ if (mInitialSelection >=0 && mInitialSelection < mCursorAdapter.getCount()) {
+ setSelection(mInitialSelection);
+ getListView().setFocusableInTouchMode(true);
+ boolean gotfocus = getListView().requestFocus();
+ }
+ }
+
+ protected CursorAdapter newAdapter() {
+ return new SimpleCursorAdapter(this,
+ android.R.layout.simple_list_item_2,
+ mCursor, COLUMN_NAMES, VIEW_NAMES);
+ }
+
+ private void displayProgress(boolean loading) {
+ if (DBG) log("displayProgress: " + loading);
+
+ mEmptyText.setText(loading ? R.string.simContacts_emptyLoading:
+ (isAirplaneModeOn(this) ? R.string.simContacts_airplaneMode :
+ R.string.simContacts_empty));
+ getWindow().setFeatureInt(
+ Window.FEATURE_INDETERMINATE_PROGRESS,
+ loading ? PROGRESS_VISIBILITY_ON : PROGRESS_VISIBILITY_OFF);
+ }
+
+ private static boolean isAirplaneModeOn(Context context) {
+ return Settings.System.getInt(context.getContentResolver(),
+ Settings.System.AIRPLANE_MODE_ON, 0) != 0;
+ }
+
+ private class QueryHandler extends AsyncQueryHandler {
+ public QueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor c) {
+ if (DBG) log("onQueryComplete: cursor.count=" + c.getCount());
+ mCursor = c;
+ setAdapter();
+ displayProgress(false);
+
+ // Cursor is refreshed and inherited classes may have menu items depending on it.
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ protected void onInsertComplete(int token, Object cookie, Uri uri) {
+ if (DBG) log("onInsertComplete: requery");
+ reQuery();
+ }
+
+ @Override
+ protected void onUpdateComplete(int token, Object cookie, int result) {
+ if (DBG) log("onUpdateComplete: requery");
+ reQuery();
+ }
+
+ @Override
+ protected void onDeleteComplete(int token, Object cookie, int result) {
+ if (DBG) log("onDeleteComplete: requery");
+ reQuery();
+ }
+ }
+
+ protected void log(String msg) {
+ Log.d(TAG, "[ADNList] " + msg);
+ }
+}
diff --git a/src/com/android/phone/AccelerometerListener.java b/src/com/android/phone/AccelerometerListener.java
new file mode 100644
index 0000000..49d4a72
--- /dev/null
+++ b/src/com/android/phone/AccelerometerListener.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.content.Context;
+import android.hardware.Sensor;
+import android.hardware.SensorEvent;
+import android.hardware.SensorEventListener;
+import android.hardware.SensorManager;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+/**
+ * This class is used to listen to the accelerometer to monitor the
+ * orientation of the phone. The client of this class is notified when
+ * the orientation changes between horizontal and vertical.
+ */
+public final class AccelerometerListener {
+ private static final String TAG = "AccelerometerListener";
+ private static final boolean DEBUG = true;
+ private static final boolean VDEBUG = false;
+
+ private SensorManager mSensorManager;
+ private Sensor mSensor;
+
+ // mOrientation is the orientation value most recently reported to the client.
+ private int mOrientation;
+
+ // mPendingOrientation is the latest orientation computed based on the sensor value.
+ // This is sent to the client after a rebounce delay, at which point it is copied to
+ // mOrientation.
+ private int mPendingOrientation;
+
+ private OrientationListener mListener;
+
+ // Device orientation
+ public static final int ORIENTATION_UNKNOWN = 0;
+ public static final int ORIENTATION_VERTICAL = 1;
+ public static final int ORIENTATION_HORIZONTAL = 2;
+
+ private static final int ORIENTATION_CHANGED = 1234;
+
+ private static final int VERTICAL_DEBOUNCE = 100;
+ private static final int HORIZONTAL_DEBOUNCE = 500;
+ private static final double VERTICAL_ANGLE = 50.0;
+
+ public interface OrientationListener {
+ public void orientationChanged(int orientation);
+ }
+
+ public AccelerometerListener(Context context, OrientationListener listener) {
+ mListener = listener;
+ mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
+ mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
+ }
+
+ public void enable(boolean enable) {
+ if (DEBUG) Log.d(TAG, "enable(" + enable + ")");
+ synchronized (this) {
+ if (enable) {
+ mOrientation = ORIENTATION_UNKNOWN;
+ mPendingOrientation = ORIENTATION_UNKNOWN;
+ mSensorManager.registerListener(mSensorListener, mSensor,
+ SensorManager.SENSOR_DELAY_NORMAL);
+ } else {
+ mSensorManager.unregisterListener(mSensorListener);
+ mHandler.removeMessages(ORIENTATION_CHANGED);
+ }
+ }
+ }
+
+ private void setOrientation(int orientation) {
+ synchronized (this) {
+ if (mPendingOrientation == orientation) {
+ // Pending orientation has not changed, so do nothing.
+ return;
+ }
+
+ // Cancel any pending messages.
+ // We will either start a new timer or cancel alltogether
+ // if the orientation has not changed.
+ mHandler.removeMessages(ORIENTATION_CHANGED);
+
+ if (mOrientation != orientation) {
+ // Set timer to send an event if the orientation has changed since its
+ // previously reported value.
+ mPendingOrientation = orientation;
+ Message m = mHandler.obtainMessage(ORIENTATION_CHANGED);
+ // set delay to our debounce timeout
+ int delay = (orientation == ORIENTATION_VERTICAL ? VERTICAL_DEBOUNCE
+ : HORIZONTAL_DEBOUNCE);
+ mHandler.sendMessageDelayed(m, delay);
+ } else {
+ // no message is pending
+ mPendingOrientation = ORIENTATION_UNKNOWN;
+ }
+ }
+ }
+
+ private void onSensorEvent(double x, double y, double z) {
+ if (VDEBUG) Log.d(TAG, "onSensorEvent(" + x + ", " + y + ", " + z + ")");
+
+ // If some values are exactly zero, then likely the sensor is not powered up yet.
+ // ignore these events to avoid false horizontal positives.
+ if (x == 0.0 || y == 0.0 || z == 0.0) return;
+
+ // magnitude of the acceleration vector projected onto XY plane
+ double xy = Math.sqrt(x*x + y*y);
+ // compute the vertical angle
+ double angle = Math.atan2(xy, z);
+ // convert to degrees
+ angle = angle * 180.0 / Math.PI;
+ int orientation = (angle > VERTICAL_ANGLE ? ORIENTATION_VERTICAL : ORIENTATION_HORIZONTAL);
+ if (VDEBUG) Log.d(TAG, "angle: " + angle + " orientation: " + orientation);
+ setOrientation(orientation);
+ }
+
+ SensorEventListener mSensorListener = new SensorEventListener() {
+ public void onSensorChanged(SensorEvent event) {
+ onSensorEvent(event.values[0], event.values[1], event.values[2]);
+ }
+
+ public void onAccuracyChanged(Sensor sensor, int accuracy) {
+ // ignore
+ }
+ };
+
+ Handler mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case ORIENTATION_CHANGED:
+ synchronized (this) {
+ mOrientation = mPendingOrientation;
+ if (DEBUG) {
+ Log.d(TAG, "orientation: " +
+ (mOrientation == ORIENTATION_HORIZONTAL ? "horizontal"
+ : (mOrientation == ORIENTATION_VERTICAL ? "vertical"
+ : "unknown")));
+ }
+ mListener.orientationChanged(mOrientation);
+ }
+ break;
+ }
+ }
+ };
+}
diff --git a/src/com/android/phone/AnimationUtils.java b/src/com/android/phone/AnimationUtils.java
new file mode 100644
index 0000000..f7d9e2e
--- /dev/null
+++ b/src/com/android/phone/AnimationUtils.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewPropertyAnimator;
+import android.widget.ImageView;
+
+/**
+ * Utilities for Animation.
+ */
+public class AnimationUtils {
+ private static final String LOG_TAG = AnimationUtils.class.getSimpleName();
+ /**
+ * Turn on when you're interested in fading animation. Intentionally untied from other debug
+ * settings.
+ */
+ private static final boolean FADE_DBG = false;
+
+ /**
+ * Duration for animations in msec, which can be used with
+ * {@link ViewPropertyAnimator#setDuration(long)} for example.
+ */
+ public static final int ANIMATION_DURATION = 250;
+
+ private AnimationUtils() {
+ }
+
+ /**
+ * Simple Utility class that runs fading animations on specified views.
+ */
+ public static class Fade {
+
+ // View tag that's set during the fade-out animation; see hide() and
+ // isFadingOut().
+ private static final int FADE_STATE_KEY = R.id.fadeState;
+ private static final String FADING_OUT = "fading_out";
+
+ /**
+ * Sets the visibility of the specified view to View.VISIBLE and then
+ * fades it in. If the view is already visible (and not in the middle
+ * of a fade-out animation), this method will return without doing
+ * anything.
+ *
+ * @param view The view to be faded in
+ */
+ public static void show(final View view) {
+ if (FADE_DBG) log("Fade: SHOW view " + view + "...");
+ if (FADE_DBG) log("Fade: - visibility = " + view.getVisibility());
+ if ((view.getVisibility() != View.VISIBLE) || isFadingOut(view)) {
+ view.animate().cancel();
+ // ...and clear the FADE_STATE_KEY tag in case we just
+ // canceled an in-progress fade-out animation.
+ view.setTag(FADE_STATE_KEY, null);
+
+ view.setAlpha(0);
+ view.setVisibility(View.VISIBLE);
+ view.animate().setDuration(ANIMATION_DURATION);
+ view.animate().alpha(1);
+ if (FADE_DBG) log("Fade: ==> SHOW " + view
+ + " DONE. Set visibility = " + View.VISIBLE);
+ } else {
+ if (FADE_DBG) log("Fade: ==> Ignoring, already visible AND not fading out.");
+ }
+ }
+
+ /**
+ * Fades out the specified view and then sets its visibility to the
+ * specified value (either View.INVISIBLE or View.GONE). If the view
+ * is not currently visibile, the method will return without doing
+ * anything.
+ *
+ * Note that *during* the fade-out the view itself will still have
+ * visibility View.VISIBLE, although the isFadingOut() method will
+ * return true (in case the UI code needs to detect this state.)
+ *
+ * @param view The view to be hidden
+ * @param visibility The value to which the view's visibility will be
+ * set after it fades out.
+ * Must be either View.INVISIBLE or View.GONE.
+ */
+ public static void hide(final View view, final int visibility) {
+ if (FADE_DBG) log("Fade: HIDE view " + view + "...");
+ if (view.getVisibility() == View.VISIBLE &&
+ (visibility == View.INVISIBLE || visibility == View.GONE)) {
+
+ // Use a view tag to mark this view as being in the middle
+ // of a fade-out animation.
+ view.setTag(FADE_STATE_KEY, FADING_OUT);
+
+ view.animate().cancel();
+ view.animate().setDuration(ANIMATION_DURATION);
+ view.animate().alpha(0f).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ view.setAlpha(1);
+ view.setVisibility(visibility);
+ view.animate().setListener(null);
+ // ...and we're done with the fade-out, so clear the view tag.
+ view.setTag(FADE_STATE_KEY, null);
+ if (FADE_DBG) log("Fade: HIDE " + view
+ + " DONE. Set visibility = " + visibility);
+ }
+ });
+ }
+ }
+
+ /**
+ * @return true if the specified view is currently in the middle
+ * of a fade-out animation. (During the fade-out, the view's
+ * visibility is still VISIBLE, although in many cases the UI
+ * should behave as if it's already invisible or gone. This
+ * method allows the UI code to detect that state.)
+ *
+ * @see #hide(View, int)
+ */
+ public static boolean isFadingOut(final View view) {
+ if (FADE_DBG) {
+ log("Fade: isFadingOut view " + view + "...");
+ log("Fade: - getTag() returns: " + view.getTag(FADE_STATE_KEY));
+ log("Fade: - returning: " + (view.getTag(FADE_STATE_KEY) == FADING_OUT));
+ }
+ return (view.getTag(FADE_STATE_KEY) == FADING_OUT);
+ }
+
+ }
+
+ /**
+ * Drawable achieving cross-fade, just like TransitionDrawable. We can have
+ * call-backs via animator object (see also {@link CrossFadeDrawable#getAnimator()}).
+ */
+ private static class CrossFadeDrawable extends LayerDrawable {
+ private final ObjectAnimator mAnimator;
+
+ public CrossFadeDrawable(Drawable[] layers) {
+ super(layers);
+ mAnimator = ObjectAnimator.ofInt(this, "crossFadeAlpha", 0xff, 0);
+ }
+
+ private int mCrossFadeAlpha;
+
+ /**
+ * This will be used from ObjectAnimator.
+ * Note: this method is protected by proguard.flags so that it won't be removed
+ * automatically.
+ */
+ @SuppressWarnings("unused")
+ public void setCrossFadeAlpha(int alpha) {
+ mCrossFadeAlpha = alpha;
+ invalidateSelf();
+ }
+
+ public ObjectAnimator getAnimator() {
+ return mAnimator;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ Drawable first = getDrawable(0);
+ Drawable second = getDrawable(1);
+
+ if (mCrossFadeAlpha > 0) {
+ first.setAlpha(mCrossFadeAlpha);
+ first.draw(canvas);
+ first.setAlpha(255);
+ }
+
+ if (mCrossFadeAlpha < 0xff) {
+ second.setAlpha(0xff - mCrossFadeAlpha);
+ second.draw(canvas);
+ second.setAlpha(0xff);
+ }
+ }
+ }
+
+ private static CrossFadeDrawable newCrossFadeDrawable(Drawable first, Drawable second) {
+ Drawable[] layers = new Drawable[2];
+ layers[0] = first;
+ layers[1] = second;
+ return new CrossFadeDrawable(layers);
+ }
+
+ /**
+ * Starts cross-fade animation using TransitionDrawable. Nothing will happen if "from" and "to"
+ * are the same.
+ */
+ public static void startCrossFade(
+ final ImageView imageView, final Drawable from, final Drawable to) {
+ // We skip the cross-fade when those two Drawables are equal, or they are BitmapDrawables
+ // pointing to the same Bitmap.
+ final boolean areSameImage = from.equals(to) ||
+ ((from instanceof BitmapDrawable)
+ && (to instanceof BitmapDrawable)
+ && ((BitmapDrawable) from).getBitmap()
+ .equals(((BitmapDrawable) to).getBitmap()));
+ if (!areSameImage) {
+ if (FADE_DBG) {
+ log("Start cross-fade animation for " + imageView
+ + "(" + Integer.toHexString(from.hashCode()) + " -> "
+ + Integer.toHexString(to.hashCode()) + ")");
+ }
+
+ CrossFadeDrawable crossFadeDrawable = newCrossFadeDrawable(from, to);
+ ObjectAnimator animator = crossFadeDrawable.getAnimator();
+ imageView.setImageDrawable(crossFadeDrawable);
+ animator.setDuration(ANIMATION_DURATION);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (FADE_DBG) {
+ log("cross-fade animation start ("
+ + Integer.toHexString(from.hashCode()) + " -> "
+ + Integer.toHexString(to.hashCode()) + ")");
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (FADE_DBG) {
+ log("cross-fade animation ended ("
+ + Integer.toHexString(from.hashCode()) + " -> "
+ + Integer.toHexString(to.hashCode()) + ")");
+ }
+ animation.removeAllListeners();
+ // Workaround for issue 6300562; this will force the drawable to the
+ // resultant one regardless of animation glitch.
+ imageView.setImageDrawable(to);
+ }
+ });
+ animator.start();
+
+ /* We could use TransitionDrawable here, but it may cause some weird animation in
+ * some corner cases. See issue 6300562
+ * TODO: decide which to be used in the long run. TransitionDrawable is old but system
+ * one. Ours uses new animation framework and thus have callback (great for testing),
+ * while no framework support for the exact class.
+
+ Drawable[] layers = new Drawable[2];
+ layers[0] = from;
+ layers[1] = to;
+ TransitionDrawable transitionDrawable = new TransitionDrawable(layers);
+ imageView.setImageDrawable(transitionDrawable);
+ transitionDrawable.startTransition(ANIMATION_DURATION); */
+ imageView.setTag(to);
+ } else {
+ if (FADE_DBG) {
+ log("*Not* start cross-fade. " + imageView);
+ }
+ }
+ }
+
+ // Debugging / testing code
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/BitmapUtils.java b/src/com/android/phone/BitmapUtils.java
new file mode 100644
index 0000000..94d4bf9
--- /dev/null
+++ b/src/com/android/phone/BitmapUtils.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2011 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.phone;
+
+import android.graphics.Bitmap;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.util.Log;
+
+
+/**
+ * Image effects used by the in-call UI.
+ */
+public class BitmapUtils {
+ private static final String TAG = "BitmapUtils";
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+
+ /** This class is never instantiated. */
+ private BitmapUtils() {
+ }
+
+ //
+ // Gaussian blur effect
+ //
+ // gaussianBlur() and related methods are borrowed from
+ // BackgroundUtils.java in the Music2 code (which itself was based on
+ // code from the old Cooliris android Gallery app.)
+ //
+ // TODO: possibly consider caching previously-generated blurred bitmaps;
+ // see getAdaptedBitmap() and mAdaptedBitmapCache in the music app code.
+ //
+
+ private static final int RED_MASK = 0xff0000;
+ private static final int RED_MASK_SHIFT = 16;
+ private static final int GREEN_MASK = 0x00ff00;
+ private static final int GREEN_MASK_SHIFT = 8;
+ private static final int BLUE_MASK = 0x0000ff;
+
+ /**
+ * Creates a blurred version of the given Bitmap.
+ *
+ * @param bitmap the input bitmap, presumably a 96x96 pixel contact
+ * thumbnail.
+ */
+ public static Bitmap createBlurredBitmap(Bitmap bitmap) {
+ if (DBG) log("createBlurredBitmap()...");
+ long startTime = SystemClock.uptimeMillis();
+ if (bitmap == null) {
+ Log.w(TAG, "createBlurredBitmap: null bitmap");
+ return null;
+ }
+
+ if (DBG) log("- input bitmap: " + bitmap.getWidth() + " x " + bitmap.getHeight());
+
+ // The bitmap we pass to gaussianBlur() needs to have a width
+ // that's a power of 2, so scale up to 128x128.
+ final int scaledSize = 128;
+ bitmap = Bitmap.createScaledBitmap(bitmap,
+ scaledSize, scaledSize,
+ true /* filter */);
+ if (DBG) log("- after resize: " + bitmap.getWidth() + " x " + bitmap.getHeight());
+
+ bitmap = gaussianBlur(bitmap);
+ if (DBG) log("- after blur: " + bitmap.getWidth() + " x " + bitmap.getHeight());
+
+ long endTime = SystemClock.uptimeMillis();
+ if (DBG) log("createBlurredBitmap() done (elapsed = " + (endTime - startTime) + " msec)");
+ return bitmap;
+ }
+
+ /**
+ * Apply a gaussian blur filter, and return a new (blurred) bitmap
+ * that's the same size as the input bitmap.
+ *
+ * @param source input bitmap, whose width must be a power of 2
+ */
+ public static Bitmap gaussianBlur(Bitmap source) {
+ int width = source.getWidth();
+ int height = source.getHeight();
+ if (DBG) log("gaussianBlur(): input: " + width + " x " + height);
+
+ // Create a source and destination buffer for the image.
+ int numPixels = width * height;
+ int[] in = new int[numPixels];
+ int[] tmp = new int[numPixels];
+
+ // Get the source pixels as 32-bit ARGB.
+ source.getPixels(in, 0, width, 0, 0, width, height);
+
+ // Gaussian is a separable kernel, so it is decomposed into a horizontal
+ // and vertical pass.
+ // The filter function applies the kernel across each row and transposes
+ // the output.
+ // Hence we apply it twice to provide efficient horizontal and vertical
+ // convolution.
+ // The filter discards the alpha channel.
+ gaussianBlurFilter(in, tmp, width, height);
+ gaussianBlurFilter(tmp, in, width, height);
+
+ // Return a bitmap scaled to the desired size.
+ Bitmap filtered = Bitmap.createBitmap(in, width, height, Bitmap.Config.ARGB_8888);
+ source.recycle();
+ return filtered;
+ }
+
+ private static void gaussianBlurFilter(int[] in, int[] out, int width, int height) {
+ // This function is currently hardcoded to blur with RADIUS = 4.
+ // (If you change RADIUS, you'll have to change the weights[] too.)
+ final int RADIUS = 4;
+ final int[] weights = { 13, 23, 32, 39, 42, 39, 32, 23, 13}; // Adds up to 256
+ int inPos = 0;
+ int widthMask = width - 1; // width must be a power of two.
+ for (int y = 0; y < height; ++y) {
+ // Compute the alpha value.
+ int alpha = 0xff;
+ // Compute output values for the row.
+ int outPos = y;
+ for (int x = 0; x < width; ++x) {
+ int red = 0;
+ int green = 0;
+ int blue = 0;
+ for (int i = -RADIUS; i <= RADIUS; ++i) {
+ int argb = in[inPos + (widthMask & (x + i))];
+ int weight = weights[i+RADIUS];
+ red += weight *((argb & RED_MASK) >> RED_MASK_SHIFT);
+ green += weight *((argb & GREEN_MASK) >> GREEN_MASK_SHIFT);
+ blue += weight *(argb & BLUE_MASK);
+ }
+ // Output the current pixel.
+ out[outPos] = (alpha << 24) | ((red >> 8) << RED_MASK_SHIFT)
+ | ((green >> 8) << GREEN_MASK_SHIFT)
+ | (blue >> 8);
+ outPos += height;
+ }
+ inPos += width;
+ }
+ }
+
+ //
+ // Debugging
+ //
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/BluetoothPhoneService.java b/src/com/android/phone/BluetoothPhoneService.java
new file mode 100644
index 0000000..aff6bf2
--- /dev/null
+++ b/src/com/android/phone/BluetoothPhoneService.java
@@ -0,0 +1,903 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import android.app.Service;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.IBluetoothHeadsetPhone;
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.PowerManager.WakeLock;
+import android.os.SystemProperties;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.ServiceState;
+import android.util.Log;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.CallManager;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Bluetooth headset manager for the Phone app.
+ * @hide
+ */
+public class BluetoothPhoneService extends Service {
+ private static final String TAG = "BluetoothPhoneService";
+ private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 1)
+ && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2); // even more logging
+
+ private static final String MODIFY_PHONE_STATE = android.Manifest.permission.MODIFY_PHONE_STATE;
+
+ private BluetoothAdapter mAdapter;
+ private CallManager mCM;
+
+ private BluetoothHeadset mBluetoothHeadset;
+
+ private PowerManager mPowerManager;
+
+ private WakeLock mStartCallWakeLock; // held while waiting for the intent to start call
+
+ private PhoneConstants.State mPhoneState = PhoneConstants.State.IDLE;
+ CdmaPhoneCallState.PhoneCallState mCdmaThreeWayCallState =
+ CdmaPhoneCallState.PhoneCallState.IDLE;
+
+ private Call.State mForegroundCallState;
+ private Call.State mRingingCallState;
+ private CallNumber mRingNumber;
+ // number of active calls
+ int mNumActive;
+ // number of background (held) calls
+ int mNumHeld;
+
+ long mBgndEarliestConnectionTime = 0;
+
+ // CDMA specific flag used in context with BT devices having display capabilities
+ // to show which Caller is active. This state might not be always true as in CDMA
+ // networks if a caller drops off no update is provided to the Phone.
+ // This flag is just used as a toggle to provide a update to the BT device to specify
+ // which caller is active.
+ private boolean mCdmaIsSecondCallActive = false;
+ private boolean mCdmaCallsSwapped = false;
+
+ private long[] mClccTimestamps; // Timestamps associated with each clcc index
+ private boolean[] mClccUsed; // Is this clcc index in use
+
+ private static final int GSM_MAX_CONNECTIONS = 6; // Max connections allowed by GSM
+ private static final int CDMA_MAX_CONNECTIONS = 2; // Max connections allowed by CDMA
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mCM = CallManager.getInstance();
+ mAdapter = BluetoothAdapter.getDefaultAdapter();
+ if (mAdapter == null) {
+ if (VDBG) Log.d(TAG, "mAdapter null");
+ return;
+ }
+
+ mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ mStartCallWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
+ TAG + ":StartCall");
+ mStartCallWakeLock.setReferenceCounted(false);
+
+ mAdapter.getProfileProxy(this, mProfileListener, BluetoothProfile.HEADSET);
+
+ mForegroundCallState = Call.State.IDLE;
+ mRingingCallState = Call.State.IDLE;
+ mNumActive = 0;
+ mNumHeld = 0;
+ mRingNumber = new CallNumber("", 0);;
+
+ handlePreciseCallStateChange(null);
+
+ if(VDBG) Log.d(TAG, "registerForServiceStateChanged");
+ // register for updates
+ mCM.registerForPreciseCallStateChanged(mHandler,
+ PRECISE_CALL_STATE_CHANGED, null);
+ mCM.registerForCallWaiting(mHandler,
+ PHONE_CDMA_CALL_WAITING, null);
+ // TODO(BT) registerForIncomingRing?
+ // TODO(BT) registerdisconnection?
+ mClccTimestamps = new long[GSM_MAX_CONNECTIONS];
+ mClccUsed = new boolean[GSM_MAX_CONNECTIONS];
+ for (int i = 0; i < GSM_MAX_CONNECTIONS; i++) {
+ mClccUsed[i] = false;
+ }
+ }
+
+ @Override
+ public void onStart(Intent intent, int startId) {
+ if (mAdapter == null) {
+ Log.w(TAG, "Stopping Bluetooth BluetoothPhoneService Service: device does not have BT");
+ stopSelf();
+ }
+ if (VDBG) Log.d(TAG, "BluetoothPhoneService started");
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (DBG) log("Stopping Bluetooth BluetoothPhoneService Service");
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ private static final int PRECISE_CALL_STATE_CHANGED = 1;
+ private static final int PHONE_CDMA_CALL_WAITING = 2;
+ private static final int LIST_CURRENT_CALLS = 3;
+ private static final int QUERY_PHONE_STATE = 4;
+ private static final int CDMA_SWAP_SECOND_CALL_STATE = 5;
+ private static final int CDMA_SET_SECOND_CALL_STATE = 6;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (VDBG) Log.d(TAG, "handleMessage: " + msg.what);
+ switch(msg.what) {
+ case PRECISE_CALL_STATE_CHANGED:
+ case PHONE_CDMA_CALL_WAITING:
+ Connection connection = null;
+ if (((AsyncResult) msg.obj).result instanceof Connection) {
+ connection = (Connection) ((AsyncResult) msg.obj).result;
+ }
+ handlePreciseCallStateChange(connection);
+ break;
+ case LIST_CURRENT_CALLS:
+ handleListCurrentCalls();
+ break;
+ case QUERY_PHONE_STATE:
+ handleQueryPhoneState();
+ break;
+ case CDMA_SWAP_SECOND_CALL_STATE:
+ handleCdmaSwapSecondCallState();
+ break;
+ case CDMA_SET_SECOND_CALL_STATE:
+ handleCdmaSetSecondCallState((Boolean) msg.obj);
+ break;
+ }
+ }
+ };
+
+ private void updateBtPhoneStateAfterRadioTechnologyChange() {
+ if(VDBG) Log.d(TAG, "updateBtPhoneStateAfterRadioTechnologyChange...");
+
+ //Unregister all events from the old obsolete phone
+ mCM.unregisterForPreciseCallStateChanged(mHandler);
+ mCM.unregisterForCallWaiting(mHandler);
+
+ //Register all events new to the new active phone
+ mCM.registerForPreciseCallStateChanged(mHandler,
+ PRECISE_CALL_STATE_CHANGED, null);
+ mCM.registerForCallWaiting(mHandler,
+ PHONE_CDMA_CALL_WAITING, null);
+ }
+
+ private void handlePreciseCallStateChange(Connection connection) {
+ // get foreground call state
+ int oldNumActive = mNumActive;
+ int oldNumHeld = mNumHeld;
+ Call.State oldRingingCallState = mRingingCallState;
+ Call.State oldForegroundCallState = mForegroundCallState;
+ CallNumber oldRingNumber = mRingNumber;
+
+ Call foregroundCall = mCM.getActiveFgCall();
+
+ if (VDBG)
+ Log.d(TAG, " handlePreciseCallStateChange: foreground: " + foregroundCall +
+ " background: " + mCM.getFirstActiveBgCall() + " ringing: " +
+ mCM.getFirstActiveRingingCall());
+
+ mForegroundCallState = foregroundCall.getState();
+ /* if in transition, do not update */
+ if (mForegroundCallState == Call.State.DISCONNECTING)
+ {
+ Log.d(TAG, "handlePreciseCallStateChange. Call disconnecting, wait before update");
+ return;
+ }
+ else
+ mNumActive = (mForegroundCallState == Call.State.ACTIVE) ? 1 : 0;
+
+ Call ringingCall = mCM.getFirstActiveRingingCall();
+ mRingingCallState = ringingCall.getState();
+ mRingNumber = getCallNumber(connection, ringingCall);
+
+ if (mCM.getDefaultPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ mNumHeld = getNumHeldCdma();
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ if (app.cdmaPhoneCallState != null) {
+ CdmaPhoneCallState.PhoneCallState currCdmaThreeWayCallState =
+ app.cdmaPhoneCallState.getCurrentCallState();
+ CdmaPhoneCallState.PhoneCallState prevCdmaThreeWayCallState =
+ app.cdmaPhoneCallState.getPreviousCallState();
+
+ log("CDMA call state: " + currCdmaThreeWayCallState + " prev state:" +
+ prevCdmaThreeWayCallState);
+
+ if ((mBluetoothHeadset != null) &&
+ (mCdmaThreeWayCallState != currCdmaThreeWayCallState)) {
+ // In CDMA, the network does not provide any feedback
+ // to the phone when the 2nd MO call goes through the
+ // stages of DIALING > ALERTING -> ACTIVE we fake the
+ // sequence
+ log("CDMA 3way call state change. mNumActive: " + mNumActive +
+ " mNumHeld: " + mNumHeld + " IsThreeWayCallOrigStateDialing: " +
+ app.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing());
+ if ((currCdmaThreeWayCallState ==
+ CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE)
+ && app.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) {
+ // Mimic dialing, put the call on hold, alerting
+ mBluetoothHeadset.phoneStateChanged(0, mNumHeld,
+ convertCallState(Call.State.IDLE, Call.State.DIALING),
+ mRingNumber.mNumber, mRingNumber.mType);
+
+ mBluetoothHeadset.phoneStateChanged(0, mNumHeld,
+ convertCallState(Call.State.IDLE, Call.State.ALERTING),
+ mRingNumber.mNumber, mRingNumber.mType);
+
+ }
+
+ // In CDMA, the network does not provide any feedback to
+ // the phone when a user merges a 3way call or swaps
+ // between two calls we need to send a CIEV response
+ // indicating that a call state got changed which should
+ // trigger a CLCC update request from the BT client.
+ if (currCdmaThreeWayCallState ==
+ CdmaPhoneCallState.PhoneCallState.CONF_CALL &&
+ prevCdmaThreeWayCallState ==
+ CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) {
+ log("CDMA 3way conf call. mNumActive: " + mNumActive +
+ " mNumHeld: " + mNumHeld);
+ mBluetoothHeadset.phoneStateChanged(mNumActive, mNumHeld,
+ convertCallState(Call.State.IDLE, mForegroundCallState),
+ mRingNumber.mNumber, mRingNumber.mType);
+ }
+ }
+ mCdmaThreeWayCallState = currCdmaThreeWayCallState;
+ }
+ } else {
+ mNumHeld = getNumHeldUmts();
+ }
+
+ boolean callsSwitched = false;
+ if (mCM.getDefaultPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA &&
+ mCdmaThreeWayCallState == CdmaPhoneCallState.PhoneCallState.CONF_CALL) {
+ callsSwitched = mCdmaCallsSwapped;
+ } else {
+ Call backgroundCall = mCM.getFirstActiveBgCall();
+ callsSwitched =
+ (mNumHeld == 1 && ! (backgroundCall.getEarliestConnectTime() ==
+ mBgndEarliestConnectionTime));
+ mBgndEarliestConnectionTime = backgroundCall.getEarliestConnectTime();
+ }
+
+ if (mNumActive != oldNumActive || mNumHeld != oldNumHeld ||
+ mRingingCallState != oldRingingCallState ||
+ mForegroundCallState != oldForegroundCallState ||
+ !mRingNumber.equalTo(oldRingNumber) ||
+ callsSwitched) {
+ if (mBluetoothHeadset != null) {
+ mBluetoothHeadset.phoneStateChanged(mNumActive, mNumHeld,
+ convertCallState(mRingingCallState, mForegroundCallState),
+ mRingNumber.mNumber, mRingNumber.mType);
+ }
+ }
+ }
+
+ private void handleListCurrentCalls() {
+ Phone phone = mCM.getDefaultPhone();
+ int phoneType = phone.getPhoneType();
+
+ // TODO(BT) handle virtual call
+
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ listCurrentCallsCdma();
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+ listCurrentCallsGsm();
+ } else {
+ Log.e(TAG, "Unexpected phone type: " + phoneType);
+ }
+ // end the result
+ // when index is 0, other parameter does not matter
+ mBluetoothHeadset.clccResponse(0, 0, 0, 0, false, "", 0);
+ }
+
+ private void handleQueryPhoneState() {
+ if (mBluetoothHeadset != null) {
+ mBluetoothHeadset.phoneStateChanged(mNumActive, mNumHeld,
+ convertCallState(mRingingCallState, mForegroundCallState),
+ mRingNumber.mNumber, mRingNumber.mType);
+ }
+ }
+
+ private int getNumHeldUmts() {
+ int countHeld = 0;
+ List<Call> heldCalls = mCM.getBackgroundCalls();
+
+ for (Call call : heldCalls) {
+ if (call.getState() == Call.State.HOLDING) {
+ countHeld++;
+ }
+ }
+ return countHeld;
+ }
+
+ private int getNumHeldCdma() {
+ int numHeld = 0;
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ if (app.cdmaPhoneCallState != null) {
+ CdmaPhoneCallState.PhoneCallState curr3WayCallState =
+ app.cdmaPhoneCallState.getCurrentCallState();
+ CdmaPhoneCallState.PhoneCallState prev3WayCallState =
+ app.cdmaPhoneCallState.getPreviousCallState();
+
+ log("CDMA call state: " + curr3WayCallState + " prev state:" +
+ prev3WayCallState);
+ if (curr3WayCallState == CdmaPhoneCallState.PhoneCallState.CONF_CALL) {
+ if (prev3WayCallState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) {
+ numHeld = 0; //0: no calls held, as now *both* the caller are active
+ } else {
+ numHeld = 1; //1: held call and active call, as on answering a
+ // Call Waiting, one of the caller *is* put on hold
+ }
+ } else if (curr3WayCallState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) {
+ numHeld = 1; //1: held call and active call, as on make a 3 Way Call
+ // the first caller *is* put on hold
+ } else {
+ numHeld = 0; //0: no calls held as this is a SINGLE_ACTIVE call
+ }
+ }
+ return numHeld;
+ }
+
+ private CallNumber getCallNumber(Connection connection, Call call) {
+ String number = null;
+ int type = 128;
+ // find phone number and type
+ if (connection == null) {
+ connection = call.getEarliestConnection();
+ if (connection == null) {
+ Log.e(TAG, "Could not get a handle on Connection object for the call");
+ }
+ }
+ if (connection != null) {
+ number = connection.getAddress();
+ if (number != null) {
+ type = PhoneNumberUtils.toaFromString(number);
+ }
+ }
+ if (number == null) {
+ number = "";
+ }
+ return new CallNumber(number, type);
+ }
+
+ private class CallNumber
+ {
+ private String mNumber = null;
+ private int mType = 0;
+
+ private CallNumber(String number, int type) {
+ mNumber = number;
+ mType = type;
+ }
+
+ private boolean equalTo(CallNumber callNumber)
+ {
+ if (mType != callNumber.mType) return false;
+
+ if (mNumber != null && mNumber.compareTo(callNumber.mNumber) == 0) {
+ return true;
+ }
+ return false;
+ }
+ }
+
+ private BluetoothProfile.ServiceListener mProfileListener =
+ new BluetoothProfile.ServiceListener() {
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ mBluetoothHeadset = (BluetoothHeadset) proxy;
+ }
+ public void onServiceDisconnected(int profile) {
+ mBluetoothHeadset = null;
+ }
+ };
+
+ private void listCurrentCallsGsm() {
+ // Collect all known connections
+ // clccConnections isindexed by CLCC index
+ Connection[] clccConnections = new Connection[GSM_MAX_CONNECTIONS];
+ LinkedList<Connection> newConnections = new LinkedList<Connection>();
+ LinkedList<Connection> connections = new LinkedList<Connection>();
+
+ Call foregroundCall = mCM.getActiveFgCall();
+ Call backgroundCall = mCM.getFirstActiveBgCall();
+ Call ringingCall = mCM.getFirstActiveRingingCall();
+
+ if (ringingCall.getState().isAlive()) {
+ connections.addAll(ringingCall.getConnections());
+ }
+ if (foregroundCall.getState().isAlive()) {
+ connections.addAll(foregroundCall.getConnections());
+ }
+ if (backgroundCall.getState().isAlive()) {
+ connections.addAll(backgroundCall.getConnections());
+ }
+
+ // Mark connections that we already known about
+ boolean clccUsed[] = new boolean[GSM_MAX_CONNECTIONS];
+ for (int i = 0; i < GSM_MAX_CONNECTIONS; i++) {
+ clccUsed[i] = mClccUsed[i];
+ mClccUsed[i] = false;
+ }
+ for (Connection c : connections) {
+ boolean found = false;
+ long timestamp = c.getCreateTime();
+ for (int i = 0; i < GSM_MAX_CONNECTIONS; i++) {
+ if (clccUsed[i] && timestamp == mClccTimestamps[i]) {
+ mClccUsed[i] = true;
+ found = true;
+ clccConnections[i] = c;
+ break;
+ }
+ }
+ if (!found) {
+ newConnections.add(c);
+ }
+ }
+
+ // Find a CLCC index for new connections
+ while (!newConnections.isEmpty()) {
+ // Find lowest empty index
+ int i = 0;
+ while (mClccUsed[i]) i++;
+ // Find earliest connection
+ long earliestTimestamp = newConnections.get(0).getCreateTime();
+ Connection earliestConnection = newConnections.get(0);
+ for (int j = 0; j < newConnections.size(); j++) {
+ long timestamp = newConnections.get(j).getCreateTime();
+ if (timestamp < earliestTimestamp) {
+ earliestTimestamp = timestamp;
+ earliestConnection = newConnections.get(j);
+ }
+ }
+
+ // update
+ mClccUsed[i] = true;
+ mClccTimestamps[i] = earliestTimestamp;
+ clccConnections[i] = earliestConnection;
+ newConnections.remove(earliestConnection);
+ }
+
+ // Send CLCC response to Bluetooth headset service
+ for (int i = 0; i < clccConnections.length; i++) {
+ if (mClccUsed[i]) {
+ sendClccResponseGsm(i, clccConnections[i]);
+ }
+ }
+ }
+
+ /** Convert a Connection object into a single +CLCC result */
+ private void sendClccResponseGsm(int index, Connection connection) {
+ int state = convertCallState(connection.getState());
+ boolean mpty = false;
+ Call call = connection.getCall();
+ if (call != null) {
+ mpty = call.isMultiparty();
+ }
+
+ int direction = connection.isIncoming() ? 1 : 0;
+
+ String number = connection.getAddress();
+ int type = -1;
+ if (number != null) {
+ type = PhoneNumberUtils.toaFromString(number);
+ }
+
+ mBluetoothHeadset.clccResponse(index + 1, direction, state, 0, mpty, number, type);
+ }
+
+ /** Build the +CLCC result for CDMA
+ * The complexity arises from the fact that we need to maintain the same
+ * CLCC index even as a call moves between states. */
+ private synchronized void listCurrentCallsCdma() {
+ // In CDMA at one time a user can have only two live/active connections
+ Connection[] clccConnections = new Connection[CDMA_MAX_CONNECTIONS];// indexed by CLCC index
+ Call foregroundCall = mCM.getActiveFgCall();
+ Call ringingCall = mCM.getFirstActiveRingingCall();
+
+ Call.State ringingCallState = ringingCall.getState();
+ // If the Ringing Call state is INCOMING, that means this is the very first call
+ // hence there should not be any Foreground Call
+ if (ringingCallState == Call.State.INCOMING) {
+ if (VDBG) log("Filling clccConnections[0] for INCOMING state");
+ clccConnections[0] = ringingCall.getLatestConnection();
+ } else if (foregroundCall.getState().isAlive()) {
+ // Getting Foreground Call connection based on Call state
+ if (ringingCall.isRinging()) {
+ if (VDBG) log("Filling clccConnections[0] & [1] for CALL WAITING state");
+ clccConnections[0] = foregroundCall.getEarliestConnection();
+ clccConnections[1] = ringingCall.getLatestConnection();
+ } else {
+ if (foregroundCall.getConnections().size() <= 1) {
+ // Single call scenario
+ if (VDBG) {
+ log("Filling clccConnections[0] with ForgroundCall latest connection");
+ }
+ clccConnections[0] = foregroundCall.getLatestConnection();
+ } else {
+ // Multiple Call scenario. This would be true for both
+ // CONF_CALL and THRWAY_ACTIVE state
+ if (VDBG) {
+ log("Filling clccConnections[0] & [1] with ForgroundCall connections");
+ }
+ clccConnections[0] = foregroundCall.getEarliestConnection();
+ clccConnections[1] = foregroundCall.getLatestConnection();
+ }
+ }
+ }
+
+ // Update the mCdmaIsSecondCallActive flag based on the Phone call state
+ if (PhoneGlobals.getInstance().cdmaPhoneCallState.getCurrentCallState()
+ == CdmaPhoneCallState.PhoneCallState.SINGLE_ACTIVE) {
+ Message msg = mHandler.obtainMessage(CDMA_SET_SECOND_CALL_STATE, false);
+ mHandler.sendMessage(msg);
+ } else if (PhoneGlobals.getInstance().cdmaPhoneCallState.getCurrentCallState()
+ == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) {
+ Message msg = mHandler.obtainMessage(CDMA_SET_SECOND_CALL_STATE, true);
+ mHandler.sendMessage(msg);
+ }
+
+ // send CLCC result
+ for (int i = 0; (i < clccConnections.length) && (clccConnections[i] != null); i++) {
+ sendClccResponseCdma(i, clccConnections[i]);
+ }
+ }
+
+ /** Send ClCC results for a Connection object for CDMA phone */
+ private void sendClccResponseCdma(int index, Connection connection) {
+ int state;
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ CdmaPhoneCallState.PhoneCallState currCdmaCallState =
+ app.cdmaPhoneCallState.getCurrentCallState();
+ CdmaPhoneCallState.PhoneCallState prevCdmaCallState =
+ app.cdmaPhoneCallState.getPreviousCallState();
+
+ if ((prevCdmaCallState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE)
+ && (currCdmaCallState == CdmaPhoneCallState.PhoneCallState.CONF_CALL)) {
+ // If the current state is reached after merging two calls
+ // we set the state of all the connections as ACTIVE
+ state = CALL_STATE_ACTIVE;
+ } else {
+ Call.State callState = connection.getState();
+ switch (callState) {
+ case ACTIVE:
+ // For CDMA since both the connections are set as active by FW after accepting
+ // a Call waiting or making a 3 way call, we need to set the state specifically
+ // to ACTIVE/HOLDING based on the mCdmaIsSecondCallActive flag. This way the
+ // CLCC result will allow BT devices to enable the swap or merge options
+ if (index == 0) { // For the 1st active connection
+ state = mCdmaIsSecondCallActive ? CALL_STATE_HELD : CALL_STATE_ACTIVE;
+ } else { // for the 2nd active connection
+ state = mCdmaIsSecondCallActive ? CALL_STATE_ACTIVE : CALL_STATE_HELD;
+ }
+ break;
+ case HOLDING:
+ state = CALL_STATE_HELD;
+ break;
+ case DIALING:
+ state = CALL_STATE_DIALING;
+ break;
+ case ALERTING:
+ state = CALL_STATE_ALERTING;
+ break;
+ case INCOMING:
+ state = CALL_STATE_INCOMING;
+ break;
+ case WAITING:
+ state = CALL_STATE_WAITING;
+ break;
+ default:
+ Log.e(TAG, "bad call state: " + callState);
+ return;
+ }
+ }
+
+ boolean mpty = false;
+ if (currCdmaCallState == CdmaPhoneCallState.PhoneCallState.CONF_CALL) {
+ if (prevCdmaCallState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) {
+ // If the current state is reached after merging two calls
+ // we set the multiparty call true.
+ mpty = true;
+ } // else
+ // CALL_CONF state is not from merging two calls, but from
+ // accepting the second call. In this case first will be on
+ // hold in most cases but in some cases its already merged.
+ // However, we will follow the common case and the test case
+ // as per Bluetooth SIG PTS
+ }
+
+ int direction = connection.isIncoming() ? 1 : 0;
+
+ String number = connection.getAddress();
+ int type = -1;
+ if (number != null) {
+ type = PhoneNumberUtils.toaFromString(number);
+ } else {
+ number = "";
+ }
+
+ mBluetoothHeadset.clccResponse(index + 1, direction, state, 0, mpty, number, type);
+ }
+
+ private void handleCdmaSwapSecondCallState() {
+ if (VDBG) log("cdmaSwapSecondCallState: Toggling mCdmaIsSecondCallActive");
+ mCdmaIsSecondCallActive = !mCdmaIsSecondCallActive;
+ mCdmaCallsSwapped = true;
+ }
+
+ private void handleCdmaSetSecondCallState(boolean state) {
+ if (VDBG) log("cdmaSetSecondCallState: Setting mCdmaIsSecondCallActive to " + state);
+ mCdmaIsSecondCallActive = state;
+
+ if (!mCdmaIsSecondCallActive) {
+ mCdmaCallsSwapped = false;
+ }
+ }
+
+ private final IBluetoothHeadsetPhone.Stub mBinder = new IBluetoothHeadsetPhone.Stub() {
+ public boolean answerCall() {
+ enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
+ return PhoneUtils.answerCall(mCM.getFirstActiveRingingCall());
+ }
+
+ public boolean hangupCall() {
+ enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
+ if (mCM.hasActiveFgCall()) {
+ return PhoneUtils.hangupActiveCall(mCM.getActiveFgCall());
+ } else if (mCM.hasActiveRingingCall()) {
+ return PhoneUtils.hangupRingingCall(mCM.getFirstActiveRingingCall());
+ } else if (mCM.hasActiveBgCall()) {
+ return PhoneUtils.hangupHoldingCall(mCM.getFirstActiveBgCall());
+ }
+ // TODO(BT) handle virtual voice call
+ return false;
+ }
+
+ public boolean sendDtmf(int dtmf) {
+ enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
+ return mCM.sendDtmf((char) dtmf);
+ }
+
+ public boolean processChld(int chld) {
+ enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
+ Phone phone = mCM.getDefaultPhone();
+ int phoneType = phone.getPhoneType();
+ Call ringingCall = mCM.getFirstActiveRingingCall();
+ Call backgroundCall = mCM.getFirstActiveBgCall();
+
+ if (chld == CHLD_TYPE_RELEASEHELD) {
+ if (ringingCall.isRinging()) {
+ return PhoneUtils.hangupRingingCall(ringingCall);
+ } else {
+ return PhoneUtils.hangupHoldingCall(backgroundCall);
+ }
+ } else if (chld == CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD) {
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ if (ringingCall.isRinging()) {
+ // Hangup the active call and then answer call waiting call.
+ if (VDBG) log("CHLD:1 Callwaiting Answer call");
+ PhoneUtils.hangupRingingAndActive(phone);
+ } else {
+ // If there is no Call waiting then just hangup
+ // the active call. In CDMA this mean that the complete
+ // call session would be ended
+ if (VDBG) log("CHLD:1 Hangup Call");
+ PhoneUtils.hangup(PhoneGlobals.getInstance().mCM);
+ }
+ return true;
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+ // Hangup active call, answer held call
+ return PhoneUtils.answerAndEndActive(PhoneGlobals.getInstance().mCM, ringingCall);
+ } else {
+ Log.e(TAG, "bad phone type: " + phoneType);
+ return false;
+ }
+ } else if (chld == CHLD_TYPE_HOLDACTIVE_ACCEPTHELD) {
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ // For CDMA, the way we switch to a new incoming call is by
+ // calling PhoneUtils.answerCall(). switchAndHoldActive() won't
+ // properly update the call state within telephony.
+ // If the Phone state is already in CONF_CALL then we simply send
+ // a flash cmd by calling switchHoldingAndActive()
+ if (ringingCall.isRinging()) {
+ if (VDBG) log("CHLD:2 Callwaiting Answer call");
+ PhoneUtils.answerCall(ringingCall);
+ PhoneUtils.setMute(false);
+ // Setting the second callers state flag to TRUE (i.e. active)
+ cdmaSetSecondCallState(true);
+ return true;
+ } else if (PhoneGlobals.getInstance().cdmaPhoneCallState
+ .getCurrentCallState()
+ == CdmaPhoneCallState.PhoneCallState.CONF_CALL) {
+ if (VDBG) log("CHLD:2 Swap Calls");
+ PhoneUtils.switchHoldingAndActive(backgroundCall);
+ // Toggle the second callers active state flag
+ cdmaSwapSecondCallState();
+ return true;
+ }
+ Log.e(TAG, "CDMA fail to do hold active and accept held");
+ return false;
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+ PhoneUtils.switchHoldingAndActive(backgroundCall);
+ return true;
+ } else {
+ Log.e(TAG, "Unexpected phone type: " + phoneType);
+ return false;
+ }
+ } else if (chld == CHLD_TYPE_ADDHELDTOCONF) {
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ CdmaPhoneCallState.PhoneCallState state =
+ PhoneGlobals.getInstance().cdmaPhoneCallState.getCurrentCallState();
+ // For CDMA, we need to check if the call is in THRWAY_ACTIVE state
+ if (state == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) {
+ if (VDBG) log("CHLD:3 Merge Calls");
+ PhoneUtils.mergeCalls();
+ return true;
+ } else if (state == CdmaPhoneCallState.PhoneCallState.CONF_CALL) {
+ // State is CONF_CALL already and we are getting a merge call
+ // This can happen when CONF_CALL was entered from a Call Waiting
+ // TODO(BT)
+ return false;
+ }
+ Log.e(TAG, "GSG no call to add conference");
+ return false;
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+ if (mCM.hasActiveFgCall() && mCM.hasActiveBgCall()) {
+ PhoneUtils.mergeCalls();
+ return true;
+ } else {
+ Log.e(TAG, "GSG no call to merge");
+ return false;
+ }
+ } else {
+ Log.e(TAG, "Unexpected phone type: " + phoneType);
+ return false;
+ }
+ } else {
+ Log.e(TAG, "bad CHLD value: " + chld);
+ return false;
+ }
+ }
+
+ public String getNetworkOperator() {
+ enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
+ return mCM.getDefaultPhone().getServiceState().getOperatorAlphaLong();
+ }
+
+ public String getSubscriberNumber() {
+ enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
+ return mCM.getDefaultPhone().getLine1Number();
+ }
+
+ public boolean listCurrentCalls() {
+ enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
+ Message msg = Message.obtain(mHandler, LIST_CURRENT_CALLS);
+ mHandler.sendMessage(msg);
+ return true;
+ }
+
+ public boolean queryPhoneState() {
+ enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
+ Message msg = Message.obtain(mHandler, QUERY_PHONE_STATE);
+ mHandler.sendMessage(msg);
+ return true;
+ }
+
+ public void updateBtHandsfreeAfterRadioTechnologyChange() {
+ enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
+ if (VDBG) Log.d(TAG, "updateBtHandsfreeAfterRadioTechnologyChange...");
+ updateBtPhoneStateAfterRadioTechnologyChange();
+ }
+
+ public void cdmaSwapSecondCallState() {
+ enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
+ Message msg = Message.obtain(mHandler, CDMA_SWAP_SECOND_CALL_STATE);
+ mHandler.sendMessage(msg);
+ }
+
+ public void cdmaSetSecondCallState(boolean state) {
+ enforceCallingOrSelfPermission(MODIFY_PHONE_STATE, null);
+ Message msg = mHandler.obtainMessage(CDMA_SET_SECOND_CALL_STATE, state);
+ mHandler.sendMessage(msg);
+ }
+ };
+
+ // match up with bthf_call_state_t of bt_hf.h
+ final static int CALL_STATE_ACTIVE = 0;
+ final static int CALL_STATE_HELD = 1;
+ final static int CALL_STATE_DIALING = 2;
+ final static int CALL_STATE_ALERTING = 3;
+ final static int CALL_STATE_INCOMING = 4;
+ final static int CALL_STATE_WAITING = 5;
+ final static int CALL_STATE_IDLE = 6;
+
+ // match up with bthf_chld_type_t of bt_hf.h
+ final static int CHLD_TYPE_RELEASEHELD = 0;
+ final static int CHLD_TYPE_RELEASEACTIVE_ACCEPTHELD = 1;
+ final static int CHLD_TYPE_HOLDACTIVE_ACCEPTHELD = 2;
+ final static int CHLD_TYPE_ADDHELDTOCONF = 3;
+
+ /* Convert telephony phone call state into hf hal call state */
+ static int convertCallState(Call.State ringingState, Call.State foregroundState) {
+ if ((ringingState == Call.State.INCOMING) ||
+ (ringingState == Call.State.WAITING) )
+ return CALL_STATE_INCOMING;
+ else if (foregroundState == Call.State.DIALING)
+ return CALL_STATE_DIALING;
+ else if (foregroundState == Call.State.ALERTING)
+ return CALL_STATE_ALERTING;
+ else
+ return CALL_STATE_IDLE;
+ }
+
+ static int convertCallState(Call.State callState) {
+ switch (callState) {
+ case IDLE:
+ case DISCONNECTED:
+ case DISCONNECTING:
+ return CALL_STATE_IDLE;
+ case ACTIVE:
+ return CALL_STATE_ACTIVE;
+ case HOLDING:
+ return CALL_STATE_HELD;
+ case DIALING:
+ return CALL_STATE_DIALING;
+ case ALERTING:
+ return CALL_STATE_ALERTING;
+ case INCOMING:
+ return CALL_STATE_INCOMING;
+ case WAITING:
+ return CALL_STATE_WAITING;
+ default:
+ Log.e(TAG, "bad call state: " + callState);
+ return CALL_STATE_IDLE;
+ }
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/CLIRListPreference.java b/src/com/android/phone/CLIRListPreference.java
new file mode 100644
index 0000000..198bdb0
--- /dev/null
+++ b/src/com/android/phone/CLIRListPreference.java
@@ -0,0 +1,170 @@
+package com.android.phone;
+
+import static com.android.phone.TimeConsumingPreferenceActivity.RESPONSE_ERROR;
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.Phone;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcelable;
+import android.preference.ListPreference;
+import android.util.AttributeSet;
+import android.util.Log;
+
+/**
+ * {@link ListPreference} for CLIR (Calling Line Identification Restriction).
+ * Right now this is used for "Caller ID" setting.
+ */
+public class CLIRListPreference extends ListPreference {
+ private static final String LOG_TAG = "CLIRListPreference";
+ private final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ private final MyHandler mHandler = new MyHandler();
+ private final Phone mPhone;
+ private TimeConsumingPreferenceListener mTcpListener;
+
+ int clirArray[];
+
+ public CLIRListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mPhone = PhoneGlobals.getPhone();
+ }
+
+ public CLIRListPreference(Context context) {
+ this(context, null);
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+
+ mPhone.setOutgoingCallerIdDisplay(findIndexOfValue(getValue()),
+ mHandler.obtainMessage(MyHandler.MESSAGE_SET_CLIR));
+ if (mTcpListener != null) {
+ mTcpListener.onStarted(this, false);
+ }
+ }
+
+ /* package */ void init(TimeConsumingPreferenceListener listener, boolean skipReading) {
+ mTcpListener = listener;
+ if (!skipReading) {
+ mPhone.getOutgoingCallerIdDisplay(mHandler.obtainMessage(MyHandler.MESSAGE_GET_CLIR,
+ MyHandler.MESSAGE_GET_CLIR, MyHandler.MESSAGE_GET_CLIR));
+ if (mTcpListener != null) {
+ mTcpListener.onStarted(this, true);
+ }
+ }
+ }
+
+ /* package */ void handleGetCLIRResult(int tmpClirArray[]) {
+ clirArray = tmpClirArray;
+ final boolean enabled =
+ tmpClirArray[1] == 1 || tmpClirArray[1] == 3 || tmpClirArray[1] == 4;
+ setEnabled(enabled);
+
+ // set the value of the preference based upon the clirArgs.
+ int value = CommandsInterface.CLIR_DEFAULT;
+ switch (tmpClirArray[1]) {
+ case 1: // Permanently provisioned
+ case 3: // Temporary presentation disallowed
+ case 4: // Temporary presentation allowed
+ switch (tmpClirArray[0]) {
+ case 1: // CLIR invoked
+ value = CommandsInterface.CLIR_INVOCATION;
+ break;
+ case 2: // CLIR suppressed
+ value = CommandsInterface.CLIR_SUPPRESSION;
+ break;
+ case 0: // Network default
+ default:
+ value = CommandsInterface.CLIR_DEFAULT;
+ break;
+ }
+ break;
+ case 0: // Not Provisioned
+ case 2: // Unknown (network error, etc)
+ default:
+ value = CommandsInterface.CLIR_DEFAULT;
+ break;
+ }
+ setValueIndex(value);
+
+ // set the string summary to reflect the value
+ int summary = R.string.sum_default_caller_id;
+ switch (value) {
+ case CommandsInterface.CLIR_SUPPRESSION:
+ summary = R.string.sum_show_caller_id;
+ break;
+ case CommandsInterface.CLIR_INVOCATION:
+ summary = R.string.sum_hide_caller_id;
+ break;
+ case CommandsInterface.CLIR_DEFAULT:
+ summary = R.string.sum_default_caller_id;
+ break;
+ }
+ setSummary(summary);
+ }
+
+ private class MyHandler extends Handler {
+ static final int MESSAGE_GET_CLIR = 0;
+ static final int MESSAGE_SET_CLIR = 1;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_GET_CLIR:
+ handleGetCLIRResponse(msg);
+ break;
+ case MESSAGE_SET_CLIR:
+ handleSetCLIRResponse(msg);
+ break;
+ }
+ }
+
+ private void handleGetCLIRResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (msg.arg2 == MESSAGE_SET_CLIR) {
+ mTcpListener.onFinished(CLIRListPreference.this, false);
+ } else {
+ mTcpListener.onFinished(CLIRListPreference.this, true);
+ }
+ clirArray = null;
+ if (ar.exception != null) {
+ if (DBG) Log.d(LOG_TAG, "handleGetCLIRResponse: ar.exception="+ar.exception);
+ mTcpListener.onException(CLIRListPreference.this, (CommandException) ar.exception);
+ } else if (ar.userObj instanceof Throwable) {
+ mTcpListener.onError(CLIRListPreference.this, RESPONSE_ERROR);
+ } else {
+ int clirArray[] = (int[]) ar.result;
+ if (clirArray.length != 2) {
+ mTcpListener.onError(CLIRListPreference.this, RESPONSE_ERROR);
+ } else {
+ if (DBG) {
+ Log.d(LOG_TAG, "handleGetCLIRResponse: CLIR successfully queried,"
+ + " clirArray[0]=" + clirArray[0]
+ + ", clirArray[1]=" + clirArray[1]);
+ }
+ handleGetCLIRResult(clirArray);
+ }
+ }
+ }
+
+ private void handleSetCLIRResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (ar.exception != null) {
+ if (DBG) Log.d(LOG_TAG, "handleSetCallWaitingResponse: ar.exception="+ar.exception);
+ //setEnabled(false);
+ }
+ if (DBG) Log.d(LOG_TAG, "handleSetCallWaitingResponse: re get");
+
+ mPhone.getOutgoingCallerIdDisplay(obtainMessage(MESSAGE_GET_CLIR,
+ MESSAGE_SET_CLIR, MESSAGE_SET_CLIR, ar.exception));
+ }
+ }
+}
diff --git a/src/com/android/phone/CallCard.java b/src/com/android/phone/CallCard.java
new file mode 100644
index 0000000..682113f
--- /dev/null
+++ b/src/com/android/phone/CallCard.java
@@ -0,0 +1,1787 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.animation.LayoutTransition;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.ContactsContract.Contacts;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.CallerInfoAsyncQuery;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+
+import java.util.List;
+
+
+/**
+ * "Call card" UI element: the in-call screen contains a tiled layout of call
+ * cards, each representing the state of a current "call" (ie. an active call,
+ * a call on hold, or an incoming call.)
+ */
+public class CallCard extends LinearLayout
+ implements CallTime.OnTickListener, CallerInfoAsyncQuery.OnQueryCompleteListener,
+ ContactsAsyncHelper.OnImageLoadCompleteListener {
+ private static final String LOG_TAG = "CallCard";
+ private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ private static final int TOKEN_UPDATE_PHOTO_FOR_CALL_STATE = 0;
+ private static final int TOKEN_DO_NOTHING = 1;
+
+ /**
+ * Used with {@link ContactsAsyncHelper#startObtainPhotoAsync(int, Context, Uri,
+ * ContactsAsyncHelper.OnImageLoadCompleteListener, Object)}
+ */
+ private static class AsyncLoadCookie {
+ public final ImageView imageView;
+ public final CallerInfo callerInfo;
+ public final Call call;
+ public AsyncLoadCookie(ImageView imageView, CallerInfo callerInfo, Call call) {
+ this.imageView = imageView;
+ this.callerInfo = callerInfo;
+ this.call = call;
+ }
+ }
+
+ /**
+ * Reference to the InCallScreen activity that owns us. This may be
+ * null if we haven't been initialized yet *or* after the InCallScreen
+ * activity has been destroyed.
+ */
+ private InCallScreen mInCallScreen;
+
+ // Phone app instance
+ private PhoneGlobals mApplication;
+
+ // Top-level subviews of the CallCard
+ /** Container for info about the current call(s) */
+ private ViewGroup mCallInfoContainer;
+ /** Primary "call info" block (the foreground or ringing call) */
+ private ViewGroup mPrimaryCallInfo;
+ /** "Call banner" for the primary call */
+ private ViewGroup mPrimaryCallBanner;
+ /** Secondary "call info" block (the background "on hold" call) */
+ private ViewStub mSecondaryCallInfo;
+
+ /**
+ * Container for both provider info and call state. This will take care of showing/hiding
+ * animation for those views.
+ */
+ private ViewGroup mSecondaryInfoContainer;
+ private ViewGroup mProviderInfo;
+ private TextView mProviderLabel;
+ private TextView mProviderAddress;
+
+ // "Call state" widgets
+ private TextView mCallStateLabel;
+ private TextView mElapsedTime;
+
+ // Text colors, used for various labels / titles
+ private int mTextColorCallTypeSip;
+
+ // The main block of info about the "primary" or "active" call,
+ // including photo / name / phone number / etc.
+ private ImageView mPhoto;
+ private View mPhotoDimEffect;
+
+ private TextView mName;
+ private TextView mPhoneNumber;
+ private TextView mLabel;
+ private TextView mCallTypeLabel;
+ // private TextView mSocialStatus;
+
+ /**
+ * Uri being used to load contact photo for mPhoto. Will be null when nothing is being loaded,
+ * or a photo is already loaded.
+ */
+ private Uri mLoadingPersonUri;
+
+ // Info about the "secondary" call, which is the "call on hold" when
+ // two lines are in use.
+ private TextView mSecondaryCallName;
+ private ImageView mSecondaryCallPhoto;
+ private View mSecondaryCallPhotoDimEffect;
+
+ // Onscreen hint for the incoming call RotarySelector widget.
+ private int mIncomingCallWidgetHintTextResId;
+ private int mIncomingCallWidgetHintColorResId;
+
+ private CallTime mCallTime;
+
+ // Track the state for the photo.
+ private ContactsAsyncHelper.ImageTracker mPhotoTracker;
+
+ // Cached DisplayMetrics density.
+ private float mDensity;
+
+ /**
+ * Sent when it takes too long (MESSAGE_DELAY msec) to load a contact photo for the given
+ * person, at which we just start showing the default avatar picture instead of the person's
+ * one. Note that we will *not* cancel the ongoing query and eventually replace the avatar
+ * with the person's photo, when it is available anyway.
+ */
+ private static final int MESSAGE_SHOW_UNKNOWN_PHOTO = 101;
+ private static final int MESSAGE_DELAY = 500; // msec
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_SHOW_UNKNOWN_PHOTO:
+ showImage(mPhoto, R.drawable.picture_unknown);
+ break;
+ default:
+ Log.wtf(LOG_TAG, "mHandler: unexpected message: " + msg);
+ break;
+ }
+ }
+ };
+
+ public CallCard(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ if (DBG) log("CallCard constructor...");
+ if (DBG) log("- this = " + this);
+ if (DBG) log("- context " + context + ", attrs " + attrs);
+
+ mApplication = PhoneGlobals.getInstance();
+
+ mCallTime = new CallTime(this);
+
+ // create a new object to track the state for the photo.
+ mPhotoTracker = new ContactsAsyncHelper.ImageTracker();
+
+ mDensity = getResources().getDisplayMetrics().density;
+ if (DBG) log("- Density: " + mDensity);
+ }
+
+ /* package */ void setInCallScreenInstance(InCallScreen inCallScreen) {
+ mInCallScreen = inCallScreen;
+ }
+
+ @Override
+ public void onTickForCallTimeElapsed(long timeElapsed) {
+ // While a call is in progress, update the elapsed time shown
+ // onscreen.
+ updateElapsedTimeWidget(timeElapsed);
+ }
+
+ /* package */ void stopTimer() {
+ mCallTime.cancelTimer();
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ if (DBG) log("CallCard onFinishInflate(this = " + this + ")...");
+
+ mCallInfoContainer = (ViewGroup) findViewById(R.id.call_info_container);
+ mPrimaryCallInfo = (ViewGroup) findViewById(R.id.primary_call_info);
+ mPrimaryCallBanner = (ViewGroup) findViewById(R.id.primary_call_banner);
+
+ mSecondaryInfoContainer = (ViewGroup) findViewById(R.id.secondary_info_container);
+ mProviderInfo = (ViewGroup) findViewById(R.id.providerInfo);
+ mProviderLabel = (TextView) findViewById(R.id.providerLabel);
+ mProviderAddress = (TextView) findViewById(R.id.providerAddress);
+ mCallStateLabel = (TextView) findViewById(R.id.callStateLabel);
+ mElapsedTime = (TextView) findViewById(R.id.elapsedTime);
+
+ // Text colors
+ mTextColorCallTypeSip = getResources().getColor(R.color.incall_callTypeSip);
+
+ // "Caller info" area, including photo / name / phone numbers / etc
+ mPhoto = (ImageView) findViewById(R.id.photo);
+ mPhotoDimEffect = findViewById(R.id.dim_effect_for_primary_photo);
+
+ mName = (TextView) findViewById(R.id.name);
+ mPhoneNumber = (TextView) findViewById(R.id.phoneNumber);
+ mLabel = (TextView) findViewById(R.id.label);
+ mCallTypeLabel = (TextView) findViewById(R.id.callTypeLabel);
+ // mSocialStatus = (TextView) findViewById(R.id.socialStatus);
+
+ // Secondary info area, for the background ("on hold") call
+ mSecondaryCallInfo = (ViewStub) findViewById(R.id.secondary_call_info);
+ }
+
+ /**
+ * Updates the state of all UI elements on the CallCard, based on the
+ * current state of the phone.
+ */
+ /* package */ void updateState(CallManager cm) {
+ if (DBG) log("updateState(" + cm + ")...");
+
+ // Update the onscreen UI based on the current state of the phone.
+
+ PhoneConstants.State state = cm.getState(); // IDLE, RINGING, or OFFHOOK
+ Call ringingCall = cm.getFirstActiveRingingCall();
+ Call fgCall = cm.getActiveFgCall();
+ Call bgCall = cm.getFirstActiveBgCall();
+
+ // Update the overall layout of the onscreen elements, if in PORTRAIT.
+ // Portrait uses a programatically altered layout, whereas landscape uses layout xml's.
+ // Landscape view has the views side by side, so no shifting of the picture is needed
+ if (!PhoneUtils.isLandscape(this.getContext())) {
+ updateCallInfoLayout(state);
+ }
+
+ // If the FG call is dialing/alerting, we should display for that call
+ // and ignore the ringing call. This case happens when the telephony
+ // layer rejects the ringing call while the FG call is dialing/alerting,
+ // but the incoming call *does* briefly exist in the DISCONNECTING or
+ // DISCONNECTED state.
+ if ((ringingCall.getState() != Call.State.IDLE)
+ && !fgCall.getState().isDialing()) {
+ // A phone call is ringing, call waiting *or* being rejected
+ // (ie. another call may also be active as well.)
+ updateRingingCall(cm);
+ } else if ((fgCall.getState() != Call.State.IDLE)
+ || (bgCall.getState() != Call.State.IDLE)) {
+ // We are here because either:
+ // (1) the phone is off hook. At least one call exists that is
+ // dialing, active, or holding, and no calls are ringing or waiting,
+ // or:
+ // (2) the phone is IDLE but a call just ended and it's still in
+ // the DISCONNECTING or DISCONNECTED state. In this case, we want
+ // the main CallCard to display "Hanging up" or "Call ended".
+ // The normal "foreground call" code path handles both cases.
+ updateForegroundCall(cm);
+ } else {
+ // We don't have any DISCONNECTED calls, which means that the phone
+ // is *truly* idle.
+ if (mApplication.inCallUiState.showAlreadyDisconnectedState) {
+ // showAlreadyDisconnectedState implies the phone call is disconnected
+ // and we want to show the disconnected phone call for a moment.
+ //
+ // This happens when a phone call ends while the screen is off,
+ // which means the user had no chance to see the last status of
+ // the call. We'll turn off showAlreadyDisconnectedState flag
+ // and bail out of the in-call screen soon.
+ updateAlreadyDisconnected(cm);
+ } else {
+ // It's very rare to be on the InCallScreen at all in this
+ // state, but it can happen in some cases:
+ // - A stray onPhoneStateChanged() event came in to the
+ // InCallScreen *after* it was dismissed.
+ // - We're allowed to be on the InCallScreen because
+ // an MMI or USSD is running, but there's no actual "call"
+ // to display.
+ // - We're displaying an error dialog to the user
+ // (explaining why the call failed), so we need to stay on
+ // the InCallScreen so that the dialog will be visible.
+ //
+ // In these cases, put the callcard into a sane but "blank" state:
+ updateNoCall(cm);
+ }
+ }
+ }
+
+ /**
+ * Updates the overall size and positioning of mCallInfoContainer and
+ * the "Call info" blocks, based on the phone state.
+ */
+ private void updateCallInfoLayout(PhoneConstants.State state) {
+ boolean ringing = (state == PhoneConstants.State.RINGING);
+ if (DBG) log("updateCallInfoLayout()... ringing = " + ringing);
+
+ // Based on the current state, update the overall
+ // CallCard layout:
+
+ // - Update the bottom margin of mCallInfoContainer to make sure
+ // the call info area won't overlap with the touchable
+ // controls on the bottom part of the screen.
+
+ int reservedVerticalSpace = mInCallScreen.getInCallTouchUi().getTouchUiHeight();
+ ViewGroup.MarginLayoutParams callInfoLp =
+ (ViewGroup.MarginLayoutParams) mCallInfoContainer.getLayoutParams();
+ callInfoLp.bottomMargin = reservedVerticalSpace; // Equivalent to setting
+ // android:layout_marginBottom in XML
+ if (DBG) log(" ==> callInfoLp.bottomMargin: " + reservedVerticalSpace);
+ mCallInfoContainer.setLayoutParams(callInfoLp);
+ }
+
+ /**
+ * Updates the UI for the state where the phone is in use, but not ringing.
+ */
+ private void updateForegroundCall(CallManager cm) {
+ if (DBG) log("updateForegroundCall()...");
+ // if (DBG) PhoneUtils.dumpCallManager();
+
+ Call fgCall = cm.getActiveFgCall();
+ Call bgCall = cm.getFirstActiveBgCall();
+
+ if (fgCall.getState() == Call.State.IDLE) {
+ if (DBG) log("updateForegroundCall: no active call, show holding call");
+ // TODO: make sure this case agrees with the latest UI spec.
+
+ // Display the background call in the main info area of the
+ // CallCard, since there is no foreground call. Note that
+ // displayMainCallStatus() will notice if the call we passed in is on
+ // hold, and display the "on hold" indication.
+ fgCall = bgCall;
+
+ // And be sure to not display anything in the "on hold" box.
+ bgCall = null;
+ }
+
+ displayMainCallStatus(cm, fgCall);
+
+ Phone phone = fgCall.getPhone();
+
+ int phoneType = phone.getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ if ((mApplication.cdmaPhoneCallState.getCurrentCallState()
+ == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE)
+ && mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) {
+ displaySecondaryCallStatus(cm, fgCall);
+ } else {
+ //This is required so that even if a background call is not present
+ // we need to clean up the background call area.
+ displaySecondaryCallStatus(cm, bgCall);
+ }
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ displaySecondaryCallStatus(cm, bgCall);
+ }
+ }
+
+ /**
+ * Updates the UI for the state where an incoming call is ringing (or
+ * call waiting), regardless of whether the phone's already offhook.
+ */
+ private void updateRingingCall(CallManager cm) {
+ if (DBG) log("updateRingingCall()...");
+
+ Call ringingCall = cm.getFirstActiveRingingCall();
+
+ // Display caller-id info and photo from the incoming call:
+ displayMainCallStatus(cm, ringingCall);
+
+ // And even in the Call Waiting case, *don't* show any info about
+ // the current ongoing call and/or the current call on hold.
+ // (Since the caller-id info for the incoming call totally trumps
+ // any info about the current call(s) in progress.)
+ displaySecondaryCallStatus(cm, null);
+ }
+
+ /**
+ * Updates the UI for the state where an incoming call is just disconnected while we want to
+ * show the screen for a moment.
+ *
+ * This case happens when the whole in-call screen is in background when phone calls are hanged
+ * up, which means there's no way to determine which call was the last call finished. Right now
+ * this method simply shows the previous primary call status with a photo, closing the
+ * secondary call status. In most cases (including conference call or misc call happening in
+ * CDMA) this behaves right.
+ *
+ * If there were two phone calls both of which were hung up but the primary call was the
+ * first, this would behave a bit odd (since the first one still appears as the
+ * "last disconnected").
+ */
+ private void updateAlreadyDisconnected(CallManager cm) {
+ // For the foreground call, we manually set up every component based on previous state.
+ mPrimaryCallInfo.setVisibility(View.VISIBLE);
+ mSecondaryInfoContainer.setLayoutTransition(null);
+ mProviderInfo.setVisibility(View.GONE);
+ mCallStateLabel.setVisibility(View.VISIBLE);
+ mCallStateLabel.setText(mContext.getString(R.string.card_title_call_ended));
+ mElapsedTime.setVisibility(View.VISIBLE);
+ mCallTime.cancelTimer();
+
+ // Just hide it.
+ displaySecondaryCallStatus(cm, null);
+ }
+
+ /**
+ * Updates the UI for the state where the phone is not in use.
+ * This is analogous to updateForegroundCall() and updateRingingCall(),
+ * but for the (uncommon) case where the phone is
+ * totally idle. (See comments in updateState() above.)
+ *
+ * This puts the callcard into a sane but "blank" state.
+ */
+ private void updateNoCall(CallManager cm) {
+ if (DBG) log("updateNoCall()...");
+
+ displayMainCallStatus(cm, null);
+ displaySecondaryCallStatus(cm, null);
+ }
+
+ /**
+ * Updates the main block of caller info on the CallCard
+ * (ie. the stuff in the primaryCallInfo block) based on the specified Call.
+ */
+ private void displayMainCallStatus(CallManager cm, Call call) {
+ if (DBG) log("displayMainCallStatus(call " + call + ")...");
+
+ if (call == null) {
+ // There's no call to display, presumably because the phone is idle.
+ mPrimaryCallInfo.setVisibility(View.GONE);
+ return;
+ }
+ mPrimaryCallInfo.setVisibility(View.VISIBLE);
+
+ Call.State state = call.getState();
+ if (DBG) log(" - call.state: " + call.getState());
+
+ switch (state) {
+ case ACTIVE:
+ case DISCONNECTING:
+ // update timer field
+ if (DBG) log("displayMainCallStatus: start periodicUpdateTimer");
+ mCallTime.setActiveCallMode(call);
+ mCallTime.reset();
+ mCallTime.periodicUpdateTimer();
+
+ break;
+
+ case HOLDING:
+ // update timer field
+ mCallTime.cancelTimer();
+
+ break;
+
+ case DISCONNECTED:
+ // Stop getting timer ticks from this call
+ mCallTime.cancelTimer();
+
+ break;
+
+ case DIALING:
+ case ALERTING:
+ // Stop getting timer ticks from a previous call
+ mCallTime.cancelTimer();
+
+ break;
+
+ case INCOMING:
+ case WAITING:
+ // Stop getting timer ticks from a previous call
+ mCallTime.cancelTimer();
+
+ break;
+
+ case IDLE:
+ // The "main CallCard" should never be trying to display
+ // an idle call! In updateState(), if the phone is idle,
+ // we call updateNoCall(), which means that we shouldn't
+ // have passed a call into this method at all.
+ Log.w(LOG_TAG, "displayMainCallStatus: IDLE call in the main call card!");
+
+ // (It is possible, though, that we had a valid call which
+ // became idle *after* the check in updateState() but
+ // before we get here... So continue the best we can,
+ // with whatever (stale) info we can get from the
+ // passed-in Call object.)
+
+ break;
+
+ default:
+ Log.w(LOG_TAG, "displayMainCallStatus: unexpected call state: " + state);
+ break;
+ }
+
+ updateCallStateWidgets(call);
+
+ if (PhoneUtils.isConferenceCall(call)) {
+ // Update onscreen info for a conference call.
+ updateDisplayForConference(call);
+ } else {
+ // Update onscreen info for a regular call (which presumably
+ // has only one connection.)
+ Connection conn = null;
+ int phoneType = call.getPhone().getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ conn = call.getLatestConnection();
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ conn = call.getEarliestConnection();
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+
+ if (conn == null) {
+ if (DBG) log("displayMainCallStatus: connection is null, using default values.");
+ // if the connection is null, we run through the behaviour
+ // we had in the past, which breaks down into trivial steps
+ // with the current implementation of getCallerInfo and
+ // updateDisplayForPerson.
+ CallerInfo info = PhoneUtils.getCallerInfo(getContext(), null /* conn */);
+ updateDisplayForPerson(info, PhoneConstants.PRESENTATION_ALLOWED, false, call,
+ conn);
+ } else {
+ if (DBG) log(" - CONN: " + conn + ", state = " + conn.getState());
+ int presentation = conn.getNumberPresentation();
+
+ // make sure that we only make a new query when the current
+ // callerinfo differs from what we've been requested to display.
+ boolean runQuery = true;
+ Object o = conn.getUserData();
+ if (o instanceof PhoneUtils.CallerInfoToken) {
+ runQuery = mPhotoTracker.isDifferentImageRequest(
+ ((PhoneUtils.CallerInfoToken) o).currentInfo);
+ } else {
+ runQuery = mPhotoTracker.isDifferentImageRequest(conn);
+ }
+
+ // Adding a check to see if the update was caused due to a Phone number update
+ // or CNAP update. If so then we need to start a new query
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ Object obj = conn.getUserData();
+ String updatedNumber = conn.getAddress();
+ String updatedCnapName = conn.getCnapName();
+ CallerInfo info = null;
+ if (obj instanceof PhoneUtils.CallerInfoToken) {
+ info = ((PhoneUtils.CallerInfoToken) o).currentInfo;
+ } else if (o instanceof CallerInfo) {
+ info = (CallerInfo) o;
+ }
+
+ if (info != null) {
+ if (updatedNumber != null && !updatedNumber.equals(info.phoneNumber)) {
+ if (DBG) log("- displayMainCallStatus: updatedNumber = "
+ + updatedNumber);
+ runQuery = true;
+ }
+ if (updatedCnapName != null && !updatedCnapName.equals(info.cnapName)) {
+ if (DBG) log("- displayMainCallStatus: updatedCnapName = "
+ + updatedCnapName);
+ runQuery = true;
+ }
+ }
+ }
+
+ if (runQuery) {
+ if (DBG) log("- displayMainCallStatus: starting CallerInfo query...");
+ PhoneUtils.CallerInfoToken info =
+ PhoneUtils.startGetCallerInfo(getContext(), conn, this, call);
+ updateDisplayForPerson(info.currentInfo, presentation, !info.isFinal,
+ call, conn);
+ } else {
+ // No need to fire off a new query. We do still need
+ // to update the display, though (since we might have
+ // previously been in the "conference call" state.)
+ if (DBG) log("- displayMainCallStatus: using data we already have...");
+ if (o instanceof CallerInfo) {
+ CallerInfo ci = (CallerInfo) o;
+ // Update CNAP information if Phone state change occurred
+ ci.cnapName = conn.getCnapName();
+ ci.numberPresentation = conn.getNumberPresentation();
+ ci.namePresentation = conn.getCnapNamePresentation();
+ if (DBG) log("- displayMainCallStatus: CNAP data from Connection: "
+ + "CNAP name=" + ci.cnapName
+ + ", Number/Name Presentation=" + ci.numberPresentation);
+ if (DBG) log(" ==> Got CallerInfo; updating display: ci = " + ci);
+ updateDisplayForPerson(ci, presentation, false, call, conn);
+ } else if (o instanceof PhoneUtils.CallerInfoToken){
+ CallerInfo ci = ((PhoneUtils.CallerInfoToken) o).currentInfo;
+ if (DBG) log("- displayMainCallStatus: CNAP data from Connection: "
+ + "CNAP name=" + ci.cnapName
+ + ", Number/Name Presentation=" + ci.numberPresentation);
+ if (DBG) log(" ==> Got CallerInfoToken; updating display: ci = " + ci);
+ updateDisplayForPerson(ci, presentation, true, call, conn);
+ } else {
+ Log.w(LOG_TAG, "displayMainCallStatus: runQuery was false, "
+ + "but we didn't have a cached CallerInfo object! o = " + o);
+ // TODO: any easy way to recover here (given that
+ // the CallCard is probably displaying stale info
+ // right now?) Maybe force the CallCard into the
+ // "Unknown" state?
+ }
+ }
+ }
+ }
+
+ // In some states we override the "photo" ImageView to be an
+ // indication of the current state, rather than displaying the
+ // regular photo as set above.
+ updatePhotoForCallState(call);
+
+ // One special feature of the "number" text field: For incoming
+ // calls, while the user is dragging the RotarySelector widget, we
+ // use mPhoneNumber to display a hint like "Rotate to answer".
+ if (mIncomingCallWidgetHintTextResId != 0) {
+ // Display the hint!
+ mPhoneNumber.setText(mIncomingCallWidgetHintTextResId);
+ mPhoneNumber.setTextColor(getResources().getColor(mIncomingCallWidgetHintColorResId));
+ mPhoneNumber.setVisibility(View.VISIBLE);
+ mLabel.setVisibility(View.GONE);
+ }
+ // If we don't have a hint to display, just don't touch
+ // mPhoneNumber and mLabel. (Their text / color / visibility have
+ // already been set correctly, by either updateDisplayForPerson()
+ // or updateDisplayForConference().)
+ }
+
+ /**
+ * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
+ * refreshes the CallCard data when it called.
+ */
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+ if (DBG) log("onQueryComplete: token " + token + ", cookie " + cookie + ", ci " + ci);
+
+ if (cookie instanceof Call) {
+ // grab the call object and update the display for an individual call,
+ // as well as the successive call to update image via call state.
+ // If the object is a textview instead, we update it as we need to.
+ if (DBG) log("callerinfo query complete, updating ui from displayMainCallStatus()");
+ Call call = (Call) cookie;
+ Connection conn = null;
+ int phoneType = call.getPhone().getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ conn = call.getLatestConnection();
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ conn = call.getEarliestConnection();
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ PhoneUtils.CallerInfoToken cit =
+ PhoneUtils.startGetCallerInfo(getContext(), conn, this, null);
+
+ int presentation = PhoneConstants.PRESENTATION_ALLOWED;
+ if (conn != null) presentation = conn.getNumberPresentation();
+ if (DBG) log("- onQueryComplete: presentation=" + presentation
+ + ", contactExists=" + ci.contactExists);
+
+ // Depending on whether there was a contact match or not, we want to pass in different
+ // CallerInfo (for CNAP). Therefore if ci.contactExists then use the ci passed in.
+ // Otherwise, regenerate the CIT from the Connection and use the CallerInfo from there.
+ if (ci.contactExists) {
+ updateDisplayForPerson(ci, PhoneConstants.PRESENTATION_ALLOWED, false, call, conn);
+ } else {
+ updateDisplayForPerson(cit.currentInfo, presentation, false, call, conn);
+ }
+ updatePhotoForCallState(call);
+
+ } else if (cookie instanceof TextView){
+ if (DBG) log("callerinfo query complete, updating ui from ongoing or onhold");
+ ((TextView) cookie).setText(PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
+ }
+ }
+
+ /**
+ * Implemented for ContactsAsyncHelper.OnImageLoadCompleteListener interface.
+ * make sure that the call state is reflected after the image is loaded.
+ */
+ @Override
+ public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon, Object cookie) {
+ mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO);
+ if (mLoadingPersonUri != null) {
+ // Start sending view notification after the current request being done.
+ // New image may possibly be available from the next phone calls.
+ //
+ // TODO: may be nice to update the image view again once the newer one
+ // is available on contacts database.
+ PhoneUtils.sendViewNotificationAsync(mApplication, mLoadingPersonUri);
+ } else {
+ // This should not happen while we need some verbose info if it happens..
+ Log.w(LOG_TAG, "Person Uri isn't available while Image is successfully loaded.");
+ }
+ mLoadingPersonUri = null;
+
+ AsyncLoadCookie asyncLoadCookie = (AsyncLoadCookie) cookie;
+ CallerInfo callerInfo = asyncLoadCookie.callerInfo;
+ ImageView imageView = asyncLoadCookie.imageView;
+ Call call = asyncLoadCookie.call;
+
+ callerInfo.cachedPhoto = photo;
+ callerInfo.cachedPhotoIcon = photoIcon;
+ callerInfo.isCachedPhotoCurrent = true;
+
+ // Note: previously ContactsAsyncHelper has done this job.
+ // TODO: We will need fade-in animation. See issue 5236130.
+ if (photo != null) {
+ showImage(imageView, photo);
+ } else if (photoIcon != null) {
+ showImage(imageView, photoIcon);
+ } else {
+ showImage(imageView, R.drawable.picture_unknown);
+ }
+
+ if (token == TOKEN_UPDATE_PHOTO_FOR_CALL_STATE) {
+ updatePhotoForCallState(call);
+ }
+ }
+
+ /**
+ * Updates the "call state label" and the elapsed time widget based on the
+ * current state of the call.
+ */
+ private void updateCallStateWidgets(Call call) {
+ if (DBG) log("updateCallStateWidgets(call " + call + ")...");
+ final Call.State state = call.getState();
+ final Context context = getContext();
+ final Phone phone = call.getPhone();
+ final int phoneType = phone.getPhoneType();
+
+ String callStateLabel = null; // Label to display as part of the call banner
+ int bluetoothIconId = 0; // Icon to display alongside the call state label
+
+ switch (state) {
+ case IDLE:
+ // "Call state" is meaningless in this state.
+ break;
+
+ case ACTIVE:
+ // We normally don't show a "call state label" at all in
+ // this state (but see below for some special cases).
+ break;
+
+ case HOLDING:
+ callStateLabel = context.getString(R.string.card_title_on_hold);
+ break;
+
+ case DIALING:
+ case ALERTING:
+ callStateLabel = context.getString(R.string.card_title_dialing);
+ break;
+
+ case INCOMING:
+ case WAITING:
+ callStateLabel = context.getString(R.string.card_title_incoming_call);
+
+ // Also, display a special icon (alongside the "Incoming call"
+ // label) if there's an incoming call and audio will be routed
+ // to bluetooth when you answer it.
+ if (mApplication.showBluetoothIndication()) {
+ bluetoothIconId = R.drawable.ic_incoming_call_bluetooth;
+ }
+ break;
+
+ case DISCONNECTING:
+ // While in the DISCONNECTING state we display a "Hanging up"
+ // message in order to make the UI feel more responsive. (In
+ // GSM it's normal to see a delay of a couple of seconds while
+ // negotiating the disconnect with the network, so the "Hanging
+ // up" state at least lets the user know that we're doing
+ // something. This state is currently not used with CDMA.)
+ callStateLabel = context.getString(R.string.card_title_hanging_up);
+ break;
+
+ case DISCONNECTED:
+ callStateLabel = getCallFailedString(call);
+ break;
+
+ default:
+ Log.wtf(LOG_TAG, "updateCallStateWidgets: unexpected call state: " + state);
+ break;
+ }
+
+ // Check a couple of other special cases (these are all CDMA-specific).
+
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ if ((state == Call.State.ACTIVE)
+ && mApplication.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing()) {
+ // Display "Dialing" while dialing a 3Way call, even
+ // though the foreground call state is actually ACTIVE.
+ callStateLabel = context.getString(R.string.card_title_dialing);
+ } else if (PhoneGlobals.getInstance().notifier.getIsCdmaRedialCall()) {
+ callStateLabel = context.getString(R.string.card_title_redialing);
+ }
+ }
+ if (PhoneUtils.isPhoneInEcm(phone)) {
+ // In emergency callback mode (ECM), use a special label
+ // that shows your own phone number.
+ callStateLabel = getECMCardTitle(context, phone);
+ }
+
+ final InCallUiState inCallUiState = mApplication.inCallUiState;
+ if (DBG) {
+ log("==> callStateLabel: '" + callStateLabel
+ + "', bluetoothIconId = " + bluetoothIconId
+ + ", providerInfoVisible = " + inCallUiState.providerInfoVisible);
+ }
+
+ // Animation will be done by mCallerDetail's LayoutTransition, but in some cases, we don't
+ // want that.
+ // - DIALING: This is at the beginning of the phone call.
+ // - DISCONNECTING, DISCONNECTED: Screen will disappear soon; we have no time for animation.
+ final boolean skipAnimation = (state == Call.State.DIALING
+ || state == Call.State.DISCONNECTING
+ || state == Call.State.DISCONNECTED);
+ LayoutTransition layoutTransition = null;
+ if (skipAnimation) {
+ // Evict LayoutTransition object to skip animation.
+ layoutTransition = mSecondaryInfoContainer.getLayoutTransition();
+ mSecondaryInfoContainer.setLayoutTransition(null);
+ }
+
+ if (inCallUiState.providerInfoVisible) {
+ mProviderInfo.setVisibility(View.VISIBLE);
+ mProviderLabel.setText(context.getString(R.string.calling_via_template,
+ inCallUiState.providerLabel));
+ mProviderAddress.setText(inCallUiState.providerAddress);
+
+ mInCallScreen.requestRemoveProviderInfoWithDelay();
+ } else {
+ mProviderInfo.setVisibility(View.GONE);
+ }
+
+ if (!TextUtils.isEmpty(callStateLabel)) {
+ mCallStateLabel.setVisibility(View.VISIBLE);
+ mCallStateLabel.setText(callStateLabel);
+
+ // ...and display the icon too if necessary.
+ if (bluetoothIconId != 0) {
+ mCallStateLabel.setCompoundDrawablesWithIntrinsicBounds(bluetoothIconId, 0, 0, 0);
+ mCallStateLabel.setCompoundDrawablePadding((int) (mDensity * 5));
+ } else {
+ // Clear out any icons
+ mCallStateLabel.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
+ }
+ } else {
+ mCallStateLabel.setVisibility(View.GONE);
+ // Gravity is aligned left when receiving an incoming call in landscape.
+ // In that rare case, the gravity needs to be reset to the right.
+ // Also, setText("") is used since there is a delay in making the view GONE,
+ // so the user will otherwise see the text jump to the right side before disappearing.
+ if(mCallStateLabel.getGravity() != Gravity.END) {
+ mCallStateLabel.setText("");
+ mCallStateLabel.setGravity(Gravity.END);
+ }
+ }
+ if (skipAnimation) {
+ // Restore LayoutTransition object to recover animation.
+ mSecondaryInfoContainer.setLayoutTransition(layoutTransition);
+ }
+
+ // ...and update the elapsed time widget too.
+ switch (state) {
+ case ACTIVE:
+ case DISCONNECTING:
+ // Show the time with fade-in animation.
+ AnimationUtils.Fade.show(mElapsedTime);
+ updateElapsedTimeWidget(call);
+ break;
+
+ case DISCONNECTED:
+ // In the "Call ended" state, leave the mElapsedTime widget
+ // visible, but don't touch it (so we continue to see the
+ // elapsed time of the call that just ended.)
+ // Check visibility to keep possible fade-in animation.
+ if (mElapsedTime.getVisibility() != View.VISIBLE) {
+ mElapsedTime.setVisibility(View.VISIBLE);
+ }
+ break;
+
+ default:
+ // Call state here is IDLE, ACTIVE, HOLDING, DIALING, ALERTING,
+ // INCOMING, or WAITING.
+ // In all of these states, the "elapsed time" is meaningless, so
+ // don't show it.
+ AnimationUtils.Fade.hide(mElapsedTime, View.INVISIBLE);
+
+ // Additionally, in call states that can only occur at the start
+ // of a call, reset the elapsed time to be sure we won't display
+ // stale info later (like if we somehow go straight from DIALING
+ // or ALERTING to DISCONNECTED, which can actually happen in
+ // some failure cases like "line busy").
+ if ((state == Call.State.DIALING) || (state == Call.State.ALERTING)) {
+ updateElapsedTimeWidget(0);
+ }
+
+ break;
+ }
+ }
+
+ /**
+ * Updates mElapsedTime based on the given {@link Call} object's information.
+ *
+ * @see CallTime#getCallDuration(Call)
+ * @see Connection#getDurationMillis()
+ */
+ /* package */ void updateElapsedTimeWidget(Call call) {
+ long duration = CallTime.getCallDuration(call); // msec
+ updateElapsedTimeWidget(duration / 1000);
+ // Also see onTickForCallTimeElapsed(), which updates this
+ // widget once per second while the call is active.
+ }
+
+ /**
+ * Updates mElapsedTime based on the specified number of seconds.
+ */
+ private void updateElapsedTimeWidget(long timeElapsed) {
+ // if (DBG) log("updateElapsedTimeWidget: " + timeElapsed);
+ mElapsedTime.setText(DateUtils.formatElapsedTime(timeElapsed));
+ }
+
+ /**
+ * Updates the "on hold" box in the "other call" info area
+ * (ie. the stuff in the secondaryCallInfo block)
+ * based on the specified Call.
+ * Or, clear out the "on hold" box if the specified call
+ * is null or idle.
+ */
+ private void displaySecondaryCallStatus(CallManager cm, Call call) {
+ if (DBG) log("displayOnHoldCallStatus(call =" + call + ")...");
+
+ if ((call == null) || (PhoneGlobals.getInstance().isOtaCallInActiveState())) {
+ mSecondaryCallInfo.setVisibility(View.GONE);
+ return;
+ }
+
+ Call.State state = call.getState();
+ switch (state) {
+ case HOLDING:
+ // Ok, there actually is a background call on hold.
+ // Display the "on hold" box.
+
+ // Note this case occurs only on GSM devices. (On CDMA,
+ // the "call on hold" is actually the 2nd connection of
+ // that ACTIVE call; see the ACTIVE case below.)
+ showSecondaryCallInfo();
+
+ if (PhoneUtils.isConferenceCall(call)) {
+ if (DBG) log("==> conference call.");
+ mSecondaryCallName.setText(getContext().getString(R.string.confCall));
+ showImage(mSecondaryCallPhoto, R.drawable.picture_conference);
+ } else {
+ // perform query and update the name temporarily
+ // make sure we hand the textview we want updated to the
+ // callback function.
+ if (DBG) log("==> NOT a conf call; call startGetCallerInfo...");
+ PhoneUtils.CallerInfoToken infoToken = PhoneUtils.startGetCallerInfo(
+ getContext(), call, this, mSecondaryCallName);
+ mSecondaryCallName.setText(
+ PhoneUtils.getCompactNameFromCallerInfo(infoToken.currentInfo,
+ getContext()));
+
+ // Also pull the photo out of the current CallerInfo.
+ // (Note we assume we already have a valid photo at
+ // this point, since *presumably* the caller-id query
+ // was already run at some point *before* this call
+ // got put on hold. If there's no cached photo, just
+ // fall back to the default "unknown" image.)
+ if (infoToken.isFinal) {
+ showCachedImage(mSecondaryCallPhoto, infoToken.currentInfo);
+ } else {
+ showImage(mSecondaryCallPhoto, R.drawable.picture_unknown);
+ }
+ }
+
+ AnimationUtils.Fade.show(mSecondaryCallPhotoDimEffect);
+ break;
+
+ case ACTIVE:
+ // CDMA: This is because in CDMA when the user originates the second call,
+ // although the Foreground call state is still ACTIVE in reality the network
+ // put the first call on hold.
+ if (mApplication.phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ showSecondaryCallInfo();
+
+ List<Connection> connections = call.getConnections();
+ if (connections.size() > 2) {
+ // This means that current Mobile Originated call is the not the first 3-Way
+ // call the user is making, which in turn tells the PhoneGlobals that we no
+ // longer know which previous caller/party had dropped out before the user
+ // made this call.
+ mSecondaryCallName.setText(
+ getContext().getString(R.string.card_title_in_call));
+ showImage(mSecondaryCallPhoto, R.drawable.picture_unknown);
+ } else {
+ // This means that the current Mobile Originated call IS the first 3-Way
+ // and hence we display the first callers/party's info here.
+ Connection conn = call.getEarliestConnection();
+ PhoneUtils.CallerInfoToken infoToken = PhoneUtils.startGetCallerInfo(
+ getContext(), conn, this, mSecondaryCallName);
+
+ // Get the compactName to be displayed, but then check that against
+ // the number presentation value for the call. If it's not an allowed
+ // presentation, then display the appropriate presentation string instead.
+ CallerInfo info = infoToken.currentInfo;
+
+ String name = PhoneUtils.getCompactNameFromCallerInfo(info, getContext());
+ boolean forceGenericPhoto = false;
+ if (info != null && info.numberPresentation !=
+ PhoneConstants.PRESENTATION_ALLOWED) {
+ name = PhoneUtils.getPresentationString(
+ getContext(), info.numberPresentation);
+ forceGenericPhoto = true;
+ }
+ mSecondaryCallName.setText(name);
+
+ // Also pull the photo out of the current CallerInfo.
+ // (Note we assume we already have a valid photo at
+ // this point, since *presumably* the caller-id query
+ // was already run at some point *before* this call
+ // got put on hold. If there's no cached photo, just
+ // fall back to the default "unknown" image.)
+ if (!forceGenericPhoto && infoToken.isFinal) {
+ showCachedImage(mSecondaryCallPhoto, info);
+ } else {
+ showImage(mSecondaryCallPhoto, R.drawable.picture_unknown);
+ }
+ }
+ } else {
+ // We shouldn't ever get here at all for non-CDMA devices.
+ Log.w(LOG_TAG, "displayOnHoldCallStatus: ACTIVE state on non-CDMA device");
+ mSecondaryCallInfo.setVisibility(View.GONE);
+ }
+
+ AnimationUtils.Fade.hide(mSecondaryCallPhotoDimEffect, View.GONE);
+ break;
+
+ default:
+ // There's actually no call on hold. (Presumably this call's
+ // state is IDLE, since any other state is meaningless for the
+ // background call.)
+ mSecondaryCallInfo.setVisibility(View.GONE);
+ break;
+ }
+ }
+
+ private void showSecondaryCallInfo() {
+ // This will call ViewStub#inflate() when needed.
+ mSecondaryCallInfo.setVisibility(View.VISIBLE);
+ if (mSecondaryCallName == null) {
+ mSecondaryCallName = (TextView) findViewById(R.id.secondaryCallName);
+ }
+ if (mSecondaryCallPhoto == null) {
+ mSecondaryCallPhoto = (ImageView) findViewById(R.id.secondaryCallPhoto);
+ }
+ if (mSecondaryCallPhotoDimEffect == null) {
+ mSecondaryCallPhotoDimEffect = findViewById(R.id.dim_effect_for_secondary_photo);
+ mSecondaryCallPhotoDimEffect.setOnClickListener(mInCallScreen);
+ // Add a custom OnTouchListener to manually shrink the "hit target".
+ mSecondaryCallPhotoDimEffect.setOnTouchListener(new SmallerHitTargetTouchListener());
+ }
+ mInCallScreen.updateButtonStateOutsideInCallTouchUi();
+ }
+
+ /**
+ * Method which is expected to be called from
+ * {@link InCallScreen#updateButtonStateOutsideInCallTouchUi()}.
+ */
+ /* package */ void setSecondaryCallClickable(boolean clickable) {
+ if (mSecondaryCallPhotoDimEffect != null) {
+ mSecondaryCallPhotoDimEffect.setEnabled(clickable);
+ }
+ }
+
+ private String getCallFailedString(Call call) {
+ Connection c = call.getEarliestConnection();
+ int resID;
+
+ if (c == null) {
+ if (DBG) log("getCallFailedString: connection is null, using default values.");
+ // if this connection is null, just assume that the
+ // default case occurs.
+ resID = R.string.card_title_call_ended;
+ } else {
+
+ Connection.DisconnectCause cause = c.getDisconnectCause();
+
+ // TODO: The card *title* should probably be "Call ended" in all
+ // cases, but if the DisconnectCause was an error condition we should
+ // probably also display the specific failure reason somewhere...
+
+ switch (cause) {
+ case BUSY:
+ resID = R.string.callFailed_userBusy;
+ break;
+
+ case CONGESTION:
+ resID = R.string.callFailed_congestion;
+ break;
+
+ case TIMED_OUT:
+ resID = R.string.callFailed_timedOut;
+ break;
+
+ case SERVER_UNREACHABLE:
+ resID = R.string.callFailed_server_unreachable;
+ break;
+
+ case NUMBER_UNREACHABLE:
+ resID = R.string.callFailed_number_unreachable;
+ break;
+
+ case INVALID_CREDENTIALS:
+ resID = R.string.callFailed_invalid_credentials;
+ break;
+
+ case SERVER_ERROR:
+ resID = R.string.callFailed_server_error;
+ break;
+
+ case OUT_OF_NETWORK:
+ resID = R.string.callFailed_out_of_network;
+ break;
+
+ case LOST_SIGNAL:
+ case CDMA_DROP:
+ resID = R.string.callFailed_noSignal;
+ break;
+
+ case LIMIT_EXCEEDED:
+ resID = R.string.callFailed_limitExceeded;
+ break;
+
+ case POWER_OFF:
+ resID = R.string.callFailed_powerOff;
+ break;
+
+ case ICC_ERROR:
+ resID = R.string.callFailed_simError;
+ break;
+
+ case OUT_OF_SERVICE:
+ resID = R.string.callFailed_outOfService;
+ break;
+
+ case INVALID_NUMBER:
+ case UNOBTAINABLE_NUMBER:
+ resID = R.string.callFailed_unobtainable_number;
+ break;
+
+ default:
+ resID = R.string.card_title_call_ended;
+ break;
+ }
+ }
+ return getContext().getString(resID);
+ }
+
+ /**
+ * Updates the name / photo / number / label fields on the CallCard
+ * based on the specified CallerInfo.
+ *
+ * If the current call is a conference call, use
+ * updateDisplayForConference() instead.
+ */
+ private void updateDisplayForPerson(CallerInfo info,
+ int presentation,
+ boolean isTemporary,
+ Call call,
+ Connection conn) {
+ if (DBG) log("updateDisplayForPerson(" + info + ")\npresentation:" +
+ presentation + " isTemporary:" + isTemporary);
+
+ // inform the state machine that we are displaying a photo.
+ mPhotoTracker.setPhotoRequest(info);
+ mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
+
+ // The actual strings we're going to display onscreen:
+ String displayName;
+ String displayNumber = null;
+ String label = null;
+ Uri personUri = null;
+ // String socialStatusText = null;
+ // Drawable socialStatusBadge = null;
+
+ // Gather missing info unless the call is generic, in which case we wouldn't use
+ // the gathered information anyway.
+ if (info != null && !call.isGeneric()) {
+
+ // It appears that there is a small change in behaviour with the
+ // PhoneUtils' startGetCallerInfo whereby if we query with an
+ // empty number, we will get a valid CallerInfo object, but with
+ // fields that are all null, and the isTemporary boolean input
+ // parameter as true.
+
+ // In the past, we would see a NULL callerinfo object, but this
+ // ends up causing null pointer exceptions elsewhere down the
+ // line in other cases, so we need to make this fix instead. It
+ // appears that this was the ONLY call to PhoneUtils
+ // .getCallerInfo() that relied on a NULL CallerInfo to indicate
+ // an unknown contact.
+
+ // Currently, infi.phoneNumber may actually be a SIP address, and
+ // if so, it might sometimes include the "sip:" prefix. That
+ // prefix isn't really useful to the user, though, so strip it off
+ // if present. (For any other URI scheme, though, leave the
+ // prefix alone.)
+ // TODO: It would be cleaner for CallerInfo to explicitly support
+ // SIP addresses instead of overloading the "phoneNumber" field.
+ // Then we could remove this hack, and instead ask the CallerInfo
+ // for a "user visible" form of the SIP address.
+ String number = info.phoneNumber;
+ if ((number != null) && number.startsWith("sip:")) {
+ number = number.substring(4);
+ }
+
+ if (TextUtils.isEmpty(info.name)) {
+ // No valid "name" in the CallerInfo, so fall back to
+ // something else.
+ // (Typically, we promote the phone number up to the "name" slot
+ // onscreen, and possibly display a descriptive string in the
+ // "number" slot.)
+ if (TextUtils.isEmpty(number)) {
+ // No name *or* number! Display a generic "unknown" string
+ // (or potentially some other default based on the presentation.)
+ displayName = PhoneUtils.getPresentationString(getContext(), presentation);
+ if (DBG) log(" ==> no name *or* number! displayName = " + displayName);
+ } else if (presentation != PhoneConstants.PRESENTATION_ALLOWED) {
+ // This case should never happen since the network should never send a phone #
+ // AND a restricted presentation. However we leave it here in case of weird
+ // network behavior
+ displayName = PhoneUtils.getPresentationString(getContext(), presentation);
+ if (DBG) log(" ==> presentation not allowed! displayName = " + displayName);
+ } else if (!TextUtils.isEmpty(info.cnapName)) {
+ // No name, but we do have a valid CNAP name, so use that.
+ displayName = info.cnapName;
+ info.name = info.cnapName;
+ displayNumber = number;
+ if (DBG) log(" ==> cnapName available: displayName '"
+ + displayName + "', displayNumber '" + displayNumber + "'");
+ } else {
+ // No name; all we have is a number. This is the typical
+ // case when an incoming call doesn't match any contact,
+ // or if you manually dial an outgoing number using the
+ // dialpad.
+
+ // Promote the phone number up to the "name" slot:
+ displayName = number;
+
+ // ...and use the "number" slot for a geographical description
+ // string if available (but only for incoming calls.)
+ if ((conn != null) && (conn.isIncoming())) {
+ // TODO (CallerInfoAsyncQuery cleanup): Fix the CallerInfo
+ // query to only do the geoDescription lookup in the first
+ // place for incoming calls.
+ displayNumber = info.geoDescription; // may be null
+ }
+
+ if (DBG) log(" ==> no name; falling back to number: displayName '"
+ + displayName + "', displayNumber '" + displayNumber + "'");
+ }
+ } else {
+ // We do have a valid "name" in the CallerInfo. Display that
+ // in the "name" slot, and the phone number in the "number" slot.
+ if (presentation != PhoneConstants.PRESENTATION_ALLOWED) {
+ // This case should never happen since the network should never send a name
+ // AND a restricted presentation. However we leave it here in case of weird
+ // network behavior
+ displayName = PhoneUtils.getPresentationString(getContext(), presentation);
+ if (DBG) log(" ==> valid name, but presentation not allowed!"
+ + " displayName = " + displayName);
+ } else {
+ displayName = info.name;
+ displayNumber = number;
+ label = info.phoneLabel;
+ if (DBG) log(" ==> name is present in CallerInfo: displayName '"
+ + displayName + "', displayNumber '" + displayNumber + "'");
+ }
+ }
+ personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, info.person_id);
+ if (DBG) log("- got personUri: '" + personUri
+ + "', based on info.person_id: " + info.person_id);
+ } else {
+ displayName = PhoneUtils.getPresentationString(getContext(), presentation);
+ }
+
+ if (call.isGeneric()) {
+ updateGenericInfoUi();
+ } else {
+ updateInfoUi(displayName, displayNumber, label);
+ }
+
+ // Update mPhoto
+ // if the temporary flag is set, we know we'll be getting another call after
+ // the CallerInfo has been correctly updated. So, we can skip the image
+ // loading until then.
+
+ // If the photoResource is filled in for the CallerInfo, (like with the
+ // Emergency Number case), then we can just set the photo image without
+ // requesting for an image load. Please refer to CallerInfoAsyncQuery.java
+ // for cases where CallerInfo.photoResource may be set. We can also avoid
+ // the image load step if the image data is cached.
+ if (isTemporary && (info == null || !info.isCachedPhotoCurrent)) {
+ mPhoto.setTag(null);
+ mPhoto.setVisibility(View.INVISIBLE);
+ } else if (info != null && info.photoResource != 0){
+ showImage(mPhoto, info.photoResource);
+ } else if (!showCachedImage(mPhoto, info)) {
+ if (personUri == null) {
+ Log.w(LOG_TAG, "personPri is null. Just use Unknown picture.");
+ showImage(mPhoto, R.drawable.picture_unknown);
+ } else if (personUri.equals(mLoadingPersonUri)) {
+ if (DBG) {
+ log("The requested Uri (" + personUri + ") is being loaded already."
+ + " Ignoret the duplicate load request.");
+ }
+ } else {
+ // Remember which person's photo is being loaded right now so that we won't issue
+ // unnecessary load request multiple times, which will mess up animation around
+ // the contact photo.
+ mLoadingPersonUri = personUri;
+
+ // Forget the drawable previously used.
+ mPhoto.setTag(null);
+ // Show empty screen for a moment.
+ mPhoto.setVisibility(View.INVISIBLE);
+ // Load the image with a callback to update the image state.
+ // When the load is finished, onImageLoadComplete() will be called.
+ ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_UPDATE_PHOTO_FOR_CALL_STATE,
+ getContext(), personUri, this, new AsyncLoadCookie(mPhoto, info, call));
+
+ // If the image load is too slow, we show a default avatar icon afterward.
+ // If it is fast enough, this message will be canceled on onImageLoadComplete().
+ mHandler.removeMessages(MESSAGE_SHOW_UNKNOWN_PHOTO);
+ mHandler.sendEmptyMessageDelayed(MESSAGE_SHOW_UNKNOWN_PHOTO, MESSAGE_DELAY);
+ }
+ }
+
+ // If the phone call is on hold, show it with darker status.
+ // Right now we achieve it by overlaying opaque View.
+ // Note: See also layout file about why so and what is the other possibilities.
+ if (call.getState() == Call.State.HOLDING) {
+ AnimationUtils.Fade.show(mPhotoDimEffect);
+ } else {
+ AnimationUtils.Fade.hide(mPhotoDimEffect, View.GONE);
+ }
+
+ // Other text fields:
+ updateCallTypeLabel(call);
+ // updateSocialStatus(socialStatusText, socialStatusBadge, call); // Currently unused
+ }
+
+ /**
+ * Updates the info portion of the UI to be generic. Used for CDMA 3-way calls.
+ */
+ private void updateGenericInfoUi() {
+ mName.setText(R.string.card_title_in_call);
+ mPhoneNumber.setVisibility(View.GONE);
+ mLabel.setVisibility(View.GONE);
+ }
+
+ /**
+ * Updates the info portion of the call card with passed in values.
+ */
+ private void updateInfoUi(String displayName, String displayNumber, String label) {
+ mName.setText(displayName);
+ mName.setVisibility(View.VISIBLE);
+
+ if (TextUtils.isEmpty(displayNumber)) {
+ mPhoneNumber.setVisibility(View.GONE);
+ // We have a real phone number as "mName" so make it always LTR
+ mName.setTextDirection(View.TEXT_DIRECTION_LTR);
+ } else {
+ mPhoneNumber.setText(displayNumber);
+ mPhoneNumber.setVisibility(View.VISIBLE);
+ // We have a real phone number as "mPhoneNumber" so make it always LTR
+ mPhoneNumber.setTextDirection(View.TEXT_DIRECTION_LTR);
+ }
+
+ if (TextUtils.isEmpty(label)) {
+ mLabel.setVisibility(View.GONE);
+ } else {
+ mLabel.setText(label);
+ mLabel.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Updates the name / photo / number / label fields
+ * for the special "conference call" state.
+ *
+ * If the current call has only a single connection, use
+ * updateDisplayForPerson() instead.
+ */
+ private void updateDisplayForConference(Call call) {
+ if (DBG) log("updateDisplayForConference()...");
+
+ int phoneType = call.getPhone().getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ // This state corresponds to both 3-Way merged call and
+ // Call Waiting accepted call.
+ // In this case we display the UI in a "generic" state, with
+ // the generic "dialing" icon and no caller information,
+ // because in this state in CDMA the user does not really know
+ // which caller party he is talking to.
+ showImage(mPhoto, R.drawable.picture_dialing);
+ mName.setText(R.string.card_title_in_call);
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ // Normal GSM (or possibly SIP?) conference call.
+ // Display the "conference call" image as the contact photo.
+ // TODO: Better visual treatment for contact photos in a
+ // conference call (see bug 1313252).
+ showImage(mPhoto, R.drawable.picture_conference);
+ mName.setText(R.string.card_title_conf_call);
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+
+ mName.setVisibility(View.VISIBLE);
+
+ // TODO: For a conference call, the "phone number" slot is specced
+ // to contain a summary of who's on the call, like "Bill Foldes
+ // and Hazel Nutt" or "Bill Foldes and 2 others".
+ // But for now, just hide it:
+ mPhoneNumber.setVisibility(View.GONE);
+ mLabel.setVisibility(View.GONE);
+
+ // Other text fields:
+ updateCallTypeLabel(call);
+ // updateSocialStatus(null, null, null); // socialStatus is never visible in this state
+
+ // TODO: for a GSM conference call, since we do actually know who
+ // you're talking to, consider also showing names / numbers /
+ // photos of some of the people on the conference here, so you can
+ // see that info without having to click "Manage conference". We
+ // probably have enough space to show info for 2 people, at least.
+ //
+ // To do this, our caller would pass us the activeConnections
+ // list, and we'd call PhoneUtils.getCallerInfo() separately for
+ // each connection.
+ }
+
+ /**
+ * Updates the CallCard "photo" IFF the specified Call is in a state
+ * that needs a special photo (like "busy" or "dialing".)
+ *
+ * If the current call does not require a special image in the "photo"
+ * slot onscreen, don't do anything, since presumably the photo image
+ * has already been set (to the photo of the person we're talking, or
+ * the generic "picture_unknown" image, or the "conference call"
+ * image.)
+ */
+ private void updatePhotoForCallState(Call call) {
+ if (DBG) log("updatePhotoForCallState(" + call + ")...");
+ int photoImageResource = 0;
+
+ // Check for the (relatively few) telephony states that need a
+ // special image in the "photo" slot.
+ Call.State state = call.getState();
+ switch (state) {
+ case DISCONNECTED:
+ // Display the special "busy" photo for BUSY or CONGESTION.
+ // Otherwise (presumably the normal "call ended" state)
+ // leave the photo alone.
+ Connection c = call.getEarliestConnection();
+ // if the connection is null, we assume the default case,
+ // otherwise update the image resource normally.
+ if (c != null) {
+ Connection.DisconnectCause cause = c.getDisconnectCause();
+ if ((cause == Connection.DisconnectCause.BUSY)
+ || (cause == Connection.DisconnectCause.CONGESTION)) {
+ photoImageResource = R.drawable.picture_busy;
+ }
+ } else if (DBG) {
+ log("updatePhotoForCallState: connection is null, ignoring.");
+ }
+
+ // TODO: add special images for any other DisconnectCauses?
+ break;
+
+ case ALERTING:
+ case DIALING:
+ default:
+ // Leave the photo alone in all other states.
+ // If this call is an individual call, and the image is currently
+ // displaying a state, (rather than a photo), we'll need to update
+ // the image.
+ // This is for the case where we've been displaying the state and
+ // now we need to restore the photo. This can happen because we
+ // only query the CallerInfo once, and limit the number of times
+ // the image is loaded. (So a state image may overwrite the photo
+ // and we would otherwise have no way of displaying the photo when
+ // the state goes away.)
+
+ // if the photoResource field is filled-in in the Connection's
+ // caller info, then we can just use that instead of requesting
+ // for a photo load.
+
+ // look for the photoResource if it is available.
+ CallerInfo ci = null;
+ {
+ Connection conn = null;
+ int phoneType = call.getPhone().getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ conn = call.getLatestConnection();
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ conn = call.getEarliestConnection();
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+
+ if (conn != null) {
+ Object o = conn.getUserData();
+ if (o instanceof CallerInfo) {
+ ci = (CallerInfo) o;
+ } else if (o instanceof PhoneUtils.CallerInfoToken) {
+ ci = ((PhoneUtils.CallerInfoToken) o).currentInfo;
+ }
+ }
+ }
+
+ if (ci != null) {
+ photoImageResource = ci.photoResource;
+ }
+
+ // If no photoResource found, check to see if this is a conference call. If
+ // it is not a conference call:
+ // 1. Try to show the cached image
+ // 2. If the image is not cached, check to see if a load request has been
+ // made already.
+ // 3. If the load request has not been made [DISPLAY_DEFAULT], start the
+ // request and note that it has started by updating photo state with
+ // [DISPLAY_IMAGE].
+ if (photoImageResource == 0) {
+ if (!PhoneUtils.isConferenceCall(call)) {
+ if (!showCachedImage(mPhoto, ci) && (mPhotoTracker.getPhotoState() ==
+ ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT)) {
+ Uri photoUri = mPhotoTracker.getPhotoUri();
+ if (photoUri == null) {
+ Log.w(LOG_TAG, "photoUri became null. Show default avatar icon");
+ showImage(mPhoto, R.drawable.picture_unknown);
+ } else {
+ if (DBG) {
+ log("start asynchronous load inside updatePhotoForCallState()");
+ }
+ mPhoto.setTag(null);
+ // Make it invisible for a moment
+ mPhoto.setVisibility(View.INVISIBLE);
+ ContactsAsyncHelper.startObtainPhotoAsync(TOKEN_DO_NOTHING,
+ getContext(), photoUri, this,
+ new AsyncLoadCookie(mPhoto, ci, null));
+ }
+ mPhotoTracker.setPhotoState(
+ ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
+ }
+ }
+ } else {
+ showImage(mPhoto, photoImageResource);
+ mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_IMAGE);
+ return;
+ }
+ break;
+ }
+
+ if (photoImageResource != 0) {
+ if (DBG) log("- overrriding photo image: " + photoImageResource);
+ showImage(mPhoto, photoImageResource);
+ // Track the image state.
+ mPhotoTracker.setPhotoState(ContactsAsyncHelper.ImageTracker.DISPLAY_DEFAULT);
+ }
+ }
+
+ /**
+ * Try to display the cached image from the callerinfo object.
+ *
+ * @return true if we were able to find the image in the cache, false otherwise.
+ */
+ private static final boolean showCachedImage(ImageView view, CallerInfo ci) {
+ if ((ci != null) && ci.isCachedPhotoCurrent) {
+ if (ci.cachedPhoto != null) {
+ showImage(view, ci.cachedPhoto);
+ } else {
+ showImage(view, R.drawable.picture_unknown);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /** Helper function to display the resource in the imageview AND ensure its visibility.*/
+ private static final void showImage(ImageView view, int resource) {
+ showImage(view, view.getContext().getResources().getDrawable(resource));
+ }
+
+ private static final void showImage(ImageView view, Bitmap bitmap) {
+ showImage(view, new BitmapDrawable(view.getContext().getResources(), bitmap));
+ }
+
+ /** Helper function to display the drawable in the imageview AND ensure its visibility.*/
+ private static final void showImage(ImageView view, Drawable drawable) {
+ Resources res = view.getContext().getResources();
+ Drawable current = (Drawable) view.getTag();
+
+ if (current == null) {
+ if (DBG) log("Start fade-in animation for " + view);
+ view.setImageDrawable(drawable);
+ AnimationUtils.Fade.show(view);
+ view.setTag(drawable);
+ } else {
+ AnimationUtils.startCrossFade(view, current, drawable);
+ view.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /**
+ * Returns the special card title used in emergency callback mode (ECM),
+ * which shows your own phone number.
+ */
+ private String getECMCardTitle(Context context, Phone phone) {
+ String rawNumber = phone.getLine1Number(); // may be null or empty
+ String formattedNumber;
+ if (!TextUtils.isEmpty(rawNumber)) {
+ formattedNumber = PhoneNumberUtils.formatNumber(rawNumber);
+ } else {
+ formattedNumber = context.getString(R.string.unknown);
+ }
+ String titleFormat = context.getString(R.string.card_title_my_phone_number);
+ return String.format(titleFormat, formattedNumber);
+ }
+
+ /**
+ * Updates the "Call type" label, based on the current foreground call.
+ * This is a special label and/or branding we display for certain
+ * kinds of calls.
+ *
+ * (So far, this is used only for SIP calls, which get an
+ * "Internet call" label. TODO: But eventually, the telephony
+ * layer might allow each pluggable "provider" to specify a string
+ * and/or icon to be displayed here.)
+ */
+ private void updateCallTypeLabel(Call call) {
+ int phoneType = (call != null) ? call.getPhone().getPhoneType() :
+ PhoneConstants.PHONE_TYPE_NONE;
+ if (phoneType == PhoneConstants.PHONE_TYPE_SIP) {
+ mCallTypeLabel.setVisibility(View.VISIBLE);
+ mCallTypeLabel.setText(R.string.incall_call_type_label_sip);
+ mCallTypeLabel.setTextColor(mTextColorCallTypeSip);
+ // If desired, we could also display a "badge" next to the label, as follows:
+ // mCallTypeLabel.setCompoundDrawablesWithIntrinsicBounds(
+ // callTypeSpecificBadge, null, null, null);
+ // mCallTypeLabel.setCompoundDrawablePadding((int) (mDensity * 6));
+ } else {
+ mCallTypeLabel.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Updates the "social status" label with the specified text and
+ * (optional) badge.
+ */
+ /*private void updateSocialStatus(String socialStatusText,
+ Drawable socialStatusBadge,
+ Call call) {
+ // The socialStatus field is *only* visible while an incoming call
+ // is ringing, never in any other call state.
+ if ((socialStatusText != null)
+ && (call != null)
+ && call.isRinging()
+ && !call.isGeneric()) {
+ mSocialStatus.setVisibility(View.VISIBLE);
+ mSocialStatus.setText(socialStatusText);
+ mSocialStatus.setCompoundDrawablesWithIntrinsicBounds(
+ socialStatusBadge, null, null, null);
+ mSocialStatus.setCompoundDrawablePadding((int) (mDensity * 6));
+ } else {
+ mSocialStatus.setVisibility(View.GONE);
+ }
+ }*/
+
+ /**
+ * Hides the top-level UI elements of the call card: The "main
+ * call card" element representing the current active or ringing call,
+ * and also the info areas for "ongoing" or "on hold" calls in some
+ * states.
+ *
+ * This is intended to be used in special states where the normal
+ * in-call UI is totally replaced by some other UI, like OTA mode on a
+ * CDMA device.
+ *
+ * To bring back the regular CallCard UI, just re-run the normal
+ * updateState() call sequence.
+ */
+ public void hideCallCardElements() {
+ mPrimaryCallInfo.setVisibility(View.GONE);
+ mSecondaryCallInfo.setVisibility(View.GONE);
+ }
+
+ /*
+ * Updates the hint (like "Rotate to answer") that we display while
+ * the user is dragging the incoming call RotarySelector widget.
+ */
+ /* package */ void setIncomingCallWidgetHint(int hintTextResId, int hintColorResId) {
+ mIncomingCallWidgetHintTextResId = hintTextResId;
+ mIncomingCallWidgetHintColorResId = hintColorResId;
+ }
+
+ // Accessibility event support.
+ // Since none of the CallCard elements are focusable, we need to manually
+ // fill in the AccessibilityEvent here (so that the name / number / etc will
+ // get pronounced by a screen reader, for example.)
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) {
+ dispatchPopulateAccessibilityEvent(event, mName);
+ dispatchPopulateAccessibilityEvent(event, mPhoneNumber);
+ return true;
+ }
+
+ dispatchPopulateAccessibilityEvent(event, mCallStateLabel);
+ dispatchPopulateAccessibilityEvent(event, mPhoto);
+ dispatchPopulateAccessibilityEvent(event, mName);
+ dispatchPopulateAccessibilityEvent(event, mPhoneNumber);
+ dispatchPopulateAccessibilityEvent(event, mLabel);
+ // dispatchPopulateAccessibilityEvent(event, mSocialStatus);
+ if (mSecondaryCallName != null) {
+ dispatchPopulateAccessibilityEvent(event, mSecondaryCallName);
+ }
+ if (mSecondaryCallPhoto != null) {
+ dispatchPopulateAccessibilityEvent(event, mSecondaryCallPhoto);
+ }
+ return true;
+ }
+
+ private void dispatchPopulateAccessibilityEvent(AccessibilityEvent event, View view) {
+ List<CharSequence> eventText = event.getText();
+ int size = eventText.size();
+ view.dispatchPopulateAccessibilityEvent(event);
+ // if no text added write null to keep relative position
+ if (size == eventText.size()) {
+ eventText.add(null);
+ }
+ }
+
+ public void clear() {
+ // The existing phone design is to keep an instance of call card forever. Until that
+ // design changes, this method is needed to clear (reset) the call card for the next call
+ // so old data is not shown.
+
+ // Other elements can also be cleared here. Starting with elapsed time to fix a bug.
+ mElapsedTime.setVisibility(View.GONE);
+ mElapsedTime.setText(null);
+ }
+
+
+ // Debugging / testing code
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/CallController.java b/src/com/android/phone/CallController.java
new file mode 100644
index 0000000..11340aa
--- /dev/null
+++ b/src/com/android/phone/CallController.java
@@ -0,0 +1,793 @@
+/*
+ * Copyright (C) 2011 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.phone;
+
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyCapabilities;
+import com.android.phone.Constants.CallStatusCode;
+import com.android.phone.InCallUiState.InCallScreenMode;
+import com.android.phone.OtaUtils.CdmaOtaScreenState;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.provider.CallLog.Calls;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.ServiceState;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+/**
+ * Phone app module in charge of "call control".
+ *
+ * This is a singleton object which acts as the interface to the telephony layer
+ * (and other parts of the Android framework) for all user-initiated telephony
+ * functionality, like making outgoing calls.
+ *
+ * This functionality includes things like:
+ * - actually running the placeCall() method and handling errors or retries
+ * - running the whole "emergency call in airplane mode" sequence
+ * - running the state machine of MMI sequences
+ * - restoring/resetting mute and speaker state when a new call starts
+ * - updating the prox sensor wake lock state
+ * - resolving what the voicemail: intent should mean (and making the call)
+ *
+ * The single CallController instance stays around forever; it's not tied
+ * to the lifecycle of any particular Activity (like the InCallScreen).
+ * There's also no implementation of onscreen UI here (that's all in InCallScreen).
+ *
+ * Note that this class does not handle asynchronous events from the telephony
+ * layer, like reacting to an incoming call; see CallNotifier for that. This
+ * class purely handles actions initiated by the user, like outgoing calls.
+ */
+public class CallController extends Handler {
+ private static final String TAG = "CallController";
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ // Do not check in with VDBG = true, since that may write PII to the system log.
+ private static final boolean VDBG = false;
+
+ /** The singleton CallController instance. */
+ private static CallController sInstance;
+
+ private PhoneGlobals mApp;
+ private CallManager mCM;
+ private CallLogger mCallLogger;
+
+ /** Helper object for emergency calls in some rare use cases. Created lazily. */
+ private EmergencyCallHelper mEmergencyCallHelper;
+
+
+ //
+ // Message codes; see handleMessage().
+ //
+
+ private static final int THREEWAY_CALLERINFO_DISPLAY_DONE = 1;
+
+
+ //
+ // Misc constants.
+ //
+
+ // Amount of time the UI should display "Dialing" when initiating a CDMA
+ // 3way call. (See comments on the THRWAY_ACTIVE case in
+ // placeCallInternal() for more info.)
+ private static final int THREEWAY_CALLERINFO_DISPLAY_TIME = 3000; // msec
+
+
+ /**
+ * Initialize the singleton CallController instance.
+ *
+ * This is only done once, at startup, from PhoneApp.onCreate().
+ * From then on, the CallController instance is available via the
+ * PhoneApp's public "callController" field, which is why there's no
+ * getInstance() method here.
+ */
+ /* package */ static CallController init(PhoneGlobals app, CallLogger callLogger) {
+ synchronized (CallController.class) {
+ if (sInstance == null) {
+ sInstance = new CallController(app, callLogger);
+ } else {
+ Log.wtf(TAG, "init() called multiple times! sInstance = " + sInstance);
+ }
+ return sInstance;
+ }
+ }
+
+ /**
+ * Private constructor (this is a singleton).
+ * @see init()
+ */
+ private CallController(PhoneGlobals app, CallLogger callLogger) {
+ if (DBG) log("CallController constructor: app = " + app);
+ mApp = app;
+ mCM = app.mCM;
+ mCallLogger = callLogger;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (VDBG) log("handleMessage: " + msg);
+ switch (msg.what) {
+
+ case THREEWAY_CALLERINFO_DISPLAY_DONE:
+ if (DBG) log("THREEWAY_CALLERINFO_DISPLAY_DONE...");
+
+ if (mApp.cdmaPhoneCallState.getCurrentCallState()
+ == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) {
+ // Reset the mThreeWayCallOrigStateDialing state
+ mApp.cdmaPhoneCallState.setThreeWayCallOrigState(false);
+
+ // Refresh the in-call UI (based on the current ongoing call)
+ mApp.updateInCallScreen();
+ }
+ break;
+
+ default:
+ Log.wtf(TAG, "handleMessage: unexpected code: " + msg);
+ break;
+ }
+ }
+
+ //
+ // Outgoing call sequence
+ //
+
+ /**
+ * Initiate an outgoing call.
+ *
+ * Here's the most typical outgoing call sequence:
+ *
+ * (1) OutgoingCallBroadcaster receives a CALL intent and sends the
+ * NEW_OUTGOING_CALL broadcast
+ *
+ * (2) The broadcast finally reaches OutgoingCallReceiver, which stashes
+ * away a copy of the original CALL intent and launches
+ * SipCallOptionHandler
+ *
+ * (3) SipCallOptionHandler decides whether this is a PSTN or SIP call (and
+ * in some cases brings up a dialog to let the user choose), and
+ * ultimately calls CallController.placeCall() (from the
+ * setResultAndFinish() method) with the stashed-away intent from step
+ * (2) as the "intent" parameter.
+ *
+ * (4) Here in CallController.placeCall() we read the phone number or SIP
+ * address out of the intent and actually initiate the call, and
+ * simultaneously launch the InCallScreen to display the in-call UI.
+ *
+ * (5) We handle various errors by directing the InCallScreen to
+ * display error messages or dialogs (via the InCallUiState
+ * "pending call status code" flag), and in some cases we also
+ * sometimes continue working in the background to resolve the
+ * problem (like in the case of an emergency call while in
+ * airplane mode). Any time that some onscreen indication to the
+ * user needs to change, we update the "status dialog" info in
+ * the inCallUiState and (re)launch the InCallScreen to make sure
+ * it's visible.
+ */
+ public void placeCall(Intent intent) {
+ log("placeCall()... intent = " + intent);
+ if (VDBG) log(" extras = " + intent.getExtras());
+
+ final InCallUiState inCallUiState = mApp.inCallUiState;
+
+ // TODO: Do we need to hold a wake lock while this method runs?
+ // Or did we already acquire one somewhere earlier
+ // in this sequence (like when we first received the CALL intent?)
+
+ if (intent == null) {
+ Log.wtf(TAG, "placeCall: called with null intent");
+ throw new IllegalArgumentException("placeCall: called with null intent");
+ }
+
+ String action = intent.getAction();
+ Uri uri = intent.getData();
+ if (uri == null) {
+ Log.wtf(TAG, "placeCall: intent had no data");
+ throw new IllegalArgumentException("placeCall: intent had no data");
+ }
+
+ String scheme = uri.getScheme();
+ String number = PhoneNumberUtils.getNumberFromIntent(intent, mApp);
+ if (VDBG) {
+ log("- action: " + action);
+ log("- uri: " + uri);
+ log("- scheme: " + scheme);
+ log("- number: " + number);
+ }
+
+ // This method should only be used with the various flavors of CALL
+ // intents. (It doesn't make sense for any other action to trigger an
+ // outgoing call!)
+ if (!(Intent.ACTION_CALL.equals(action)
+ || Intent.ACTION_CALL_EMERGENCY.equals(action)
+ || Intent.ACTION_CALL_PRIVILEGED.equals(action))) {
+ Log.wtf(TAG, "placeCall: unexpected intent action " + action);
+ throw new IllegalArgumentException("Unexpected action: " + action);
+ }
+
+ // Check to see if this is an OTASP call (the "activation" call
+ // used to provision CDMA devices), and if so, do some
+ // OTASP-specific setup.
+ Phone phone = mApp.mCM.getDefaultPhone();
+ if (TelephonyCapabilities.supportsOtasp(phone)) {
+ checkForOtaspCall(intent);
+ }
+
+ // Clear out the "restore mute state" flag since we're
+ // initiating a brand-new call.
+ //
+ // (This call to setRestoreMuteOnInCallResume(false) informs the
+ // phone app that we're dealing with a new connection
+ // (i.e. placing an outgoing call, and NOT handling an aborted
+ // "Add Call" request), so we should let the mute state be handled
+ // by the PhoneUtils phone state change handler.)
+ mApp.setRestoreMuteOnInCallResume(false);
+
+ // If a provider is used, extract the info to build the
+ // overlay and route the call. The overlay will be
+ // displayed when the InCallScreen becomes visible.
+ if (PhoneUtils.hasPhoneProviderExtras(intent)) {
+ inCallUiState.setProviderInfo(intent);
+ } else {
+ inCallUiState.clearProviderInfo();
+ }
+
+ CallStatusCode status = placeCallInternal(intent);
+
+ switch (status) {
+ // Call was placed successfully:
+ case SUCCESS:
+ case EXITED_ECM:
+ if (DBG) log("==> placeCall(): success from placeCallInternal(): " + status);
+
+ if (status == CallStatusCode.EXITED_ECM) {
+ // Call succeeded, but we also need to tell the
+ // InCallScreen to show the "Exiting ECM" warning.
+ inCallUiState.setPendingCallStatusCode(CallStatusCode.EXITED_ECM);
+ } else {
+ // Call succeeded. There's no "error condition" that
+ // needs to be displayed to the user, so clear out the
+ // InCallUiState's "pending call status code".
+ inCallUiState.clearPendingCallStatusCode();
+ }
+
+ // Notify the phone app that a call is beginning so it can
+ // enable the proximity sensor
+ mApp.setBeginningCall(true);
+ break;
+
+ default:
+ // Any other status code is a failure.
+ log("==> placeCall(): failure code from placeCallInternal(): " + status);
+ // Handle the various error conditions that can occur when
+ // initiating an outgoing call, typically by directing the
+ // InCallScreen to display a diagnostic message (via the
+ // "pending call status code" flag.)
+ handleOutgoingCallError(status);
+ break;
+ }
+
+ // Finally, regardless of whether we successfully initiated the
+ // outgoing call or not, force the InCallScreen to come to the
+ // foreground.
+ //
+ // (For successful calls the the user will just see the normal
+ // in-call UI. Or if there was an error, the InCallScreen will
+ // notice the InCallUiState pending call status code flag and display an
+ // error indication instead.)
+
+ // TODO: double-check the behavior of mApp.displayCallScreen()
+ // if the InCallScreen is already visible:
+ // - make sure it forces the UI to refresh
+ // - make sure it does NOT launch a new InCallScreen on top
+ // of the current one (i.e. the Back button should not take
+ // you back to the previous InCallScreen)
+ // - it's probably OK to go thru a fresh pause/resume sequence
+ // though (since that should be fast now)
+ // - if necessary, though, maybe PhoneApp.displayCallScreen()
+ // could notice that the InCallScreen is already in the foreground,
+ // and if so simply call updateInCallScreen() instead.
+
+ mApp.displayCallScreen();
+ }
+
+ /**
+ * Actually make a call to whomever the intent tells us to.
+ *
+ * Note that there's no need to explicitly update (or refresh) the
+ * in-call UI at any point in this method, since a fresh InCallScreen
+ * instance will be launched automatically after we return (see
+ * placeCall() above.)
+ *
+ * @param intent the CALL intent describing whom to call
+ * @return CallStatusCode.SUCCESS if we successfully initiated an
+ * outgoing call. If there was some kind of failure, return one of
+ * the other CallStatusCode codes indicating what went wrong.
+ */
+ private CallStatusCode placeCallInternal(Intent intent) {
+ if (DBG) log("placeCallInternal()... intent = " + intent);
+
+ // TODO: This method is too long. Break it down into more
+ // manageable chunks.
+
+ final InCallUiState inCallUiState = mApp.inCallUiState;
+ final Uri uri = intent.getData();
+ final String scheme = (uri != null) ? uri.getScheme() : null;
+ String number;
+ Phone phone = null;
+
+ // Check the current ServiceState to make sure it's OK
+ // to even try making a call.
+ CallStatusCode okToCallStatus = checkIfOkToInitiateOutgoingCall(
+ mCM.getServiceState());
+
+ // TODO: Streamline the logic here. Currently, the code is
+ // unchanged from its original form in InCallScreen.java. But we
+ // should fix a couple of things:
+ // - Don't call checkIfOkToInitiateOutgoingCall() more than once
+ // - Wrap the try/catch for VoiceMailNumberMissingException
+ // around *only* the call that can throw that exception.
+
+ try {
+ number = PhoneUtils.getInitialNumber(intent);
+ if (VDBG) log("- actual number to dial: '" + number + "'");
+
+ // find the phone first
+ // TODO Need a way to determine which phone to place the call
+ // It could be determined by SIP setting, i.e. always,
+ // or by number, i.e. for international,
+ // or by user selection, i.e., dialog query,
+ // or any of combinations
+ String sipPhoneUri = intent.getStringExtra(
+ OutgoingCallBroadcaster.EXTRA_SIP_PHONE_URI);
+ phone = PhoneUtils.pickPhoneBasedOnNumber(mCM, scheme, number, sipPhoneUri);
+ if (VDBG) log("- got Phone instance: " + phone + ", class = " + phone.getClass());
+
+ // update okToCallStatus based on new phone
+ okToCallStatus = checkIfOkToInitiateOutgoingCall(
+ phone.getServiceState().getState());
+
+ } catch (PhoneUtils.VoiceMailNumberMissingException ex) {
+ // If the call status is NOT in an acceptable state, it
+ // may effect the way the voicemail number is being
+ // retrieved. Mask the VoiceMailNumberMissingException
+ // with the underlying issue of the phone state.
+ if (okToCallStatus != CallStatusCode.SUCCESS) {
+ if (DBG) log("Voicemail number not reachable in current SIM card state.");
+ return okToCallStatus;
+ }
+ if (DBG) log("VoiceMailNumberMissingException from getInitialNumber()");
+ return CallStatusCode.VOICEMAIL_NUMBER_MISSING;
+ }
+
+ if (number == null) {
+ Log.w(TAG, "placeCall: couldn't get a phone number from Intent " + intent);
+ return CallStatusCode.NO_PHONE_NUMBER_SUPPLIED;
+ }
+
+
+ // Sanity-check that ACTION_CALL_EMERGENCY is used if and only if
+ // this is a call to an emergency number
+ // (This is just a sanity-check; this policy *should* really be
+ // enforced in OutgoingCallBroadcaster.onCreate(), which is the
+ // main entry point for the CALL and CALL_* intents.)
+ boolean isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(number, mApp);
+ boolean isPotentialEmergencyNumber =
+ PhoneNumberUtils.isPotentialLocalEmergencyNumber(number, mApp);
+ boolean isEmergencyIntent = Intent.ACTION_CALL_EMERGENCY.equals(intent.getAction());
+
+ if (isPotentialEmergencyNumber && !isEmergencyIntent) {
+ Log.e(TAG, "Non-CALL_EMERGENCY Intent " + intent
+ + " attempted to call potential emergency number " + number
+ + ".");
+ return CallStatusCode.CALL_FAILED;
+ } else if (!isPotentialEmergencyNumber && isEmergencyIntent) {
+ Log.e(TAG, "Received CALL_EMERGENCY Intent " + intent
+ + " with non-potential-emergency number " + number
+ + " -- failing call.");
+ return CallStatusCode.CALL_FAILED;
+ }
+
+ // If we're trying to call an emergency number, then it's OK to
+ // proceed in certain states where we'd otherwise bring up
+ // an error dialog:
+ // - If we're in EMERGENCY_ONLY mode, then (obviously) you're allowed
+ // to dial emergency numbers.
+ // - If we're OUT_OF_SERVICE, we still attempt to make a call,
+ // since the radio will register to any available network.
+
+ if (isEmergencyNumber
+ && ((okToCallStatus == CallStatusCode.EMERGENCY_ONLY)
+ || (okToCallStatus == CallStatusCode.OUT_OF_SERVICE))) {
+ if (DBG) log("placeCall: Emergency number detected with status = " + okToCallStatus);
+ okToCallStatus = CallStatusCode.SUCCESS;
+ if (DBG) log("==> UPDATING status to: " + okToCallStatus);
+ }
+
+ if (okToCallStatus != CallStatusCode.SUCCESS) {
+ // If this is an emergency call, launch the EmergencyCallHelperService
+ // to turn on the radio and retry the call.
+ if (isEmergencyNumber && (okToCallStatus == CallStatusCode.POWER_OFF)) {
+ Log.i(TAG, "placeCall: Trying to make emergency call while POWER_OFF!");
+
+ // If needed, lazily instantiate an EmergencyCallHelper instance.
+ synchronized (this) {
+ if (mEmergencyCallHelper == null) {
+ mEmergencyCallHelper = new EmergencyCallHelper(this);
+ }
+ }
+
+ // ...and kick off the "emergency call from airplane mode" sequence.
+ mEmergencyCallHelper.startEmergencyCallFromAirplaneModeSequence(number);
+
+ // Finally, return CallStatusCode.SUCCESS right now so
+ // that the in-call UI will remain visible (in order to
+ // display the progress indication.)
+ // TODO: or maybe it would be more clear to return a whole
+ // new CallStatusCode called "TURNING_ON_RADIO" here.
+ // That way, we'd update inCallUiState.progressIndication from
+ // the handleOutgoingCallError() method, rather than here.
+ return CallStatusCode.SUCCESS;
+ } else {
+ // Otherwise, just return the (non-SUCCESS) status code
+ // back to our caller.
+ if (DBG) log("==> placeCallInternal(): non-success status: " + okToCallStatus);
+
+ // Log failed call.
+ // Note: Normally, many of these values we gather from the Connection object but
+ // since no such object is created for unconnected calls, we have to build them
+ // manually.
+ // TODO(santoscordon): Try to restructure code so that we can handle failure-
+ // condition call logging in a single place (placeCall()) that also has access to
+ // the number we attempted to dial (not placeCall()).
+ mCallLogger.logCall(null /* callerInfo */, number, 0 /* presentation */,
+ Calls.OUTGOING_TYPE, System.currentTimeMillis(), 0 /* duration */);
+
+ return okToCallStatus;
+ }
+ }
+
+ // Ok, we can proceed with this outgoing call.
+
+ // Reset some InCallUiState flags, just in case they're still set
+ // from a prior call.
+ inCallUiState.needToShowCallLostDialog = false;
+ inCallUiState.clearProgressIndication();
+
+ // We have a valid number, so try to actually place a call:
+ // make sure we pass along the intent's URI which is a
+ // reference to the contact. We may have a provider gateway
+ // phone number to use for the outgoing call.
+ Uri contactUri = intent.getData();
+
+ // Watch out: PhoneUtils.placeCall() returns one of the
+ // CALL_STATUS_* constants, not a CallStatusCode enum value.
+ int callStatus = PhoneUtils.placeCall(mApp,
+ phone,
+ number,
+ contactUri,
+ (isEmergencyNumber || isEmergencyIntent),
+ inCallUiState.providerGatewayUri);
+
+ switch (callStatus) {
+ case PhoneUtils.CALL_STATUS_DIALED:
+ if (VDBG) log("placeCall: PhoneUtils.placeCall() succeeded for regular call '"
+ + number + "'.");
+
+
+ // TODO(OTASP): still need more cleanup to simplify the mApp.cdma*State objects:
+ // - Rather than checking inCallUiState.inCallScreenMode, the
+ // code here could also check for
+ // app.getCdmaOtaInCallScreenUiState() returning NORMAL.
+ // - But overall, app.inCallUiState.inCallScreenMode and
+ // app.cdmaOtaInCallScreenUiState.state are redundant.
+ // Combine them.
+
+ if (VDBG) log ("- inCallUiState.inCallScreenMode = "
+ + inCallUiState.inCallScreenMode);
+ if (inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL) {
+ if (VDBG) log ("==> OTA_NORMAL note: switching to OTA_STATUS_LISTENING.");
+ mApp.cdmaOtaScreenState.otaScreenState =
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_LISTENING;
+ }
+
+ boolean voicemailUriSpecified = scheme != null && scheme.equals("voicemail");
+ // When voicemail is requested most likely the user wants to open
+ // dialpad immediately, so we show it in the first place.
+ // Otherwise we want to make sure the user can see the regular
+ // in-call UI while the new call is dialing, and when it
+ // first gets connected.)
+ inCallUiState.showDialpad = voicemailUriSpecified;
+
+ // For voicemails, we add context text to let the user know they
+ // are dialing their voicemail.
+ // TODO: This is only set here and becomes problematic when swapping calls
+ inCallUiState.dialpadContextText = voicemailUriSpecified ?
+ phone.getVoiceMailAlphaTag() : "";
+
+ // Also, in case a previous call was already active (i.e. if
+ // we just did "Add call"), clear out the "history" of DTMF
+ // digits you typed, to make sure it doesn't persist from the
+ // previous call to the new call.
+ // TODO: it would be more precise to do this when the actual
+ // phone state change happens (i.e. when a new foreground
+ // call appears and the previous call moves to the
+ // background), but the InCallScreen doesn't keep enough
+ // state right now to notice that specific transition in
+ // onPhoneStateChanged().
+ inCallUiState.dialpadDigits = null;
+
+ // Check for an obscure ECM-related scenario: If the phone
+ // is currently in ECM (Emergency callback mode) and we
+ // dial a non-emergency number, that automatically
+ // *cancels* ECM. So warn the user about it.
+ // (See InCallScreen.showExitingECMDialog() for more info.)
+ boolean exitedEcm = false;
+ if (PhoneUtils.isPhoneInEcm(phone) && !isEmergencyNumber) {
+ Log.i(TAG, "About to exit ECM because of an outgoing non-emergency call");
+ exitedEcm = true; // this will cause us to return EXITED_ECM from this method
+ }
+
+ if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ // Start the timer for 3 Way CallerInfo
+ if (mApp.cdmaPhoneCallState.getCurrentCallState()
+ == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) {
+ //Unmute for the second MO call
+ PhoneUtils.setMute(false);
+
+ // This is a "CDMA 3-way call", which means that you're dialing a
+ // 2nd outgoing call while a previous call is already in progress.
+ //
+ // Due to the limitations of CDMA this call doesn't actually go
+ // through the DIALING/ALERTING states, so we can't tell for sure
+ // when (or if) it's actually answered. But we want to show
+ // *some* indication of what's going on in the UI, so we "fake it"
+ // by displaying the "Dialing" state for 3 seconds.
+
+ // Set the mThreeWayCallOrigStateDialing state to true
+ mApp.cdmaPhoneCallState.setThreeWayCallOrigState(true);
+
+ // Schedule the "Dialing" indication to be taken down in 3 seconds:
+ sendEmptyMessageDelayed(THREEWAY_CALLERINFO_DISPLAY_DONE,
+ THREEWAY_CALLERINFO_DISPLAY_TIME);
+ }
+ }
+
+ // Success!
+ if (exitedEcm) {
+ return CallStatusCode.EXITED_ECM;
+ } else {
+ return CallStatusCode.SUCCESS;
+ }
+
+ case PhoneUtils.CALL_STATUS_DIALED_MMI:
+ if (DBG) log("placeCall: specified number was an MMI code: '" + number + "'.");
+ // The passed-in number was an MMI code, not a regular phone number!
+ // This isn't really a failure; the Dialer may have deliberately
+ // fired an ACTION_CALL intent to dial an MMI code, like for a
+ // USSD call.
+ //
+ // Presumably an MMI_INITIATE message will come in shortly
+ // (and we'll bring up the "MMI Started" dialog), or else
+ // an MMI_COMPLETE will come in (which will take us to a
+ // different Activity; see PhoneUtils.displayMMIComplete()).
+ return CallStatusCode.DIALED_MMI;
+
+ case PhoneUtils.CALL_STATUS_FAILED:
+ Log.w(TAG, "placeCall: PhoneUtils.placeCall() FAILED for number '"
+ + number + "'.");
+ // We couldn't successfully place the call; there was some
+ // failure in the telephony layer.
+
+ // Log failed call.
+ mCallLogger.logCall(null /* callerInfo */, number, 0 /* presentation */,
+ Calls.OUTGOING_TYPE, System.currentTimeMillis(), 0 /* duration */);
+
+ return CallStatusCode.CALL_FAILED;
+
+ default:
+ Log.wtf(TAG, "placeCall: unknown callStatus " + callStatus
+ + " from PhoneUtils.placeCall() for number '" + number + "'.");
+ return CallStatusCode.SUCCESS; // Try to continue anyway...
+ }
+ }
+
+ /**
+ * Checks the current ServiceState to make sure it's OK
+ * to try making an outgoing call to the specified number.
+ *
+ * @return CallStatusCode.SUCCESS if it's OK to try calling the specified
+ * number. If not, like if the radio is powered off or we have no
+ * signal, return one of the other CallStatusCode codes indicating what
+ * the problem is.
+ */
+ private CallStatusCode checkIfOkToInitiateOutgoingCall(int state) {
+ if (VDBG) log("checkIfOkToInitiateOutgoingCall: ServiceState = " + state);
+
+ switch (state) {
+ case ServiceState.STATE_IN_SERVICE:
+ // Normal operation. It's OK to make outgoing calls.
+ return CallStatusCode.SUCCESS;
+
+ case ServiceState.STATE_POWER_OFF:
+ // Radio is explictly powered off.
+ return CallStatusCode.POWER_OFF;
+
+ case ServiceState.STATE_EMERGENCY_ONLY:
+ // The phone is registered, but locked. Only emergency
+ // numbers are allowed.
+ // Note that as of Android 2.0 at least, the telephony layer
+ // does not actually use ServiceState.STATE_EMERGENCY_ONLY,
+ // mainly since there's no guarantee that the radio/RIL can
+ // make this distinction. So in practice the
+ // CallStatusCode.EMERGENCY_ONLY state and the string
+ // "incall_error_emergency_only" are totally unused.
+ return CallStatusCode.EMERGENCY_ONLY;
+
+ case ServiceState.STATE_OUT_OF_SERVICE:
+ // No network connection.
+ return CallStatusCode.OUT_OF_SERVICE;
+
+ default:
+ throw new IllegalStateException("Unexpected ServiceState: " + state);
+ }
+ }
+
+
+
+ /**
+ * Handles the various error conditions that can occur when initiating
+ * an outgoing call.
+ *
+ * Most error conditions are "handled" by simply displaying an error
+ * message to the user. This is accomplished by setting the
+ * inCallUiState pending call status code flag, which tells the
+ * InCallScreen to display an appropriate message to the user when the
+ * in-call UI comes to the foreground.
+ *
+ * @param status one of the CallStatusCode error codes.
+ */
+ private void handleOutgoingCallError(CallStatusCode status) {
+ if (DBG) log("handleOutgoingCallError(): status = " + status);
+ final InCallUiState inCallUiState = mApp.inCallUiState;
+
+ // In most cases we simply want to have the InCallScreen display
+ // an appropriate error dialog, so we simply copy the specified
+ // status code into the InCallUiState "pending call status code"
+ // field. (See InCallScreen.showStatusIndication() for the next
+ // step of the sequence.)
+
+ switch (status) {
+ case SUCCESS:
+ // This case shouldn't happen; you're only supposed to call
+ // handleOutgoingCallError() if there was actually an error!
+ Log.wtf(TAG, "handleOutgoingCallError: SUCCESS isn't an error");
+ break;
+
+ case VOICEMAIL_NUMBER_MISSING:
+ // Bring up the "Missing Voicemail Number" dialog, which
+ // will ultimately take us to some other Activity (or else
+ // just bail out of this activity.)
+
+ // Send a request to the InCallScreen to display the
+ // "voicemail missing" dialog when it (the InCallScreen)
+ // comes to the foreground.
+ inCallUiState.setPendingCallStatusCode(CallStatusCode.VOICEMAIL_NUMBER_MISSING);
+ break;
+
+ case POWER_OFF:
+ // Radio is explictly powered off, presumably because the
+ // device is in airplane mode.
+ //
+ // TODO: For now this UI is ultra-simple: we simply display
+ // a message telling the user to turn off airplane mode.
+ // But it might be nicer for the dialog to offer the option
+ // to turn the radio on right there (and automatically retry
+ // the call once network registration is complete.)
+ inCallUiState.setPendingCallStatusCode(CallStatusCode.POWER_OFF);
+ break;
+
+ case EMERGENCY_ONLY:
+ // Only emergency numbers are allowed, but we tried to dial
+ // a non-emergency number.
+ // (This state is currently unused; see comments above.)
+ inCallUiState.setPendingCallStatusCode(CallStatusCode.EMERGENCY_ONLY);
+ break;
+
+ case OUT_OF_SERVICE:
+ // No network connection.
+ inCallUiState.setPendingCallStatusCode(CallStatusCode.OUT_OF_SERVICE);
+ break;
+
+ case NO_PHONE_NUMBER_SUPPLIED:
+ // The supplied Intent didn't contain a valid phone number.
+ // (This is rare and should only ever happen with broken
+ // 3rd-party apps.) For now just show a generic error.
+ inCallUiState.setPendingCallStatusCode(CallStatusCode.NO_PHONE_NUMBER_SUPPLIED);
+ break;
+
+ case DIALED_MMI:
+ // Our initial phone number was actually an MMI sequence.
+ // There's no real "error" here, but we do bring up the
+ // a Toast (as requested of the New UI paradigm).
+ //
+ // In-call MMIs do not trigger the normal MMI Initiate
+ // Notifications, so we should notify the user here.
+ // Otherwise, the code in PhoneUtils.java should handle
+ // user notifications in the form of Toasts or Dialogs.
+ //
+ // TODO: Rather than launching a toast from here, it would
+ // be cleaner to just set a pending call status code here,
+ // and then let the InCallScreen display the toast...
+ if (mCM.getState() == PhoneConstants.State.OFFHOOK) {
+ Toast.makeText(mApp, R.string.incall_status_dialed_mmi, Toast.LENGTH_SHORT)
+ .show();
+ }
+ break;
+
+ case CALL_FAILED:
+ // We couldn't successfully place the call; there was some
+ // failure in the telephony layer.
+ // TODO: Need UI spec for this failure case; for now just
+ // show a generic error.
+ inCallUiState.setPendingCallStatusCode(CallStatusCode.CALL_FAILED);
+ break;
+
+ default:
+ Log.wtf(TAG, "handleOutgoingCallError: unexpected status code " + status);
+ // Show a generic "call failed" error.
+ inCallUiState.setPendingCallStatusCode(CallStatusCode.CALL_FAILED);
+ break;
+ }
+ }
+
+ /**
+ * Checks the current outgoing call to see if it's an OTASP call (the
+ * "activation" call used to provision CDMA devices). If so, do any
+ * necessary OTASP-specific setup before actually placing the call.
+ */
+ private void checkForOtaspCall(Intent intent) {
+ if (OtaUtils.isOtaspCallIntent(intent)) {
+ Log.i(TAG, "checkForOtaspCall: handling OTASP intent! " + intent);
+
+ // ("OTASP-specific setup" basically means creating and initializing
+ // the OtaUtils instance. Note that this setup needs to be here in
+ // the CallController.placeCall() sequence, *not* in
+ // OtaUtils.startInteractiveOtasp(), since it's also possible to
+ // start an OTASP call by manually dialing "*228" (in which case
+ // OtaUtils.startInteractiveOtasp() never gets run at all.)
+ OtaUtils.setupOtaspCall(intent);
+ } else {
+ if (DBG) log("checkForOtaspCall: not an OTASP call.");
+ }
+ }
+
+
+ //
+ // Debugging
+ //
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/CallFeaturesSetting.java b/src/com/android/phone/CallFeaturesSetting.java
new file mode 100644
index 0000000..1848e54
--- /dev/null
+++ b/src/com/android/phone/CallFeaturesSetting.java
@@ -0,0 +1,2205 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.app.ActionBar;
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.SharedPreferences.Editor;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.media.AudioManager;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.net.sip.SipManager;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserHandle;
+import android.os.Vibrator;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceManager;
+import android.preference.PreferenceScreen;
+import android.provider.ContactsContract.CommonDataKinds;
+import android.provider.MediaStore;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.WindowManager;
+import android.widget.ListAdapter;
+
+import com.android.internal.telephony.CallForwardInfo;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.cdma.TtyIntent;
+import com.android.phone.sip.SipSharedPreferences;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Top level "Call settings" UI; see res/xml/call_feature_setting.xml
+ *
+ * This preference screen is the root of the "Call settings" hierarchy
+ * available from the Phone app; the settings here let you control various
+ * features related to phone calls (including voicemail settings, SIP
+ * settings, the "Respond via SMS" feature, and others.) It's used only
+ * on voice-capable phone devices.
+ *
+ * Note that this activity is part of the package com.android.phone, even
+ * though you reach it from the "Phone" app (i.e. DialtactsActivity) which
+ * is from the package com.android.contacts.
+ *
+ * For the "Mobile network settings" screen under the main Settings app,
+ * See {@link MobileNetworkSettings}.
+ *
+ * @see com.android.phone.MobileNetworkSettings
+ */
+public class CallFeaturesSetting extends PreferenceActivity
+ implements DialogInterface.OnClickListener,
+ Preference.OnPreferenceChangeListener,
+ EditPhoneNumberPreference.OnDialogClosedListener,
+ EditPhoneNumberPreference.GetDefaultNumberListener{
+ private static final String LOG_TAG = "CallFeaturesSetting";
+ private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ /**
+ * Intent action to bring up Voicemail Provider settings.
+ *
+ * @see #IGNORE_PROVIDER_EXTRA
+ */
+ public static final String ACTION_ADD_VOICEMAIL =
+ "com.android.phone.CallFeaturesSetting.ADD_VOICEMAIL";
+ // intent action sent by this activity to a voice mail provider
+ // to trigger its configuration UI
+ public static final String ACTION_CONFIGURE_VOICEMAIL =
+ "com.android.phone.CallFeaturesSetting.CONFIGURE_VOICEMAIL";
+ // Extra put in the return from VM provider config containing voicemail number to set
+ public static final String VM_NUMBER_EXTRA = "com.android.phone.VoicemailNumber";
+ // Extra put in the return from VM provider config containing call forwarding number to set
+ public static final String FWD_NUMBER_EXTRA = "com.android.phone.ForwardingNumber";
+ // Extra put in the return from VM provider config containing call forwarding number to set
+ public static final String FWD_NUMBER_TIME_EXTRA = "com.android.phone.ForwardingNumberTime";
+ // If the VM provider returns non null value in this extra we will force the user to
+ // choose another VM provider
+ public static final String SIGNOUT_EXTRA = "com.android.phone.Signout";
+ //Information about logical "up" Activity
+ private static final String UP_ACTIVITY_PACKAGE = "com.android.dialer";
+ private static final String UP_ACTIVITY_CLASS =
+ "com.android.dialer.DialtactsActivity";
+
+ // Used to tell the saving logic to leave forwarding number as is
+ public static final CallForwardInfo[] FWD_SETTINGS_DONT_TOUCH = null;
+ // Suffix appended to provider key for storing vm number
+ public static final String VM_NUMBER_TAG = "#VMNumber";
+ // Suffix appended to provider key for storing forwarding settings
+ public static final String FWD_SETTINGS_TAG = "#FWDSettings";
+ // Suffix appended to forward settings key for storing length of settings array
+ public static final String FWD_SETTINGS_LENGTH_TAG = "#Length";
+ // Suffix appended to forward settings key for storing an individual setting
+ public static final String FWD_SETTING_TAG = "#Setting";
+ // Suffixes appended to forward setting key for storing an individual setting properties
+ public static final String FWD_SETTING_STATUS = "#Status";
+ public static final String FWD_SETTING_REASON = "#Reason";
+ public static final String FWD_SETTING_NUMBER = "#Number";
+ public static final String FWD_SETTING_TIME = "#Time";
+
+ // Key identifying the default vocie mail provider
+ public static final String DEFAULT_VM_PROVIDER_KEY = "";
+
+ /**
+ * String Extra put into ACTION_ADD_VOICEMAIL call to indicate which provider should be hidden
+ * in the list of providers presented to the user. This allows a provider which is being
+ * disabled (e.g. GV user logging out) to force the user to pick some other provider.
+ */
+ public static final String IGNORE_PROVIDER_EXTRA = "com.android.phone.ProviderToIgnore";
+
+ // string constants
+ private static final String NUM_PROJECTION[] = {CommonDataKinds.Phone.NUMBER};
+
+ // String keys for preference lookup
+ // TODO: Naming these "BUTTON_*" is confusing since they're not actually buttons(!)
+ private static final String BUTTON_VOICEMAIL_KEY = "button_voicemail_key";
+ private static final String BUTTON_VOICEMAIL_PROVIDER_KEY = "button_voicemail_provider_key";
+ private static final String BUTTON_VOICEMAIL_SETTING_KEY = "button_voicemail_setting_key";
+ // New preference key for voicemail notification vibration
+ /* package */ static final String BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY =
+ "button_voicemail_notification_vibrate_key";
+ // Old preference key for voicemail notification vibration. Used for migration to the new
+ // preference key only.
+ /* package */ static final String BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_WHEN_KEY =
+ "button_voicemail_notification_vibrate_when_key";
+ /* package */ static final String BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY =
+ "button_voicemail_notification_ringtone_key";
+ private static final String BUTTON_FDN_KEY = "button_fdn_key";
+ private static final String BUTTON_RESPOND_VIA_SMS_KEY = "button_respond_via_sms_key";
+
+ private static final String BUTTON_RINGTONE_KEY = "button_ringtone_key";
+ private static final String BUTTON_VIBRATE_ON_RING = "button_vibrate_on_ring";
+ private static final String BUTTON_PLAY_DTMF_TONE = "button_play_dtmf_tone";
+ private static final String BUTTON_DTMF_KEY = "button_dtmf_settings";
+ private static final String BUTTON_RETRY_KEY = "button_auto_retry_key";
+ private static final String BUTTON_TTY_KEY = "button_tty_mode_key";
+ private static final String BUTTON_HAC_KEY = "button_hac_key";
+ private static final String BUTTON_DIALPAD_AUTOCOMPLETE = "button_dialpad_autocomplete";
+
+ private static final String BUTTON_GSM_UMTS_OPTIONS = "button_gsm_more_expand_key";
+ private static final String BUTTON_CDMA_OPTIONS = "button_cdma_more_expand_key";
+
+ private static final String VM_NUMBERS_SHARED_PREFERENCES_NAME = "vm_numbers";
+
+ private static final String BUTTON_SIP_CALL_OPTIONS =
+ "sip_call_options_key";
+ private static final String BUTTON_SIP_CALL_OPTIONS_WIFI_ONLY =
+ "sip_call_options_wifi_only_key";
+ private static final String SIP_SETTINGS_CATEGORY_KEY =
+ "sip_settings_category_key";
+
+ private Intent mContactListIntent;
+
+ /** Event for Async voicemail change call */
+ private static final int EVENT_VOICEMAIL_CHANGED = 500;
+ private static final int EVENT_FORWARDING_CHANGED = 501;
+ private static final int EVENT_FORWARDING_GET_COMPLETED = 502;
+
+ private static final int MSG_UPDATE_RINGTONE_SUMMARY = 1;
+ private static final int MSG_UPDATE_VOICEMAIL_RINGTONE_SUMMARY = 2;
+
+ // preferred TTY mode
+ // Phone.TTY_MODE_xxx
+ static final int preferredTtyMode = Phone.TTY_MODE_OFF;
+
+ public static final String HAC_KEY = "HACSetting";
+ public static final String HAC_VAL_ON = "ON";
+ public static final String HAC_VAL_OFF = "OFF";
+
+ /** Handle to voicemail pref */
+ private static final int VOICEMAIL_PREF_ID = 1;
+ private static final int VOICEMAIL_PROVIDER_CFG_ID = 2;
+
+ private Phone mPhone;
+
+ private AudioManager mAudioManager;
+ private SipManager mSipManager;
+
+ private static final int VM_NOCHANGE_ERROR = 400;
+ private static final int VM_RESPONSE_ERROR = 500;
+ private static final int FW_SET_RESPONSE_ERROR = 501;
+ private static final int FW_GET_RESPONSE_ERROR = 502;
+
+
+ // dialog identifiers for voicemail
+ private static final int VOICEMAIL_DIALOG_CONFIRM = 600;
+ private static final int VOICEMAIL_FWD_SAVING_DIALOG = 601;
+ private static final int VOICEMAIL_FWD_READING_DIALOG = 602;
+ private static final int VOICEMAIL_REVERTING_DIALOG = 603;
+
+ // status message sent back from handlers
+ private static final int MSG_OK = 100;
+
+ // special statuses for voicemail controls.
+ private static final int MSG_VM_EXCEPTION = 400;
+ private static final int MSG_FW_SET_EXCEPTION = 401;
+ private static final int MSG_FW_GET_EXCEPTION = 402;
+ private static final int MSG_VM_OK = 600;
+ private static final int MSG_VM_NOCHANGE = 700;
+
+ // voicemail notification vibration string constants
+ private static final String VOICEMAIL_VIBRATION_ALWAYS = "always";
+ private static final String VOICEMAIL_VIBRATION_NEVER = "never";
+
+ private EditPhoneNumberPreference mSubMenuVoicemailSettings;
+
+ private Runnable mRingtoneLookupRunnable;
+ private final Handler mRingtoneLookupComplete = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_RINGTONE_SUMMARY:
+ mRingtonePreference.setSummary((CharSequence) msg.obj);
+ break;
+ case MSG_UPDATE_VOICEMAIL_RINGTONE_SUMMARY:
+ mVoicemailNotificationRingtone.setSummary((CharSequence) msg.obj);
+ break;
+ }
+ }
+ };
+
+ private Preference mRingtonePreference;
+ private CheckBoxPreference mVibrateWhenRinging;
+ /** Whether dialpad plays DTMF tone or not. */
+ private CheckBoxPreference mPlayDtmfTone;
+ private CheckBoxPreference mDialpadAutocomplete;
+ private CheckBoxPreference mButtonAutoRetry;
+ private CheckBoxPreference mButtonHAC;
+ private ListPreference mButtonDTMF;
+ private ListPreference mButtonTTY;
+ private ListPreference mButtonSipCallOptions;
+ private ListPreference mVoicemailProviders;
+ private PreferenceScreen mVoicemailSettings;
+ private Preference mVoicemailNotificationRingtone;
+ private CheckBoxPreference mVoicemailNotificationVibrate;
+ private SipSharedPreferences mSipSharedPreferences;
+
+ private class VoiceMailProvider {
+ public VoiceMailProvider(String name, Intent intent) {
+ this.name = name;
+ this.intent = intent;
+ }
+ public String name;
+ public Intent intent;
+ }
+
+ /**
+ * Forwarding settings we are going to save.
+ */
+ private static final int [] FORWARDING_SETTINGS_REASONS = new int[] {
+ CommandsInterface.CF_REASON_UNCONDITIONAL,
+ CommandsInterface.CF_REASON_BUSY,
+ CommandsInterface.CF_REASON_NO_REPLY,
+ CommandsInterface.CF_REASON_NOT_REACHABLE
+ };
+
+ private class VoiceMailProviderSettings {
+ /**
+ * Constructs settings object, setting all conditional forwarding to the specified number
+ */
+ public VoiceMailProviderSettings(String voicemailNumber, String forwardingNumber,
+ int timeSeconds) {
+ this.voicemailNumber = voicemailNumber;
+ if (forwardingNumber == null || forwardingNumber.length() == 0) {
+ this.forwardingSettings = FWD_SETTINGS_DONT_TOUCH;
+ } else {
+ this.forwardingSettings = new CallForwardInfo[FORWARDING_SETTINGS_REASONS.length];
+ for (int i = 0; i < this.forwardingSettings.length; i++) {
+ CallForwardInfo fi = new CallForwardInfo();
+ this.forwardingSettings[i] = fi;
+ fi.reason = FORWARDING_SETTINGS_REASONS[i];
+ fi.status = (fi.reason == CommandsInterface.CF_REASON_UNCONDITIONAL) ? 0 : 1;
+ fi.serviceClass = CommandsInterface.SERVICE_CLASS_VOICE;
+ fi.toa = PhoneNumberUtils.TOA_International;
+ fi.number = forwardingNumber;
+ fi.timeSeconds = timeSeconds;
+ }
+ }
+ }
+
+ public VoiceMailProviderSettings(String voicemailNumber, CallForwardInfo[] infos) {
+ this.voicemailNumber = voicemailNumber;
+ this.forwardingSettings = infos;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null) return false;
+ if (!(o instanceof VoiceMailProviderSettings)) return false;
+ final VoiceMailProviderSettings v = (VoiceMailProviderSettings)o;
+
+ return ((this.voicemailNumber == null &&
+ v.voicemailNumber == null) ||
+ this.voicemailNumber != null &&
+ this.voicemailNumber.equals(v.voicemailNumber))
+ &&
+ forwardingSettingsEqual(this.forwardingSettings,
+ v.forwardingSettings);
+ }
+
+ private boolean forwardingSettingsEqual(CallForwardInfo[] infos1,
+ CallForwardInfo[] infos2) {
+ if (infos1 == infos2) return true;
+ if (infos1 == null || infos2 == null) return false;
+ if (infos1.length != infos2.length) return false;
+ for (int i = 0; i < infos1.length; i++) {
+ CallForwardInfo i1 = infos1[i];
+ CallForwardInfo i2 = infos2[i];
+ if (i1.status != i2.status ||
+ i1.reason != i2.reason ||
+ i1.serviceClass != i2.serviceClass ||
+ i1.toa != i2.toa ||
+ i1.number != i2.number ||
+ i1.timeSeconds != i2.timeSeconds) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return voicemailNumber + ((forwardingSettings != null ) ? (", " +
+ forwardingSettings.toString()) : "");
+ }
+
+ public String voicemailNumber;
+ public CallForwardInfo[] forwardingSettings;
+ }
+
+ private SharedPreferences mPerProviderSavedVMNumbers;
+
+ /**
+ * Results of reading forwarding settings
+ */
+ private CallForwardInfo[] mForwardingReadResults = null;
+
+ /**
+ * Result of forwarding number change.
+ * Keys are reasons (eg. unconditional forwarding).
+ */
+ private Map<Integer, AsyncResult> mForwardingChangeResults = null;
+
+ /**
+ * Expected CF read result types.
+ * This set keeps track of the CF types for which we've issued change
+ * commands so we can tell when we've received all of the responses.
+ */
+ private Collection<Integer> mExpectedChangeResultReasons = null;
+
+ /**
+ * Result of vm number change
+ */
+ private AsyncResult mVoicemailChangeResult = null;
+
+ /**
+ * Previous VM provider setting so we can return to it in case of failure.
+ */
+ private String mPreviousVMProviderKey = null;
+
+ /**
+ * Id of the dialog being currently shown.
+ */
+ private int mCurrentDialogId = 0;
+
+ /**
+ * Flag indicating that we are invoking settings for the voicemail provider programmatically
+ * due to vm provider change.
+ */
+ private boolean mVMProviderSettingsForced = false;
+
+ /**
+ * Flag indicating that we are making changes to vm or fwd numbers
+ * due to vm provider change.
+ */
+ private boolean mChangingVMorFwdDueToProviderChange = false;
+
+ /**
+ * True if we are in the process of vm & fwd number change and vm has already been changed.
+ * This is used to decide what to do in case of rollback.
+ */
+ private boolean mVMChangeCompletedSuccessfully = false;
+
+ /**
+ * True if we had full or partial failure setting forwarding numbers and so need to roll them
+ * back.
+ */
+ private boolean mFwdChangesRequireRollback = false;
+
+ /**
+ * Id of error msg to display to user once we are done reverting the VM provider to the previous
+ * one.
+ */
+ private int mVMOrFwdSetError = 0;
+
+ /**
+ * Data about discovered voice mail settings providers.
+ * Is populated by querying which activities can handle ACTION_CONFIGURE_VOICEMAIL.
+ * They key in this map is package name + activity name.
+ * We always add an entry for the default provider with a key of empty
+ * string and intent value of null.
+ * @see #initVoiceMailProviders()
+ */
+ private final Map<String, VoiceMailProvider> mVMProvidersData =
+ new HashMap<String, VoiceMailProvider>();
+
+ /** string to hold old voicemail number as it is being updated. */
+ private String mOldVmNumber;
+
+ // New call forwarding settings and vm number we will be setting
+ // Need to save these since before we get to saving we need to asynchronously
+ // query the existing forwarding settings.
+ private CallForwardInfo[] mNewFwdSettings;
+ private String mNewVMNumber;
+
+ private boolean mForeground;
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mForeground = false;
+ }
+
+ /**
+ * We have to pull current settings from the network for all kinds of
+ * voicemail providers so we can tell whether we have to update them,
+ * so use this bit to keep track of whether we're reading settings for the
+ * default provider and should therefore save them out when done.
+ */
+ private boolean mReadingSettingsForDefaultProvider = false;
+
+ /*
+ * Click Listeners, handle click based on objects attached to UI.
+ */
+
+ // Click listener for all toggle events
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
+ if (preference == mSubMenuVoicemailSettings) {
+ return true;
+ } else if (preference == mPlayDtmfTone) {
+ Settings.System.putInt(getContentResolver(), Settings.System.DTMF_TONE_WHEN_DIALING,
+ mPlayDtmfTone.isChecked() ? 1 : 0);
+ } else if (preference == mDialpadAutocomplete) {
+ Settings.Secure.putInt(getContentResolver(), Settings.Secure.DIALPAD_AUTOCOMPLETE,
+ mDialpadAutocomplete.isChecked() ? 1 : 0);
+ } else if (preference == mButtonDTMF) {
+ return true;
+ } else if (preference == mButtonTTY) {
+ return true;
+ } else if (preference == mButtonAutoRetry) {
+ android.provider.Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.CALL_AUTO_RETRY,
+ mButtonAutoRetry.isChecked() ? 1 : 0);
+ return true;
+ } else if (preference == mButtonHAC) {
+ int hac = mButtonHAC.isChecked() ? 1 : 0;
+ // Update HAC value in Settings database
+ Settings.System.putInt(mPhone.getContext().getContentResolver(),
+ Settings.System.HEARING_AID, hac);
+
+ // Update HAC Value in AudioManager
+ mAudioManager.setParameter(HAC_KEY, hac != 0 ? HAC_VAL_ON : HAC_VAL_OFF);
+ return true;
+ } else if (preference == mVoicemailSettings) {
+ if (DBG) log("onPreferenceTreeClick: Voicemail Settings Preference is clicked.");
+ if (preference.getIntent() != null) {
+ if (DBG) {
+ log("onPreferenceTreeClick: Invoking cfg intent "
+ + preference.getIntent().getPackage());
+ }
+
+ // onActivityResult() will be responsible for resetting some of variables.
+ this.startActivityForResult(preference.getIntent(), VOICEMAIL_PROVIDER_CFG_ID);
+ return true;
+ } else {
+ if (DBG) {
+ log("onPreferenceTreeClick:"
+ + " No Intent is available. Use default behavior defined in xml.");
+ }
+
+ // There's no onActivityResult(), so we need to take care of some of variables
+ // which should be reset here.
+ mPreviousVMProviderKey = DEFAULT_VM_PROVIDER_KEY;
+ mVMProviderSettingsForced = false;
+
+ // This should let the preference use default behavior in the xml.
+ return false;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Implemented to support onPreferenceChangeListener to look for preference
+ * changes.
+ *
+ * @param preference is the preference to be changed
+ * @param objValue should be the value of the selection, NOT its localized
+ * display value.
+ */
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object objValue) {
+ if (DBG) {
+ log("onPreferenceChange(). preferenece: \"" + preference + "\""
+ + ", value: \"" + objValue + "\"");
+ }
+ if (preference == mVibrateWhenRinging) {
+ boolean doVibrate = (Boolean) objValue;
+ Settings.System.putInt(mPhone.getContext().getContentResolver(),
+ Settings.System.VIBRATE_WHEN_RINGING, doVibrate ? 1 : 0);
+ } else if (preference == mButtonDTMF) {
+ int index = mButtonDTMF.findIndexOfValue((String) objValue);
+ Settings.System.putInt(mPhone.getContext().getContentResolver(),
+ Settings.System.DTMF_TONE_TYPE_WHEN_DIALING, index);
+ } else if (preference == mButtonTTY) {
+ handleTTYChange(preference, objValue);
+ } else if (preference == mVoicemailProviders) {
+ final String newProviderKey = (String) objValue;
+ if (DBG) {
+ log("Voicemail Provider changes from \"" + mPreviousVMProviderKey
+ + "\" to \"" + newProviderKey + "\".");
+ }
+ // If previous provider key and the new one is same, we don't need to handle it.
+ if (mPreviousVMProviderKey.equals(newProviderKey)) {
+ if (DBG) log("No change is made toward VM provider setting.");
+ return true;
+ }
+ updateVMPreferenceWidgets(newProviderKey);
+
+ final VoiceMailProviderSettings newProviderSettings =
+ loadSettingsForVoiceMailProvider(newProviderKey);
+
+ // If the user switches to a voice mail provider and we have a
+ // numbers stored for it we will automatically change the
+ // phone's
+ // voice mail and forwarding number to the stored ones.
+ // Otherwise we will bring up provider's configuration UI.
+
+ if (newProviderSettings == null) {
+ // Force the user into a configuration of the chosen provider
+ Log.w(LOG_TAG, "Saved preferences not found - invoking config");
+ mVMProviderSettingsForced = true;
+ simulatePreferenceClick(mVoicemailSettings);
+ } else {
+ if (DBG) log("Saved preferences found - switching to them");
+ // Set this flag so if we get a failure we revert to previous provider
+ mChangingVMorFwdDueToProviderChange = true;
+ saveVoiceMailAndForwardingNumber(newProviderKey, newProviderSettings);
+ }
+ } else if (preference == mButtonSipCallOptions) {
+ handleSipCallOptionsChange(objValue);
+ }
+ // always let the preference setting proceed.
+ return true;
+ }
+
+ @Override
+ public void onDialogClosed(EditPhoneNumberPreference preference, int buttonClicked) {
+ if (DBG) log("onPreferenceClick: request preference click on dialog close: " +
+ buttonClicked);
+ if (buttonClicked == DialogInterface.BUTTON_NEGATIVE) {
+ return;
+ }
+
+ if (preference == mSubMenuVoicemailSettings) {
+ handleVMBtnClickRequest();
+ }
+ }
+
+ /**
+ * Implemented for EditPhoneNumberPreference.GetDefaultNumberListener.
+ * This method set the default values for the various
+ * EditPhoneNumberPreference dialogs.
+ */
+ @Override
+ public String onGetDefaultNumber(EditPhoneNumberPreference preference) {
+ if (preference == mSubMenuVoicemailSettings) {
+ // update the voicemail number field, which takes care of the
+ // mSubMenuVoicemailSettings itself, so we should return null.
+ if (DBG) log("updating default for voicemail dialog");
+ updateVoiceNumberField();
+ return null;
+ }
+
+ String vmDisplay = mPhone.getVoiceMailNumber();
+ if (TextUtils.isEmpty(vmDisplay)) {
+ // if there is no voicemail number, we just return null to
+ // indicate no contribution.
+ return null;
+ }
+
+ // Return the voicemail number prepended with "VM: "
+ if (DBG) log("updating default for call forwarding dialogs");
+ return getString(R.string.voicemail_abbreviated) + " " + vmDisplay;
+ }
+
+
+ // override the startsubactivity call to make changes in state consistent.
+ @Override
+ public void startActivityForResult(Intent intent, int requestCode) {
+ if (requestCode == -1) {
+ // this is an intent requested from the preference framework.
+ super.startActivityForResult(intent, requestCode);
+ return;
+ }
+
+ if (DBG) log("startSubActivity: starting requested subactivity");
+ super.startActivityForResult(intent, requestCode);
+ }
+
+ private void switchToPreviousVoicemailProvider() {
+ if (DBG) log("switchToPreviousVoicemailProvider " + mPreviousVMProviderKey);
+ if (mPreviousVMProviderKey != null) {
+ if (mVMChangeCompletedSuccessfully || mFwdChangesRequireRollback) {
+ // we have to revert with carrier
+ if (DBG) {
+ log("Needs to rollback."
+ + " mVMChangeCompletedSuccessfully=" + mVMChangeCompletedSuccessfully
+ + ", mFwdChangesRequireRollback=" + mFwdChangesRequireRollback);
+ }
+
+ showDialogIfForeground(VOICEMAIL_REVERTING_DIALOG);
+ final VoiceMailProviderSettings prevSettings =
+ loadSettingsForVoiceMailProvider(mPreviousVMProviderKey);
+ if (prevSettings == null) {
+ // prevSettings never becomes null since it should be already loaded!
+ Log.e(LOG_TAG, "VoiceMailProviderSettings for the key \""
+ + mPreviousVMProviderKey + "\" becomes null, which is unexpected.");
+ if (DBG) {
+ Log.e(LOG_TAG,
+ "mVMChangeCompletedSuccessfully: " + mVMChangeCompletedSuccessfully
+ + ", mFwdChangesRequireRollback: " + mFwdChangesRequireRollback);
+ }
+ }
+ if (mVMChangeCompletedSuccessfully) {
+ mNewVMNumber = prevSettings.voicemailNumber;
+ Log.i(LOG_TAG, "VM change is already completed successfully."
+ + "Have to revert VM back to " + mNewVMNumber + " again.");
+ mPhone.setVoiceMailNumber(
+ mPhone.getVoiceMailAlphaTag().toString(),
+ mNewVMNumber,
+ Message.obtain(mRevertOptionComplete, EVENT_VOICEMAIL_CHANGED));
+ }
+ if (mFwdChangesRequireRollback) {
+ Log.i(LOG_TAG, "Requested to rollback Fwd changes.");
+ final CallForwardInfo[] prevFwdSettings =
+ prevSettings.forwardingSettings;
+ if (prevFwdSettings != null) {
+ Map<Integer, AsyncResult> results =
+ mForwardingChangeResults;
+ resetForwardingChangeState();
+ for (int i = 0; i < prevFwdSettings.length; i++) {
+ CallForwardInfo fi = prevFwdSettings[i];
+ if (DBG) log("Reverting fwd #: " + i + ": " + fi.toString());
+ // Only revert the settings for which the update
+ // succeeded
+ AsyncResult result = results.get(fi.reason);
+ if (result != null && result.exception == null) {
+ mExpectedChangeResultReasons.add(fi.reason);
+ mPhone.setCallForwardingOption(
+ (fi.status == 1 ?
+ CommandsInterface.CF_ACTION_REGISTRATION :
+ CommandsInterface.CF_ACTION_DISABLE),
+ fi.reason,
+ fi.number,
+ fi.timeSeconds,
+ mRevertOptionComplete.obtainMessage(
+ EVENT_FORWARDING_CHANGED, i, 0));
+ }
+ }
+ }
+ }
+ } else {
+ if (DBG) log("No need to revert");
+ onRevertDone();
+ }
+ }
+ }
+
+ private void onRevertDone() {
+ if (DBG) log("Flipping provider key back to " + mPreviousVMProviderKey);
+ mVoicemailProviders.setValue(mPreviousVMProviderKey);
+ updateVMPreferenceWidgets(mPreviousVMProviderKey);
+ updateVoiceNumberField();
+ if (mVMOrFwdSetError != 0) {
+ showVMDialog(mVMOrFwdSetError);
+ mVMOrFwdSetError = 0;
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (DBG) {
+ log("onActivityResult: requestCode: " + requestCode
+ + ", resultCode: " + resultCode
+ + ", data: " + data);
+ }
+ // there are cases where the contact picker may end up sending us more than one
+ // request. We want to ignore the request if we're not in the correct state.
+ if (requestCode == VOICEMAIL_PROVIDER_CFG_ID) {
+ boolean failure = false;
+
+ // No matter how the processing of result goes lets clear the flag
+ if (DBG) log("mVMProviderSettingsForced: " + mVMProviderSettingsForced);
+ final boolean isVMProviderSettingsForced = mVMProviderSettingsForced;
+ mVMProviderSettingsForced = false;
+
+ String vmNum = null;
+ if (resultCode != RESULT_OK) {
+ if (DBG) log("onActivityResult: vm provider cfg result not OK.");
+ failure = true;
+ } else {
+ if (data == null) {
+ if (DBG) log("onActivityResult: vm provider cfg result has no data");
+ failure = true;
+ } else {
+ if (data.getBooleanExtra(SIGNOUT_EXTRA, false)) {
+ if (DBG) log("Provider requested signout");
+ if (isVMProviderSettingsForced) {
+ if (DBG) log("Going back to previous provider on signout");
+ switchToPreviousVoicemailProvider();
+ } else {
+ final String victim = getCurrentVoicemailProviderKey();
+ if (DBG) log("Relaunching activity and ignoring " + victim);
+ Intent i = new Intent(ACTION_ADD_VOICEMAIL);
+ i.putExtra(IGNORE_PROVIDER_EXTRA, victim);
+ i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ this.startActivity(i);
+ }
+ return;
+ }
+ vmNum = data.getStringExtra(VM_NUMBER_EXTRA);
+ if (vmNum == null || vmNum.length() == 0) {
+ if (DBG) log("onActivityResult: vm provider cfg result has no vmnum");
+ failure = true;
+ }
+ }
+ }
+ if (failure) {
+ if (DBG) log("Failure in return from voicemail provider");
+ if (isVMProviderSettingsForced) {
+ switchToPreviousVoicemailProvider();
+ } else {
+ if (DBG) log("Not switching back the provider since this is not forced config");
+ }
+ return;
+ }
+ mChangingVMorFwdDueToProviderChange = isVMProviderSettingsForced;
+ final String fwdNum = data.getStringExtra(FWD_NUMBER_EXTRA);
+
+ // TODO(iliat): It would be nice to load the current network setting for this and
+ // send it to the provider when it's config is invoked so it can use this as default
+ final int fwdNumTime = data.getIntExtra(FWD_NUMBER_TIME_EXTRA, 20);
+
+ if (DBG) log("onActivityResult: vm provider cfg result " +
+ (fwdNum != null ? "has" : " does not have") + " forwarding number");
+ saveVoiceMailAndForwardingNumber(getCurrentVoicemailProviderKey(),
+ new VoiceMailProviderSettings(vmNum, fwdNum, fwdNumTime));
+ return;
+ }
+
+ if (requestCode == VOICEMAIL_PREF_ID) {
+ if (resultCode != RESULT_OK) {
+ if (DBG) log("onActivityResult: contact picker result not OK.");
+ return;
+ }
+
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(data.getData(),
+ NUM_PROJECTION, null, null, null);
+ if ((cursor == null) || (!cursor.moveToFirst())) {
+ if (DBG) log("onActivityResult: bad contact data, no results found.");
+ return;
+ }
+ mSubMenuVoicemailSettings.onPickActivityResult(cursor.getString(0));
+ return;
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ // Voicemail button logic
+ private void handleVMBtnClickRequest() {
+ // normally called on the dialog close.
+
+ // Since we're stripping the formatting out on the getPhoneNumber()
+ // call now, we won't need to do so here anymore.
+
+ saveVoiceMailAndForwardingNumber(
+ getCurrentVoicemailProviderKey(),
+ new VoiceMailProviderSettings(mSubMenuVoicemailSettings.getPhoneNumber(),
+ FWD_SETTINGS_DONT_TOUCH));
+ }
+
+
+ /**
+ * Wrapper around showDialog() that will silently do nothing if we're
+ * not in the foreground.
+ *
+ * This is useful here because most of the dialogs we display from
+ * this class are triggered by asynchronous events (like
+ * success/failure messages from the telephony layer) and it's
+ * possible for those events to come in even after the user has gone
+ * to a different screen.
+ */
+ // TODO: this is too brittle: it's still easy to accidentally add new
+ // code here that calls showDialog() directly (which will result in a
+ // WindowManager$BadTokenException if called after the activity has
+ // been stopped.)
+ //
+ // It would be cleaner to do the "if (mForeground)" check in one
+ // central place, maybe by using a single Handler for all asynchronous
+ // events (and have *that* discard events if we're not in the
+ // foreground.)
+ //
+ // Unfortunately it's not that simple, since we sometimes need to do
+ // actual work to handle these events whether or not we're in the
+ // foreground (see the Handler code in mSetOptionComplete for
+ // example.)
+ private void showDialogIfForeground(int id) {
+ if (mForeground) {
+ showDialog(id);
+ }
+ }
+
+ private void dismissDialogSafely(int id) {
+ try {
+ dismissDialog(id);
+ } catch (IllegalArgumentException e) {
+ // This is expected in the case where we were in the background
+ // at the time we would normally have shown the dialog, so we didn't
+ // show it.
+ }
+ }
+
+ private void saveVoiceMailAndForwardingNumber(String key,
+ VoiceMailProviderSettings newSettings) {
+ if (DBG) log("saveVoiceMailAndForwardingNumber: " + newSettings.toString());
+ mNewVMNumber = newSettings.voicemailNumber;
+ // empty vm number == clearing the vm number ?
+ if (mNewVMNumber == null) {
+ mNewVMNumber = "";
+ }
+
+ mNewFwdSettings = newSettings.forwardingSettings;
+ if (DBG) log("newFwdNumber " +
+ String.valueOf((mNewFwdSettings != null ? mNewFwdSettings.length : 0))
+ + " settings");
+
+ // No fwd settings on CDMA
+ if (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ if (DBG) log("ignoring forwarding setting since this is CDMA phone");
+ mNewFwdSettings = FWD_SETTINGS_DONT_TOUCH;
+ }
+
+ //throw a warning if the vm is the same and we do not touch forwarding.
+ if (mNewVMNumber.equals(mOldVmNumber) && mNewFwdSettings == FWD_SETTINGS_DONT_TOUCH) {
+ showVMDialog(MSG_VM_NOCHANGE);
+ return;
+ }
+
+ maybeSaveSettingsForVoicemailProvider(key, newSettings);
+ mVMChangeCompletedSuccessfully = false;
+ mFwdChangesRequireRollback = false;
+ mVMOrFwdSetError = 0;
+ if (!key.equals(mPreviousVMProviderKey)) {
+ mReadingSettingsForDefaultProvider =
+ mPreviousVMProviderKey.equals(DEFAULT_VM_PROVIDER_KEY);
+ if (DBG) log("Reading current forwarding settings");
+ mForwardingReadResults = new CallForwardInfo[FORWARDING_SETTINGS_REASONS.length];
+ for (int i = 0; i < FORWARDING_SETTINGS_REASONS.length; i++) {
+ mForwardingReadResults[i] = null;
+ mPhone.getCallForwardingOption(FORWARDING_SETTINGS_REASONS[i],
+ mGetOptionComplete.obtainMessage(EVENT_FORWARDING_GET_COMPLETED, i, 0));
+ }
+ showDialogIfForeground(VOICEMAIL_FWD_READING_DIALOG);
+ } else {
+ saveVoiceMailAndForwardingNumberStage2();
+ }
+ }
+
+ private final Handler mGetOptionComplete = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ AsyncResult result = (AsyncResult) msg.obj;
+ switch (msg.what) {
+ case EVENT_FORWARDING_GET_COMPLETED:
+ handleForwardingSettingsReadResult(result, msg.arg1);
+ break;
+ }
+ }
+ };
+
+ private void handleForwardingSettingsReadResult(AsyncResult ar, int idx) {
+ if (DBG) Log.d(LOG_TAG, "handleForwardingSettingsReadResult: " + idx);
+ Throwable error = null;
+ if (ar.exception != null) {
+ if (DBG) Log.d(LOG_TAG, "FwdRead: ar.exception=" +
+ ar.exception.getMessage());
+ error = ar.exception;
+ }
+ if (ar.userObj instanceof Throwable) {
+ if (DBG) Log.d(LOG_TAG, "FwdRead: userObj=" +
+ ((Throwable)ar.userObj).getMessage());
+ error = (Throwable)ar.userObj;
+ }
+
+ // We may have already gotten an error and decided to ignore the other results.
+ if (mForwardingReadResults == null) {
+ if (DBG) Log.d(LOG_TAG, "ignoring fwd reading result: " + idx);
+ return;
+ }
+
+ // In case of error ignore other results, show an error dialog
+ if (error != null) {
+ if (DBG) Log.d(LOG_TAG, "Error discovered for fwd read : " + idx);
+ mForwardingReadResults = null;
+ dismissDialogSafely(VOICEMAIL_FWD_READING_DIALOG);
+ showVMDialog(MSG_FW_GET_EXCEPTION);
+ return;
+ }
+
+ // Get the forwarding info
+ final CallForwardInfo cfInfoArray[] = (CallForwardInfo[]) ar.result;
+ CallForwardInfo fi = null;
+ for (int i = 0 ; i < cfInfoArray.length; i++) {
+ if ((cfInfoArray[i].serviceClass & CommandsInterface.SERVICE_CLASS_VOICE) != 0) {
+ fi = cfInfoArray[i];
+ break;
+ }
+ }
+ if (fi == null) {
+
+ // In case we go nothing it means we need this reason disabled
+ // so create a CallForwardInfo for capturing this
+ if (DBG) Log.d(LOG_TAG, "Creating default info for " + idx);
+ fi = new CallForwardInfo();
+ fi.status = 0;
+ fi.reason = FORWARDING_SETTINGS_REASONS[idx];
+ fi.serviceClass = CommandsInterface.SERVICE_CLASS_VOICE;
+ } else {
+ // if there is not a forwarding number, ensure the entry is set to "not active."
+ if (fi.number == null || fi.number.length() == 0) {
+ fi.status = 0;
+ }
+
+ if (DBG) Log.d(LOG_TAG, "Got " + fi.toString() + " for " + idx);
+ }
+ mForwardingReadResults[idx] = fi;
+
+ // Check if we got all the results already
+ boolean done = true;
+ for (int i = 0; i < mForwardingReadResults.length; i++) {
+ if (mForwardingReadResults[i] == null) {
+ done = false;
+ break;
+ }
+ }
+ if (done) {
+ if (DBG) Log.d(LOG_TAG, "Done receiving fwd info");
+ dismissDialogSafely(VOICEMAIL_FWD_READING_DIALOG);
+ if (mReadingSettingsForDefaultProvider) {
+ maybeSaveSettingsForVoicemailProvider(DEFAULT_VM_PROVIDER_KEY,
+ new VoiceMailProviderSettings(this.mOldVmNumber,
+ mForwardingReadResults));
+ mReadingSettingsForDefaultProvider = false;
+ }
+ saveVoiceMailAndForwardingNumberStage2();
+ } else {
+ if (DBG) Log.d(LOG_TAG, "Not done receiving fwd info");
+ }
+ }
+
+ private CallForwardInfo infoForReason(CallForwardInfo[] infos, int reason) {
+ CallForwardInfo result = null;
+ if (null != infos) {
+ for (CallForwardInfo info : infos) {
+ if (info.reason == reason) {
+ result = info;
+ break;
+ }
+ }
+ }
+ return result;
+ }
+
+ private boolean isUpdateRequired(CallForwardInfo oldInfo,
+ CallForwardInfo newInfo) {
+ boolean result = true;
+ if (0 == newInfo.status) {
+ // If we're disabling a type of forwarding, and it's already
+ // disabled for the account, don't make any change
+ if (oldInfo != null && oldInfo.status == 0) {
+ result = false;
+ }
+ }
+ return result;
+ }
+
+ private void resetForwardingChangeState() {
+ mForwardingChangeResults = new HashMap<Integer, AsyncResult>();
+ mExpectedChangeResultReasons = new HashSet<Integer>();
+ }
+
+ // Called after we are done saving the previous forwarding settings if
+ // we needed.
+ private void saveVoiceMailAndForwardingNumberStage2() {
+ mForwardingChangeResults = null;
+ mVoicemailChangeResult = null;
+ if (mNewFwdSettings != FWD_SETTINGS_DONT_TOUCH) {
+ resetForwardingChangeState();
+ for (int i = 0; i < mNewFwdSettings.length; i++) {
+ CallForwardInfo fi = mNewFwdSettings[i];
+
+ final boolean doUpdate = isUpdateRequired(infoForReason(
+ mForwardingReadResults, fi.reason), fi);
+
+ if (doUpdate) {
+ if (DBG) log("Setting fwd #: " + i + ": " + fi.toString());
+ mExpectedChangeResultReasons.add(i);
+
+ mPhone.setCallForwardingOption(
+ fi.status == 1 ?
+ CommandsInterface.CF_ACTION_REGISTRATION :
+ CommandsInterface.CF_ACTION_DISABLE,
+ fi.reason,
+ fi.number,
+ fi.timeSeconds,
+ mSetOptionComplete.obtainMessage(
+ EVENT_FORWARDING_CHANGED, fi.reason, 0));
+ }
+ }
+ showDialogIfForeground(VOICEMAIL_FWD_SAVING_DIALOG);
+ } else {
+ if (DBG) log("Not touching fwd #");
+ setVMNumberWithCarrier();
+ }
+ }
+
+ private void setVMNumberWithCarrier() {
+ if (DBG) log("save voicemail #: " + mNewVMNumber);
+ mPhone.setVoiceMailNumber(
+ mPhone.getVoiceMailAlphaTag().toString(),
+ mNewVMNumber,
+ Message.obtain(mSetOptionComplete, EVENT_VOICEMAIL_CHANGED));
+ }
+
+ /**
+ * Callback to handle option update completions
+ */
+ private final Handler mSetOptionComplete = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ AsyncResult result = (AsyncResult) msg.obj;
+ boolean done = false;
+ switch (msg.what) {
+ case EVENT_VOICEMAIL_CHANGED:
+ mVoicemailChangeResult = result;
+ mVMChangeCompletedSuccessfully = checkVMChangeSuccess() == null;
+ if (DBG) log("VM change complete msg, VM change done = " +
+ String.valueOf(mVMChangeCompletedSuccessfully));
+ done = true;
+ break;
+ case EVENT_FORWARDING_CHANGED:
+ mForwardingChangeResults.put(msg.arg1, result);
+ if (result.exception != null) {
+ Log.w(LOG_TAG, "Error in setting fwd# " + msg.arg1 + ": " +
+ result.exception.getMessage());
+ } else {
+ if (DBG) log("Success in setting fwd# " + msg.arg1);
+ }
+ final boolean completed = checkForwardingCompleted();
+ if (completed) {
+ if (checkFwdChangeSuccess() == null) {
+ if (DBG) log("Overall fwd changes completed ok, starting vm change");
+ setVMNumberWithCarrier();
+ } else {
+ Log.w(LOG_TAG, "Overall fwd changes completed in failure. " +
+ "Check if we need to try rollback for some settings.");
+ mFwdChangesRequireRollback = false;
+ Iterator<Map.Entry<Integer,AsyncResult>> it =
+ mForwardingChangeResults.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<Integer,AsyncResult> entry = it.next();
+ if (entry.getValue().exception == null) {
+ // If at least one succeeded we have to revert
+ Log.i(LOG_TAG, "Rollback will be required");
+ mFwdChangesRequireRollback = true;
+ break;
+ }
+ }
+ if (!mFwdChangesRequireRollback) {
+ Log.i(LOG_TAG, "No rollback needed.");
+ }
+ done = true;
+ }
+ }
+ break;
+ default:
+ // TODO: should never reach this, may want to throw exception
+ }
+ if (done) {
+ if (DBG) log("All VM provider related changes done");
+ if (mForwardingChangeResults != null) {
+ dismissDialogSafely(VOICEMAIL_FWD_SAVING_DIALOG);
+ }
+ handleSetVMOrFwdMessage();
+ }
+ }
+ };
+
+ /**
+ * Callback to handle option revert completions
+ */
+ private final Handler mRevertOptionComplete = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ AsyncResult result = (AsyncResult) msg.obj;
+ switch (msg.what) {
+ case EVENT_VOICEMAIL_CHANGED:
+ mVoicemailChangeResult = result;
+ if (DBG) log("VM revert complete msg");
+ break;
+ case EVENT_FORWARDING_CHANGED:
+ mForwardingChangeResults.put(msg.arg1, result);
+ if (result.exception != null) {
+ if (DBG) log("Error in reverting fwd# " + msg.arg1 + ": " +
+ result.exception.getMessage());
+ } else {
+ if (DBG) log("Success in reverting fwd# " + msg.arg1);
+ }
+ if (DBG) log("FWD revert complete msg ");
+ break;
+ default:
+ // TODO: should never reach this, may want to throw exception
+ }
+ final boolean done =
+ (!mVMChangeCompletedSuccessfully || mVoicemailChangeResult != null) &&
+ (!mFwdChangesRequireRollback || checkForwardingCompleted());
+ if (done) {
+ if (DBG) log("All VM reverts done");
+ dismissDialogSafely(VOICEMAIL_REVERTING_DIALOG);
+ onRevertDone();
+ }
+ }
+ };
+
+ /**
+ * @return true if forwarding change has completed
+ */
+ private boolean checkForwardingCompleted() {
+ boolean result;
+ if (mForwardingChangeResults == null) {
+ result = true;
+ } else {
+ // return true iff there is a change result for every reason for
+ // which we expected a result
+ result = true;
+ for (Integer reason : mExpectedChangeResultReasons) {
+ if (mForwardingChangeResults.get(reason) == null) {
+ result = false;
+ break;
+ }
+ }
+ }
+ return result;
+ }
+ /**
+ * @return error string or null if successful
+ */
+ private String checkFwdChangeSuccess() {
+ String result = null;
+ Iterator<Map.Entry<Integer,AsyncResult>> it =
+ mForwardingChangeResults.entrySet().iterator();
+ while (it.hasNext()) {
+ Map.Entry<Integer,AsyncResult> entry = it.next();
+ Throwable exception = entry.getValue().exception;
+ if (exception != null) {
+ result = exception.getMessage();
+ if (result == null) {
+ result = "";
+ }
+ break;
+ }
+ }
+ return result;
+ }
+
+ /**
+ * @return error string or null if successful
+ */
+ private String checkVMChangeSuccess() {
+ if (mVoicemailChangeResult.exception != null) {
+ final String msg = mVoicemailChangeResult.exception.getMessage();
+ if (msg == null) {
+ return "";
+ }
+ return msg;
+ }
+ return null;
+ }
+
+ private void handleSetVMOrFwdMessage() {
+ if (DBG) {
+ log("handleSetVMMessage: set VM request complete");
+ }
+ boolean success = true;
+ boolean fwdFailure = false;
+ String exceptionMessage = "";
+ if (mForwardingChangeResults != null) {
+ exceptionMessage = checkFwdChangeSuccess();
+ if (exceptionMessage != null) {
+ success = false;
+ fwdFailure = true;
+ }
+ }
+ if (success) {
+ exceptionMessage = checkVMChangeSuccess();
+ if (exceptionMessage != null) {
+ success = false;
+ }
+ }
+ if (success) {
+ if (DBG) log("change VM success!");
+ handleVMAndFwdSetSuccess(MSG_VM_OK);
+ } else {
+ if (fwdFailure) {
+ Log.w(LOG_TAG, "Failed to change fowarding setting. Reason: " + exceptionMessage);
+ handleVMOrFwdSetError(MSG_FW_SET_EXCEPTION);
+ } else {
+ Log.w(LOG_TAG, "Failed to change voicemail. Reason: " + exceptionMessage);
+ handleVMOrFwdSetError(MSG_VM_EXCEPTION);
+ }
+ }
+ }
+
+ /**
+ * Called when Voicemail Provider or its forwarding settings failed. Rolls back partly made
+ * changes to those settings and show "failure" dialog.
+ *
+ * @param msgId Message ID used for the specific error case. {@link #MSG_FW_SET_EXCEPTION} or
+ * {@link #MSG_VM_EXCEPTION}
+ */
+ private void handleVMOrFwdSetError(int msgId) {
+ if (mChangingVMorFwdDueToProviderChange) {
+ mVMOrFwdSetError = msgId;
+ mChangingVMorFwdDueToProviderChange = false;
+ switchToPreviousVoicemailProvider();
+ return;
+ }
+ mChangingVMorFwdDueToProviderChange = false;
+ showVMDialog(msgId);
+ updateVoiceNumberField();
+ }
+
+ /**
+ * Called when Voicemail Provider and its forwarding settings were successfully finished.
+ * This updates a bunch of variables and show "success" dialog.
+ */
+ private void handleVMAndFwdSetSuccess(int msg) {
+ if (DBG) {
+ log("handleVMAndFwdSetSuccess(). current voicemail provider key: "
+ + getCurrentVoicemailProviderKey());
+ }
+ mPreviousVMProviderKey = getCurrentVoicemailProviderKey();
+ mChangingVMorFwdDueToProviderChange = false;
+ showVMDialog(msg);
+ updateVoiceNumberField();
+ }
+
+ /**
+ * Update the voicemail number from what we've recorded on the sim.
+ */
+ private void updateVoiceNumberField() {
+ if (DBG) {
+ log("updateVoiceNumberField(). mSubMenuVoicemailSettings=" + mSubMenuVoicemailSettings);
+ }
+ if (mSubMenuVoicemailSettings == null) {
+ return;
+ }
+
+ mOldVmNumber = mPhone.getVoiceMailNumber();
+ if (mOldVmNumber == null) {
+ mOldVmNumber = "";
+ }
+ mSubMenuVoicemailSettings.setPhoneNumber(mOldVmNumber);
+ final String summary = (mOldVmNumber.length() > 0) ? mOldVmNumber :
+ getString(R.string.voicemail_number_not_set);
+ mSubMenuVoicemailSettings.setSummary(summary);
+ }
+
+ /*
+ * Helper Methods for Activity class.
+ * The initial query commands are split into two pieces now
+ * for individual expansion. This combined with the ability
+ * to cancel queries allows for a much better user experience,
+ * and also ensures that the user only waits to update the
+ * data that is relevant.
+ */
+
+ @Override
+ protected void onPrepareDialog(int id, Dialog dialog) {
+ super.onPrepareDialog(id, dialog);
+ mCurrentDialogId = id;
+ }
+
+ // dialog creation method, called by showDialog()
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ if ((id == VM_RESPONSE_ERROR) || (id == VM_NOCHANGE_ERROR) ||
+ (id == FW_SET_RESPONSE_ERROR) || (id == FW_GET_RESPONSE_ERROR) ||
+ (id == VOICEMAIL_DIALOG_CONFIRM)) {
+
+ AlertDialog.Builder b = new AlertDialog.Builder(this);
+
+ int msgId;
+ int titleId = R.string.error_updating_title;
+ switch (id) {
+ case VOICEMAIL_DIALOG_CONFIRM:
+ msgId = R.string.vm_changed;
+ titleId = R.string.voicemail;
+ // Set Button 2
+ b.setNegativeButton(R.string.close_dialog, this);
+ break;
+ case VM_NOCHANGE_ERROR:
+ // even though this is technically an error,
+ // keep the title friendly.
+ msgId = R.string.no_change;
+ titleId = R.string.voicemail;
+ // Set Button 2
+ b.setNegativeButton(R.string.close_dialog, this);
+ break;
+ case VM_RESPONSE_ERROR:
+ msgId = R.string.vm_change_failed;
+ // Set Button 1
+ b.setPositiveButton(R.string.close_dialog, this);
+ break;
+ case FW_SET_RESPONSE_ERROR:
+ msgId = R.string.fw_change_failed;
+ // Set Button 1
+ b.setPositiveButton(R.string.close_dialog, this);
+ break;
+ case FW_GET_RESPONSE_ERROR:
+ msgId = R.string.fw_get_in_vm_failed;
+ b.setPositiveButton(R.string.alert_dialog_yes, this);
+ b.setNegativeButton(R.string.alert_dialog_no, this);
+ break;
+ default:
+ msgId = R.string.exception_error;
+ // Set Button 3, tells the activity that the error is
+ // not recoverable on dialog exit.
+ b.setNeutralButton(R.string.close_dialog, this);
+ break;
+ }
+
+ b.setTitle(getText(titleId));
+ String message = getText(msgId).toString();
+ b.setMessage(message);
+ b.setCancelable(false);
+ AlertDialog dialog = b.create();
+
+ // make the dialog more obvious by bluring the background.
+ dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+
+ return dialog;
+ } else if (id == VOICEMAIL_FWD_SAVING_DIALOG || id == VOICEMAIL_FWD_READING_DIALOG ||
+ id == VOICEMAIL_REVERTING_DIALOG) {
+ ProgressDialog dialog = new ProgressDialog(this);
+ dialog.setTitle(getText(R.string.updating_title));
+ dialog.setIndeterminate(true);
+ dialog.setCancelable(false);
+ dialog.setMessage(getText(
+ id == VOICEMAIL_FWD_SAVING_DIALOG ? R.string.updating_settings :
+ (id == VOICEMAIL_REVERTING_DIALOG ? R.string.reverting_settings :
+ R.string.reading_settings)));
+ return dialog;
+ }
+
+
+ return null;
+ }
+
+ // This is a method implemented for DialogInterface.OnClickListener.
+ // Used with the error dialog to close the app, voicemail dialog to just dismiss.
+ // Close button is mapped to BUTTON_POSITIVE for the errors that close the activity,
+ // while those that are mapped to BUTTON_NEUTRAL only move the preference focus.
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ switch (which){
+ case DialogInterface.BUTTON_NEUTRAL:
+ if (DBG) log("Neutral button");
+ break;
+ case DialogInterface.BUTTON_NEGATIVE:
+ if (DBG) log("Negative button");
+ if (mCurrentDialogId == FW_GET_RESPONSE_ERROR) {
+ // We failed to get current forwarding settings and the user
+ // does not wish to continue.
+ switchToPreviousVoicemailProvider();
+ }
+ break;
+ case DialogInterface.BUTTON_POSITIVE:
+ if (DBG) log("Positive button");
+ if (mCurrentDialogId == FW_GET_RESPONSE_ERROR) {
+ // We failed to get current forwarding settings but the user
+ // wishes to continue changing settings to the new vm provider
+ saveVoiceMailAndForwardingNumberStage2();
+ } else {
+ finish();
+ }
+ return;
+ default:
+ // just let the dialog close and go back to the input
+ }
+ // In all dialogs, all buttons except BUTTON_POSITIVE lead to the end of user interaction
+ // with settings UI. If we were called to explicitly configure voice mail then
+ // we finish the settings activity here to come back to whatever the user was doing.
+ if (getIntent().getAction().equals(ACTION_ADD_VOICEMAIL)) {
+ finish();
+ }
+ }
+
+ // set the app state with optional status.
+ private void showVMDialog(int msgStatus) {
+ switch (msgStatus) {
+ // It's a bit worrisome to punt in the error cases here when we're
+ // not in the foreground; maybe toast instead?
+ case MSG_VM_EXCEPTION:
+ showDialogIfForeground(VM_RESPONSE_ERROR);
+ break;
+ case MSG_FW_SET_EXCEPTION:
+ showDialogIfForeground(FW_SET_RESPONSE_ERROR);
+ break;
+ case MSG_FW_GET_EXCEPTION:
+ showDialogIfForeground(FW_GET_RESPONSE_ERROR);
+ break;
+ case MSG_VM_NOCHANGE:
+ showDialogIfForeground(VM_NOCHANGE_ERROR);
+ break;
+ case MSG_VM_OK:
+ showDialogIfForeground(VOICEMAIL_DIALOG_CONFIRM);
+ break;
+ case MSG_OK:
+ default:
+ // This should never happen.
+ }
+ }
+
+ /*
+ * Activity class methods
+ */
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ if (DBG) log("onCreate(). Intent: " + getIntent());
+ mPhone = PhoneGlobals.getPhone();
+
+ addPreferencesFromResource(R.xml.call_feature_setting);
+
+ mAudioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+
+ // get buttons
+ PreferenceScreen prefSet = getPreferenceScreen();
+ mSubMenuVoicemailSettings = (EditPhoneNumberPreference)findPreference(BUTTON_VOICEMAIL_KEY);
+ if (mSubMenuVoicemailSettings != null) {
+ mSubMenuVoicemailSettings.setParentActivity(this, VOICEMAIL_PREF_ID, this);
+ mSubMenuVoicemailSettings.setDialogOnClosedListener(this);
+ mSubMenuVoicemailSettings.setDialogTitle(R.string.voicemail_settings_number_label);
+ }
+
+ mRingtonePreference = findPreference(BUTTON_RINGTONE_KEY);
+ mVibrateWhenRinging = (CheckBoxPreference) findPreference(BUTTON_VIBRATE_ON_RING);
+ mPlayDtmfTone = (CheckBoxPreference) findPreference(BUTTON_PLAY_DTMF_TONE);
+ mDialpadAutocomplete = (CheckBoxPreference) findPreference(BUTTON_DIALPAD_AUTOCOMPLETE);
+ mButtonDTMF = (ListPreference) findPreference(BUTTON_DTMF_KEY);
+ mButtonAutoRetry = (CheckBoxPreference) findPreference(BUTTON_RETRY_KEY);
+ mButtonHAC = (CheckBoxPreference) findPreference(BUTTON_HAC_KEY);
+ mButtonTTY = (ListPreference) findPreference(BUTTON_TTY_KEY);
+ mVoicemailProviders = (ListPreference) findPreference(BUTTON_VOICEMAIL_PROVIDER_KEY);
+ if (mVoicemailProviders != null) {
+ mVoicemailProviders.setOnPreferenceChangeListener(this);
+ mVoicemailSettings = (PreferenceScreen)findPreference(BUTTON_VOICEMAIL_SETTING_KEY);
+ mVoicemailNotificationRingtone =
+ findPreference(BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY);
+ mVoicemailNotificationVibrate =
+ (CheckBoxPreference) findPreference(BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY);
+ initVoiceMailProviders();
+ }
+
+ if (mVibrateWhenRinging != null) {
+ Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
+ if (vibrator != null && vibrator.hasVibrator()) {
+ mVibrateWhenRinging.setOnPreferenceChangeListener(this);
+ } else {
+ prefSet.removePreference(mVibrateWhenRinging);
+ mVibrateWhenRinging = null;
+ }
+ }
+
+ final ContentResolver contentResolver = getContentResolver();
+
+ if (mPlayDtmfTone != null) {
+ mPlayDtmfTone.setChecked(Settings.System.getInt(contentResolver,
+ Settings.System.DTMF_TONE_WHEN_DIALING, 1) != 0);
+ }
+
+ if (mDialpadAutocomplete != null) {
+ mDialpadAutocomplete.setChecked(Settings.Secure.getInt(contentResolver,
+ Settings.Secure.DIALPAD_AUTOCOMPLETE, 0) != 0);
+ }
+
+ if (mButtonDTMF != null) {
+ if (getResources().getBoolean(R.bool.dtmf_type_enabled)) {
+ mButtonDTMF.setOnPreferenceChangeListener(this);
+ } else {
+ prefSet.removePreference(mButtonDTMF);
+ mButtonDTMF = null;
+ }
+ }
+
+ if (mButtonAutoRetry != null) {
+ if (getResources().getBoolean(R.bool.auto_retry_enabled)) {
+ mButtonAutoRetry.setOnPreferenceChangeListener(this);
+ } else {
+ prefSet.removePreference(mButtonAutoRetry);
+ mButtonAutoRetry = null;
+ }
+ }
+
+ if (mButtonHAC != null) {
+ if (getResources().getBoolean(R.bool.hac_enabled)) {
+
+ mButtonHAC.setOnPreferenceChangeListener(this);
+ } else {
+ prefSet.removePreference(mButtonHAC);
+ mButtonHAC = null;
+ }
+ }
+
+ if (mButtonTTY != null) {
+ if (getResources().getBoolean(R.bool.tty_enabled)) {
+ mButtonTTY.setOnPreferenceChangeListener(this);
+ } else {
+ prefSet.removePreference(mButtonTTY);
+ mButtonTTY = null;
+ }
+ }
+
+ if (!getResources().getBoolean(R.bool.world_phone)) {
+ Preference options = prefSet.findPreference(BUTTON_CDMA_OPTIONS);
+ if (options != null)
+ prefSet.removePreference(options);
+ options = prefSet.findPreference(BUTTON_GSM_UMTS_OPTIONS);
+ if (options != null)
+ prefSet.removePreference(options);
+
+ int phoneType = mPhone.getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ Preference fdnButton = prefSet.findPreference(BUTTON_FDN_KEY);
+ if (fdnButton != null)
+ prefSet.removePreference(fdnButton);
+ if (!getResources().getBoolean(R.bool.config_voice_privacy_disable)) {
+ addPreferencesFromResource(R.xml.cdma_call_privacy);
+ }
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+ addPreferencesFromResource(R.xml.gsm_umts_call_options);
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ }
+
+ // create intent to bring up contact list
+ mContactListIntent = new Intent(Intent.ACTION_GET_CONTENT);
+ mContactListIntent.setType(android.provider.Contacts.Phones.CONTENT_ITEM_TYPE);
+
+ // check the intent that started this activity and pop up the voicemail
+ // dialog if we've been asked to.
+ // If we have at least one non default VM provider registered then bring up
+ // the selection for the VM provider, otherwise bring up a VM number dialog.
+ // We only bring up the dialog the first time we are called (not after orientation change)
+ if (icicle == null) {
+ if (getIntent().getAction().equals(ACTION_ADD_VOICEMAIL) &&
+ mVoicemailProviders != null) {
+ if (DBG) {
+ log("ACTION_ADD_VOICEMAIL Intent is thrown. current VM data size: "
+ + mVMProvidersData.size());
+ }
+ if (mVMProvidersData.size() > 1) {
+ simulatePreferenceClick(mVoicemailProviders);
+ } else {
+ onPreferenceChange(mVoicemailProviders, DEFAULT_VM_PROVIDER_KEY);
+ mVoicemailProviders.setValue(DEFAULT_VM_PROVIDER_KEY);
+ }
+ }
+ }
+ updateVoiceNumberField();
+ mVMProviderSettingsForced = false;
+ createSipCallSettings();
+
+ mRingtoneLookupRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mRingtonePreference != null) {
+ updateRingtoneName(RingtoneManager.TYPE_RINGTONE, mRingtonePreference,
+ MSG_UPDATE_RINGTONE_SUMMARY);
+ }
+ if (mVoicemailNotificationRingtone != null) {
+ updateRingtoneName(RingtoneManager.TYPE_NOTIFICATION,
+ mVoicemailNotificationRingtone, MSG_UPDATE_VOICEMAIL_RINGTONE_SUMMARY);
+ }
+ }
+ };
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ // android.R.id.home will be triggered in onOptionsItemSelected()
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ /**
+ * Updates ringtone name. This is a method copied from com.android.settings.SoundSettings
+ *
+ * @see com.android.settings.SoundSettings
+ */
+ private void updateRingtoneName(int type, Preference preference, int msg) {
+ if (preference == null) return;
+ final Uri ringtoneUri;
+ boolean defaultRingtone = false;
+ if (type == RingtoneManager.TYPE_RINGTONE) {
+ // For ringtones, we can just lookup the system default because changing the settings
+ // in Call Settings changes the system default.
+ ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(this, type);
+ } else {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
+ mPhone.getContext());
+ // for voicemail notifications, we use the value saved in Phone's shared preferences.
+ String uriString = prefs.getString(preference.getKey(), null);
+ if (TextUtils.isEmpty(uriString)) {
+ // silent ringtone
+ ringtoneUri = null;
+ } else {
+ if (uriString.equals(Settings.System.DEFAULT_NOTIFICATION_URI.toString())) {
+ // If it turns out that the voicemail notification is set to the system
+ // default notification, we retrieve the actual URI to prevent it from showing
+ // up as "Unknown Ringtone".
+ defaultRingtone = true;
+ ringtoneUri = RingtoneManager.getActualDefaultRingtoneUri(this, type);
+ } else {
+ ringtoneUri = Uri.parse(uriString);
+ }
+ }
+ }
+ CharSequence summary = getString(com.android.internal.R.string.ringtone_unknown);
+ // Is it a silent ringtone?
+ if (ringtoneUri == null) {
+ summary = getString(com.android.internal.R.string.ringtone_silent);
+ } else {
+ // Fetch the ringtone title from the media provider
+ try {
+ Cursor cursor = getContentResolver().query(ringtoneUri,
+ new String[] { MediaStore.Audio.Media.TITLE }, null, null, null);
+ if (cursor != null) {
+ if (cursor.moveToFirst()) {
+ summary = cursor.getString(0);
+ }
+ cursor.close();
+ }
+ } catch (SQLiteException sqle) {
+ // Unknown title for the ringtone
+ }
+ }
+ if (defaultRingtone) {
+ summary = mPhone.getContext().getString(
+ R.string.default_notification_description, summary);
+ }
+ mRingtoneLookupComplete.sendMessage(mRingtoneLookupComplete.obtainMessage(msg, summary));
+ }
+
+ private void createSipCallSettings() {
+ // Add Internet call settings.
+ if (PhoneUtils.isVoipSupported()) {
+ mSipManager = SipManager.newInstance(this);
+ mSipSharedPreferences = new SipSharedPreferences(this);
+ addPreferencesFromResource(R.xml.sip_settings_category);
+ mButtonSipCallOptions = getSipCallOptionPreference();
+ mButtonSipCallOptions.setOnPreferenceChangeListener(this);
+ mButtonSipCallOptions.setValueIndex(
+ mButtonSipCallOptions.findIndexOfValue(
+ mSipSharedPreferences.getSipCallOption()));
+ mButtonSipCallOptions.setSummary(mButtonSipCallOptions.getEntry());
+ }
+ }
+
+ // Gets the call options for SIP depending on whether SIP is allowed only
+ // on Wi-Fi only; also make the other options preference invisible.
+ private ListPreference getSipCallOptionPreference() {
+ ListPreference wifiAnd3G = (ListPreference)
+ findPreference(BUTTON_SIP_CALL_OPTIONS);
+ ListPreference wifiOnly = (ListPreference)
+ findPreference(BUTTON_SIP_CALL_OPTIONS_WIFI_ONLY);
+ PreferenceGroup sipSettings = (PreferenceGroup)
+ findPreference(SIP_SETTINGS_CATEGORY_KEY);
+ if (SipManager.isSipWifiOnly(this)) {
+ sipSettings.removePreference(wifiAnd3G);
+ return wifiOnly;
+ } else {
+ sipSettings.removePreference(wifiOnly);
+ return wifiAnd3G;
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mForeground = true;
+
+ if (isAirplaneModeOn()) {
+ Preference sipSettings = findPreference(SIP_SETTINGS_CATEGORY_KEY);
+ PreferenceScreen screen = getPreferenceScreen();
+ int count = screen.getPreferenceCount();
+ for (int i = 0 ; i < count ; ++i) {
+ Preference pref = screen.getPreference(i);
+ if (pref != sipSettings) pref.setEnabled(false);
+ }
+ return;
+ }
+
+ if (mVibrateWhenRinging != null) {
+ mVibrateWhenRinging.setChecked(getVibrateWhenRinging(this));
+ }
+
+ if (mButtonDTMF != null) {
+ int dtmf = Settings.System.getInt(getContentResolver(),
+ Settings.System.DTMF_TONE_TYPE_WHEN_DIALING, Constants.DTMF_TONE_TYPE_NORMAL);
+ mButtonDTMF.setValueIndex(dtmf);
+ }
+
+ if (mButtonAutoRetry != null) {
+ int autoretry = Settings.Global.getInt(getContentResolver(),
+ Settings.Global.CALL_AUTO_RETRY, 0);
+ mButtonAutoRetry.setChecked(autoretry != 0);
+ }
+
+ if (mButtonHAC != null) {
+ int hac = Settings.System.getInt(getContentResolver(), Settings.System.HEARING_AID, 0);
+ mButtonHAC.setChecked(hac != 0);
+ }
+
+ if (mButtonTTY != null) {
+ int settingsTtyMode = Settings.Secure.getInt(getContentResolver(),
+ Settings.Secure.PREFERRED_TTY_MODE,
+ Phone.TTY_MODE_OFF);
+ mButtonTTY.setValue(Integer.toString(settingsTtyMode));
+ updatePreferredTtyModeSummary(settingsTtyMode);
+ }
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(
+ mPhone.getContext());
+ if (migrateVoicemailVibrationSettingsIfNeeded(prefs)) {
+ mVoicemailNotificationVibrate.setChecked(prefs.getBoolean(
+ BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false));
+ }
+
+ lookupRingtoneName();
+ }
+
+ // Migrate settings from BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_WHEN_KEY to
+ // BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, if the latter does not exist.
+ // Returns true if migration was performed.
+ public static boolean migrateVoicemailVibrationSettingsIfNeeded(SharedPreferences prefs) {
+ if (!prefs.contains(BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY)) {
+ String vibrateWhen = prefs.getString(
+ BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_WHEN_KEY, VOICEMAIL_VIBRATION_NEVER);
+ // If vibrateWhen is always, then voicemailVibrate should be True.
+ // otherwise if vibrateWhen is "only in silent mode", or "never", then
+ // voicemailVibrate = False.
+ boolean voicemailVibrate = vibrateWhen.equals(VOICEMAIL_VIBRATION_ALWAYS);
+ final SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean(BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, voicemailVibrate);
+ editor.commit();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Obtain the setting for "vibrate when ringing" setting.
+ *
+ * Watch out: if the setting is missing in the device, this will try obtaining the old
+ * "vibrate on ring" setting from AudioManager, and save the previous setting to the new one.
+ */
+ public static boolean getVibrateWhenRinging(Context context) {
+ Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ if (vibrator == null || !vibrator.hasVibrator()) {
+ return false;
+ }
+ return Settings.System.getInt(context.getContentResolver(),
+ Settings.System.VIBRATE_WHEN_RINGING, 0) != 0;
+ }
+
+ /**
+ * Lookups ringtone name asynchronously and updates the relevant Preference.
+ */
+ private void lookupRingtoneName() {
+ new Thread(mRingtoneLookupRunnable).start();
+ }
+
+ private boolean isAirplaneModeOn() {
+ return Settings.System.getInt(getContentResolver(),
+ Settings.System.AIRPLANE_MODE_ON, 0) != 0;
+ }
+
+ private void handleTTYChange(Preference preference, Object objValue) {
+ int buttonTtyMode;
+ buttonTtyMode = Integer.valueOf((String) objValue).intValue();
+ int settingsTtyMode = android.provider.Settings.Secure.getInt(
+ getContentResolver(),
+ android.provider.Settings.Secure.PREFERRED_TTY_MODE, preferredTtyMode);
+ if (DBG) log("handleTTYChange: requesting set TTY mode enable (TTY) to" +
+ Integer.toString(buttonTtyMode));
+
+ if (buttonTtyMode != settingsTtyMode) {
+ switch(buttonTtyMode) {
+ case Phone.TTY_MODE_OFF:
+ case Phone.TTY_MODE_FULL:
+ case Phone.TTY_MODE_HCO:
+ case Phone.TTY_MODE_VCO:
+ android.provider.Settings.Secure.putInt(getContentResolver(),
+ android.provider.Settings.Secure.PREFERRED_TTY_MODE, buttonTtyMode);
+ break;
+ default:
+ buttonTtyMode = Phone.TTY_MODE_OFF;
+ }
+
+ mButtonTTY.setValue(Integer.toString(buttonTtyMode));
+ updatePreferredTtyModeSummary(buttonTtyMode);
+ Intent ttyModeChanged = new Intent(TtyIntent.TTY_PREFERRED_MODE_CHANGE_ACTION);
+ ttyModeChanged.putExtra(TtyIntent.TTY_PREFFERED_MODE, buttonTtyMode);
+ sendBroadcastAsUser(ttyModeChanged, UserHandle.ALL);
+ }
+ }
+
+ private void handleSipCallOptionsChange(Object objValue) {
+ String option = objValue.toString();
+ mSipSharedPreferences.setSipCallOption(option);
+ mButtonSipCallOptions.setValueIndex(
+ mButtonSipCallOptions.findIndexOfValue(option));
+ mButtonSipCallOptions.setSummary(mButtonSipCallOptions.getEntry());
+ }
+
+ private void updatePreferredTtyModeSummary(int TtyMode) {
+ String [] txts = getResources().getStringArray(R.array.tty_mode_entries);
+ switch(TtyMode) {
+ case Phone.TTY_MODE_OFF:
+ case Phone.TTY_MODE_HCO:
+ case Phone.TTY_MODE_VCO:
+ case Phone.TTY_MODE_FULL:
+ mButtonTTY.setSummary(txts[TtyMode]);
+ break;
+ default:
+ mButtonTTY.setEnabled(false);
+ mButtonTTY.setSummary(txts[Phone.TTY_MODE_OFF]);
+ }
+ }
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+
+ /**
+ * Updates the look of the VM preference widgets based on current VM provider settings.
+ * Note that the provider name is loaded form the found activity via loadLabel in
+ * {@link #initVoiceMailProviders()} in order for it to be localizable.
+ */
+ private void updateVMPreferenceWidgets(String currentProviderSetting) {
+ final String key = currentProviderSetting;
+ final VoiceMailProvider provider = mVMProvidersData.get(key);
+
+ /* This is the case when we are coming up on a freshly wiped phone and there is no
+ persisted value for the list preference mVoicemailProviders.
+ In this case we want to show the UI asking the user to select a voicemail provider as
+ opposed to silently falling back to default one. */
+ if (provider == null) {
+ if (DBG) {
+ log("updateVMPreferenceWidget: provider for the key \"" + key + "\" is null.");
+ }
+ mVoicemailProviders.setSummary(getString(R.string.sum_voicemail_choose_provider));
+ mVoicemailSettings.setEnabled(false);
+ mVoicemailSettings.setIntent(null);
+
+ mVoicemailNotificationVibrate.setEnabled(false);
+ } else {
+ if (DBG) {
+ log("updateVMPreferenceWidget: provider for the key \"" + key + "\".."
+ + "name: " + provider.name
+ + ", intent: " + provider.intent);
+ }
+ final String providerName = provider.name;
+ mVoicemailProviders.setSummary(providerName);
+ mVoicemailSettings.setEnabled(true);
+ mVoicemailSettings.setIntent(provider.intent);
+
+ mVoicemailNotificationVibrate.setEnabled(true);
+ }
+ }
+
+ /**
+ * Enumerates existing VM providers and puts their data into the list and populates
+ * the preference list objects with their names.
+ * In case we are called with ACTION_ADD_VOICEMAIL intent the intent may have
+ * an extra string called IGNORE_PROVIDER_EXTRA with "package.activityName" of the provider
+ * which should be hidden when we bring up the list of possible VM providers to choose.
+ */
+ private void initVoiceMailProviders() {
+ if (DBG) log("initVoiceMailProviders()");
+ mPerProviderSavedVMNumbers =
+ this.getApplicationContext().getSharedPreferences(
+ VM_NUMBERS_SHARED_PREFERENCES_NAME, MODE_PRIVATE);
+
+ String providerToIgnore = null;
+ if (getIntent().getAction().equals(ACTION_ADD_VOICEMAIL)) {
+ if (getIntent().hasExtra(IGNORE_PROVIDER_EXTRA)) {
+ providerToIgnore = getIntent().getStringExtra(IGNORE_PROVIDER_EXTRA);
+ }
+ if (DBG) log("Found ACTION_ADD_VOICEMAIL. providerToIgnore=" + providerToIgnore);
+ if (providerToIgnore != null) {
+ // IGNORE_PROVIDER_EXTRA implies we want to remove the choice from the list.
+ deleteSettingsForVoicemailProvider(providerToIgnore);
+ }
+ }
+
+ mVMProvidersData.clear();
+
+ // Stick the default element which is always there
+ final String myCarrier = getString(R.string.voicemail_default);
+ mVMProvidersData.put(DEFAULT_VM_PROVIDER_KEY, new VoiceMailProvider(myCarrier, null));
+
+ // Enumerate providers
+ PackageManager pm = getPackageManager();
+ Intent intent = new Intent();
+ intent.setAction(ACTION_CONFIGURE_VOICEMAIL);
+ List<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, 0);
+ int len = resolveInfos.size() + 1; // +1 for the default choice we will insert.
+
+ // Go through the list of discovered providers populating the data map
+ // skip the provider we were instructed to ignore if there was one
+ for (int i = 0; i < resolveInfos.size(); i++) {
+ final ResolveInfo ri= resolveInfos.get(i);
+ final ActivityInfo currentActivityInfo = ri.activityInfo;
+ final String key = makeKeyForActivity(currentActivityInfo);
+ if (key.equals(providerToIgnore)) {
+ if (DBG) log("Ignoring key: " + key);
+ len--;
+ continue;
+ }
+ if (DBG) log("Loading key: " + key);
+ final String nameForDisplay = ri.loadLabel(pm).toString();
+ Intent providerIntent = new Intent();
+ providerIntent.setAction(ACTION_CONFIGURE_VOICEMAIL);
+ providerIntent.setClassName(currentActivityInfo.packageName,
+ currentActivityInfo.name);
+ if (DBG) {
+ log("Store loaded VoiceMailProvider. key: " + key
+ + " -> name: " + nameForDisplay + ", intent: " + providerIntent);
+ }
+ mVMProvidersData.put(
+ key,
+ new VoiceMailProvider(nameForDisplay, providerIntent));
+
+ }
+
+ // Now we know which providers to display - create entries and values array for
+ // the list preference
+ String [] entries = new String [len];
+ String [] values = new String [len];
+ entries[0] = myCarrier;
+ values[0] = DEFAULT_VM_PROVIDER_KEY;
+ int entryIdx = 1;
+ for (int i = 0; i < resolveInfos.size(); i++) {
+ final String key = makeKeyForActivity(resolveInfos.get(i).activityInfo);
+ if (!mVMProvidersData.containsKey(key)) {
+ continue;
+ }
+ entries[entryIdx] = mVMProvidersData.get(key).name;
+ values[entryIdx] = key;
+ entryIdx++;
+ }
+
+ // ListPreference is now updated.
+ mVoicemailProviders.setEntries(entries);
+ mVoicemailProviders.setEntryValues(values);
+
+ // Remember the current Voicemail Provider key as a "previous" key. This will be used
+ // when we fail to update Voicemail Provider, which requires rollback.
+ // We will update this when the VM Provider setting is successfully updated.
+ mPreviousVMProviderKey = getCurrentVoicemailProviderKey();
+ if (DBG) log("Set up the first mPreviousVMProviderKey: " + mPreviousVMProviderKey);
+
+ // Finally update the preference texts.
+ updateVMPreferenceWidgets(mPreviousVMProviderKey);
+ }
+
+ private String makeKeyForActivity(ActivityInfo ai) {
+ return ai.name;
+ }
+
+ /**
+ * Simulates user clicking on a passed preference.
+ * Usually needed when the preference is a dialog preference and we want to invoke
+ * a dialog for this preference programmatically.
+ * TODO(iliat): figure out if there is a cleaner way to cause preference dlg to come up
+ */
+ private void simulatePreferenceClick(Preference preference) {
+ // Go through settings until we find our setting
+ // and then simulate a click on it to bring up the dialog
+ final ListAdapter adapter = getPreferenceScreen().getRootAdapter();
+ for (int idx = 0; idx < adapter.getCount(); idx++) {
+ if (adapter.getItem(idx) == preference) {
+ getPreferenceScreen().onItemClick(this.getListView(),
+ null, idx, adapter.getItemId(idx));
+ break;
+ }
+ }
+ }
+
+ /**
+ * Saves new VM provider settings associating them with the currently selected
+ * provider if settings are different than the ones already stored for this
+ * provider.
+ * Later on these will be used when the user switches a provider.
+ */
+ private void maybeSaveSettingsForVoicemailProvider(String key,
+ VoiceMailProviderSettings newSettings) {
+ if (mVoicemailProviders == null) {
+ return;
+ }
+ final VoiceMailProviderSettings curSettings = loadSettingsForVoiceMailProvider(key);
+ if (newSettings.equals(curSettings)) {
+ if (DBG) {
+ log("maybeSaveSettingsForVoicemailProvider:"
+ + " Not saving setting for " + key + " since they have not changed");
+ }
+ return;
+ }
+ if (DBG) log("Saving settings for " + key + ": " + newSettings.toString());
+ Editor editor = mPerProviderSavedVMNumbers.edit();
+ editor.putString(key + VM_NUMBER_TAG, newSettings.voicemailNumber);
+ String fwdKey = key + FWD_SETTINGS_TAG;
+ CallForwardInfo[] s = newSettings.forwardingSettings;
+ if (s != FWD_SETTINGS_DONT_TOUCH) {
+ editor.putInt(fwdKey + FWD_SETTINGS_LENGTH_TAG, s.length);
+ for (int i = 0; i < s.length; i++) {
+ final String settingKey = fwdKey + FWD_SETTING_TAG + String.valueOf(i);
+ final CallForwardInfo fi = s[i];
+ editor.putInt(settingKey + FWD_SETTING_STATUS, fi.status);
+ editor.putInt(settingKey + FWD_SETTING_REASON, fi.reason);
+ editor.putString(settingKey + FWD_SETTING_NUMBER, fi.number);
+ editor.putInt(settingKey + FWD_SETTING_TIME, fi.timeSeconds);
+ }
+ } else {
+ editor.putInt(fwdKey + FWD_SETTINGS_LENGTH_TAG, 0);
+ }
+ editor.apply();
+ }
+
+ /**
+ * Returns settings previously stored for the currently selected
+ * voice mail provider. If none is stored returns null.
+ * If the user switches to a voice mail provider and we have settings
+ * stored for it we will automatically change the phone's voice mail number
+ * and forwarding number to the stored one. Otherwise we will bring up provider's configuration
+ * UI.
+ */
+ private VoiceMailProviderSettings loadSettingsForVoiceMailProvider(String key) {
+ final String vmNumberSetting = mPerProviderSavedVMNumbers.getString(key + VM_NUMBER_TAG,
+ null);
+ if (vmNumberSetting == null) {
+ Log.w(LOG_TAG, "VoiceMailProvider settings for the key \"" + key + "\""
+ + " was not found. Returning null.");
+ return null;
+ }
+
+ CallForwardInfo[] cfi = FWD_SETTINGS_DONT_TOUCH;
+ String fwdKey = key + FWD_SETTINGS_TAG;
+ final int fwdLen = mPerProviderSavedVMNumbers.getInt(fwdKey + FWD_SETTINGS_LENGTH_TAG, 0);
+ if (fwdLen > 0) {
+ cfi = new CallForwardInfo[fwdLen];
+ for (int i = 0; i < cfi.length; i++) {
+ final String settingKey = fwdKey + FWD_SETTING_TAG + String.valueOf(i);
+ cfi[i] = new CallForwardInfo();
+ cfi[i].status = mPerProviderSavedVMNumbers.getInt(
+ settingKey + FWD_SETTING_STATUS, 0);
+ cfi[i].reason = mPerProviderSavedVMNumbers.getInt(
+ settingKey + FWD_SETTING_REASON,
+ CommandsInterface.CF_REASON_ALL_CONDITIONAL);
+ cfi[i].serviceClass = CommandsInterface.SERVICE_CLASS_VOICE;
+ cfi[i].toa = PhoneNumberUtils.TOA_International;
+ cfi[i].number = mPerProviderSavedVMNumbers.getString(
+ settingKey + FWD_SETTING_NUMBER, "");
+ cfi[i].timeSeconds = mPerProviderSavedVMNumbers.getInt(
+ settingKey + FWD_SETTING_TIME, 20);
+ }
+ }
+
+ VoiceMailProviderSettings settings = new VoiceMailProviderSettings(vmNumberSetting, cfi);
+ if (DBG) log("Loaded settings for " + key + ": " + settings.toString());
+ return settings;
+ }
+
+ /**
+ * Deletes settings for the specified provider.
+ */
+ private void deleteSettingsForVoicemailProvider(String key) {
+ if (DBG) log("Deleting settings for" + key);
+ if (mVoicemailProviders == null) {
+ return;
+ }
+ mPerProviderSavedVMNumbers.edit()
+ .putString(key + VM_NUMBER_TAG, null)
+ .putInt(key + FWD_SETTINGS_TAG + FWD_SETTINGS_LENGTH_TAG, 0)
+ .commit();
+ }
+
+ private String getCurrentVoicemailProviderKey() {
+ final String key = mVoicemailProviders.getValue();
+ return (key != null) ? key : DEFAULT_VM_PROVIDER_KEY;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled()
+ Intent intent = new Intent();
+ intent.setClassName(UP_ACTIVITY_PACKAGE, UP_ACTIVITY_CLASS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * Finish current Activity and go up to the top level Settings ({@link CallFeaturesSetting}).
+ * This is useful for implementing "HomeAsUp" capability for second-level Settings.
+ */
+ public static void goUpToTopLevelSetting(Activity activity) {
+ Intent intent = new Intent(activity, CallFeaturesSetting.class);
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ activity.startActivity(intent);
+ activity.finish();
+ }
+}
diff --git a/src/com/android/phone/CallForwardEditPreference.java b/src/com/android/phone/CallForwardEditPreference.java
new file mode 100644
index 0000000..f925022
--- /dev/null
+++ b/src/com/android/phone/CallForwardEditPreference.java
@@ -0,0 +1,265 @@
+package com.android.phone;
+
+import com.android.internal.telephony.CallForwardInfo;
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.Phone;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.TypedArray;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+import static com.android.phone.TimeConsumingPreferenceActivity.RESPONSE_ERROR;
+
+public class CallForwardEditPreference extends EditPhoneNumberPreference {
+ private static final String LOG_TAG = "CallForwardEditPreference";
+ private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ private static final String SRC_TAGS[] = {"{0}"};
+ private CharSequence mSummaryOnTemplate;
+ /**
+ * Remembers which button was clicked by a user. If no button is clicked yet, this should have
+ * {@link DialogInterface#BUTTON_NEGATIVE}, meaning "cancel".
+ *
+ * TODO: consider removing this variable and having getButtonClicked() in
+ * EditPhoneNumberPreference instead.
+ */
+ private int mButtonClicked;
+ private int mServiceClass;
+ private MyHandler mHandler = new MyHandler();
+ int reason;
+ Phone phone;
+ CallForwardInfo callForwardInfo;
+ TimeConsumingPreferenceListener tcpListener;
+
+ public CallForwardEditPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ phone = PhoneGlobals.getPhone();
+ mSummaryOnTemplate = this.getSummaryOn();
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.CallForwardEditPreference, 0, R.style.EditPhoneNumberPreference);
+ mServiceClass = a.getInt(R.styleable.CallForwardEditPreference_serviceClass,
+ CommandsInterface.SERVICE_CLASS_VOICE);
+ reason = a.getInt(R.styleable.CallForwardEditPreference_reason,
+ CommandsInterface.CF_REASON_UNCONDITIONAL);
+ a.recycle();
+
+ if (DBG) Log.d(LOG_TAG, "mServiceClass=" + mServiceClass + ", reason=" + reason);
+ }
+
+ public CallForwardEditPreference(Context context) {
+ this(context, null);
+ }
+
+ void init(TimeConsumingPreferenceListener listener, boolean skipReading) {
+ tcpListener = listener;
+ if (!skipReading) {
+ phone.getCallForwardingOption(reason,
+ mHandler.obtainMessage(MyHandler.MESSAGE_GET_CF,
+ // unused in this case
+ CommandsInterface.CF_ACTION_DISABLE,
+ MyHandler.MESSAGE_GET_CF, null));
+ if (tcpListener != null) {
+ tcpListener.onStarted(this, true);
+ }
+ }
+ }
+
+ @Override
+ protected void onBindDialogView(View view) {
+ // default the button clicked to be the cancel button.
+ mButtonClicked = DialogInterface.BUTTON_NEGATIVE;
+ super.onBindDialogView(view);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ super.onClick(dialog, which);
+ mButtonClicked = which;
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+
+ if (DBG) Log.d(LOG_TAG, "mButtonClicked=" + mButtonClicked
+ + ", positiveResult=" + positiveResult);
+ // Ignore this event if the user clicked the cancel button, or if the dialog is dismissed
+ // without any button being pressed (back button press or click event outside the dialog).
+ if (this.mButtonClicked != DialogInterface.BUTTON_NEGATIVE) {
+ int action = (isToggled() || (mButtonClicked == DialogInterface.BUTTON_POSITIVE)) ?
+ CommandsInterface.CF_ACTION_REGISTRATION :
+ CommandsInterface.CF_ACTION_DISABLE;
+ int time = (reason != CommandsInterface.CF_REASON_NO_REPLY) ? 0 : 20;
+ final String number = getPhoneNumber();
+
+ if (DBG) Log.d(LOG_TAG, "callForwardInfo=" + callForwardInfo);
+
+ if (action == CommandsInterface.CF_ACTION_REGISTRATION
+ && callForwardInfo != null
+ && callForwardInfo.status == 1
+ && number.equals(callForwardInfo.number)) {
+ // no change, do nothing
+ if (DBG) Log.d(LOG_TAG, "no change, do nothing");
+ } else {
+ // set to network
+ if (DBG) Log.d(LOG_TAG, "reason=" + reason + ", action=" + action
+ + ", number=" + number);
+
+ // Display no forwarding number while we're waiting for
+ // confirmation
+ setSummaryOn("");
+
+ // the interface of Phone.setCallForwardingOption has error:
+ // should be action, reason...
+ phone.setCallForwardingOption(action,
+ reason,
+ number,
+ time,
+ mHandler.obtainMessage(MyHandler.MESSAGE_SET_CF,
+ action,
+ MyHandler.MESSAGE_SET_CF));
+
+ if (tcpListener != null) {
+ tcpListener.onStarted(this, false);
+ }
+ }
+ }
+ }
+
+ void handleCallForwardResult(CallForwardInfo cf) {
+ callForwardInfo = cf;
+ if (DBG) Log.d(LOG_TAG, "handleGetCFResponse done, callForwardInfo=" + callForwardInfo);
+
+ setToggled(callForwardInfo.status == 1);
+ setPhoneNumber(callForwardInfo.number);
+ }
+
+ private void updateSummaryText() {
+ if (isToggled()) {
+ CharSequence summaryOn;
+ final String number = getRawPhoneNumber();
+ if (number != null && number.length() > 0) {
+ String values[] = { number };
+ summaryOn = TextUtils.replace(mSummaryOnTemplate, SRC_TAGS, values);
+ } else {
+ summaryOn = getContext().getString(R.string.sum_cfu_enabled_no_number);
+ }
+ setSummaryOn(summaryOn);
+ }
+
+ }
+
+ // Message protocol:
+ // what: get vs. set
+ // arg1: action -- register vs. disable
+ // arg2: get vs. set for the preceding request
+ private class MyHandler extends Handler {
+ static final int MESSAGE_GET_CF = 0;
+ static final int MESSAGE_SET_CF = 1;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_GET_CF:
+ handleGetCFResponse(msg);
+ break;
+ case MESSAGE_SET_CF:
+ handleSetCFResponse(msg);
+ break;
+ }
+ }
+
+ private void handleGetCFResponse(Message msg) {
+ if (DBG) Log.d(LOG_TAG, "handleGetCFResponse: done");
+
+ if (msg.arg2 == MESSAGE_SET_CF) {
+ tcpListener.onFinished(CallForwardEditPreference.this, false);
+ } else {
+ tcpListener.onFinished(CallForwardEditPreference.this, true);
+ }
+
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ callForwardInfo = null;
+ if (ar.exception != null) {
+ if (DBG) Log.d(LOG_TAG, "handleGetCFResponse: ar.exception=" + ar.exception);
+ tcpListener.onException(CallForwardEditPreference.this,
+ (CommandException) ar.exception);
+ } else {
+ if (ar.userObj instanceof Throwable) {
+ tcpListener.onError(CallForwardEditPreference.this, RESPONSE_ERROR);
+ }
+ CallForwardInfo cfInfoArray[] = (CallForwardInfo[]) ar.result;
+ if (cfInfoArray.length == 0) {
+ if (DBG) Log.d(LOG_TAG, "handleGetCFResponse: cfInfoArray.length==0");
+ setEnabled(false);
+ tcpListener.onError(CallForwardEditPreference.this, RESPONSE_ERROR);
+ } else {
+ for (int i = 0, length = cfInfoArray.length; i < length; i++) {
+ if (DBG) Log.d(LOG_TAG, "handleGetCFResponse, cfInfoArray[" + i + "]="
+ + cfInfoArray[i]);
+ if ((mServiceClass & cfInfoArray[i].serviceClass) != 0) {
+ // corresponding class
+ CallForwardInfo info = cfInfoArray[i];
+ handleCallForwardResult(info);
+
+ // Show an alert if we got a success response but
+ // with unexpected values.
+ // Currently only handle the fail-to-disable case
+ // since we haven't observed fail-to-enable.
+ if (msg.arg2 == MESSAGE_SET_CF &&
+ msg.arg1 == CommandsInterface.CF_ACTION_DISABLE &&
+ info.status == 1) {
+ CharSequence s;
+ switch (reason) {
+ case CommandsInterface.CF_REASON_BUSY:
+ s = getContext().getText(R.string.disable_cfb_forbidden);
+ break;
+ case CommandsInterface.CF_REASON_NO_REPLY:
+ s = getContext().getText(R.string.disable_cfnry_forbidden);
+ break;
+ default: // not reachable
+ s = getContext().getText(R.string.disable_cfnrc_forbidden);
+ }
+ AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+ builder.setNeutralButton(R.string.close_dialog, null);
+ builder.setTitle(getContext().getText(R.string.error_updating_title));
+ builder.setMessage(s);
+ builder.setCancelable(true);
+ builder.create().show();
+ }
+ }
+ }
+ }
+ }
+
+ // Now whether or not we got a new number, reset our enabled
+ // summary text since it may have been replaced by an empty
+ // placeholder.
+ updateSummaryText();
+ }
+
+ private void handleSetCFResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (ar.exception != null) {
+ if (DBG) Log.d(LOG_TAG, "handleSetCFResponse: ar.exception=" + ar.exception);
+ // setEnabled(false);
+ }
+ if (DBG) Log.d(LOG_TAG, "handleSetCFResponse: re get");
+ phone.getCallForwardingOption(reason,
+ obtainMessage(MESSAGE_GET_CF, msg.arg1, MESSAGE_SET_CF, ar.exception));
+ }
+ }
+}
diff --git a/src/com/android/phone/CallLogger.java b/src/com/android/phone/CallLogger.java
new file mode 100644
index 0000000..644812f
--- /dev/null
+++ b/src/com/android/phone/CallLogger.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2013 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.phone;
+
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyCapabilities;
+import com.android.phone.common.CallLogAsync;
+
+import android.net.Uri;
+import android.os.SystemProperties;
+import android.provider.CallLog.Calls;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * Helper class for interacting with the call log.
+ */
+class CallLogger {
+ private static final String LOG_TAG = CallLogger.class.getSimpleName();
+ private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 1) &&
+ (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ private PhoneGlobals mApplication;
+ private CallLogAsync mCallLog;
+
+ public CallLogger(PhoneGlobals application, CallLogAsync callLogAsync) {
+ mApplication = application;
+ mCallLog = callLogAsync;
+ }
+
+ /**
+ * Logs a call to the call log based on the connection object passed in.
+ *
+ * @param c The connection object for the call being logged.
+ * @param callLogType The type of call log entry.
+ */
+ public void logCall(Connection c, int callLogType) {
+ final String number = c.getAddress();
+ final long date = c.getCreateTime();
+ final long duration = c.getDurationMillis();
+ final Phone phone = c.getCall().getPhone();
+
+ final CallerInfo ci = getCallerInfoFromConnection(c); // May be null.
+ final String logNumber = getLogNumber(c, ci);
+
+ if (DBG) {
+ log("- onDisconnect(): logNumber set to:" + PhoneUtils.toLogSafePhoneNumber(logNumber) +
+ ", number set to: " + PhoneUtils.toLogSafePhoneNumber(number));
+ }
+
+ // TODO: In getLogNumber we use the presentation from
+ // the connection for the CNAP. Should we use the one
+ // below instead? (comes from caller info)
+
+ // For international calls, 011 needs to be logged as +
+ final int presentation = getPresentation(c, ci);
+
+ final boolean isOtaspNumber = TelephonyCapabilities.supportsOtasp(phone)
+ && phone.isOtaSpNumber(number);
+
+ // Don't log OTASP calls.
+ if (!isOtaspNumber) {
+ logCall(ci, logNumber, presentation, callLogType, date, duration);
+ }
+ }
+
+ /**
+ * Came as logCall(Connection,int) but calculates the call type from the connection object.
+ */
+ public void logCall(Connection c) {
+ final Connection.DisconnectCause cause = c.getDisconnectCause();
+
+ // Set the "type" to be displayed in the call log (see constants in CallLog.Calls)
+ final int callLogType;
+
+ if (c.isIncoming()) {
+ callLogType = (cause == Connection.DisconnectCause.INCOMING_MISSED ?
+ Calls.MISSED_TYPE : Calls.INCOMING_TYPE);
+ } else {
+ callLogType = Calls.OUTGOING_TYPE;
+ }
+ if (VDBG) log("- callLogType: " + callLogType + ", UserData: " + c.getUserData());
+
+ logCall(c, callLogType);
+ }
+
+ /**
+ * Logs a call to the call from the parameters passed in.
+ */
+ public void logCall(CallerInfo ci, String number, int presentation, int callType, long start,
+ long duration) {
+ final boolean isEmergencyNumber = PhoneNumberUtils.isLocalEmergencyNumber(number,
+ mApplication);
+
+ // On some devices, to avoid accidental redialing of
+ // emergency numbers, we *never* log emergency calls to
+ // the Call Log. (This behavior is set on a per-product
+ // basis, based on carrier requirements.)
+ final boolean okToLogEmergencyNumber =
+ mApplication.getResources().getBoolean(
+ R.bool.allow_emergency_numbers_in_call_log);
+
+ // Don't log emergency numbers if the device doesn't allow it,
+ boolean isOkToLogThisCall = !isEmergencyNumber || okToLogEmergencyNumber;
+
+ if (isOkToLogThisCall) {
+ if (DBG) {
+ log("sending Calllog entry: " + ci + ", " + PhoneUtils.toLogSafePhoneNumber(number)
+ + "," + presentation + ", " + callType + ", " + start + ", " + duration);
+ }
+
+ CallLogAsync.AddCallArgs args = new CallLogAsync.AddCallArgs(mApplication, ci, number,
+ presentation, callType, start, duration);
+ mCallLog.addCall(args);
+ }
+ }
+
+ /**
+ * Get the caller info.
+ *
+ * @param conn The phone connection.
+ * @return The CallerInfo associated with the connection. Maybe null.
+ */
+ private CallerInfo getCallerInfoFromConnection(Connection conn) {
+ CallerInfo ci = null;
+ Object o = conn.getUserData();
+
+ if ((o == null) || (o instanceof CallerInfo)) {
+ ci = (CallerInfo) o;
+ } else if (o instanceof Uri) {
+ ci = CallerInfo.getCallerInfo(mApplication.getApplicationContext(), (Uri) o);
+ } else {
+ ci = ((PhoneUtils.CallerInfoToken) o).currentInfo;
+ }
+ return ci;
+ }
+
+ /**
+ * Retrieve the phone number from the caller info or the connection.
+ *
+ * For incoming call the number is in the Connection object. For
+ * outgoing call we use the CallerInfo phoneNumber field if
+ * present. All the processing should have been done already (CDMA vs GSM numbers).
+ *
+ * If CallerInfo is missing the phone number, get it from the connection.
+ * Apply the Call Name Presentation (CNAP) transform in the connection on the number.
+ *
+ * @param conn The phone connection.
+ * @param callerInfo The CallerInfo. Maybe null.
+ * @return the phone number.
+ */
+ private String getLogNumber(Connection conn, CallerInfo callerInfo) {
+ String number = null;
+
+ if (conn.isIncoming()) {
+ number = conn.getAddress();
+ } else {
+ // For emergency and voicemail calls,
+ // CallerInfo.phoneNumber does *not* contain a valid phone
+ // number. Instead it contains an I18N'd string such as
+ // "Emergency Number" or "Voice Mail" so we get the number
+ // from the connection.
+ if (null == callerInfo || TextUtils.isEmpty(callerInfo.phoneNumber) ||
+ callerInfo.isEmergencyNumber() || callerInfo.isVoiceMailNumber()) {
+ if (conn.getCall().getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ // In cdma getAddress() is not always equals to getOrigDialString().
+ number = conn.getOrigDialString();
+ } else {
+ number = conn.getAddress();
+ }
+ } else {
+ number = callerInfo.phoneNumber;
+ }
+ }
+
+ if (null == number) {
+ return null;
+ } else {
+ int presentation = conn.getNumberPresentation();
+
+ // Do final CNAP modifications.
+ String newNumber = PhoneUtils.modifyForSpecialCnapCases(mApplication, callerInfo,
+ number, presentation);
+
+ if (!PhoneNumberUtils.isUriNumber(number)) {
+ number = PhoneNumberUtils.stripSeparators(number);
+ }
+ if (VDBG) log("getLogNumber: " + number);
+ return number;
+ }
+ }
+
+ /**
+ * Get the presentation from the callerinfo if not null otherwise,
+ * get it from the connection.
+ *
+ * @param conn The phone connection.
+ * @param callerInfo The CallerInfo. Maybe null.
+ * @return The presentation to use in the logs.
+ */
+ private int getPresentation(Connection conn, CallerInfo callerInfo) {
+ int presentation;
+
+ if (null == callerInfo) {
+ presentation = conn.getNumberPresentation();
+ } else {
+ presentation = callerInfo.numberPresentation;
+ if (DBG) log("- getPresentation(): ignoring connection's presentation: " +
+ conn.getNumberPresentation());
+ }
+ if (DBG) log("- getPresentation: presentation: " + presentation);
+ return presentation;
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/CallNotifier.java b/src/com/android/phone/CallNotifier.java
new file mode 100644
index 0000000..39feb25
--- /dev/null
+++ b/src/com/android/phone/CallNotifier.java
@@ -0,0 +1,1907 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.CallerInfoAsyncQuery;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneBase;
+import com.android.internal.telephony.TelephonyCapabilities;
+import com.android.internal.telephony.cdma.CdmaCallWaitingNotification;
+import com.android.internal.telephony.cdma.CdmaInformationRecords.CdmaDisplayInfoRec;
+import com.android.internal.telephony.cdma.CdmaInformationRecords.CdmaSignalInfoRec;
+import com.android.internal.telephony.cdma.SignalToneUtil;
+
+import android.app.ActivityManagerNative;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.os.SystemVibrator;
+import android.os.Vibrator;
+import android.provider.CallLog.Calls;
+import android.provider.Settings;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.PhoneStateListener;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.EventLog;
+import android.util.Log;
+
+/**
+ * Phone app module that listens for phone state changes and various other
+ * events from the telephony layer, and triggers any resulting UI behavior
+ * (like starting the Ringer and Incoming Call UI, playing in-call tones,
+ * updating notifications, writing call log entries, etc.)
+ */
+public class CallNotifier extends Handler
+ implements CallerInfoAsyncQuery.OnQueryCompleteListener {
+ private static final String LOG_TAG = "CallNotifier";
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ // Maximum time we allow the CallerInfo query to run,
+ // before giving up and falling back to the default ringtone.
+ private static final int RINGTONE_QUERY_WAIT_TIME = 500; // msec
+
+ // Timers related to CDMA Call Waiting
+ // 1) For displaying Caller Info
+ // 2) For disabling "Add Call" menu option once User selects Ignore or CW Timeout occures
+ private static final int CALLWAITING_CALLERINFO_DISPLAY_TIME = 20000; // msec
+ private static final int CALLWAITING_ADDCALL_DISABLE_TIME = 30000; // msec
+
+ // Time to display the DisplayInfo Record sent by CDMA network
+ private static final int DISPLAYINFO_NOTIFICATION_TIME = 2000; // msec
+
+ /** The singleton instance. */
+ private static CallNotifier sInstance;
+
+ // Boolean to keep track of whether or not a CDMA Call Waiting call timed out.
+ //
+ // This is CDMA-specific, because with CDMA we *don't* get explicit
+ // notification from the telephony layer that a call-waiting call has
+ // stopped ringing. Instead, when a call-waiting call first comes in we
+ // start a 20-second timer (see CALLWAITING_CALLERINFO_DISPLAY_DONE), and
+ // if the timer expires we clean up the call and treat it as a missed call.
+ //
+ // If this field is true, that means that the current Call Waiting call
+ // "timed out" and should be logged in Call Log as a missed call. If it's
+ // false when we reach onCdmaCallWaitingReject(), we can assume the user
+ // explicitly rejected this call-waiting call.
+ //
+ // This field is reset to false any time a call-waiting call first comes
+ // in, and after cleaning up a missed call-waiting call. It's only ever
+ // set to true when the CALLWAITING_CALLERINFO_DISPLAY_DONE timer fires.
+ //
+ // TODO: do we really need a member variable for this? Don't we always
+ // know at the moment we call onCdmaCallWaitingReject() whether this is an
+ // explicit rejection or not?
+ // (Specifically: when we call onCdmaCallWaitingReject() from
+ // PhoneUtils.hangupRingingCall() that means the user deliberately rejected
+ // the call, and if we call onCdmaCallWaitingReject() because of a
+ // CALLWAITING_CALLERINFO_DISPLAY_DONE event that means that it timed
+ // out...)
+ private boolean mCallWaitingTimeOut = false;
+
+ // values used to track the query state
+ private static final int CALLERINFO_QUERY_READY = 0;
+ private static final int CALLERINFO_QUERYING = -1;
+
+ // the state of the CallerInfo Query.
+ private int mCallerInfoQueryState;
+
+ // object used to synchronize access to mCallerInfoQueryState
+ private Object mCallerInfoQueryStateGuard = new Object();
+
+ // Event used to indicate a query timeout.
+ private static final int RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT = 100;
+
+ // Events generated internally:
+ private static final int PHONE_MWI_CHANGED = 21;
+ private static final int CALLWAITING_CALLERINFO_DISPLAY_DONE = 22;
+ private static final int CALLWAITING_ADDCALL_DISABLE_TIMEOUT = 23;
+ private static final int DISPLAYINFO_NOTIFICATION_DONE = 24;
+ private static final int CDMA_CALL_WAITING_REJECT = 26;
+ private static final int UPDATE_IN_CALL_NOTIFICATION = 27;
+
+ // Emergency call related defines:
+ private static final int EMERGENCY_TONE_OFF = 0;
+ private static final int EMERGENCY_TONE_ALERT = 1;
+ private static final int EMERGENCY_TONE_VIBRATE = 2;
+
+ private PhoneGlobals mApplication;
+ private CallManager mCM;
+ private CallStateMonitor mCallStateMonitor;
+ private Ringer mRinger;
+ private BluetoothHeadset mBluetoothHeadset;
+ private CallLogger mCallLogger;
+ private boolean mSilentRingerRequested;
+
+ // ToneGenerator instance for playing SignalInfo tones
+ private ToneGenerator mSignalInfoToneGenerator;
+
+ // The tone volume relative to other sounds in the stream SignalInfo
+ private static final int TONE_RELATIVE_VOLUME_SIGNALINFO = 80;
+
+ private Call.State mPreviousCdmaCallState;
+ private boolean mVoicePrivacyState = false;
+ private boolean mIsCdmaRedialCall = false;
+
+ // Emergency call tone and vibrate:
+ private int mIsEmergencyToneOn;
+ private int mCurrentEmergencyToneState = EMERGENCY_TONE_OFF;
+ private EmergencyTonePlayerVibrator mEmergencyTonePlayerVibrator;
+
+ // Ringback tone player
+ private InCallTonePlayer mInCallRingbackTonePlayer;
+
+ // Call waiting tone player
+ private InCallTonePlayer mCallWaitingTonePlayer;
+
+ // Cached AudioManager
+ private AudioManager mAudioManager;
+
+ /**
+ * Initialize the singleton CallNotifier instance.
+ * This is only done once, at startup, from PhoneApp.onCreate().
+ */
+ /* package */ static CallNotifier init(PhoneGlobals app, Phone phone, Ringer ringer,
+ CallLogger callLogger, CallStateMonitor callStateMonitor) {
+ synchronized (CallNotifier.class) {
+ if (sInstance == null) {
+ sInstance = new CallNotifier(app, phone, ringer, callLogger, callStateMonitor);
+ } else {
+ Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance);
+ }
+ return sInstance;
+ }
+ }
+
+ /** Private constructor; @see init() */
+ private CallNotifier(PhoneGlobals app, Phone phone, Ringer ringer, CallLogger callLogger,
+ CallStateMonitor callStateMonitor) {
+ mApplication = app;
+ mCM = app.mCM;
+ mCallLogger = callLogger;
+ mCallStateMonitor = callStateMonitor;
+
+ mAudioManager = (AudioManager) mApplication.getSystemService(Context.AUDIO_SERVICE);
+
+ callStateMonitor.addListener(this);
+
+ createSignalInfoToneGenerator();
+
+ mRinger = ringer;
+ BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ if (adapter != null) {
+ adapter.getProfileProxy(mApplication.getApplicationContext(),
+ mBluetoothProfileServiceListener,
+ BluetoothProfile.HEADSET);
+ }
+
+ TelephonyManager telephonyManager = (TelephonyManager)app.getSystemService(
+ Context.TELEPHONY_SERVICE);
+ telephonyManager.listen(mPhoneStateListener,
+ PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR
+ | PhoneStateListener.LISTEN_CALL_FORWARDING_INDICATOR);
+ }
+
+ private void createSignalInfoToneGenerator() {
+ // Instantiate the ToneGenerator for SignalInfo and CallWaiting
+ // TODO: We probably don't need the mSignalInfoToneGenerator instance
+ // around forever. Need to change it so as to create a ToneGenerator instance only
+ // when a tone is being played and releases it after its done playing.
+ if (mSignalInfoToneGenerator == null) {
+ try {
+ mSignalInfoToneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL,
+ TONE_RELATIVE_VOLUME_SIGNALINFO);
+ Log.d(LOG_TAG, "CallNotifier: mSignalInfoToneGenerator created when toneplay");
+ } catch (RuntimeException e) {
+ Log.w(LOG_TAG, "CallNotifier: Exception caught while creating " +
+ "mSignalInfoToneGenerator: " + e);
+ mSignalInfoToneGenerator = null;
+ }
+ } else {
+ Log.d(LOG_TAG, "mSignalInfoToneGenerator created already, hence skipping");
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case CallStateMonitor.PHONE_NEW_RINGING_CONNECTION:
+ log("RINGING... (new)");
+ onNewRingingConnection((AsyncResult) msg.obj);
+ mSilentRingerRequested = false;
+ break;
+
+ case CallStateMonitor.PHONE_INCOMING_RING:
+ // repeat the ring when requested by the RIL, and when the user has NOT
+ // specifically requested silence.
+ if (msg.obj != null && ((AsyncResult) msg.obj).result != null) {
+ PhoneBase pb = (PhoneBase)((AsyncResult)msg.obj).result;
+
+ if ((pb.getState() == PhoneConstants.State.RINGING)
+ && (mSilentRingerRequested == false)) {
+ if (DBG) log("RINGING... (PHONE_INCOMING_RING event)");
+ mRinger.ring();
+ } else {
+ if (DBG) log("RING before NEW_RING, skipping");
+ }
+ }
+ break;
+
+ case CallStateMonitor.PHONE_STATE_CHANGED:
+ onPhoneStateChanged((AsyncResult) msg.obj);
+ break;
+
+ case CallStateMonitor.PHONE_DISCONNECT:
+ if (DBG) log("DISCONNECT");
+ onDisconnect((AsyncResult) msg.obj);
+ break;
+
+ case CallStateMonitor.PHONE_UNKNOWN_CONNECTION_APPEARED:
+ onUnknownConnectionAppeared((AsyncResult) msg.obj);
+ break;
+
+ case RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT:
+ onCustomRingtoneQueryTimeout((String) msg.obj);
+ break;
+
+ case PHONE_MWI_CHANGED:
+ onMwiChanged(mApplication.phone.getMessageWaitingIndicator());
+ break;
+
+ case CallStateMonitor.PHONE_CDMA_CALL_WAITING:
+ if (DBG) log("Received PHONE_CDMA_CALL_WAITING event");
+ onCdmaCallWaiting((AsyncResult) msg.obj);
+ break;
+
+ case CDMA_CALL_WAITING_REJECT:
+ Log.i(LOG_TAG, "Received CDMA_CALL_WAITING_REJECT event");
+ onCdmaCallWaitingReject();
+ break;
+
+ case CALLWAITING_CALLERINFO_DISPLAY_DONE:
+ Log.i(LOG_TAG, "Received CALLWAITING_CALLERINFO_DISPLAY_DONE event");
+ mCallWaitingTimeOut = true;
+ onCdmaCallWaitingReject();
+ break;
+
+ case CALLWAITING_ADDCALL_DISABLE_TIMEOUT:
+ if (DBG) log("Received CALLWAITING_ADDCALL_DISABLE_TIMEOUT event ...");
+ // Set the mAddCallMenuStateAfterCW state to true
+ mApplication.cdmaPhoneCallState.setAddCallMenuStateAfterCallWaiting(true);
+ mApplication.updateInCallScreen();
+ break;
+
+ case CallStateMonitor.PHONE_STATE_DISPLAYINFO:
+ if (DBG) log("Received PHONE_STATE_DISPLAYINFO event");
+ onDisplayInfo((AsyncResult) msg.obj);
+ break;
+
+ case CallStateMonitor.PHONE_STATE_SIGNALINFO:
+ if (DBG) log("Received PHONE_STATE_SIGNALINFO event");
+ onSignalInfo((AsyncResult) msg.obj);
+ break;
+
+ case DISPLAYINFO_NOTIFICATION_DONE:
+ if (DBG) log("Received Display Info notification done event ...");
+ CdmaDisplayInfo.dismissDisplayInfoRecord();
+ break;
+
+ case CallStateMonitor.EVENT_OTA_PROVISION_CHANGE:
+ if (DBG) log("EVENT_OTA_PROVISION_CHANGE...");
+ mApplication.handleOtaspEvent(msg);
+ break;
+
+ case CallStateMonitor.PHONE_ENHANCED_VP_ON:
+ if (DBG) log("PHONE_ENHANCED_VP_ON...");
+ if (!mVoicePrivacyState) {
+ int toneToPlay = InCallTonePlayer.TONE_VOICE_PRIVACY;
+ new InCallTonePlayer(toneToPlay).start();
+ mVoicePrivacyState = true;
+ // Update the VP icon:
+ if (DBG) log("- updating notification for VP state...");
+ mApplication.notificationMgr.updateInCallNotification();
+ }
+ break;
+
+ case CallStateMonitor.PHONE_ENHANCED_VP_OFF:
+ if (DBG) log("PHONE_ENHANCED_VP_OFF...");
+ if (mVoicePrivacyState) {
+ int toneToPlay = InCallTonePlayer.TONE_VOICE_PRIVACY;
+ new InCallTonePlayer(toneToPlay).start();
+ mVoicePrivacyState = false;
+ // Update the VP icon:
+ if (DBG) log("- updating notification for VP state...");
+ mApplication.notificationMgr.updateInCallNotification();
+ }
+ break;
+
+ case CallStateMonitor.PHONE_RINGBACK_TONE:
+ onRingbackTone((AsyncResult) msg.obj);
+ break;
+
+ case CallStateMonitor.PHONE_RESEND_MUTE:
+ onResendMute();
+ break;
+
+ case UPDATE_IN_CALL_NOTIFICATION:
+ mApplication.notificationMgr.updateInCallNotification();
+ break;
+
+ default:
+ // super.handleMessage(msg);
+ }
+ }
+
+ PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
+ @Override
+ public void onMessageWaitingIndicatorChanged(boolean mwi) {
+ onMwiChanged(mwi);
+ }
+
+ @Override
+ public void onCallForwardingIndicatorChanged(boolean cfi) {
+ onCfiChanged(cfi);
+ }
+ };
+
+ /**
+ * Handles a "new ringing connection" event from the telephony layer.
+ */
+ private void onNewRingingConnection(AsyncResult r) {
+ Connection c = (Connection) r.result;
+ log("onNewRingingConnection(): state = " + mCM.getState() + ", conn = { " + c + " }");
+ Call ringing = c.getCall();
+ Phone phone = ringing.getPhone();
+
+ // Check for a few cases where we totally ignore incoming calls.
+ if (ignoreAllIncomingCalls(phone)) {
+ // Immediately reject the call, without even indicating to the user
+ // that an incoming call occurred. (This will generally send the
+ // caller straight to voicemail, just as if we *had* shown the
+ // incoming-call UI and the user had declined the call.)
+ PhoneUtils.hangupRingingCall(ringing);
+ return;
+ }
+
+ if (!c.isRinging()) {
+ Log.i(LOG_TAG, "CallNotifier.onNewRingingConnection(): connection not ringing!");
+ // This is a very strange case: an incoming call that stopped
+ // ringing almost instantly after the onNewRingingConnection()
+ // event. There's nothing we can do here, so just bail out
+ // without doing anything. (But presumably we'll log it in
+ // the call log when the disconnect event comes in...)
+ return;
+ }
+
+ // Stop any signalInfo tone being played on receiving a Call
+ stopSignalInfoTone();
+
+ Call.State state = c.getState();
+ // State will be either INCOMING or WAITING.
+ if (VDBG) log("- connection is ringing! state = " + state);
+ // if (DBG) PhoneUtils.dumpCallState(mPhone);
+
+ // No need to do any service state checks here (like for
+ // "emergency mode"), since in those states the SIM won't let
+ // us get incoming connections in the first place.
+
+ // TODO: Consider sending out a serialized broadcast Intent here
+ // (maybe "ACTION_NEW_INCOMING_CALL"), *before* starting the
+ // ringer and going to the in-call UI. The intent should contain
+ // the caller-id info for the current connection, and say whether
+ // it would be a "call waiting" call or a regular ringing call.
+ // If anybody consumed the broadcast, we'd bail out without
+ // ringing or bringing up the in-call UI.
+ //
+ // This would give 3rd party apps a chance to listen for (and
+ // intercept) new ringing connections. An app could reject the
+ // incoming call by consuming the broadcast and doing nothing, or
+ // it could "pick up" the call (without any action by the user!)
+ // via some future TelephonyManager API.
+ //
+ // See bug 1312336 for more details.
+ // We'd need to protect this with a new "intercept incoming calls"
+ // system permission.
+
+ // Obtain a partial wake lock to make sure the CPU doesn't go to
+ // sleep before we finish bringing up the InCallScreen.
+ // (This will be upgraded soon to a full wake lock; see
+ // showIncomingCall().)
+ if (VDBG) log("Holding wake lock on new incoming connection.");
+ mApplication.requestWakeState(PhoneGlobals.WakeState.PARTIAL);
+
+ // - don't ring for call waiting connections
+ // - do this before showing the incoming call panel
+ if (PhoneUtils.isRealIncomingCall(state)) {
+ startIncomingCallQuery(c);
+ } else {
+ if (VDBG) log("- starting call waiting tone...");
+ if (mCallWaitingTonePlayer == null) {
+ mCallWaitingTonePlayer = new InCallTonePlayer(InCallTonePlayer.TONE_CALL_WAITING);
+ mCallWaitingTonePlayer.start();
+ }
+ // in this case, just fall through like before, and call
+ // showIncomingCall().
+ if (DBG) log("- showing incoming call (this is a WAITING call)...");
+ showIncomingCall();
+ }
+
+ // Note we *don't* post a status bar notification here, since
+ // we're not necessarily ready to actually show the incoming call
+ // to the user. (For calls in the INCOMING state, at least, we
+ // still need to run a caller-id query, and we may not even ring
+ // at all if the "send directly to voicemail" flag is set.)
+ //
+ // Instead, we update the notification (and potentially launch the
+ // InCallScreen) from the showIncomingCall() method, which runs
+ // when the caller-id query completes or times out.
+
+ if (VDBG) log("- onNewRingingConnection() done.");
+ }
+
+ /**
+ * Determines whether or not we're allowed to present incoming calls to the
+ * user, based on the capabilities and/or current state of the device.
+ *
+ * If this method returns true, that means we should immediately reject the
+ * current incoming call, without even indicating to the user that an
+ * incoming call occurred.
+ *
+ * (We only reject incoming calls in a few cases, like during an OTASP call
+ * when we can't interrupt the user, or if the device hasn't completed the
+ * SetupWizard yet. We also don't allow incoming calls on non-voice-capable
+ * devices. But note that we *always* allow incoming calls while in ECM.)
+ *
+ * @return true if we're *not* allowed to present an incoming call to
+ * the user.
+ */
+ private boolean ignoreAllIncomingCalls(Phone phone) {
+ // Incoming calls are totally ignored on non-voice-capable devices.
+ if (!PhoneGlobals.sVoiceCapable) {
+ // ...but still log a warning, since we shouldn't have gotten this
+ // event in the first place! (Incoming calls *should* be blocked at
+ // the telephony layer on non-voice-capable capable devices.)
+ Log.w(LOG_TAG, "Got onNewRingingConnection() on non-voice-capable device! Ignoring...");
+ return true;
+ }
+
+ // In ECM (emergency callback mode), we ALWAYS allow incoming calls
+ // to get through to the user. (Note that ECM is applicable only to
+ // voice-capable CDMA devices).
+ if (PhoneUtils.isPhoneInEcm(phone)) {
+ if (DBG) log("Incoming call while in ECM: always allow...");
+ return false;
+ }
+
+ // Incoming calls are totally ignored if the device isn't provisioned yet.
+ boolean provisioned = Settings.Global.getInt(mApplication.getContentResolver(),
+ Settings.Global.DEVICE_PROVISIONED, 0) != 0;
+ if (!provisioned) {
+ Log.i(LOG_TAG, "Ignoring incoming call: not provisioned");
+ return true;
+ }
+
+ // Incoming calls are totally ignored if an OTASP call is active.
+ if (TelephonyCapabilities.supportsOtasp(phone)) {
+ boolean activateState = (mApplication.cdmaOtaScreenState.otaScreenState
+ == OtaUtils.CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION);
+ boolean dialogState = (mApplication.cdmaOtaScreenState.otaScreenState
+ == OtaUtils.CdmaOtaScreenState.OtaScreenState.OTA_STATUS_SUCCESS_FAILURE_DLG);
+ boolean spcState = mApplication.cdmaOtaProvisionData.inOtaSpcState;
+
+ if (spcState) {
+ Log.i(LOG_TAG, "Ignoring incoming call: OTA call is active");
+ return true;
+ } else if (activateState || dialogState) {
+ // We *are* allowed to receive incoming calls at this point.
+ // But clear out any residual OTASP UI first.
+ // TODO: It's an MVC violation to twiddle the OTA UI state here;
+ // we should instead provide a higher-level API via OtaUtils.
+ if (dialogState) mApplication.dismissOtaDialogs();
+ mApplication.clearOtaState();
+ mApplication.clearInCallScreenMode();
+ return false;
+ }
+ }
+
+ // Normal case: allow this call to be presented to the user.
+ return false;
+ }
+
+ /**
+ * Helper method to manage the start of incoming call queries
+ */
+ private void startIncomingCallQuery(Connection c) {
+ // TODO: cache the custom ringer object so that subsequent
+ // calls will not need to do this query work. We can keep
+ // the MRU ringtones in memory. We'll still need to hit
+ // the database to get the callerinfo to act as a key,
+ // but at least we can save the time required for the
+ // Media player setup. The only issue with this is that
+ // we may need to keep an eye on the resources the Media
+ // player uses to keep these ringtones around.
+
+ // make sure we're in a state where we can be ready to
+ // query a ringtone uri.
+ boolean shouldStartQuery = false;
+ synchronized (mCallerInfoQueryStateGuard) {
+ if (mCallerInfoQueryState == CALLERINFO_QUERY_READY) {
+ mCallerInfoQueryState = CALLERINFO_QUERYING;
+ shouldStartQuery = true;
+ }
+ }
+ if (shouldStartQuery) {
+ // Reset the ringtone to the default first.
+ mRinger.setCustomRingtoneUri(Settings.System.DEFAULT_RINGTONE_URI);
+
+ // query the callerinfo to try to get the ringer.
+ PhoneUtils.CallerInfoToken cit = PhoneUtils.startGetCallerInfo(
+ mApplication, c, this, this);
+
+ // if this has already been queried then just ring, otherwise
+ // we wait for the alloted time before ringing.
+ if (cit.isFinal) {
+ if (VDBG) log("- CallerInfo already up to date, using available data");
+ onQueryComplete(0, this, cit.currentInfo);
+ } else {
+ if (VDBG) log("- Starting query, posting timeout message.");
+
+ // Phone number (via getAddress()) is stored in the message to remember which
+ // number is actually used for the look up.
+ sendMessageDelayed(
+ Message.obtain(this, RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT, c.getAddress()),
+ RINGTONE_QUERY_WAIT_TIME);
+ }
+ // The call to showIncomingCall() will happen after the
+ // queries are complete (or time out).
+ } else {
+ // This should never happen; its the case where an incoming call
+ // arrives at the same time that the query is still being run,
+ // and before the timeout window has closed.
+ EventLog.writeEvent(EventLogTags.PHONE_UI_MULTIPLE_QUERY);
+
+ // In this case, just log the request and ring.
+ if (VDBG) log("RINGING... (request to ring arrived while query is running)");
+ mRinger.ring();
+
+ // in this case, just fall through like before, and call
+ // showIncomingCall().
+ if (DBG) log("- showing incoming call (couldn't start query)...");
+ showIncomingCall();
+ }
+ }
+
+ /**
+ * Performs the final steps of the onNewRingingConnection sequence:
+ * starts the ringer, and brings up the "incoming call" UI.
+ *
+ * Normally, this is called when the CallerInfo query completes (see
+ * onQueryComplete()). In this case, onQueryComplete() has already
+ * configured the Ringer object to use the custom ringtone (if there
+ * is one) for this caller. So we just tell the Ringer to start, and
+ * proceed to the InCallScreen.
+ *
+ * But this method can *also* be called if the
+ * RINGTONE_QUERY_WAIT_TIME timeout expires, which means that the
+ * CallerInfo query is taking too long. In that case, we log a
+ * warning but otherwise we behave the same as in the normal case.
+ * (We still tell the Ringer to start, but it's going to use the
+ * default ringtone.)
+ */
+ private void onCustomRingQueryComplete() {
+ boolean isQueryExecutionTimeExpired = false;
+ synchronized (mCallerInfoQueryStateGuard) {
+ if (mCallerInfoQueryState == CALLERINFO_QUERYING) {
+ mCallerInfoQueryState = CALLERINFO_QUERY_READY;
+ isQueryExecutionTimeExpired = true;
+ }
+ }
+ if (isQueryExecutionTimeExpired) {
+ // There may be a problem with the query here, since the
+ // default ringtone is playing instead of the custom one.
+ Log.w(LOG_TAG, "CallerInfo query took too long; falling back to default ringtone");
+ EventLog.writeEvent(EventLogTags.PHONE_UI_RINGER_QUERY_ELAPSED);
+ }
+
+ // Make sure we still have an incoming call!
+ //
+ // (It's possible for the incoming call to have been disconnected
+ // while we were running the query. In that case we better not
+ // start the ringer here, since there won't be any future
+ // DISCONNECT event to stop it!)
+ //
+ // Note we don't have to worry about the incoming call going away
+ // *after* this check but before we call mRinger.ring() below,
+ // since in that case we *will* still get a DISCONNECT message sent
+ // to our handler. (And we will correctly stop the ringer when we
+ // process that event.)
+ if (mCM.getState() != PhoneConstants.State.RINGING) {
+ Log.i(LOG_TAG, "onCustomRingQueryComplete: No incoming call! Bailing out...");
+ // Don't start the ringer *or* bring up the "incoming call" UI.
+ // Just bail out.
+ return;
+ }
+
+ // Ring, either with the queried ringtone or default one.
+ if (VDBG) log("RINGING... (onCustomRingQueryComplete)");
+ mRinger.ring();
+
+ // ...and display the incoming call to the user:
+ if (DBG) log("- showing incoming call (custom ring query complete)...");
+ showIncomingCall();
+ }
+
+ private void onUnknownConnectionAppeared(AsyncResult r) {
+ PhoneConstants.State state = mCM.getState();
+
+ if (state == PhoneConstants.State.OFFHOOK) {
+ // basically do onPhoneStateChanged + display the incoming call UI
+ onPhoneStateChanged(r);
+ if (DBG) log("- showing incoming call (unknown connection appeared)...");
+ showIncomingCall();
+ }
+ }
+
+ /**
+ * Informs the user about a new incoming call.
+ *
+ * In most cases this means "bring up the full-screen incoming call
+ * UI". However, if an immersive activity is running, the system
+ * NotificationManager will instead pop up a small notification window
+ * on top of the activity.
+ *
+ * Watch out: be sure to call this method only once per incoming call,
+ * or otherwise we may end up launching the InCallScreen multiple
+ * times (which can lead to slow responsiveness and/or visible
+ * glitches.)
+ *
+ * Note this method handles only the onscreen UI for incoming calls;
+ * the ringer and/or vibrator are started separately (see the various
+ * calls to Ringer.ring() in this class.)
+ *
+ * @see NotificationMgr#updateNotificationAndLaunchIncomingCallUi()
+ */
+ private void showIncomingCall() {
+ log("showIncomingCall()... phone state = " + mCM.getState());
+
+ // Before bringing up the "incoming call" UI, force any system
+ // dialogs (like "recent tasks" or the power dialog) to close first.
+ try {
+ ActivityManagerNative.getDefault().closeSystemDialogs("call");
+ } catch (RemoteException e) {
+ }
+
+ // Go directly to the in-call screen.
+ // (No need to do anything special if we're already on the in-call
+ // screen; it'll notice the phone state change and update itself.)
+ mApplication.requestWakeState(PhoneGlobals.WakeState.FULL);
+
+ // Post the "incoming call" notification *and* include the
+ // fullScreenIntent that'll launch the incoming-call UI.
+ // (This will usually take us straight to the incoming call
+ // screen, but if an immersive activity is running it'll just
+ // appear as a notification.)
+ if (DBG) log("- updating notification from showIncomingCall()...");
+ mApplication.notificationMgr.updateNotificationAndLaunchIncomingCallUi();
+ }
+
+ /**
+ * Updates the phone UI in response to phone state changes.
+ *
+ * Watch out: certain state changes are actually handled by their own
+ * specific methods:
+ * - see onNewRingingConnection() for new incoming calls
+ * - see onDisconnect() for calls being hung up or disconnected
+ */
+ private void onPhoneStateChanged(AsyncResult r) {
+ PhoneConstants.State state = mCM.getState();
+ if (VDBG) log("onPhoneStateChanged: state = " + state);
+
+ // Turn status bar notifications on or off depending upon the state
+ // of the phone. Notification Alerts (audible or vibrating) should
+ // be on if and only if the phone is IDLE.
+ mApplication.notificationMgr.statusBarHelper
+ .enableNotificationAlerts(state == PhoneConstants.State.IDLE);
+
+ Phone fgPhone = mCM.getFgPhone();
+ if (fgPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ if ((fgPhone.getForegroundCall().getState() == Call.State.ACTIVE)
+ && ((mPreviousCdmaCallState == Call.State.DIALING)
+ || (mPreviousCdmaCallState == Call.State.ALERTING))) {
+ if (mIsCdmaRedialCall) {
+ int toneToPlay = InCallTonePlayer.TONE_REDIAL;
+ new InCallTonePlayer(toneToPlay).start();
+ }
+ // Stop any signal info tone when call moves to ACTIVE state
+ stopSignalInfoTone();
+ }
+ mPreviousCdmaCallState = fgPhone.getForegroundCall().getState();
+ }
+
+ // Have the PhoneApp recompute its mShowBluetoothIndication
+ // flag based on the (new) telephony state.
+ // There's no need to force a UI update since we update the
+ // in-call notification ourselves (below), and the InCallScreen
+ // listens for phone state changes itself.
+ mApplication.updateBluetoothIndication(false);
+
+
+ // Update the phone state and other sensor/lock.
+ mApplication.updatePhoneState(state);
+
+ if (state == PhoneConstants.State.OFFHOOK) {
+ // stop call waiting tone if needed when answering
+ if (mCallWaitingTonePlayer != null) {
+ mCallWaitingTonePlayer.stopTone();
+ mCallWaitingTonePlayer = null;
+ }
+
+ if (VDBG) log("onPhoneStateChanged: OFF HOOK");
+ // make sure audio is in in-call mode now
+ PhoneUtils.setAudioMode(mCM);
+
+ // if the call screen is showing, let it handle the event,
+ // otherwise handle it here.
+ if (!mApplication.isShowingCallScreen()) {
+ mApplication.requestWakeState(PhoneGlobals.WakeState.SLEEP);
+ }
+
+ // Since we're now in-call, the Ringer should definitely *not*
+ // be ringing any more. (This is just a sanity-check; we
+ // already stopped the ringer explicitly back in
+ // PhoneUtils.answerCall(), before the call to phone.acceptCall().)
+ // TODO: Confirm that this call really *is* unnecessary, and if so,
+ // remove it!
+ if (DBG) log("stopRing()... (OFFHOOK state)");
+ mRinger.stopRing();
+
+ // Post a request to update the "in-call" status bar icon.
+ //
+ // We don't call NotificationMgr.updateInCallNotification()
+ // directly here, for two reasons:
+ // (1) a single phone state change might actually trigger multiple
+ // onPhoneStateChanged() callbacks, so this prevents redundant
+ // updates of the notification.
+ // (2) we suppress the status bar icon while the in-call UI is
+ // visible (see updateInCallNotification()). But when launching
+ // an outgoing call the phone actually goes OFFHOOK slightly
+ // *before* the InCallScreen comes up, so the delay here avoids a
+ // brief flicker of the icon at that point.
+
+ if (DBG) log("- posting UPDATE_IN_CALL_NOTIFICATION request...");
+ // Remove any previous requests in the queue
+ removeMessages(UPDATE_IN_CALL_NOTIFICATION);
+ final int IN_CALL_NOTIFICATION_UPDATE_DELAY = 1000; // msec
+ sendEmptyMessageDelayed(UPDATE_IN_CALL_NOTIFICATION,
+ IN_CALL_NOTIFICATION_UPDATE_DELAY);
+ }
+
+ if (fgPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ Connection c = fgPhone.getForegroundCall().getLatestConnection();
+ if ((c != null) && (PhoneNumberUtils.isLocalEmergencyNumber(c.getAddress(),
+ mApplication))) {
+ if (VDBG) log("onPhoneStateChanged: it is an emergency call.");
+ Call.State callState = fgPhone.getForegroundCall().getState();
+ if (mEmergencyTonePlayerVibrator == null) {
+ mEmergencyTonePlayerVibrator = new EmergencyTonePlayerVibrator();
+ }
+
+ if (callState == Call.State.DIALING || callState == Call.State.ALERTING) {
+ mIsEmergencyToneOn = Settings.Global.getInt(
+ mApplication.getContentResolver(),
+ Settings.Global.EMERGENCY_TONE, EMERGENCY_TONE_OFF);
+ if (mIsEmergencyToneOn != EMERGENCY_TONE_OFF &&
+ mCurrentEmergencyToneState == EMERGENCY_TONE_OFF) {
+ if (mEmergencyTonePlayerVibrator != null) {
+ mEmergencyTonePlayerVibrator.start();
+ }
+ }
+ } else if (callState == Call.State.ACTIVE) {
+ if (mCurrentEmergencyToneState != EMERGENCY_TONE_OFF) {
+ if (mEmergencyTonePlayerVibrator != null) {
+ mEmergencyTonePlayerVibrator.stop();
+ }
+ }
+ }
+ }
+ }
+
+ if ((fgPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM)
+ || (fgPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_SIP)) {
+ Call.State callState = mCM.getActiveFgCallState();
+ if (!callState.isDialing()) {
+ // If call get activated or disconnected before the ringback
+ // tone stops, we have to stop it to prevent disturbing.
+ if (mInCallRingbackTonePlayer != null) {
+ mInCallRingbackTonePlayer.stopTone();
+ mInCallRingbackTonePlayer = null;
+ }
+ }
+ }
+ }
+
+ void updateCallNotifierRegistrationsAfterRadioTechnologyChange() {
+ if (DBG) Log.d(LOG_TAG, "updateCallNotifierRegistrationsAfterRadioTechnologyChange...");
+
+ // Clear ringback tone player
+ mInCallRingbackTonePlayer = null;
+
+ // Clear call waiting tone player
+ mCallWaitingTonePlayer = null;
+
+ // Instantiate mSignalInfoToneGenerator
+ createSignalInfoToneGenerator();
+ }
+
+ /**
+ * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
+ * refreshes the CallCard data when it called. If called with this
+ * class itself, it is assumed that we have been waiting for the ringtone
+ * and direct to voicemail settings to update.
+ */
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+ if (cookie instanceof Long) {
+ if (VDBG) log("CallerInfo query complete, posting missed call notification");
+
+ mApplication.notificationMgr.notifyMissedCall(ci.name, ci.phoneNumber,
+ ci.phoneLabel, ci.cachedPhoto, ci.cachedPhotoIcon,
+ ((Long) cookie).longValue());
+ } else if (cookie instanceof CallNotifier) {
+ if (VDBG) log("CallerInfo query complete (for CallNotifier), "
+ + "updating state for incoming call..");
+
+ // get rid of the timeout messages
+ removeMessages(RINGER_CUSTOM_RINGTONE_QUERY_TIMEOUT);
+
+ boolean isQueryExecutionTimeOK = false;
+ synchronized (mCallerInfoQueryStateGuard) {
+ if (mCallerInfoQueryState == CALLERINFO_QUERYING) {
+ mCallerInfoQueryState = CALLERINFO_QUERY_READY;
+ isQueryExecutionTimeOK = true;
+ }
+ }
+ //if we're in the right state
+ if (isQueryExecutionTimeOK) {
+
+ // send directly to voicemail.
+ if (ci.shouldSendToVoicemail) {
+ if (DBG) log("send to voicemail flag detected. hanging up.");
+ PhoneUtils.hangupRingingCall(mCM.getFirstActiveRingingCall());
+ return;
+ }
+
+ // set the ringtone uri to prepare for the ring.
+ if (ci.contactRingtoneUri != null) {
+ if (DBG) log("custom ringtone found, setting up ringer.");
+ Ringer r = ((CallNotifier) cookie).mRinger;
+ r.setCustomRingtoneUri(ci.contactRingtoneUri);
+ }
+ // ring, and other post-ring actions.
+ onCustomRingQueryComplete();
+ }
+ }
+ }
+
+ /**
+ * Called when asynchronous CallerInfo query is taking too long (more than
+ * {@link #RINGTONE_QUERY_WAIT_TIME} msec), but we cannot wait any more.
+ *
+ * This looks up in-memory fallback cache and use it when available. If not, it just calls
+ * {@link #onCustomRingQueryComplete()} with default ringtone ("Send to voicemail" flag will
+ * be just ignored).
+ *
+ * @param number The phone number used for the async query. This method will take care of
+ * formatting or normalization of the number.
+ */
+ private void onCustomRingtoneQueryTimeout(String number) {
+ // First of all, this case itself should be rare enough, though we cannot avoid it in
+ // some situations (e.g. IPC is slow due to system overload, database is in sync, etc.)
+ Log.w(LOG_TAG, "CallerInfo query took too long; look up local fallback cache.");
+
+ // This method is intentionally verbose for now to detect possible bad side-effect for it.
+ // TODO: Remove the verbose log when it looks stable and reliable enough.
+
+ final CallerInfoCache.CacheEntry entry =
+ mApplication.callerInfoCache.getCacheEntry(number);
+ if (entry != null) {
+ if (entry.sendToVoicemail) {
+ log("send to voicemail flag detected (in fallback cache). hanging up.");
+ PhoneUtils.hangupRingingCall(mCM.getFirstActiveRingingCall());
+ return;
+ }
+
+ if (entry.customRingtone != null) {
+ log("custom ringtone found (in fallback cache), setting up ringer: "
+ + entry.customRingtone);
+ this.mRinger.setCustomRingtoneUri(Uri.parse(entry.customRingtone));
+ }
+ } else {
+ // In this case we call onCustomRingQueryComplete(), just
+ // like if the query had completed normally. (But we're
+ // going to get the default ringtone, since we never got
+ // the chance to call Ringer.setCustomRingtoneUri()).
+ log("Failed to find fallback cache. Use default ringer tone.");
+ }
+
+ onCustomRingQueryComplete();
+ }
+
+ private void onDisconnect(AsyncResult r) {
+ if (VDBG) log("onDisconnect()... CallManager state: " + mCM.getState());
+
+ mVoicePrivacyState = false;
+ Connection c = (Connection) r.result;
+ if (c != null) {
+ log("onDisconnect: cause = " + c.getDisconnectCause()
+ + ", incoming = " + c.isIncoming()
+ + ", date = " + c.getCreateTime());
+ } else {
+ Log.w(LOG_TAG, "onDisconnect: null connection");
+ }
+
+ int autoretrySetting = 0;
+ if ((c != null) && (c.getCall().getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA)) {
+ autoretrySetting = android.provider.Settings.Global.getInt(mApplication.
+ getContentResolver(),android.provider.Settings.Global.CALL_AUTO_RETRY, 0);
+ }
+
+ // Stop any signalInfo tone being played when a call gets ended
+ stopSignalInfoTone();
+
+ if ((c != null) && (c.getCall().getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA)) {
+ // Resetting the CdmaPhoneCallState members
+ mApplication.cdmaPhoneCallState.resetCdmaPhoneCallState();
+
+ // Remove Call waiting timers
+ removeMessages(CALLWAITING_CALLERINFO_DISPLAY_DONE);
+ removeMessages(CALLWAITING_ADDCALL_DISABLE_TIMEOUT);
+ }
+
+ // Stop the ringer if it was ringing (for an incoming call that
+ // either disconnected by itself, or was rejected by the user.)
+ //
+ // TODO: We technically *shouldn't* stop the ringer if the
+ // foreground or background call disconnects while an incoming call
+ // is still ringing, but that's a really rare corner case.
+ // It's safest to just unconditionally stop the ringer here.
+
+ // CDMA: For Call collision cases i.e. when the user makes an out going call
+ // and at the same time receives an Incoming Call, the Incoming Call is given
+ // higher preference. At this time framework sends a disconnect for the Out going
+ // call connection hence we should *not* be stopping the ringer being played for
+ // the Incoming Call
+ Call ringingCall = mCM.getFirstActiveRingingCall();
+ if (ringingCall.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ if (PhoneUtils.isRealIncomingCall(ringingCall.getState())) {
+ // Also we need to take off the "In Call" icon from the Notification
+ // area as the Out going Call never got connected
+ if (DBG) log("cancelCallInProgressNotifications()... (onDisconnect)");
+ mApplication.notificationMgr.cancelCallInProgressNotifications();
+ } else {
+ if (DBG) log("stopRing()... (onDisconnect)");
+ mRinger.stopRing();
+ }
+ } else { // GSM
+ if (DBG) log("stopRing()... (onDisconnect)");
+ mRinger.stopRing();
+ }
+
+ // stop call waiting tone if needed when disconnecting
+ if (mCallWaitingTonePlayer != null) {
+ mCallWaitingTonePlayer.stopTone();
+ mCallWaitingTonePlayer = null;
+ }
+
+ // If this is the end of an OTASP call, pass it on to the PhoneApp.
+ if (c != null && TelephonyCapabilities.supportsOtasp(c.getCall().getPhone())) {
+ final String number = c.getAddress();
+ if (c.getCall().getPhone().isOtaSpNumber(number)) {
+ if (DBG) log("onDisconnect: this was an OTASP call!");
+ mApplication.handleOtaspDisconnect();
+ }
+ }
+
+ // Check for the various tones we might need to play (thru the
+ // earpiece) after a call disconnects.
+ int toneToPlay = InCallTonePlayer.TONE_NONE;
+
+ // The "Busy" or "Congestion" tone is the highest priority:
+ if (c != null) {
+ Connection.DisconnectCause cause = c.getDisconnectCause();
+ if (cause == Connection.DisconnectCause.BUSY) {
+ if (DBG) log("- need to play BUSY tone!");
+ toneToPlay = InCallTonePlayer.TONE_BUSY;
+ } else if (cause == Connection.DisconnectCause.CONGESTION) {
+ if (DBG) log("- need to play CONGESTION tone!");
+ toneToPlay = InCallTonePlayer.TONE_CONGESTION;
+ } else if (((cause == Connection.DisconnectCause.NORMAL)
+ || (cause == Connection.DisconnectCause.LOCAL))
+ && (mApplication.isOtaCallInActiveState())) {
+ if (DBG) log("- need to play OTA_CALL_END tone!");
+ toneToPlay = InCallTonePlayer.TONE_OTA_CALL_END;
+ } else if (cause == Connection.DisconnectCause.CDMA_REORDER) {
+ if (DBG) log("- need to play CDMA_REORDER tone!");
+ toneToPlay = InCallTonePlayer.TONE_REORDER;
+ } else if (cause == Connection.DisconnectCause.CDMA_INTERCEPT) {
+ if (DBG) log("- need to play CDMA_INTERCEPT tone!");
+ toneToPlay = InCallTonePlayer.TONE_INTERCEPT;
+ } else if (cause == Connection.DisconnectCause.CDMA_DROP) {
+ if (DBG) log("- need to play CDMA_DROP tone!");
+ toneToPlay = InCallTonePlayer.TONE_CDMA_DROP;
+ } else if (cause == Connection.DisconnectCause.OUT_OF_SERVICE) {
+ if (DBG) log("- need to play OUT OF SERVICE tone!");
+ toneToPlay = InCallTonePlayer.TONE_OUT_OF_SERVICE;
+ } else if (cause == Connection.DisconnectCause.UNOBTAINABLE_NUMBER) {
+ if (DBG) log("- need to play TONE_UNOBTAINABLE_NUMBER tone!");
+ toneToPlay = InCallTonePlayer.TONE_UNOBTAINABLE_NUMBER;
+ } else if (cause == Connection.DisconnectCause.ERROR_UNSPECIFIED) {
+ if (DBG) log("- DisconnectCause is ERROR_UNSPECIFIED: play TONE_CALL_ENDED!");
+ toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
+ }
+ }
+
+ // If we don't need to play BUSY or CONGESTION, then play the
+ // "call ended" tone if this was a "regular disconnect" (i.e. a
+ // normal call where one end or the other hung up) *and* this
+ // disconnect event caused the phone to become idle. (In other
+ // words, we *don't* play the sound if one call hangs up but
+ // there's still an active call on the other line.)
+ // TODO: We may eventually want to disable this via a preference.
+ if ((toneToPlay == InCallTonePlayer.TONE_NONE)
+ && (mCM.getState() == PhoneConstants.State.IDLE)
+ && (c != null)) {
+ Connection.DisconnectCause cause = c.getDisconnectCause();
+ if ((cause == Connection.DisconnectCause.NORMAL) // remote hangup
+ || (cause == Connection.DisconnectCause.LOCAL)) { // local hangup
+ if (VDBG) log("- need to play CALL_ENDED tone!");
+ toneToPlay = InCallTonePlayer.TONE_CALL_ENDED;
+ mIsCdmaRedialCall = false;
+ }
+ }
+
+ // All phone calls are disconnected.
+ if (mCM.getState() == PhoneConstants.State.IDLE) {
+ // Don't reset the audio mode or bluetooth/speakerphone state
+ // if we still need to let the user hear a tone through the earpiece.
+ if (toneToPlay == InCallTonePlayer.TONE_NONE) {
+ resetAudioStateAfterDisconnect();
+ }
+
+ mApplication.notificationMgr.cancelCallInProgressNotifications();
+ }
+
+ if (c != null) {
+ mCallLogger.logCall(c);
+
+ final String number = c.getAddress();
+ final Phone phone = c.getCall().getPhone();
+ final boolean isEmergencyNumber =
+ PhoneNumberUtils.isLocalEmergencyNumber(number, mApplication);
+
+ if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ if ((isEmergencyNumber)
+ && (mCurrentEmergencyToneState != EMERGENCY_TONE_OFF)) {
+ if (mEmergencyTonePlayerVibrator != null) {
+ mEmergencyTonePlayerVibrator.stop();
+ }
+ }
+ }
+
+ final long date = c.getCreateTime();
+ final Connection.DisconnectCause cause = c.getDisconnectCause();
+ final boolean missedCall = c.isIncoming() &&
+ (cause == Connection.DisconnectCause.INCOMING_MISSED);
+ if (missedCall) {
+ // Show the "Missed call" notification.
+ // (Note we *don't* do this if this was an incoming call that
+ // the user deliberately rejected.)
+ showMissedCallNotification(c, date);
+ }
+
+ // Possibly play a "post-disconnect tone" thru the earpiece.
+ // We do this here, rather than from the InCallScreen
+ // activity, since we need to do this even if you're not in
+ // the Phone UI at the moment the connection ends.
+ if (toneToPlay != InCallTonePlayer.TONE_NONE) {
+ if (VDBG) log("- starting post-disconnect tone (" + toneToPlay + ")...");
+ new InCallTonePlayer(toneToPlay).start();
+
+ // TODO: alternatively, we could start an InCallTonePlayer
+ // here with an "unlimited" tone length,
+ // and manually stop it later when this connection truly goes
+ // away. (The real connection over the network was closed as soon
+ // as we got the BUSY message. But our telephony layer keeps the
+ // connection open for a few extra seconds so we can show the
+ // "busy" indication to the user. We could stop the busy tone
+ // when *that* connection's "disconnect" event comes in.)
+ }
+
+ if (((mPreviousCdmaCallState == Call.State.DIALING)
+ || (mPreviousCdmaCallState == Call.State.ALERTING))
+ && (!isEmergencyNumber)
+ && (cause != Connection.DisconnectCause.INCOMING_MISSED )
+ && (cause != Connection.DisconnectCause.NORMAL)
+ && (cause != Connection.DisconnectCause.LOCAL)
+ && (cause != Connection.DisconnectCause.INCOMING_REJECTED)) {
+ if (!mIsCdmaRedialCall) {
+ if (autoretrySetting == InCallScreen.AUTO_RETRY_ON) {
+ // TODO: (Moto): The contact reference data may need to be stored and use
+ // here when redialing a call. For now, pass in NULL as the URI parameter.
+ PhoneUtils.placeCall(mApplication, phone, number, null, false, null);
+ mIsCdmaRedialCall = true;
+ } else {
+ mIsCdmaRedialCall = false;
+ }
+ } else {
+ mIsCdmaRedialCall = false;
+ }
+ }
+ }
+ }
+
+ /**
+ * Resets the audio mode and speaker state when a call ends.
+ */
+ private void resetAudioStateAfterDisconnect() {
+ if (VDBG) log("resetAudioStateAfterDisconnect()...");
+
+ if (mBluetoothHeadset != null) {
+ mBluetoothHeadset.disconnectAudio();
+ }
+
+ // call turnOnSpeaker() with state=false and store=true even if speaker
+ // is already off to reset user requested speaker state.
+ PhoneUtils.turnOnSpeaker(mApplication, false, true);
+
+ PhoneUtils.setAudioMode(mCM);
+ }
+
+ private void onMwiChanged(boolean visible) {
+ if (VDBG) log("onMwiChanged(): " + visible);
+
+ // "Voicemail" is meaningless on non-voice-capable devices,
+ // so ignore MWI events.
+ if (!PhoneGlobals.sVoiceCapable) {
+ // ...but still log a warning, since we shouldn't have gotten this
+ // event in the first place!
+ // (PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR events
+ // *should* be blocked at the telephony layer on non-voice-capable
+ // capable devices.)
+ Log.w(LOG_TAG, "Got onMwiChanged() on non-voice-capable device! Ignoring...");
+ return;
+ }
+
+ mApplication.notificationMgr.updateMwi(visible);
+ }
+
+ /**
+ * Posts a delayed PHONE_MWI_CHANGED event, to schedule a "retry" for a
+ * failed NotificationMgr.updateMwi() call.
+ */
+ /* package */ void sendMwiChangedDelayed(long delayMillis) {
+ Message message = Message.obtain(this, PHONE_MWI_CHANGED);
+ sendMessageDelayed(message, delayMillis);
+ }
+
+ private void onCfiChanged(boolean visible) {
+ if (VDBG) log("onCfiChanged(): " + visible);
+ mApplication.notificationMgr.updateCfi(visible);
+ }
+
+ /**
+ * Indicates whether or not this ringer is ringing.
+ */
+ boolean isRinging() {
+ return mRinger.isRinging();
+ }
+
+ /**
+ * Stops the current ring, and tells the notifier that future
+ * ring requests should be ignored.
+ */
+ void silenceRinger() {
+ mSilentRingerRequested = true;
+ if (DBG) log("stopRing()... (silenceRinger)");
+ mRinger.stopRing();
+ }
+
+ /**
+ * Restarts the ringer after having previously silenced it.
+ *
+ * (This is a no-op if the ringer is actually still ringing, or if the
+ * incoming ringing call no longer exists.)
+ */
+ /* package */ void restartRinger() {
+ if (DBG) log("restartRinger()...");
+ // Already ringing or Silent requested; no need to restart.
+ if (isRinging() || mSilentRingerRequested) return;
+
+ final Call ringingCall = mCM.getFirstActiveRingingCall();
+ // Don't check ringingCall.isRinging() here, since that'll be true
+ // for the WAITING state also. We only allow the ringer for
+ // regular INCOMING calls.
+ if (DBG) log("- ringingCall state: " + ringingCall.getState());
+ if (ringingCall.getState() == Call.State.INCOMING) {
+ mRinger.ring();
+ }
+ }
+
+ /**
+ * Helper class to play tones through the earpiece (or speaker / BT)
+ * during a call, using the ToneGenerator.
+ *
+ * To use, just instantiate a new InCallTonePlayer
+ * (passing in the TONE_* constant for the tone you want)
+ * and start() it.
+ *
+ * When we're done playing the tone, if the phone is idle at that
+ * point, we'll reset the audio routing and speaker state.
+ * (That means that for tones that get played *after* a call
+ * disconnects, like "busy" or "congestion" or "call ended", you
+ * should NOT call resetAudioStateAfterDisconnect() yourself.
+ * Instead, just start the InCallTonePlayer, which will automatically
+ * defer the resetAudioStateAfterDisconnect() call until the tone
+ * finishes playing.)
+ */
+ private class InCallTonePlayer extends Thread {
+ private int mToneId;
+ private int mState;
+ // The possible tones we can play.
+ public static final int TONE_NONE = 0;
+ public static final int TONE_CALL_WAITING = 1;
+ public static final int TONE_BUSY = 2;
+ public static final int TONE_CONGESTION = 3;
+ public static final int TONE_CALL_ENDED = 4;
+ public static final int TONE_VOICE_PRIVACY = 5;
+ public static final int TONE_REORDER = 6;
+ public static final int TONE_INTERCEPT = 7;
+ public static final int TONE_CDMA_DROP = 8;
+ public static final int TONE_OUT_OF_SERVICE = 9;
+ public static final int TONE_REDIAL = 10;
+ public static final int TONE_OTA_CALL_END = 11;
+ public static final int TONE_RING_BACK = 12;
+ public static final int TONE_UNOBTAINABLE_NUMBER = 13;
+
+ // The tone volume relative to other sounds in the stream
+ static final int TONE_RELATIVE_VOLUME_EMERGENCY = 100;
+ static final int TONE_RELATIVE_VOLUME_HIPRI = 80;
+ static final int TONE_RELATIVE_VOLUME_LOPRI = 50;
+
+ // Buffer time (in msec) to add on to tone timeout value.
+ // Needed mainly when the timeout value for a tone is the
+ // exact duration of the tone itself.
+ static final int TONE_TIMEOUT_BUFFER = 20;
+
+ // The tone state
+ static final int TONE_OFF = 0;
+ static final int TONE_ON = 1;
+ static final int TONE_STOPPED = 2;
+
+ InCallTonePlayer(int toneId) {
+ super();
+ mToneId = toneId;
+ mState = TONE_OFF;
+ }
+
+ @Override
+ public void run() {
+ log("InCallTonePlayer.run(toneId = " + mToneId + ")...");
+
+ int toneType = 0; // passed to ToneGenerator.startTone()
+ int toneVolume; // passed to the ToneGenerator constructor
+ int toneLengthMillis;
+ int phoneType = mCM.getFgPhone().getPhoneType();
+
+ switch (mToneId) {
+ case TONE_CALL_WAITING:
+ toneType = ToneGenerator.TONE_SUP_CALL_WAITING;
+ toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
+ // Call waiting tone is stopped by stopTone() method
+ toneLengthMillis = Integer.MAX_VALUE - TONE_TIMEOUT_BUFFER;
+ break;
+ case TONE_BUSY:
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ toneType = ToneGenerator.TONE_CDMA_NETWORK_BUSY_ONE_SHOT;
+ toneVolume = TONE_RELATIVE_VOLUME_LOPRI;
+ toneLengthMillis = 1000;
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ toneType = ToneGenerator.TONE_SUP_BUSY;
+ toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 4000;
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ break;
+ case TONE_CONGESTION:
+ toneType = ToneGenerator.TONE_SUP_CONGESTION;
+ toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 4000;
+ break;
+
+ case TONE_CALL_ENDED:
+ toneType = ToneGenerator.TONE_PROP_PROMPT;
+ toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 200;
+ break;
+ case TONE_OTA_CALL_END:
+ if (mApplication.cdmaOtaConfigData.otaPlaySuccessFailureTone ==
+ OtaUtils.OTA_PLAY_SUCCESS_FAILURE_TONE_ON) {
+ toneType = ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD;
+ toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 750;
+ } else {
+ toneType = ToneGenerator.TONE_PROP_PROMPT;
+ toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 200;
+ }
+ break;
+ case TONE_VOICE_PRIVACY:
+ toneType = ToneGenerator.TONE_CDMA_ALERT_NETWORK_LITE;
+ toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 5000;
+ break;
+ case TONE_REORDER:
+ toneType = ToneGenerator.TONE_CDMA_REORDER;
+ toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 4000;
+ break;
+ case TONE_INTERCEPT:
+ toneType = ToneGenerator.TONE_CDMA_ABBR_INTERCEPT;
+ toneVolume = TONE_RELATIVE_VOLUME_LOPRI;
+ toneLengthMillis = 500;
+ break;
+ case TONE_CDMA_DROP:
+ case TONE_OUT_OF_SERVICE:
+ toneType = ToneGenerator.TONE_CDMA_CALLDROP_LITE;
+ toneVolume = TONE_RELATIVE_VOLUME_LOPRI;
+ toneLengthMillis = 375;
+ break;
+ case TONE_REDIAL:
+ toneType = ToneGenerator.TONE_CDMA_ALERT_AUTOREDIAL_LITE;
+ toneVolume = TONE_RELATIVE_VOLUME_LOPRI;
+ toneLengthMillis = 5000;
+ break;
+ case TONE_RING_BACK:
+ toneType = ToneGenerator.TONE_SUP_RINGTONE;
+ toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
+ // Call ring back tone is stopped by stopTone() method
+ toneLengthMillis = Integer.MAX_VALUE - TONE_TIMEOUT_BUFFER;
+ break;
+ case TONE_UNOBTAINABLE_NUMBER:
+ toneType = ToneGenerator.TONE_SUP_ERROR;
+ toneVolume = TONE_RELATIVE_VOLUME_HIPRI;
+ toneLengthMillis = 4000;
+ break;
+ default:
+ throw new IllegalArgumentException("Bad toneId: " + mToneId);
+ }
+
+ // If the mToneGenerator creation fails, just continue without it. It is
+ // a local audio signal, and is not as important.
+ ToneGenerator toneGenerator;
+ try {
+ int stream;
+ if (mBluetoothHeadset != null) {
+ stream = mBluetoothHeadset.isAudioOn() ? AudioManager.STREAM_BLUETOOTH_SCO:
+ AudioManager.STREAM_VOICE_CALL;
+ } else {
+ stream = AudioManager.STREAM_VOICE_CALL;
+ }
+ toneGenerator = new ToneGenerator(stream, toneVolume);
+ // if (DBG) log("- created toneGenerator: " + toneGenerator);
+ } catch (RuntimeException e) {
+ Log.w(LOG_TAG,
+ "InCallTonePlayer: Exception caught while creating ToneGenerator: " + e);
+ toneGenerator = null;
+ }
+
+ // Using the ToneGenerator (with the CALL_WAITING / BUSY /
+ // CONGESTION tones at least), the ToneGenerator itself knows
+ // the right pattern of tones to play; we do NOT need to
+ // manually start/stop each individual tone, or manually
+ // insert the correct delay between tones. (We just start it
+ // and let it run for however long we want the tone pattern to
+ // continue.)
+ //
+ // TODO: When we stop the ToneGenerator in the middle of a
+ // "tone pattern", it sounds bad if we cut if off while the
+ // tone is actually playing. Consider adding API to the
+ // ToneGenerator to say "stop at the next silent part of the
+ // pattern", or simply "play the pattern N times and then
+ // stop."
+ boolean needToStopTone = true;
+ boolean okToPlayTone = false;
+
+ if (toneGenerator != null) {
+ int ringerMode = mAudioManager.getRingerMode();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ if (toneType == ToneGenerator.TONE_CDMA_ALERT_CALL_GUARD) {
+ if ((ringerMode != AudioManager.RINGER_MODE_SILENT) &&
+ (ringerMode != AudioManager.RINGER_MODE_VIBRATE)) {
+ if (DBG) log("- InCallTonePlayer: start playing call tone=" + toneType);
+ okToPlayTone = true;
+ needToStopTone = false;
+ }
+ } else if ((toneType == ToneGenerator.TONE_CDMA_NETWORK_BUSY_ONE_SHOT) ||
+ (toneType == ToneGenerator.TONE_CDMA_REORDER) ||
+ (toneType == ToneGenerator.TONE_CDMA_ABBR_REORDER) ||
+ (toneType == ToneGenerator.TONE_CDMA_ABBR_INTERCEPT) ||
+ (toneType == ToneGenerator.TONE_CDMA_CALLDROP_LITE)) {
+ if (ringerMode != AudioManager.RINGER_MODE_SILENT) {
+ if (DBG) log("InCallTonePlayer:playing call fail tone:" + toneType);
+ okToPlayTone = true;
+ needToStopTone = false;
+ }
+ } else if ((toneType == ToneGenerator.TONE_CDMA_ALERT_AUTOREDIAL_LITE) ||
+ (toneType == ToneGenerator.TONE_CDMA_ALERT_NETWORK_LITE)) {
+ if ((ringerMode != AudioManager.RINGER_MODE_SILENT) &&
+ (ringerMode != AudioManager.RINGER_MODE_VIBRATE)) {
+ if (DBG) log("InCallTonePlayer:playing tone for toneType=" + toneType);
+ okToPlayTone = true;
+ needToStopTone = false;
+ }
+ } else { // For the rest of the tones, always OK to play.
+ okToPlayTone = true;
+ }
+ } else { // Not "CDMA"
+ okToPlayTone = true;
+ }
+
+ synchronized (this) {
+ if (okToPlayTone && mState != TONE_STOPPED) {
+ mState = TONE_ON;
+ toneGenerator.startTone(toneType);
+ try {
+ wait(toneLengthMillis + TONE_TIMEOUT_BUFFER);
+ } catch (InterruptedException e) {
+ Log.w(LOG_TAG,
+ "InCallTonePlayer stopped: " + e);
+ }
+ if (needToStopTone) {
+ toneGenerator.stopTone();
+ }
+ }
+ // if (DBG) log("- InCallTonePlayer: done playing.");
+ toneGenerator.release();
+ mState = TONE_OFF;
+ }
+ }
+
+ // Finally, do the same cleanup we otherwise would have done
+ // in onDisconnect().
+ //
+ // (But watch out: do NOT do this if the phone is in use,
+ // since some of our tones get played *during* a call (like
+ // CALL_WAITING) and we definitely *don't*
+ // want to reset the audio mode / speaker / bluetooth after
+ // playing those!
+ // This call is really here for use with tones that get played
+ // *after* a call disconnects, like "busy" or "congestion" or
+ // "call ended", where the phone has already become idle but
+ // we need to defer the resetAudioStateAfterDisconnect() call
+ // till the tone finishes playing.)
+ if (mCM.getState() == PhoneConstants.State.IDLE) {
+ resetAudioStateAfterDisconnect();
+ }
+ }
+
+ public void stopTone() {
+ synchronized (this) {
+ if (mState == TONE_ON) {
+ notify();
+ }
+ mState = TONE_STOPPED;
+ }
+ }
+ }
+
+ /**
+ * Displays a notification when the phone receives a DisplayInfo record.
+ */
+ private void onDisplayInfo(AsyncResult r) {
+ // Extract the DisplayInfo String from the message
+ CdmaDisplayInfoRec displayInfoRec = (CdmaDisplayInfoRec)(r.result);
+
+ if (displayInfoRec != null) {
+ String displayInfo = displayInfoRec.alpha;
+ if (DBG) log("onDisplayInfo: displayInfo=" + displayInfo);
+ CdmaDisplayInfo.displayInfoRecord(mApplication, displayInfo);
+
+ // start a 2 second timer
+ sendEmptyMessageDelayed(DISPLAYINFO_NOTIFICATION_DONE,
+ DISPLAYINFO_NOTIFICATION_TIME);
+ }
+ }
+
+ /**
+ * Helper class to play SignalInfo tones using the ToneGenerator.
+ *
+ * To use, just instantiate a new SignalInfoTonePlayer
+ * (passing in the ToneID constant for the tone you want)
+ * and start() it.
+ */
+ private class SignalInfoTonePlayer extends Thread {
+ private int mToneId;
+
+ SignalInfoTonePlayer(int toneId) {
+ super();
+ mToneId = toneId;
+ }
+
+ @Override
+ public void run() {
+ log("SignalInfoTonePlayer.run(toneId = " + mToneId + ")...");
+
+ if (mSignalInfoToneGenerator != null) {
+ //First stop any ongoing SignalInfo tone
+ mSignalInfoToneGenerator.stopTone();
+
+ //Start playing the new tone if its a valid tone
+ mSignalInfoToneGenerator.startTone(mToneId);
+ }
+ }
+ }
+
+ /**
+ * Plays a tone when the phone receives a SignalInfo record.
+ */
+ private void onSignalInfo(AsyncResult r) {
+ // Signal Info are totally ignored on non-voice-capable devices.
+ if (!PhoneGlobals.sVoiceCapable) {
+ Log.w(LOG_TAG, "Got onSignalInfo() on non-voice-capable device! Ignoring...");
+ return;
+ }
+
+ if (PhoneUtils.isRealIncomingCall(mCM.getFirstActiveRingingCall().getState())) {
+ // Do not start any new SignalInfo tone when Call state is INCOMING
+ // and stop any previous SignalInfo tone which is being played
+ stopSignalInfoTone();
+ } else {
+ // Extract the SignalInfo String from the message
+ CdmaSignalInfoRec signalInfoRec = (CdmaSignalInfoRec)(r.result);
+ // Only proceed if a Signal info is present.
+ if (signalInfoRec != null) {
+ boolean isPresent = signalInfoRec.isPresent;
+ if (DBG) log("onSignalInfo: isPresent=" + isPresent);
+ if (isPresent) {// if tone is valid
+ int uSignalType = signalInfoRec.signalType;
+ int uAlertPitch = signalInfoRec.alertPitch;
+ int uSignal = signalInfoRec.signal;
+
+ if (DBG) log("onSignalInfo: uSignalType=" + uSignalType + ", uAlertPitch=" +
+ uAlertPitch + ", uSignal=" + uSignal);
+ //Map the Signal to a ToneGenerator ToneID only if Signal info is present
+ int toneID = SignalToneUtil.getAudioToneFromSignalInfo
+ (uSignalType, uAlertPitch, uSignal);
+
+ //Create the SignalInfo tone player and pass the ToneID
+ new SignalInfoTonePlayer(toneID).start();
+ }
+ }
+ }
+ }
+
+ /**
+ * Stops a SignalInfo tone in the following condition
+ * 1 - On receiving a New Ringing Call
+ * 2 - On disconnecting a call
+ * 3 - On answering a Call Waiting Call
+ */
+ /* package */ void stopSignalInfoTone() {
+ if (DBG) log("stopSignalInfoTone: Stopping SignalInfo tone player");
+ new SignalInfoTonePlayer(ToneGenerator.TONE_CDMA_SIGNAL_OFF).start();
+ }
+
+ /**
+ * Plays a Call waiting tone if it is present in the second incoming call.
+ */
+ private void onCdmaCallWaiting(AsyncResult r) {
+ // Remove any previous Call waiting timers in the queue
+ removeMessages(CALLWAITING_CALLERINFO_DISPLAY_DONE);
+ removeMessages(CALLWAITING_ADDCALL_DISABLE_TIMEOUT);
+
+ // Set the Phone Call State to SINGLE_ACTIVE as there is only one connection
+ // else we would not have received Call waiting
+ mApplication.cdmaPhoneCallState.setCurrentCallState(
+ CdmaPhoneCallState.PhoneCallState.SINGLE_ACTIVE);
+
+ // Display the incoming call to the user if the InCallScreen isn't
+ // already in the foreground.
+ if (!mApplication.isShowingCallScreen()) {
+ if (DBG) log("- showing incoming call (CDMA call waiting)...");
+ showIncomingCall();
+ }
+
+ // Start timer for CW display
+ mCallWaitingTimeOut = false;
+ sendEmptyMessageDelayed(CALLWAITING_CALLERINFO_DISPLAY_DONE,
+ CALLWAITING_CALLERINFO_DISPLAY_TIME);
+
+ // Set the mAddCallMenuStateAfterCW state to false
+ mApplication.cdmaPhoneCallState.setAddCallMenuStateAfterCallWaiting(false);
+
+ // Start the timer for disabling "Add Call" menu option
+ sendEmptyMessageDelayed(CALLWAITING_ADDCALL_DISABLE_TIMEOUT,
+ CALLWAITING_ADDCALL_DISABLE_TIME);
+
+ // Extract the Call waiting information
+ CdmaCallWaitingNotification infoCW = (CdmaCallWaitingNotification) r.result;
+ int isPresent = infoCW.isPresent;
+ if (DBG) log("onCdmaCallWaiting: isPresent=" + isPresent);
+ if (isPresent == 1 ) {//'1' if tone is valid
+ int uSignalType = infoCW.signalType;
+ int uAlertPitch = infoCW.alertPitch;
+ int uSignal = infoCW.signal;
+ if (DBG) log("onCdmaCallWaiting: uSignalType=" + uSignalType + ", uAlertPitch="
+ + uAlertPitch + ", uSignal=" + uSignal);
+ //Map the Signal to a ToneGenerator ToneID only if Signal info is present
+ int toneID =
+ SignalToneUtil.getAudioToneFromSignalInfo(uSignalType, uAlertPitch, uSignal);
+
+ //Create the SignalInfo tone player and pass the ToneID
+ new SignalInfoTonePlayer(toneID).start();
+ }
+ }
+
+ /**
+ * Posts a event causing us to clean up after rejecting (or timing-out) a
+ * CDMA call-waiting call.
+ *
+ * This method is safe to call from any thread.
+ * @see #onCdmaCallWaitingReject()
+ */
+ /* package */ void sendCdmaCallWaitingReject() {
+ sendEmptyMessage(CDMA_CALL_WAITING_REJECT);
+ }
+
+ /**
+ * Performs Call logging based on Timeout or Ignore Call Waiting Call for CDMA,
+ * and finally calls Hangup on the Call Waiting connection.
+ *
+ * This method should be called only from the UI thread.
+ * @see #sendCdmaCallWaitingReject()
+ */
+ private void onCdmaCallWaitingReject() {
+ final Call ringingCall = mCM.getFirstActiveRingingCall();
+
+ // Call waiting timeout scenario
+ if (ringingCall.getState() == Call.State.WAITING) {
+ // Code for perform Call logging and missed call notification
+ Connection c = ringingCall.getLatestConnection();
+
+ if (c != null) {
+ final int callLogType = mCallWaitingTimeOut ?
+ Calls.MISSED_TYPE : Calls.INCOMING_TYPE;
+
+ // TODO: This callLogType override is not ideal. Connection should be astracted away
+ // at a telephony-phone layer that can understand and edit the callTypes within
+ // the abstraction for CDMA devices.
+ mCallLogger.logCall(c, callLogType);
+
+ final long date = c.getCreateTime();
+ if (callLogType == Calls.MISSED_TYPE) {
+ // Add missed call notification
+ showMissedCallNotification(c, date);
+ } else {
+ // Remove Call waiting 20 second display timer in the queue
+ removeMessages(CALLWAITING_CALLERINFO_DISPLAY_DONE);
+ }
+
+ // Hangup the RingingCall connection for CW
+ PhoneUtils.hangup(c);
+ }
+
+ //Reset the mCallWaitingTimeOut boolean
+ mCallWaitingTimeOut = false;
+ }
+ }
+
+ /**
+ * Return the private variable mPreviousCdmaCallState.
+ */
+ /* package */ Call.State getPreviousCdmaCallState() {
+ return mPreviousCdmaCallState;
+ }
+
+ /**
+ * Return the private variable mVoicePrivacyState.
+ */
+ /* package */ boolean getVoicePrivacyState() {
+ return mVoicePrivacyState;
+ }
+
+ /**
+ * Return the private variable mIsCdmaRedialCall.
+ */
+ /* package */ boolean getIsCdmaRedialCall() {
+ return mIsCdmaRedialCall;
+ }
+
+ /**
+ * Helper function used to show a missed call notification.
+ */
+ private void showMissedCallNotification(Connection c, final long date) {
+ PhoneUtils.CallerInfoToken info =
+ PhoneUtils.startGetCallerInfo(mApplication, c, this, Long.valueOf(date));
+ if (info != null) {
+ // at this point, we've requested to start a query, but it makes no
+ // sense to log this missed call until the query comes back.
+ if (VDBG) log("showMissedCallNotification: Querying for CallerInfo on missed call...");
+ if (info.isFinal) {
+ // it seems that the query we have actually is up to date.
+ // send the notification then.
+ CallerInfo ci = info.currentInfo;
+
+ // Check number presentation value; if we have a non-allowed presentation,
+ // then display an appropriate presentation string instead as the missed
+ // call.
+ String name = ci.name;
+ String number = ci.phoneNumber;
+ if (ci.numberPresentation == PhoneConstants.PRESENTATION_RESTRICTED) {
+ name = mApplication.getString(R.string.private_num);
+ } else if (ci.numberPresentation != PhoneConstants.PRESENTATION_ALLOWED) {
+ name = mApplication.getString(R.string.unknown);
+ } else {
+ number = PhoneUtils.modifyForSpecialCnapCases(mApplication,
+ ci, number, ci.numberPresentation);
+ }
+ mApplication.notificationMgr.notifyMissedCall(name, number,
+ ci.phoneLabel, ci.cachedPhoto, ci.cachedPhotoIcon, date);
+ }
+ } else {
+ // getCallerInfo() can return null in rare cases, like if we weren't
+ // able to get a valid phone number out of the specified Connection.
+ Log.w(LOG_TAG, "showMissedCallNotification: got null CallerInfo for Connection " + c);
+ }
+ }
+
+ /**
+ * Inner class to handle emergency call tone and vibrator
+ */
+ private class EmergencyTonePlayerVibrator {
+ private final int EMG_VIBRATE_LENGTH = 1000; // ms.
+ private final int EMG_VIBRATE_PAUSE = 1000; // ms.
+ private final long[] mVibratePattern =
+ new long[] { EMG_VIBRATE_LENGTH, EMG_VIBRATE_PAUSE };
+
+ private ToneGenerator mToneGenerator;
+ // We don't rely on getSystemService(Context.VIBRATOR_SERVICE) to make sure this vibrator
+ // object will be isolated from others.
+ private Vibrator mEmgVibrator = new SystemVibrator();
+ private int mInCallVolume;
+
+ /**
+ * constructor
+ */
+ public EmergencyTonePlayerVibrator() {
+ }
+
+ /**
+ * Start the emergency tone or vibrator.
+ */
+ private void start() {
+ if (VDBG) log("call startEmergencyToneOrVibrate.");
+ int ringerMode = mAudioManager.getRingerMode();
+
+ if ((mIsEmergencyToneOn == EMERGENCY_TONE_ALERT) &&
+ (ringerMode == AudioManager.RINGER_MODE_NORMAL)) {
+ log("EmergencyTonePlayerVibrator.start(): emergency tone...");
+ mToneGenerator = new ToneGenerator (AudioManager.STREAM_VOICE_CALL,
+ InCallTonePlayer.TONE_RELATIVE_VOLUME_EMERGENCY);
+ if (mToneGenerator != null) {
+ mInCallVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_VOICE_CALL);
+ mAudioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL,
+ mAudioManager.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL),
+ 0);
+ mToneGenerator.startTone(ToneGenerator.TONE_CDMA_EMERGENCY_RINGBACK);
+ mCurrentEmergencyToneState = EMERGENCY_TONE_ALERT;
+ }
+ } else if (mIsEmergencyToneOn == EMERGENCY_TONE_VIBRATE) {
+ log("EmergencyTonePlayerVibrator.start(): emergency vibrate...");
+ if (mEmgVibrator != null) {
+ mEmgVibrator.vibrate(mVibratePattern, 0);
+ mCurrentEmergencyToneState = EMERGENCY_TONE_VIBRATE;
+ }
+ }
+ }
+
+ /**
+ * If the emergency tone is active, stop the tone or vibrator accordingly.
+ */
+ private void stop() {
+ if (VDBG) log("call stopEmergencyToneOrVibrate.");
+
+ if ((mCurrentEmergencyToneState == EMERGENCY_TONE_ALERT)
+ && (mToneGenerator != null)) {
+ mToneGenerator.stopTone();
+ mToneGenerator.release();
+ mAudioManager.setStreamVolume(AudioManager.STREAM_VOICE_CALL,
+ mInCallVolume,
+ 0);
+ } else if ((mCurrentEmergencyToneState == EMERGENCY_TONE_VIBRATE)
+ && (mEmgVibrator != null)) {
+ mEmgVibrator.cancel();
+ }
+ mCurrentEmergencyToneState = EMERGENCY_TONE_OFF;
+ }
+ }
+
+ private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
+ new BluetoothProfile.ServiceListener() {
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ mBluetoothHeadset = (BluetoothHeadset) proxy;
+ if (VDBG) log("- Got BluetoothHeadset: " + mBluetoothHeadset);
+ }
+
+ public void onServiceDisconnected(int profile) {
+ mBluetoothHeadset = null;
+ }
+ };
+
+ private void onRingbackTone(AsyncResult r) {
+ boolean playTone = (Boolean)(r.result);
+
+ if (playTone == true) {
+ // Only play when foreground call is in DIALING or ALERTING.
+ // to prevent a late coming playtone after ALERTING.
+ // Don't play ringback tone if it is in play, otherwise it will cut
+ // the current tone and replay it
+ if (mCM.getActiveFgCallState().isDialing() &&
+ mInCallRingbackTonePlayer == null) {
+ mInCallRingbackTonePlayer = new InCallTonePlayer(InCallTonePlayer.TONE_RING_BACK);
+ mInCallRingbackTonePlayer.start();
+ }
+ } else {
+ if (mInCallRingbackTonePlayer != null) {
+ mInCallRingbackTonePlayer.stopTone();
+ mInCallRingbackTonePlayer = null;
+ }
+ }
+ }
+
+ /**
+ * Toggle mute and unmute requests while keeping the same mute state
+ */
+ private void onResendMute() {
+ boolean muteState = PhoneUtils.getMute();
+ PhoneUtils.setMute(!muteState);
+ PhoneUtils.setMute(muteState);
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/CallStateMonitor.java b/src/com/android/phone/CallStateMonitor.java
new file mode 100644
index 0000000..6a03e22
--- /dev/null
+++ b/src/com/android/phone/CallStateMonitor.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2013 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.phone;
+
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.telephony.CallManager;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+
+/**
+ * Dedicated Call state monitoring class. This class communicates directly with
+ * the call manager to listen for call state events and notifies registered
+ * handlers.
+ * It works as an inverse multiplexor for all classes wanted Call State updates
+ * so that there exists only one channel to the telephony layer.
+ *
+ * TODO: Add manual phone state checks (getState(), etc.).
+ */
+class CallStateMonitor extends Handler {
+ private static final String LOG_TAG = CallStateMonitor.class.getSimpleName();
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+
+ // Events from the Phone object:
+ public static final int PHONE_STATE_CHANGED = 1;
+ public static final int PHONE_NEW_RINGING_CONNECTION = 2;
+ public static final int PHONE_DISCONNECT = 3;
+ public static final int PHONE_UNKNOWN_CONNECTION_APPEARED = 4;
+ public static final int PHONE_INCOMING_RING = 5;
+ public static final int PHONE_STATE_DISPLAYINFO = 6;
+ public static final int PHONE_STATE_SIGNALINFO = 7;
+ public static final int PHONE_CDMA_CALL_WAITING = 8;
+ public static final int PHONE_ENHANCED_VP_ON = 9;
+ public static final int PHONE_ENHANCED_VP_OFF = 10;
+ public static final int PHONE_RINGBACK_TONE = 11;
+ public static final int PHONE_RESEND_MUTE = 12;
+
+ // Other events from call manager
+ public static final int EVENT_OTA_PROVISION_CHANGE = 20;
+
+ private CallManager callManager;
+ private ArrayList<Handler> registeredHandlers;
+
+ // Events generated internally:
+ public CallStateMonitor(CallManager callManager) {
+ this.callManager = callManager;
+ registeredHandlers = new ArrayList<Handler>();
+
+ registerForNotifications();
+ }
+
+ /**
+ * Register for call state notifications with the CallManager.
+ */
+ private void registerForNotifications() {
+ callManager.registerForNewRingingConnection(this, PHONE_NEW_RINGING_CONNECTION, null);
+ callManager.registerForPreciseCallStateChanged(this, PHONE_STATE_CHANGED, null);
+ callManager.registerForDisconnect(this, PHONE_DISCONNECT, null);
+ callManager.registerForUnknownConnection(this, PHONE_UNKNOWN_CONNECTION_APPEARED, null);
+ callManager.registerForIncomingRing(this, PHONE_INCOMING_RING, null);
+ callManager.registerForCdmaOtaStatusChange(this, EVENT_OTA_PROVISION_CHANGE, null);
+ callManager.registerForCallWaiting(this, PHONE_CDMA_CALL_WAITING, null);
+ callManager.registerForDisplayInfo(this, PHONE_STATE_DISPLAYINFO, null);
+ callManager.registerForSignalInfo(this, PHONE_STATE_SIGNALINFO, null);
+ callManager.registerForInCallVoicePrivacyOn(this, PHONE_ENHANCED_VP_ON, null);
+ callManager.registerForInCallVoicePrivacyOff(this, PHONE_ENHANCED_VP_OFF, null);
+ callManager.registerForRingbackTone(this, PHONE_RINGBACK_TONE, null);
+ callManager.registerForResendIncallMute(this, PHONE_RESEND_MUTE, null);
+ }
+
+ public void addListener(Handler handler) {
+ if (handler != null && !registeredHandlers.contains(handler)) {
+ registeredHandlers.add(handler);
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (DBG) {
+ Log.d(LOG_TAG, "handleMessage(" + msg.what + ")");
+ }
+
+ for (Handler handler : registeredHandlers) {
+ handler.handleMessage(msg);
+ }
+ }
+
+ /**
+ * When radio technology changes, we need to to reregister for all the events which are
+ * all tied to the old radio.
+ */
+ public void updateAfterRadioTechnologyChange() {
+ if (DBG) Log.d(LOG_TAG, "updateCallNotifierRegistrationsAfterRadioTechnologyChange...");
+
+ // Unregister all events from the old obsolete phone
+ callManager.unregisterForNewRingingConnection(this);
+ callManager.unregisterForPreciseCallStateChanged(this);
+ callManager.unregisterForDisconnect(this);
+ callManager.unregisterForUnknownConnection(this);
+ callManager.unregisterForIncomingRing(this);
+ callManager.unregisterForCallWaiting(this);
+ callManager.unregisterForDisplayInfo(this);
+ callManager.unregisterForSignalInfo(this);
+ callManager.unregisterForCdmaOtaStatusChange(this);
+ callManager.unregisterForRingbackTone(this);
+ callManager.unregisterForResendIncallMute(this);
+ callManager.unregisterForInCallVoicePrivacyOn(this);
+ callManager.unregisterForInCallVoicePrivacyOff(this);
+
+ registerForNotifications();
+ }
+
+}
diff --git a/src/com/android/phone/CallTime.java b/src/com/android/phone/CallTime.java
new file mode 100644
index 0000000..92c7972
--- /dev/null
+++ b/src/com/android/phone/CallTime.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.content.Context;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.SystemClock;
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.Connection;
+import android.util.Log;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Helper class used to keep track of various "elapsed time" indications
+ * in the Phone app, and also to start and stop tracing / profiling.
+ */
+public class CallTime extends Handler {
+ private static final String LOG_TAG = "PHONE/CallTime";
+ private static final boolean DBG = false;
+ /* package */ static final boolean PROFILE = true;
+
+ private static final int PROFILE_STATE_NONE = 0;
+ private static final int PROFILE_STATE_READY = 1;
+ private static final int PROFILE_STATE_RUNNING = 2;
+
+ private static int sProfileState = PROFILE_STATE_NONE;
+
+ private Call mCall;
+ private long mLastReportedTime;
+ private boolean mTimerRunning;
+ private long mInterval;
+ private PeriodicTimerCallback mTimerCallback;
+ private OnTickListener mListener;
+
+ interface OnTickListener {
+ void onTickForCallTimeElapsed(long timeElapsed);
+ }
+
+ public CallTime(OnTickListener listener) {
+ mListener = listener;
+ mTimerCallback = new PeriodicTimerCallback();
+ }
+
+ /**
+ * Sets the call timer to "active call" mode, where the timer will
+ * periodically update the UI to show how long the specified call
+ * has been active.
+ *
+ * After calling this you should also call reset() and
+ * periodicUpdateTimer() to get the timer started.
+ */
+ /* package */ void setActiveCallMode(Call call) {
+ if (DBG) log("setActiveCallMode(" + call + ")...");
+ mCall = call;
+
+ // How frequently should we update the UI?
+ mInterval = 1000; // once per second
+ }
+
+ /* package */ void reset() {
+ if (DBG) log("reset()...");
+ mLastReportedTime = SystemClock.uptimeMillis() - mInterval;
+ }
+
+ /* package */ void periodicUpdateTimer() {
+ if (!mTimerRunning) {
+ mTimerRunning = true;
+
+ long now = SystemClock.uptimeMillis();
+ long nextReport = mLastReportedTime + mInterval;
+
+ while (now >= nextReport) {
+ nextReport += mInterval;
+ }
+
+ if (DBG) log("periodicUpdateTimer() @ " + nextReport);
+ postAtTime(mTimerCallback, nextReport);
+ mLastReportedTime = nextReport;
+
+ if (mCall != null) {
+ Call.State state = mCall.getState();
+
+ if (state == Call.State.ACTIVE) {
+ updateElapsedTime(mCall);
+ }
+ }
+
+ if (PROFILE && isTraceReady()) {
+ startTrace();
+ }
+ } else {
+ if (DBG) log("periodicUpdateTimer: timer already running, bail");
+ }
+ }
+
+ /* package */ void cancelTimer() {
+ if (DBG) log("cancelTimer()...");
+ removeCallbacks(mTimerCallback);
+ mTimerRunning = false;
+ }
+
+ private void updateElapsedTime(Call call) {
+ if (mListener != null) {
+ long duration = getCallDuration(call);
+ mListener.onTickForCallTimeElapsed(duration / 1000);
+ }
+ }
+
+ /**
+ * Returns a "call duration" value for the specified Call, in msec,
+ * suitable for display in the UI.
+ */
+ /* package */ static long getCallDuration(Call call) {
+ long duration = 0;
+ List connections = call.getConnections();
+ int count = connections.size();
+ Connection c;
+
+ if (count == 1) {
+ c = (Connection) connections.get(0);
+ //duration = (state == Call.State.ACTIVE
+ // ? c.getDurationMillis() : c.getHoldDurationMillis());
+ duration = c.getDurationMillis();
+ } else {
+ for (int i = 0; i < count; i++) {
+ c = (Connection) connections.get(i);
+ //long t = (state == Call.State.ACTIVE
+ // ? c.getDurationMillis() : c.getHoldDurationMillis());
+ long t = c.getDurationMillis();
+ if (t > duration) {
+ duration = t;
+ }
+ }
+ }
+
+ if (DBG) log("updateElapsedTime, count=" + count + ", duration=" + duration);
+ return duration;
+ }
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, "[CallTime] " + msg);
+ }
+
+ private class PeriodicTimerCallback implements Runnable {
+ PeriodicTimerCallback() {
+
+ }
+
+ public void run() {
+ if (PROFILE && isTraceRunning()) {
+ stopTrace();
+ }
+
+ mTimerRunning = false;
+ periodicUpdateTimer();
+ }
+ }
+
+ static void setTraceReady() {
+ if (sProfileState == PROFILE_STATE_NONE) {
+ sProfileState = PROFILE_STATE_READY;
+ log("trace ready...");
+ } else {
+ log("current trace state = " + sProfileState);
+ }
+ }
+
+ boolean isTraceReady() {
+ return sProfileState == PROFILE_STATE_READY;
+ }
+
+ boolean isTraceRunning() {
+ return sProfileState == PROFILE_STATE_RUNNING;
+ }
+
+ void startTrace() {
+ if (PROFILE & sProfileState == PROFILE_STATE_READY) {
+ // For now, we move away from temp directory in favor of
+ // the application's data directory to store the trace
+ // information (/data/data/com.android.phone).
+ File file = PhoneGlobals.getInstance().getDir ("phoneTrace", Context.MODE_PRIVATE);
+ if (file.exists() == false) {
+ file.mkdirs();
+ }
+ String baseName = file.getPath() + File.separator + "callstate";
+ String dataFile = baseName + ".data";
+ String keyFile = baseName + ".key";
+
+ file = new File(dataFile);
+ if (file.exists() == true) {
+ file.delete();
+ }
+
+ file = new File(keyFile);
+ if (file.exists() == true) {
+ file.delete();
+ }
+
+ sProfileState = PROFILE_STATE_RUNNING;
+ log("startTrace");
+ Debug.startMethodTracing(baseName, 8 * 1024 * 1024);
+ }
+ }
+
+ void stopTrace() {
+ if (PROFILE) {
+ if (sProfileState == PROFILE_STATE_RUNNING) {
+ sProfileState = PROFILE_STATE_NONE;
+ log("stopTrace");
+ Debug.stopMethodTracing();
+ }
+ }
+ }
+}
diff --git a/src/com/android/phone/CallWaitingCheckBoxPreference.java b/src/com/android/phone/CallWaitingCheckBoxPreference.java
new file mode 100644
index 0000000..a2f5c70
--- /dev/null
+++ b/src/com/android/phone/CallWaitingCheckBoxPreference.java
@@ -0,0 +1,134 @@
+package com.android.phone;
+
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.Phone;
+
+import static com.android.phone.TimeConsumingPreferenceActivity.RESPONSE_ERROR;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.CheckBoxPreference;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import com.android.internal.telephony.Phone;
+
+public class CallWaitingCheckBoxPreference extends CheckBoxPreference {
+ private static final String LOG_TAG = "CallWaitingCheckBoxPreference";
+ private final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ private final MyHandler mHandler = new MyHandler();
+ private final Phone mPhone;
+ private TimeConsumingPreferenceListener mTcpListener;
+
+ public CallWaitingCheckBoxPreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ mPhone = PhoneGlobals.getPhone();
+ }
+
+ public CallWaitingCheckBoxPreference(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.checkBoxPreferenceStyle);
+ }
+
+ public CallWaitingCheckBoxPreference(Context context) {
+ this(context, null);
+ }
+
+ /* package */ void init(TimeConsumingPreferenceListener listener, boolean skipReading) {
+ mTcpListener = listener;
+
+ if (!skipReading) {
+ mPhone.getCallWaiting(mHandler.obtainMessage(MyHandler.MESSAGE_GET_CALL_WAITING,
+ MyHandler.MESSAGE_GET_CALL_WAITING, MyHandler.MESSAGE_GET_CALL_WAITING));
+ if (mTcpListener != null) {
+ mTcpListener.onStarted(this, true);
+ }
+ }
+ }
+
+ @Override
+ protected void onClick() {
+ super.onClick();
+
+ mPhone.setCallWaiting(isChecked(),
+ mHandler.obtainMessage(MyHandler.MESSAGE_SET_CALL_WAITING));
+ if (mTcpListener != null) {
+ mTcpListener.onStarted(this, false);
+ }
+ }
+
+ private class MyHandler extends Handler {
+ static final int MESSAGE_GET_CALL_WAITING = 0;
+ static final int MESSAGE_SET_CALL_WAITING = 1;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_GET_CALL_WAITING:
+ handleGetCallWaitingResponse(msg);
+ break;
+ case MESSAGE_SET_CALL_WAITING:
+ handleSetCallWaitingResponse(msg);
+ break;
+ }
+ }
+
+ private void handleGetCallWaitingResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (mTcpListener != null) {
+ if (msg.arg2 == MESSAGE_SET_CALL_WAITING) {
+ mTcpListener.onFinished(CallWaitingCheckBoxPreference.this, false);
+ } else {
+ mTcpListener.onFinished(CallWaitingCheckBoxPreference.this, true);
+ }
+ }
+
+ if (ar.exception != null) {
+ if (DBG) {
+ Log.d(LOG_TAG, "handleGetCallWaitingResponse: ar.exception=" + ar.exception);
+ }
+ if (mTcpListener != null) {
+ mTcpListener.onException(CallWaitingCheckBoxPreference.this,
+ (CommandException)ar.exception);
+ }
+ } else if (ar.userObj instanceof Throwable) {
+ if (mTcpListener != null) {
+ mTcpListener.onError(CallWaitingCheckBoxPreference.this, RESPONSE_ERROR);
+ }
+ } else {
+ if (DBG) {
+ Log.d(LOG_TAG, "handleGetCallWaitingResponse: CW state successfully queried.");
+ }
+ int[] cwArray = (int[])ar.result;
+ // If cwArray[0] is = 1, then cwArray[1] must follow,
+ // with the TS 27.007 service class bit vector of services
+ // for which call waiting is enabled.
+ try {
+ setChecked(((cwArray[0] == 1) && ((cwArray[1] & 0x01) == 0x01)));
+ } catch (ArrayIndexOutOfBoundsException e) {
+ Log.e(LOG_TAG, "handleGetCallWaitingResponse: improper result: err ="
+ + e.getMessage());
+ }
+ }
+ }
+
+ private void handleSetCallWaitingResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (ar.exception != null) {
+ if (DBG) {
+ Log.d(LOG_TAG, "handleSetCallWaitingResponse: ar.exception=" + ar.exception);
+ }
+ //setEnabled(false);
+ }
+ if (DBG) Log.d(LOG_TAG, "handleSetCallWaitingResponse: re get");
+
+ mPhone.getCallWaiting(obtainMessage(MESSAGE_GET_CALL_WAITING,
+ MESSAGE_SET_CALL_WAITING, MESSAGE_SET_CALL_WAITING, ar.exception));
+ }
+ }
+}
diff --git a/src/com/android/phone/CallerInfoCache.java b/src/com/android/phone/CallerInfoCache.java
new file mode 100644
index 0000000..76f79af
--- /dev/null
+++ b/src/com/android/phone/CallerInfoCache.java
@@ -0,0 +1,337 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import android.app.AlarmManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.PowerManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.provider.ContactsContract.CommonDataKinds.Callable;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Data;
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+/**
+ * Holds "custom ringtone" and "send to voicemail" information for each contact as a fallback of
+ * contacts database. The cached information is refreshed periodically and used when database
+ * lookup (via ContentResolver) takes longer time than expected.
+ *
+ * The data inside this class shouldn't be treated as "primary"; they may not reflect the
+ * latest information stored in the original database.
+ */
+public class CallerInfoCache {
+ private static final String LOG_TAG = CallerInfoCache.class.getSimpleName();
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+
+ /** This must not be set to true when submitting changes. */
+ private static final boolean VDBG = false;
+
+ /**
+ * Interval used with {@link AlarmManager#setInexactRepeating(int, long, long, PendingIntent)},
+ * which means the actually interval may not be very accurate.
+ */
+ private static final int CACHE_REFRESH_INTERVAL = 8 * 60 * 60 * 1000; // 8 hours in millis.
+
+ public static final int MESSAGE_UPDATE_CACHE = 0;
+
+ // Assuming DATA.DATA1 corresponds to Phone.NUMBER and SipAddress.ADDRESS, we just use
+ // Data columns as much as we can. One exception: because normalized numbers won't be used in
+ // SIP cases, Phone.NORMALIZED_NUMBER is used as is instead of using Data.
+ private static final String[] PROJECTION = new String[] {
+ Data.DATA1, // 0
+ Phone.NORMALIZED_NUMBER, // 1
+ Data.CUSTOM_RINGTONE, // 2
+ Data.SEND_TO_VOICEMAIL // 3
+ };
+
+ private static final int INDEX_NUMBER = 0;
+ private static final int INDEX_NORMALIZED_NUMBER = 1;
+ private static final int INDEX_CUSTOM_RINGTONE = 2;
+ private static final int INDEX_SEND_TO_VOICEMAIL = 3;
+
+ private static final String SELECTION = "("
+ + "(" + Data.CUSTOM_RINGTONE + " IS NOT NULL OR " + Data.SEND_TO_VOICEMAIL + "=1)"
+ + " AND " + Data.DATA1 + " IS NOT NULL)";
+
+ public static class CacheEntry {
+ public final String customRingtone;
+ public final boolean sendToVoicemail;
+ public CacheEntry(String customRingtone, boolean shouldSendToVoicemail) {
+ this.customRingtone = customRingtone;
+ this.sendToVoicemail = shouldSendToVoicemail;
+ }
+
+ @Override
+ public String toString() {
+ return "ringtone: " + customRingtone + ", " + sendToVoicemail;
+ }
+ }
+
+ private class CacheAsyncTask extends AsyncTask<Void, Void, Void> {
+
+ private PowerManager.WakeLock mWakeLock;
+
+ /**
+ * Call {@link PowerManager.WakeLock#acquire} and call {@link AsyncTask#execute(Object...)},
+ * guaranteeing the lock is held during the asynchronous task.
+ */
+ public void acquireWakeLockAndExecute() {
+ // Prepare a separate partial WakeLock than what PhoneApp has so to avoid
+ // unnecessary conflict.
+ PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
+ mWakeLock.acquire();
+ execute();
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ if (DBG) log("Start refreshing cache.");
+ refreshCacheEntry();
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ if (VDBG) log("CacheAsyncTask#onPostExecute()");
+ super.onPostExecute(result);
+ releaseWakeLock();
+ }
+
+ @Override
+ protected void onCancelled(Void result) {
+ if (VDBG) log("CacheAsyncTask#onCanceled()");
+ super.onCancelled(result);
+ releaseWakeLock();
+ }
+
+ private void releaseWakeLock() {
+ if (mWakeLock != null && mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ }
+ }
+
+ private final Context mContext;
+
+ /**
+ * The mapping from number to CacheEntry.
+ *
+ * The number will be:
+ * - last 7 digits of each "normalized phone number when it is for PSTN phone call, or
+ * - a full SIP address for SIP call
+ *
+ * When cache is being refreshed, this whole object will be replaced with a newer object,
+ * instead of updating elements inside the object. "volatile" is used to make
+ * {@link #getCacheEntry(String)} access to the newer one every time when the object is
+ * being replaced.
+ */
+ private volatile HashMap<String, CacheEntry> mNumberToEntry;
+
+ /**
+ * Used to remember if the previous task is finished or not. Should be set to null when done.
+ */
+ private CacheAsyncTask mCacheAsyncTask;
+
+ public static CallerInfoCache init(Context context) {
+ if (DBG) log("init()");
+ CallerInfoCache cache = new CallerInfoCache(context);
+ // The first cache should be available ASAP.
+ cache.startAsyncCache();
+ cache.setRepeatingCacheUpdateAlarm();
+ return cache;
+ }
+
+ private CallerInfoCache(Context context) {
+ mContext = context;
+ mNumberToEntry = new HashMap<String, CacheEntry>();
+ }
+
+ /* package */ void startAsyncCache() {
+ if (DBG) log("startAsyncCache");
+
+ if (mCacheAsyncTask != null) {
+ Log.w(LOG_TAG, "Previous cache task is remaining.");
+ mCacheAsyncTask.cancel(true);
+ }
+ mCacheAsyncTask = new CacheAsyncTask();
+ mCacheAsyncTask.acquireWakeLockAndExecute();
+ }
+
+ /**
+ * Set up periodic alarm for cache update.
+ */
+ private void setRepeatingCacheUpdateAlarm() {
+ if (DBG) log("setRepeatingCacheUpdateAlarm");
+
+ Intent intent = new Intent(CallerInfoCacheUpdateReceiver.ACTION_UPDATE_CALLER_INFO_CACHE);
+ intent.setClass(mContext, CallerInfoCacheUpdateReceiver.class);
+ PendingIntent pendingIntent =
+ PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT);
+ AlarmManager alarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
+ // We don't need precise timer while this should be power efficient.
+ alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME,
+ SystemClock.uptimeMillis() + CACHE_REFRESH_INTERVAL,
+ CACHE_REFRESH_INTERVAL, pendingIntent);
+ }
+
+ private void refreshCacheEntry() {
+ if (VDBG) log("refreshCacheEntry() started");
+
+ // There's no way to know which part of the database was updated. Also we don't want
+ // to block incoming calls asking for the cache. So this method just does full query
+ // and replaces the older cache with newer one. To refrain from blocking incoming calls,
+ // it keeps older one as much as it can, and replaces it with newer one inside a very small
+ // synchronized block.
+
+ Cursor cursor = null;
+ try {
+ cursor = mContext.getContentResolver().query(Callable.CONTENT_URI,
+ PROJECTION, SELECTION, null, null);
+ if (cursor != null) {
+ // We don't want to block real in-coming call, so prepare a completely fresh
+ // cache here again, and replace it with older one.
+ final HashMap<String, CacheEntry> newNumberToEntry =
+ new HashMap<String, CacheEntry>(cursor.getCount());
+
+ while (cursor.moveToNext()) {
+ final String number = cursor.getString(INDEX_NUMBER);
+ String normalizedNumber = cursor.getString(INDEX_NORMALIZED_NUMBER);
+ if (normalizedNumber == null) {
+ // There's no guarantee normalized numbers are available every time and
+ // it may become null sometimes. Try formatting the original number.
+ normalizedNumber = PhoneNumberUtils.normalizeNumber(number);
+ }
+ final String customRingtone = cursor.getString(INDEX_CUSTOM_RINGTONE);
+ final boolean sendToVoicemail = cursor.getInt(INDEX_SEND_TO_VOICEMAIL) == 1;
+
+ if (PhoneNumberUtils.isUriNumber(number)) {
+ // SIP address case
+ putNewEntryWhenAppropriate(
+ newNumberToEntry, number, customRingtone, sendToVoicemail);
+ } else {
+ // PSTN number case
+ // Each normalized number may or may not have full content of the number.
+ // Contacts database may contain +15001234567 while a dialed number may be
+ // just 5001234567. Also we may have inappropriate country
+ // code in some cases (e.g. when the location of the device is inconsistent
+ // with the device's place). So to avoid confusion we just rely on the last
+ // 7 digits here. It may cause some kind of wrong behavior, which is
+ // unavoidable anyway in very rare cases..
+ final int length = normalizedNumber.length();
+ final String key = length > 7
+ ? normalizedNumber.substring(length - 7, length)
+ : normalizedNumber;
+ putNewEntryWhenAppropriate(
+ newNumberToEntry, key, customRingtone, sendToVoicemail);
+ }
+ }
+
+ if (VDBG) {
+ Log.d(LOG_TAG, "New cache size: " + newNumberToEntry.size());
+ for (Entry<String, CacheEntry> entry : newNumberToEntry.entrySet()) {
+ Log.d(LOG_TAG, "Number: " + entry.getKey() + " -> " + entry.getValue());
+ }
+ }
+
+ mNumberToEntry = newNumberToEntry;
+
+ if (DBG) {
+ log("Caching entries are done. Total: " + newNumberToEntry.size());
+ }
+ } else {
+ // Let's just wait for the next refresh..
+ //
+ // If the cursor became null at that exact moment, probably we don't want to
+ // drop old cache. Also the case is fairly rare in usual cases unless acore being
+ // killed, so we don't take care much of this case.
+ Log.w(LOG_TAG, "cursor is null");
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+
+ if (VDBG) log("refreshCacheEntry() ended");
+ }
+
+ private void putNewEntryWhenAppropriate(HashMap<String, CacheEntry> newNumberToEntry,
+ String numberOrSipAddress, String customRingtone, boolean sendToVoicemail) {
+ if (newNumberToEntry.containsKey(numberOrSipAddress)) {
+ // There may be duplicate entries here and we should prioritize
+ // "send-to-voicemail" flag in any case.
+ final CacheEntry entry = newNumberToEntry.get(numberOrSipAddress);
+ if (!entry.sendToVoicemail && sendToVoicemail) {
+ newNumberToEntry.put(numberOrSipAddress,
+ new CacheEntry(customRingtone, sendToVoicemail));
+ }
+ } else {
+ newNumberToEntry.put(numberOrSipAddress,
+ new CacheEntry(customRingtone, sendToVoicemail));
+ }
+ }
+
+ /**
+ * Returns CacheEntry for the given number (PSTN number or SIP address).
+ *
+ * @param number OK to be unformatted.
+ * @return CacheEntry to be used. Maybe null if there's no cache here. Note that this may
+ * return null when the cache itself is not ready. BE CAREFUL. (or might be better to throw
+ * an exception)
+ */
+ public CacheEntry getCacheEntry(String number) {
+ if (mNumberToEntry == null) {
+ // Very unusual state. This implies the cache isn't ready during the request, while
+ // it should be prepared on the boot time (i.e. a way before even the first request).
+ Log.w(LOG_TAG, "Fallback cache isn't ready.");
+ return null;
+ }
+
+ CacheEntry entry;
+ if (PhoneNumberUtils.isUriNumber(number)) {
+ if (VDBG) log("Trying to lookup " + number);
+
+ entry = mNumberToEntry.get(number);
+ } else {
+ final String normalizedNumber = PhoneNumberUtils.normalizeNumber(number);
+ final int length = normalizedNumber.length();
+ final String key =
+ (length > 7 ? normalizedNumber.substring(length - 7, length)
+ : normalizedNumber);
+ if (VDBG) log("Trying to lookup " + key);
+
+ entry = mNumberToEntry.get(key);
+ }
+ if (VDBG) log("Obtained " + entry);
+ return entry;
+ }
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/CallerInfoCacheUpdateReceiver.java b/src/com/android/phone/CallerInfoCacheUpdateReceiver.java
new file mode 100644
index 0000000..c0a2d83
--- /dev/null
+++ b/src/com/android/phone/CallerInfoCacheUpdateReceiver.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.os.SystemProperties;
+import android.util.Log;
+
+/**
+ * BroadcastReceiver responsible for (periodic) update of {@link CallerInfoCache}.
+ *
+ * This broadcast can be sent from Contacts edit screen, implying relevant settings have changed
+ * and the cache may become obsolete.
+ */
+public class CallerInfoCacheUpdateReceiver extends BroadcastReceiver {
+ private static final String LOG_TAG = CallerInfoCacheUpdateReceiver.class.getSimpleName();
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+
+ public static final String ACTION_UPDATE_CALLER_INFO_CACHE =
+ "com.android.phone.UPDATE_CALLER_INFO_CACHE";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (DBG) log("CallerInfoCacheUpdateReceiver#onReceive(). Intent: " + intent);
+ PhoneGlobals.getInstance().callerInfoCache.startAsyncCache();
+ }
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
\ No newline at end of file
diff --git a/src/com/android/phone/CarrierLogo.java b/src/com/android/phone/CarrierLogo.java
new file mode 100644
index 0000000..5ee81b8
--- /dev/null
+++ b/src/com/android/phone/CarrierLogo.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Utility class to look up carrier logo resource IDs.
+ */
+public class CarrierLogo {
+ /** This class is never instantiated. */
+ private CarrierLogo() {
+ }
+
+ private static Map<String, Integer> sLogoMap = null;
+
+ private static Map<String, Integer> getLogoMap() {
+ if (sLogoMap == null) {
+ sLogoMap = new HashMap<String, Integer>();
+
+ // TODO: Load up sLogoMap with known carriers, like:
+ // sLogoMap.put("CarrierName",
+ // Integer.valueOf(R.drawable.mobile_logo_carriername));
+
+ // TODO: ideally, read the mapping from a config file
+ // rather than manually creating it here.
+ }
+
+ return sLogoMap;
+ }
+
+ public static int getLogo(String name) {
+ Integer res = getLogoMap().get(name);
+ if (res != null) {
+ return res.intValue();
+ }
+
+ return -1;
+ }
+}
diff --git a/src/com/android/phone/CdmaCallOptions.java b/src/com/android/phone/CdmaCallOptions.java
new file mode 100644
index 0000000..8eecd27
--- /dev/null
+++ b/src/com/android/phone/CdmaCallOptions.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+
+import android.content.DialogInterface;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+import android.util.Log;
+
+public class CdmaCallOptions extends PreferenceActivity {
+ private static final String LOG_TAG = "CdmaCallOptions";
+ private final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ private static final String BUTTON_VP_KEY = "button_voice_privacy_key";
+ private CheckBoxPreference mButtonVoicePrivacy;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ addPreferencesFromResource(R.xml.cdma_call_privacy);
+
+ mButtonVoicePrivacy = (CheckBoxPreference) findPreference(BUTTON_VP_KEY);
+ if (PhoneGlobals.getPhone().getPhoneType() != PhoneConstants.PHONE_TYPE_CDMA
+ || getResources().getBoolean(R.bool.config_voice_privacy_disable)) {
+ //disable the entire screen
+ getPreferenceScreen().setEnabled(false);
+ }
+ }
+
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
+ if (preference.getKey().equals(BUTTON_VP_KEY)) {
+ return true;
+ }
+ return false;
+ }
+
+}
diff --git a/src/com/android/phone/CdmaDisplayInfo.java b/src/com/android/phone/CdmaDisplayInfo.java
new file mode 100644
index 0000000..1a88333
--- /dev/null
+++ b/src/com/android/phone/CdmaDisplayInfo.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.view.WindowManager;
+
+/**
+ * Helper class for displaying the DisplayInfo sent by CDMA network.
+ */
+public class CdmaDisplayInfo {
+ private static final String LOG_TAG = "CdmaDisplayInfo";
+ private static final boolean DBG = (SystemProperties.getInt("ro.debuggable", 0) == 1);
+
+ /** CDMA DisplayInfo dialog */
+ private static AlertDialog sDisplayInfoDialog = null;
+
+ /**
+ * Handle the DisplayInfo record and display the alert dialog with
+ * the network message.
+ *
+ * @param context context to get strings.
+ * @param infoMsg Text message from Network.
+ */
+ public static void displayInfoRecord(Context context, String infoMsg) {
+
+ if (DBG) log("displayInfoRecord: infoMsg=" + infoMsg);
+
+ if (sDisplayInfoDialog != null) {
+ sDisplayInfoDialog.dismiss();
+ }
+
+ // displaying system alert dialog on the screen instead of
+ // using another activity to display the message. This
+ // places the message at the forefront of the UI.
+ sDisplayInfoDialog = new AlertDialog.Builder(context)
+ .setIcon(android.R.drawable.ic_dialog_info)
+ .setTitle(context.getText(R.string.network_message))
+ .setMessage(infoMsg)
+ .setCancelable(true)
+ .create();
+
+ sDisplayInfoDialog.getWindow().setType(
+ WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
+ sDisplayInfoDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+
+ sDisplayInfoDialog.show();
+ PhoneGlobals.getInstance().wakeUpScreen();
+
+ }
+
+ /**
+ * Dismiss the DisplayInfo record
+ */
+ public static void dismissDisplayInfoRecord() {
+
+ if (DBG) log("Dissmissing Display Info Record...");
+
+ if (sDisplayInfoDialog != null) {
+ sDisplayInfoDialog.dismiss();
+ sDisplayInfoDialog = null;
+ }
+ }
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, "[CdmaDisplayInfo] " + msg);
+ }
+}
diff --git a/src/com/android/phone/CdmaOptions.java b/src/com/android/phone/CdmaOptions.java
new file mode 100644
index 0000000..3e3c8b5
--- /dev/null
+++ b/src/com/android/phone/CdmaOptions.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.SystemProperties;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+import android.provider.Settings;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyProperties;
+
+/**
+ * List of Phone-specific settings screens.
+ */
+public class CdmaOptions {
+ private static final String LOG_TAG = "CdmaOptions";
+
+ private CdmaSystemSelectListPreference mButtonCdmaSystemSelect;
+ private CdmaSubscriptionListPreference mButtonCdmaSubscription;
+
+ private static final String BUTTON_CDMA_SYSTEM_SELECT_KEY = "cdma_system_select_key";
+ private static final String BUTTON_CDMA_SUBSCRIPTION_KEY = "cdma_subscription_key";
+ private static final String BUTTON_CDMA_ACTIVATE_DEVICE_KEY = "cdma_activate_device_key";
+
+ private PreferenceActivity mPrefActivity;
+ private PreferenceScreen mPrefScreen;
+ private Phone mPhone;
+
+ public CdmaOptions(PreferenceActivity prefActivity, PreferenceScreen prefScreen, Phone phone) {
+ mPrefActivity = prefActivity;
+ mPrefScreen = prefScreen;
+ mPhone = phone;
+ create();
+ }
+
+ protected void create() {
+ mPrefActivity.addPreferencesFromResource(R.xml.cdma_options);
+
+ mButtonCdmaSystemSelect = (CdmaSystemSelectListPreference)mPrefScreen
+ .findPreference(BUTTON_CDMA_SYSTEM_SELECT_KEY);
+
+ mButtonCdmaSubscription = (CdmaSubscriptionListPreference)mPrefScreen
+ .findPreference(BUTTON_CDMA_SUBSCRIPTION_KEY);
+
+ mButtonCdmaSystemSelect.setEnabled(true);
+ if(deviceSupportsNvAndRuim()) {
+ log("Both NV and Ruim supported, ENABLE subscription type selection");
+ mButtonCdmaSubscription.setEnabled(true);
+ } else {
+ log("Both NV and Ruim NOT supported, REMOVE subscription type selection");
+ mPrefScreen.removePreference(mPrefScreen
+ .findPreference(BUTTON_CDMA_SUBSCRIPTION_KEY));
+ }
+
+ final boolean voiceCapable = mPrefActivity.getResources().getBoolean(
+ com.android.internal.R.bool.config_voice_capable);
+ final boolean isLTE = mPhone.getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_TRUE;
+ if (voiceCapable || isLTE) {
+ // This option should not be available on voice-capable devices (i.e. regular phones)
+ // and is replaced by the LTE data service item on LTE devices
+ mPrefScreen.removePreference(
+ mPrefScreen.findPreference(BUTTON_CDMA_ACTIVATE_DEVICE_KEY));
+ }
+ }
+
+ private boolean deviceSupportsNvAndRuim() {
+ // retrieve the list of subscription types supported by device.
+ String subscriptionsSupported = SystemProperties.get("ril.subscription.types");
+ boolean nvSupported = false;
+ boolean ruimSupported = false;
+
+ log("deviceSupportsnvAnRum: prop=" + subscriptionsSupported);
+ if (!TextUtils.isEmpty(subscriptionsSupported)) {
+ // Searches through the comma-separated list for a match for "NV"
+ // and "RUIM" to update nvSupported and ruimSupported.
+ for (String subscriptionType : subscriptionsSupported.split(",")) {
+ subscriptionType = subscriptionType.trim();
+ if (subscriptionType.equalsIgnoreCase("NV")) {
+ nvSupported = true;
+ }
+ if (subscriptionType.equalsIgnoreCase("RUIM")) {
+ ruimSupported = true;
+ }
+ }
+ }
+
+ log("deviceSupportsnvAnRum: nvSupported=" + nvSupported +
+ " ruimSupported=" + ruimSupported);
+ return (nvSupported && ruimSupported);
+ }
+
+ public boolean preferenceTreeClick(Preference preference) {
+ if (preference.getKey().equals(BUTTON_CDMA_SYSTEM_SELECT_KEY)) {
+ log("preferenceTreeClick: return BUTTON_CDMA_ROAMING_KEY true");
+ return true;
+ }
+ if (preference.getKey().equals(BUTTON_CDMA_SUBSCRIPTION_KEY)) {
+ log("preferenceTreeClick: return CDMA_SUBSCRIPTION_KEY true");
+ return true;
+ }
+ return false;
+ }
+
+ public void showDialog(Preference preference) {
+ if (preference.getKey().equals(BUTTON_CDMA_SYSTEM_SELECT_KEY)) {
+ mButtonCdmaSystemSelect.showDialog(null);
+ } else if (preference.getKey().equals(BUTTON_CDMA_SUBSCRIPTION_KEY)) {
+ mButtonCdmaSubscription.showDialog(null);
+ }
+ }
+
+ protected void log(String s) {
+ android.util.Log.d(LOG_TAG, s);
+ }
+}
diff --git a/src/com/android/phone/CdmaPhoneCallState.java b/src/com/android/phone/CdmaPhoneCallState.java
new file mode 100644
index 0000000..30ab209
--- /dev/null
+++ b/src/com/android/phone/CdmaPhoneCallState.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+/**
+ * Class to internally keep track of Call states to maintain
+ * information for Call Waiting and 3Way for CDMA instance of Phone App.
+ *
+ * Explanation for PhoneApp's Call states and why it is required:
+ * IDLE - When no call is going on. This is just required as default state to reset the PhoneApp
+ * call state to when the complete call gets disconnected
+ * SINGLE_ACTIVE - When only single call is active.
+ * In normal case(on a single call) this state would be similar for FW's state of ACTIVE
+ * call or phone state of OFFHOOK, but in more complex conditions e.g. when phone is already
+ * in a CONF_CALL state and user rejects a CW, which basically tells the PhoneApp that the
+ * Call is back to a single call, the FW's state still would remain ACTIVE or OFFHOOK and
+ * isGeneric would still be true. At this condition PhoneApp does need to enable the
+ * "Add Call" menu item and disable the "Swap" and "Merge" options
+ * THRWAY_ACTIVE - When user initiate an outgoing call when already on a call.
+ * fgCall can have more than one connections from various scenarios (accepting the CW or
+ * making a 3way call) but once we are in this state and one of the parties drops off,
+ * when the user originates another call we need to remember this state to update the menu
+ * items accordingly. FW currently does not differentiate this condition hence PhoneApp
+ * needs to maintain it.
+ * CONF_CALL - When the user merges two calls or on accepting the Call waiting call.
+ * This is required cause even though a call might be generic but that does not mean it is
+ * in conference. We can take the same example mention in the SINGLE_ACTIVE state.
+ *
+ * TODO: Eventually this state information should be maintained by Telephony FW.
+ */
+ public class CdmaPhoneCallState {
+
+ /**
+ * Allowable values for the PhoneCallState.
+ * IDLE - When no call is going on.
+ * SINGLE_ACTIVE - When only single call is active
+ * THRWAY_ACTIVE - When user initiate an outgoing call when already on a call
+ * CONF_CALL - When the user merges two calls or on accepting the Call waiting call
+ */
+ public enum PhoneCallState {
+ IDLE,
+ SINGLE_ACTIVE,
+ THRWAY_ACTIVE,
+ CONF_CALL
+ }
+
+ // For storing current and previous PhoneCallState's
+ private PhoneCallState mPreviousCallState;
+ private PhoneCallState mCurrentCallState;
+
+ // Boolean to track 3Way display state
+ private boolean mThreeWayCallOrigStateDialing;
+
+ // Flag to indicate if the "Add Call" menu item in an InCallScreen is OK
+ // to be displayed after a Call Waiting call was ignored or timed out
+ private boolean mAddCallMenuStateAfterCW;
+
+ /**
+ * Initialize PhoneCallState related members - constructor
+ */
+ public void CdmaPhoneCallStateInit() {
+ mCurrentCallState = PhoneCallState.IDLE;
+ mPreviousCallState = PhoneCallState.IDLE;
+ mThreeWayCallOrigStateDialing = false;
+ mAddCallMenuStateAfterCW = true;
+ }
+
+ /**
+ * Returns the current call state
+ */
+ public PhoneCallState getCurrentCallState() {
+ return mCurrentCallState;
+ }
+
+ /**
+ * Set current and previous PhoneCallState's
+ */
+ public void setCurrentCallState(PhoneCallState newState) {
+ mPreviousCallState = mCurrentCallState;
+ mCurrentCallState = newState;
+
+ //Reset the 3Way display boolean
+ mThreeWayCallOrigStateDialing = false;
+
+ //Set mAddCallMenuStateAfterCW to true
+ //if the current state is being set to SINGLE_ACTIVE
+ //and previous state was IDLE as we could reach the SINGLE_ACTIVE
+ //from CW ignore too. For all other cases let the timer or
+ //specific calls to setAddCallMenuStateAfterCallWaiting set
+ //mAddCallMenuStateAfterCW.
+ if ((mCurrentCallState == PhoneCallState.SINGLE_ACTIVE)
+ && (mPreviousCallState == PhoneCallState.IDLE)) {
+ mAddCallMenuStateAfterCW = true;
+ }
+ }
+
+ /**
+ * Return 3Way display information
+ */
+ public boolean IsThreeWayCallOrigStateDialing() {
+ return mThreeWayCallOrigStateDialing;
+ }
+
+ /**
+ * Set 3Way display information
+ */
+ public void setThreeWayCallOrigState(boolean newState) {
+ mThreeWayCallOrigStateDialing = newState;
+ }
+
+ /**
+ * Return information for enabling/disabling "Add Call" menu item
+ */
+ public boolean getAddCallMenuStateAfterCallWaiting() {
+ return mAddCallMenuStateAfterCW;
+ }
+
+ /**
+ * Set mAddCallMenuStateAfterCW to enabling/disabling "Add Call" menu item
+ */
+ public void setAddCallMenuStateAfterCallWaiting(boolean newState) {
+ mAddCallMenuStateAfterCW = newState;
+ }
+
+ /**
+ * Return previous PhoneCallState's
+ */
+ public PhoneCallState getPreviousCallState() {
+ return mPreviousCallState;
+ }
+
+ /**
+ * Reset all PhoneCallState
+ */
+ public void resetCdmaPhoneCallState() {
+ mCurrentCallState = PhoneCallState.IDLE;
+ mPreviousCallState = PhoneCallState.IDLE;
+ mThreeWayCallOrigStateDialing = false;
+ mAddCallMenuStateAfterCW = true;
+ }
+ }
diff --git a/src/com/android/phone/CdmaSubscriptionListPreference.java b/src/com/android/phone/CdmaSubscriptionListPreference.java
new file mode 100644
index 0000000..9b96850
--- /dev/null
+++ b/src/com/android/phone/CdmaSubscriptionListPreference.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.ListPreference;
+import android.provider.Settings;
+import android.provider.Settings.Secure;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+
+public class CdmaSubscriptionListPreference extends ListPreference {
+
+ private static final String LOG_TAG = "CdmaSubscriptionListPreference";
+
+ // Used for CDMA subscription mode
+ private static final int CDMA_SUBSCRIPTION_RUIM_SIM = 0;
+ private static final int CDMA_SUBSCRIPTION_NV = 1;
+
+ //preferredSubscriptionMode 0 - RUIM/SIM, preferred
+ // 1 - NV
+ static final int preferredSubscriptionMode = CDMA_SUBSCRIPTION_NV;
+
+ private Phone mPhone;
+ private CdmaSubscriptionButtonHandler mHandler;
+
+ public CdmaSubscriptionListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mPhone = PhoneFactory.getDefaultPhone();
+ mHandler = new CdmaSubscriptionButtonHandler();
+ setCurrentCdmaSubscriptionModeValue();
+ }
+
+ private void setCurrentCdmaSubscriptionModeValue() {
+ int cdmaSubscriptionMode = Settings.Global.getInt(mPhone.getContext().getContentResolver(),
+ Settings.Global.CDMA_SUBSCRIPTION_MODE, preferredSubscriptionMode);
+ setValue(Integer.toString(cdmaSubscriptionMode));
+ }
+
+ public CdmaSubscriptionListPreference(Context context) {
+ this(context, null);
+ }
+
+ @Override
+ protected void showDialog(Bundle state) {
+ setCurrentCdmaSubscriptionModeValue();
+
+ super.showDialog(state);
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+
+ if (!positiveResult) {
+ //The button was dismissed - no need to set new value
+ return;
+ }
+
+ int buttonCdmaSubscriptionMode = Integer.valueOf(getValue()).intValue();
+ Log.d(LOG_TAG, "Setting new value " + buttonCdmaSubscriptionMode);
+ int statusCdmaSubscriptionMode;
+ switch(buttonCdmaSubscriptionMode) {
+ case CDMA_SUBSCRIPTION_NV:
+ statusCdmaSubscriptionMode = Phone.CDMA_SUBSCRIPTION_NV;
+ break;
+ case CDMA_SUBSCRIPTION_RUIM_SIM:
+ statusCdmaSubscriptionMode = Phone.CDMA_SUBSCRIPTION_RUIM_SIM;
+ break;
+ default:
+ statusCdmaSubscriptionMode = Phone.PREFERRED_CDMA_SUBSCRIPTION;
+ }
+
+ // Set the CDMA subscription mode, when mode has been successfully changed
+ // handleSetCdmaSubscriptionMode will be invoked and the value saved.
+ mPhone.setCdmaSubscription(statusCdmaSubscriptionMode, mHandler
+ .obtainMessage(CdmaSubscriptionButtonHandler.MESSAGE_SET_CDMA_SUBSCRIPTION,
+ getValue()));
+
+ }
+
+ private class CdmaSubscriptionButtonHandler extends Handler {
+
+ static final int MESSAGE_SET_CDMA_SUBSCRIPTION = 0;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_SET_CDMA_SUBSCRIPTION:
+ handleSetCdmaSubscriptionMode(msg);
+ break;
+ }
+ }
+
+ private void handleSetCdmaSubscriptionMode(Message msg) {
+ mPhone = PhoneFactory.getDefaultPhone();
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (ar.exception == null) {
+ // Get the original string entered by the user
+ int cdmaSubscriptionMode = Integer.valueOf((String) ar.userObj).intValue();
+ Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ Settings.Global.CDMA_SUBSCRIPTION_MODE,
+ cdmaSubscriptionMode );
+ } else {
+ Log.e(LOG_TAG, "Setting Cdma subscription source failed");
+ }
+ }
+ }
+}
diff --git a/src/com/android/phone/CdmaSystemSelectListPreference.java b/src/com/android/phone/CdmaSystemSelectListPreference.java
new file mode 100644
index 0000000..d291fd7
--- /dev/null
+++ b/src/com/android/phone/CdmaSystemSelectListPreference.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.ListPreference;
+import android.provider.Settings;
+import android.provider.Settings.Secure;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.TelephonyProperties;
+
+public class CdmaSystemSelectListPreference extends ListPreference {
+
+ private static final String LOG_TAG = "CdmaRoamingListPreference";
+ private static final boolean DBG = false;
+
+ private Phone mPhone;
+ private MyHandler mHandler = new MyHandler();
+
+ public CdmaSystemSelectListPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ mPhone = PhoneGlobals.getPhone();
+ mHandler = new MyHandler();
+ mPhone.queryCdmaRoamingPreference(
+ mHandler.obtainMessage(MyHandler.MESSAGE_GET_ROAMING_PREFERENCE));
+ }
+
+ public CdmaSystemSelectListPreference(Context context) {
+ this(context, null);
+ }
+
+ @Override
+ protected void showDialog(Bundle state) {
+ if (Boolean.parseBoolean(
+ SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE))) {
+ // In ECM mode do not show selection options
+ } else {
+ super.showDialog(state);
+ }
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+
+ if (positiveResult && (getValue() != null)) {
+ int buttonCdmaRoamingMode = Integer.valueOf(getValue()).intValue();
+ int settingsCdmaRoamingMode =
+ Settings.Global.getInt(mPhone.getContext().getContentResolver(),
+ Settings.Global.CDMA_ROAMING_MODE, Phone.CDMA_RM_HOME);
+ if (buttonCdmaRoamingMode != settingsCdmaRoamingMode) {
+ int statusCdmaRoamingMode;
+ switch(buttonCdmaRoamingMode) {
+ case Phone.CDMA_RM_ANY:
+ statusCdmaRoamingMode = Phone.CDMA_RM_ANY;
+ break;
+ case Phone.CDMA_RM_HOME:
+ default:
+ statusCdmaRoamingMode = Phone.CDMA_RM_HOME;
+ }
+ //Set the Settings.Secure network mode
+ Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ Settings.Global.CDMA_ROAMING_MODE,
+ buttonCdmaRoamingMode );
+ //Set the roaming preference mode
+ mPhone.setCdmaRoamingPreference(statusCdmaRoamingMode, mHandler
+ .obtainMessage(MyHandler.MESSAGE_SET_ROAMING_PREFERENCE));
+ }
+ } else {
+ Log.d(LOG_TAG, String.format("onDialogClosed: positiveResult=%b value=%s -- do nothing",
+ positiveResult, getValue()));
+ }
+ }
+
+ private class MyHandler extends Handler {
+
+ static final int MESSAGE_GET_ROAMING_PREFERENCE = 0;
+ static final int MESSAGE_SET_ROAMING_PREFERENCE = 1;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_GET_ROAMING_PREFERENCE:
+ handleQueryCdmaRoamingPreference(msg);
+ break;
+
+ case MESSAGE_SET_ROAMING_PREFERENCE:
+ handleSetCdmaRoamingPreference(msg);
+ break;
+ }
+ }
+
+ private void handleQueryCdmaRoamingPreference(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (ar.exception == null) {
+ int statusCdmaRoamingMode = ((int[])ar.result)[0];
+ int settingsRoamingMode = Settings.Global.getInt(
+ mPhone.getContext().getContentResolver(),
+ Settings.Global.CDMA_ROAMING_MODE, Phone.CDMA_RM_HOME);
+ //check that statusCdmaRoamingMode is from an accepted value
+ if (statusCdmaRoamingMode == Phone.CDMA_RM_HOME ||
+ statusCdmaRoamingMode == Phone.CDMA_RM_ANY ) {
+ //check changes in statusCdmaRoamingMode and updates settingsRoamingMode
+ if (statusCdmaRoamingMode != settingsRoamingMode) {
+ settingsRoamingMode = statusCdmaRoamingMode;
+ //changes the Settings.Secure accordingly to statusCdmaRoamingMode
+ Settings.Global.putInt(
+ mPhone.getContext().getContentResolver(),
+ Settings.Global.CDMA_ROAMING_MODE,
+ settingsRoamingMode );
+ }
+ //changes the mButtonPreferredNetworkMode accordingly to modemNetworkMode
+ setValue(Integer.toString(statusCdmaRoamingMode));
+ }
+ else {
+ if(DBG) Log.i(LOG_TAG, "reset cdma roaming mode to default" );
+ resetCdmaRoamingModeToDefault();
+ }
+ }
+ }
+
+ private void handleSetCdmaRoamingPreference(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if ((ar.exception == null) && (getValue() != null)) {
+ int cdmaRoamingMode = Integer.valueOf(getValue()).intValue();
+ Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ Settings.Global.CDMA_ROAMING_MODE,
+ cdmaRoamingMode );
+ } else {
+ mPhone.queryCdmaRoamingPreference(obtainMessage(MESSAGE_GET_ROAMING_PREFERENCE));
+ }
+ }
+
+ private void resetCdmaRoamingModeToDefault() {
+ //set the mButtonCdmaRoam
+ setValue(Integer.toString(Phone.CDMA_RM_HOME));
+ //set the Settings.System
+ Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ Settings.Global.CDMA_ROAMING_MODE,
+ Phone.CDMA_RM_HOME );
+ //Set the Status
+ mPhone.setCdmaRoamingPreference(Phone.CDMA_RM_HOME,
+ obtainMessage(MyHandler.MESSAGE_SET_ROAMING_PREFERENCE));
+ }
+ }
+
+}
diff --git a/src/com/android/phone/CdmaVoicePrivacyCheckBoxPreference.java b/src/com/android/phone/CdmaVoicePrivacyCheckBoxPreference.java
new file mode 100644
index 0000000..a5ff37e
--- /dev/null
+++ b/src/com/android/phone/CdmaVoicePrivacyCheckBoxPreference.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import com.android.internal.telephony.Phone;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.preference.CheckBoxPreference;
+import android.util.AttributeSet;
+import android.util.Log;
+
+public class CdmaVoicePrivacyCheckBoxPreference extends CheckBoxPreference {
+ private static final String LOG_TAG = "CdmaVoicePrivacyCheckBoxPreference";
+ private final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ Phone phone;
+ private MyHandler mHandler = new MyHandler();
+
+ public CdmaVoicePrivacyCheckBoxPreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ phone = PhoneGlobals.getPhone();
+ phone.getEnhancedVoicePrivacy(mHandler.obtainMessage(MyHandler.MESSAGE_GET_VP));
+ }
+
+ public CdmaVoicePrivacyCheckBoxPreference(Context context, AttributeSet attrs) {
+ this(context, attrs, com.android.internal.R.attr.checkBoxPreferenceStyle);
+ }
+
+ public CdmaVoicePrivacyCheckBoxPreference(Context context) {
+ this(context, null);
+ }
+
+
+ @Override
+ protected void onClick() {
+ super.onClick();
+
+ phone.enableEnhancedVoicePrivacy(isChecked(),
+ mHandler.obtainMessage(MyHandler.MESSAGE_SET_VP));
+ }
+
+
+
+ private class MyHandler extends Handler {
+ static final int MESSAGE_GET_VP = 0;
+ static final int MESSAGE_SET_VP = 1;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_GET_VP:
+ handleGetVPResponse(msg);
+ break;
+ case MESSAGE_SET_VP:
+ handleSetVPResponse(msg);
+ break;
+ }
+ }
+
+ private void handleGetVPResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (ar.exception != null) {
+ if (DBG) Log.d(LOG_TAG, "handleGetVPResponse: ar.exception=" + ar.exception);
+ setEnabled(false);
+ } else {
+ if (DBG) Log.d(LOG_TAG, "handleGetVPResponse: VP state successfully queried.");
+ final int enable = ((int[]) ar.result)[0];
+ setChecked(enable != 0);
+
+ android.provider.Settings.Secure.putInt(getContext().getContentResolver(),
+ android.provider.Settings.Secure.ENHANCED_VOICE_PRIVACY_ENABLED, enable);
+ }
+ }
+
+ private void handleSetVPResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (ar.exception != null) {
+ if (DBG) Log.d(LOG_TAG, "handleSetVPResponse: ar.exception=" + ar.exception);
+ }
+ if (DBG) Log.d(LOG_TAG, "handleSetVPResponse: re get");
+
+ phone.getEnhancedVoicePrivacy(obtainMessage(MESSAGE_GET_VP));
+ }
+ }
+}
diff --git a/src/com/android/phone/CellBroadcastSms.java b/src/com/android/phone/CellBroadcastSms.java
new file mode 100644
index 0000000..7428321
--- /dev/null
+++ b/src/com/android/phone/CellBroadcastSms.java
@@ -0,0 +1,669 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceScreen;
+import android.preference.PreferenceActivity;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.RILConstants;
+
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+
+
+/**
+ * List of Phone-specific settings screens.
+ */
+public class CellBroadcastSms extends PreferenceActivity
+ implements Preference.OnPreferenceChangeListener{
+ // debug data
+ private static final String LOG_TAG = "CellBroadcastSms";
+ private static final boolean DBG = false;
+
+ //String keys for preference lookup
+ private static final String BUTTON_ENABLE_DISABLE_BC_SMS_KEY =
+ "button_enable_disable_cell_bc_sms";
+ private static final String LIST_LANGUAGE_KEY =
+ "list_language";
+ private static final String BUTTON_EMERGENCY_BROADCAST_KEY =
+ "button_emergency_broadcast";
+ private static final String BUTTON_ADMINISTRATIVE_KEY =
+ "button_administrative";
+ private static final String BUTTON_MAINTENANCE_KEY =
+ "button_maintenance";
+ private static final String BUTTON_LOCAL_WEATHER_KEY =
+ "button_local_weather";
+ private static final String BUTTON_ATR_KEY =
+ "button_atr";
+ private static final String BUTTON_LAFS_KEY =
+ "button_lafs";
+ private static final String BUTTON_RESTAURANTS_KEY =
+ "button_restaurants";
+ private static final String BUTTON_LODGINGS_KEY =
+ "button_lodgings";
+ private static final String BUTTON_RETAIL_DIRECTORY_KEY =
+ "button_retail_directory";
+ private static final String BUTTON_ADVERTISEMENTS_KEY =
+ "button_advertisements";
+ private static final String BUTTON_STOCK_QUOTES_KEY =
+ "button_stock_quotes";
+ private static final String BUTTON_EO_KEY =
+ "button_eo";
+ private static final String BUTTON_MHH_KEY =
+ "button_mhh";
+ private static final String BUTTON_TECHNOLOGY_NEWS_KEY =
+ "button_technology_news";
+ private static final String BUTTON_MULTI_CATEGORY_KEY =
+ "button_multi_category";
+
+ private static final String BUTTON_LOCAL_GENERAL_NEWS_KEY =
+ "button_local_general_news";
+ private static final String BUTTON_REGIONAL_GENERAL_NEWS_KEY =
+ "button_regional_general_news";
+ private static final String BUTTON_NATIONAL_GENERAL_NEWS_KEY =
+ "button_national_general_news";
+ private static final String BUTTON_INTERNATIONAL_GENERAL_NEWS_KEY =
+ "button_international_general_news";
+
+ private static final String BUTTON_LOCAL_BF_NEWS_KEY =
+ "button_local_bf_news";
+ private static final String BUTTON_REGIONAL_BF_NEWS_KEY =
+ "button_regional_bf_news";
+ private static final String BUTTON_NATIONAL_BF_NEWS_KEY =
+ "button_national_bf_news";
+ private static final String BUTTON_INTERNATIONAL_BF_NEWS_KEY =
+ "button_international_bf_news";
+
+ private static final String BUTTON_LOCAL_SPORTS_NEWS_KEY =
+ "button_local_sports_news";
+ private static final String BUTTON_REGIONAL_SPORTS_NEWS_KEY =
+ "button_regional_sports_news";
+ private static final String BUTTON_NATIONAL_SPORTS_NEWS_KEY =
+ "button_national_sports_news";
+ private static final String BUTTON_INTERNATIONAL_SPORTS_NEWS_KEY =
+ "button_international_sports_news";
+
+ private static final String BUTTON_LOCAL_ENTERTAINMENT_NEWS_KEY =
+ "button_local_entertainment_news";
+ private static final String BUTTON_REGIONAL_ENTERTAINMENT_NEWS_KEY =
+ "button_regional_entertainment_news";
+ private static final String BUTTON_NATIONAL_ENTERTAINMENT_NEWS_KEY =
+ "button_national_entertainment_news";
+ private static final String BUTTON_INTERNATIONAL_ENTERTAINMENT_NEWS_KEY =
+ "button_international_entertainment_news";
+
+ //Class constants
+ //These values are related to the C structs. See the comments in method
+ //setCbSmsConfig for more information.
+ private static final int NO_OF_SERVICE_CATEGORIES = 31;
+ private static final int NO_OF_INTS_STRUCT_1 = 3;
+ private static final int MAX_LENGTH_RESULT = NO_OF_SERVICE_CATEGORIES * NO_OF_INTS_STRUCT_1 + 1;
+ //Handler keys
+ private static final int MESSAGE_ACTIVATE_CB_SMS = 1;
+ private static final int MESSAGE_GET_CB_SMS_CONFIG = 2;
+ private static final int MESSAGE_SET_CB_SMS_CONFIG = 3;
+
+ //UI objects
+ private CheckBoxPreference mButtonBcSms;
+
+ private ListPreference mListLanguage;
+
+ private CheckBoxPreference mButtonEmergencyBroadcast;
+ private CheckBoxPreference mButtonAdministrative;
+ private CheckBoxPreference mButtonMaintenance;
+ private CheckBoxPreference mButtonLocalWeather;
+ private CheckBoxPreference mButtonAtr;
+ private CheckBoxPreference mButtonLafs;
+ private CheckBoxPreference mButtonRestaurants;
+ private CheckBoxPreference mButtonLodgings;
+ private CheckBoxPreference mButtonRetailDirectory;
+ private CheckBoxPreference mButtonAdvertisements;
+ private CheckBoxPreference mButtonStockQuotes;
+ private CheckBoxPreference mButtonEo;
+ private CheckBoxPreference mButtonMhh;
+ private CheckBoxPreference mButtonTechnologyNews;
+ private CheckBoxPreference mButtonMultiCategory;
+
+ private CheckBoxPreference mButtonLocal1;
+ private CheckBoxPreference mButtonRegional1;
+ private CheckBoxPreference mButtonNational1;
+ private CheckBoxPreference mButtonInternational1;
+
+ private CheckBoxPreference mButtonLocal2;
+ private CheckBoxPreference mButtonRegional2;
+ private CheckBoxPreference mButtonNational2;
+ private CheckBoxPreference mButtonInternational2;
+
+ private CheckBoxPreference mButtonLocal3;
+ private CheckBoxPreference mButtonRegional3;
+ private CheckBoxPreference mButtonNational3;
+ private CheckBoxPreference mButtonInternational3;
+
+ private CheckBoxPreference mButtonLocal4;
+ private CheckBoxPreference mButtonRegional4;
+ private CheckBoxPreference mButtonNational4;
+ private CheckBoxPreference mButtonInternational4;
+
+
+ //Member variables
+ private Phone mPhone;
+ private MyHandler mHandler;
+
+ /**
+ * Invoked on each preference click in this hierarchy, overrides
+ * PreferenceActivity's implementation. Used to make sure we track the
+ * preference click events.
+ */
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
+ Preference preference) {
+ if (preference == mButtonBcSms) {
+ if (DBG) Log.d(LOG_TAG, "onPreferenceTreeClick: preference == mButtonBcSms.");
+ if(mButtonBcSms.isChecked()) {
+ mPhone.activateCellBroadcastSms(RILConstants.CDMA_CELL_BROADCAST_SMS_ENABLED,
+ Message.obtain(mHandler, MESSAGE_ACTIVATE_CB_SMS));
+ android.provider.Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.CDMA_CELL_BROADCAST_SMS,
+ RILConstants.CDMA_CELL_BROADCAST_SMS_ENABLED);
+ enableDisableAllCbConfigButtons(true);
+ } else {
+ mPhone.activateCellBroadcastSms(RILConstants.CDMA_CELL_BROADCAST_SMS_DISABLED,
+ Message.obtain(mHandler, MESSAGE_ACTIVATE_CB_SMS));
+ android.provider.Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.CDMA_CELL_BROADCAST_SMS,
+ RILConstants.CDMA_CELL_BROADCAST_SMS_DISABLED);
+ enableDisableAllCbConfigButtons(false);
+ }
+ } else if (preference == mListLanguage) {
+ //Do nothing here, because this click will be handled in onPreferenceChange
+ } else if (preference == mButtonEmergencyBroadcast) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonEmergencyBroadcast.isChecked(), 1);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(
+ mButtonEmergencyBroadcast.isChecked(), 1);
+ } else if (preference == mButtonAdministrative) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonAdministrative.isChecked(), 2);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonAdministrative.isChecked(), 2);
+ } else if (preference == mButtonMaintenance) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonMaintenance.isChecked(), 3);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonMaintenance.isChecked(), 3);
+ } else if (preference == mButtonLocalWeather) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonLocalWeather.isChecked(), 20);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonLocalWeather.isChecked(), 20);
+ } else if (preference == mButtonAtr) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(mButtonAtr.isChecked(), 21);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonAtr.isChecked(), 21);
+ } else if (preference == mButtonLafs) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(mButtonLafs.isChecked(), 22);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonLafs.isChecked(), 22);
+ } else if (preference == mButtonRestaurants) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonRestaurants.isChecked(), 23);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonRestaurants.isChecked(), 23);
+ } else if (preference == mButtonLodgings) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(mButtonLodgings.isChecked(), 24);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonLodgings.isChecked(), 24);
+ } else if (preference == mButtonRetailDirectory) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonRetailDirectory.isChecked(), 25);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonRetailDirectory.isChecked(), 25);
+ } else if (preference == mButtonAdvertisements) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonAdvertisements.isChecked(), 26);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonAdvertisements.isChecked(), 26);
+ } else if (preference == mButtonStockQuotes) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonStockQuotes.isChecked(), 27);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonStockQuotes.isChecked(), 27);
+ } else if (preference == mButtonEo) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(mButtonEo.isChecked(), 28);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonEo.isChecked(), 28);
+ } else if (preference == mButtonMhh) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(mButtonMhh.isChecked(), 29);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonMhh.isChecked(), 29);
+ } else if (preference == mButtonTechnologyNews) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonTechnologyNews.isChecked(), 30);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonTechnologyNews.isChecked(), 30);
+ } else if (preference == mButtonMultiCategory) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonMultiCategory.isChecked(), 31);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonMultiCategory.isChecked(), 31);
+ } else if (preference == mButtonLocal1) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(mButtonLocal1.isChecked(), 4);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonLocal1.isChecked(), 4);
+ } else if (preference == mButtonRegional1) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonRegional1.isChecked(), 5);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonRegional1.isChecked(), 5);
+ } else if (preference == mButtonNational1) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonNational1.isChecked(), 6);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonNational1.isChecked(), 6);
+ } else if (preference == mButtonInternational1) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonInternational1.isChecked(), 7);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonInternational1.isChecked(), 7);
+ } else if (preference == mButtonLocal2) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(mButtonLocal2.isChecked(), 8);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonLocal2.isChecked(), 8);
+ } else if (preference == mButtonRegional2) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonRegional2.isChecked(), 9);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonRegional2.isChecked(), 9);
+ } else if (preference == mButtonNational2) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonNational2.isChecked(), 10);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonNational2.isChecked(), 10);
+ } else if (preference == mButtonInternational2) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonInternational2.isChecked(), 11);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonInternational2.isChecked(), 11);
+ } else if (preference == mButtonLocal3) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(mButtonLocal3.isChecked(), 12);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonLocal3.isChecked(), 12);
+ } else if (preference == mButtonRegional3) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonRegional3.isChecked(), 13);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonRegional3.isChecked(), 13);
+ } else if (preference == mButtonNational3) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonNational3.isChecked(), 14);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonNational3.isChecked(), 14);
+ } else if (preference == mButtonInternational3) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonInternational3.isChecked(), 15);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonInternational3.isChecked(), 15);
+ } else if (preference == mButtonLocal4) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(mButtonLocal4.isChecked(), 16);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonLocal4.isChecked(), 16);
+ } else if (preference == mButtonRegional4) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonRegional4.isChecked(), 17);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonRegional4.isChecked(), 17);
+ } else if (preference == mButtonNational4) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonNational4.isChecked(), 18);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonNational4.isChecked(), 18);
+ } else if (preference == mButtonInternational4) {
+ CellBroadcastSmsConfig.setConfigDataCompleteBSelected(
+ mButtonInternational4.isChecked(), 19);
+ CellBroadcastSmsConfig.setCbSmsBSelectedValue(mButtonInternational4.isChecked(), 19);
+ } else {
+ preferenceScreen.setEnabled(false);
+ return false;
+ }
+
+ return true;
+ }
+
+ public boolean onPreferenceChange(Preference preference, Object objValue) {
+ if (preference == mListLanguage) {
+ // set the new language to the array which will be transmitted later
+ CellBroadcastSmsConfig.setConfigDataCompleteLanguage(
+ mListLanguage.findIndexOfValue((String) objValue) + 1);
+ }
+
+ // always let the preference setting proceed.
+ return true;
+ }
+
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ addPreferencesFromResource(R.xml.cell_broadcast_sms);
+
+ mPhone = PhoneGlobals.getPhone();
+ mHandler = new MyHandler();
+
+ PreferenceScreen prefSet = getPreferenceScreen();
+
+ mButtonBcSms = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_ENABLE_DISABLE_BC_SMS_KEY);
+ mListLanguage = (ListPreference) prefSet.findPreference(
+ LIST_LANGUAGE_KEY);
+ // set the listener for the language list preference
+ mListLanguage.setOnPreferenceChangeListener(this);
+ mButtonEmergencyBroadcast = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_EMERGENCY_BROADCAST_KEY);
+ mButtonAdministrative = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_ADMINISTRATIVE_KEY);
+ mButtonMaintenance = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_MAINTENANCE_KEY);
+ mButtonLocalWeather = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_LOCAL_WEATHER_KEY);
+ mButtonAtr = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_ATR_KEY);
+ mButtonLafs = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_LAFS_KEY);
+ mButtonRestaurants = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_RESTAURANTS_KEY);
+ mButtonLodgings = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_LODGINGS_KEY);
+ mButtonRetailDirectory = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_RETAIL_DIRECTORY_KEY);
+ mButtonAdvertisements = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_ADVERTISEMENTS_KEY);
+ mButtonStockQuotes = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_STOCK_QUOTES_KEY);
+ mButtonEo = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_EO_KEY);
+ mButtonMhh = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_MHH_KEY);
+ mButtonTechnologyNews = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_TECHNOLOGY_NEWS_KEY);
+ mButtonMultiCategory = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_MULTI_CATEGORY_KEY);
+
+ mButtonLocal1 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_LOCAL_GENERAL_NEWS_KEY);
+ mButtonRegional1 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_REGIONAL_GENERAL_NEWS_KEY);
+ mButtonNational1 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_NATIONAL_GENERAL_NEWS_KEY);
+ mButtonInternational1 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_INTERNATIONAL_GENERAL_NEWS_KEY);
+
+ mButtonLocal2 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_LOCAL_BF_NEWS_KEY);
+ mButtonRegional2 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_REGIONAL_BF_NEWS_KEY);
+ mButtonNational2 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_NATIONAL_BF_NEWS_KEY);
+ mButtonInternational2 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_INTERNATIONAL_BF_NEWS_KEY);
+
+ mButtonLocal3 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_LOCAL_SPORTS_NEWS_KEY);
+ mButtonRegional3 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_REGIONAL_SPORTS_NEWS_KEY);
+ mButtonNational3 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_NATIONAL_SPORTS_NEWS_KEY);
+ mButtonInternational3 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_INTERNATIONAL_SPORTS_NEWS_KEY);
+
+ mButtonLocal4 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_LOCAL_ENTERTAINMENT_NEWS_KEY);
+ mButtonRegional4 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_REGIONAL_ENTERTAINMENT_NEWS_KEY);
+ mButtonNational4 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_NATIONAL_ENTERTAINMENT_NEWS_KEY);
+ mButtonInternational4 = (CheckBoxPreference) prefSet.findPreference(
+ BUTTON_INTERNATIONAL_ENTERTAINMENT_NEWS_KEY);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ getPreferenceScreen().setEnabled(true);
+
+ int settingCbSms = android.provider.Settings.Global.getInt(
+ mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.CDMA_CELL_BROADCAST_SMS,
+ RILConstants.CDMA_CELL_BROADCAST_SMS_DISABLED);
+ mButtonBcSms.setChecked(settingCbSms == RILConstants.CDMA_CELL_BROADCAST_SMS_ENABLED);
+
+ if(mButtonBcSms.isChecked()) {
+ enableDisableAllCbConfigButtons(true);
+ } else {
+ enableDisableAllCbConfigButtons(false);
+ }
+
+ mPhone.getCellBroadcastSmsConfig(Message.obtain(mHandler, MESSAGE_GET_CB_SMS_CONFIG));
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ CellBroadcastSmsConfig.setCbSmsNoOfStructs(NO_OF_SERVICE_CATEGORIES);
+
+ mPhone.setCellBroadcastSmsConfig(CellBroadcastSmsConfig.getCbSmsAllValues(),
+ Message.obtain(mHandler, MESSAGE_SET_CB_SMS_CONFIG));
+ }
+
+ private void enableDisableAllCbConfigButtons(boolean enable) {
+ mButtonEmergencyBroadcast.setEnabled(enable);
+ mListLanguage.setEnabled(enable);
+ mButtonAdministrative.setEnabled(enable);
+ mButtonMaintenance.setEnabled(enable);
+ mButtonLocalWeather.setEnabled(enable);
+ mButtonAtr.setEnabled(enable);
+ mButtonLafs.setEnabled(enable);
+ mButtonRestaurants.setEnabled(enable);
+ mButtonLodgings.setEnabled(enable);
+ mButtonRetailDirectory.setEnabled(enable);
+ mButtonAdvertisements.setEnabled(enable);
+ mButtonStockQuotes.setEnabled(enable);
+ mButtonEo.setEnabled(enable);
+ mButtonMhh.setEnabled(enable);
+ mButtonTechnologyNews.setEnabled(enable);
+ mButtonMultiCategory.setEnabled(enable);
+
+ mButtonLocal1.setEnabled(enable);
+ mButtonRegional1.setEnabled(enable);
+ mButtonNational1.setEnabled(enable);
+ mButtonInternational1.setEnabled(enable);
+
+ mButtonLocal2.setEnabled(enable);
+ mButtonRegional2.setEnabled(enable);
+ mButtonNational2.setEnabled(enable);
+ mButtonInternational2.setEnabled(enable);
+
+ mButtonLocal3.setEnabled(enable);
+ mButtonRegional3.setEnabled(enable);
+ mButtonNational3.setEnabled(enable);
+ mButtonInternational3.setEnabled(enable);
+
+ mButtonLocal4.setEnabled(enable);
+ mButtonRegional4.setEnabled(enable);
+ mButtonNational4.setEnabled(enable);
+ mButtonInternational4.setEnabled(enable);
+ }
+
+ private void setAllCbConfigButtons(int[] configArray) {
+ //These buttons are in a well defined sequence. If you want to change it,
+ //be sure to map the buttons to their corresponding slot in the configArray !
+ mButtonEmergencyBroadcast.setChecked(configArray[1] != 0);
+ //subtract 1, because the values are handled in an array which starts with 0 and not with 1
+ mListLanguage.setValueIndex(CellBroadcastSmsConfig.getConfigDataLanguage() - 1);
+ mButtonAdministrative.setChecked(configArray[2] != 0);
+ mButtonMaintenance.setChecked(configArray[3] != 0);
+ mButtonLocalWeather.setChecked(configArray[20] != 0);
+ mButtonAtr.setChecked(configArray[21] != 0);
+ mButtonLafs.setChecked(configArray[22] != 0);
+ mButtonRestaurants.setChecked(configArray[23] != 0);
+ mButtonLodgings.setChecked(configArray[24] != 0);
+ mButtonRetailDirectory.setChecked(configArray[25] != 0);
+ mButtonAdvertisements.setChecked(configArray[26] != 0);
+ mButtonStockQuotes.setChecked(configArray[27] != 0);
+ mButtonEo.setChecked(configArray[28] != 0);
+ mButtonMhh.setChecked(configArray[29] != 0);
+ mButtonTechnologyNews.setChecked(configArray[30] != 0);
+ mButtonMultiCategory.setChecked(configArray[31] != 0);
+
+ mButtonLocal1.setChecked(configArray[4] != 0);
+ mButtonRegional1.setChecked(configArray[5] != 0);
+ mButtonNational1.setChecked(configArray[6] != 0);
+ mButtonInternational1.setChecked(configArray[7] != 0);
+
+ mButtonLocal2.setChecked(configArray[8] != 0);
+ mButtonRegional2.setChecked(configArray[9] != 0);
+ mButtonNational2.setChecked(configArray[10] != 0);
+ mButtonInternational2.setChecked(configArray[11] != 0);
+
+ mButtonLocal3.setChecked(configArray[12] != 0);
+ mButtonRegional3.setChecked(configArray[13] != 0);
+ mButtonNational3.setChecked(configArray[14] != 0);
+ mButtonInternational3.setChecked(configArray[15] != 0);
+
+ mButtonLocal4.setChecked(configArray[16] != 0);
+ mButtonRegional4.setChecked(configArray[17] != 0);
+ mButtonNational4.setChecked(configArray[18] != 0);
+ mButtonInternational4.setChecked(configArray[19] != 0);
+ }
+
+ private class MyHandler extends Handler {
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_ACTIVATE_CB_SMS:
+ //Only a log message here, because the received response is always null
+ if (DBG) Log.d(LOG_TAG, "Cell Broadcast SMS enabled/disabled.");
+ break;
+ case MESSAGE_GET_CB_SMS_CONFIG:
+ int result[] = (int[])((AsyncResult)msg.obj).result;
+
+ // check if the actual service categoties table size on the NV is '0'
+ if (result[0] == 0) {
+ result[0] = NO_OF_SERVICE_CATEGORIES;
+
+ mButtonBcSms.setChecked(false);
+ mPhone.activateCellBroadcastSms(RILConstants.CDMA_CELL_BROADCAST_SMS_DISABLED,
+ Message.obtain(mHandler, MESSAGE_ACTIVATE_CB_SMS));
+ android.provider.Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.CDMA_CELL_BROADCAST_SMS,
+ RILConstants.CDMA_CELL_BROADCAST_SMS_DISABLED);
+ enableDisableAllCbConfigButtons(false);
+ }
+
+ CellBroadcastSmsConfig.setCbSmsConfig(result);
+ setAllCbConfigButtons(CellBroadcastSmsConfig.getCbSmsBselectedValues());
+
+ break;
+ case MESSAGE_SET_CB_SMS_CONFIG:
+ //Only a log message here, because the received response is always null
+ if (DBG) Log.d(LOG_TAG, "Set Cell Broadcast SMS values.");
+ break;
+ default:
+ Log.e(LOG_TAG, "Error! Unhandled message in CellBroadcastSms.java. Message: "
+ + msg.what);
+ break;
+ }
+ }
+ }
+
+ private static final class CellBroadcastSmsConfig {
+
+ //The values in this array are stored in a particular order. This order
+ //is calculated in the setCbSmsConfig method of this class.
+ //For more information see comments below...
+ //NO_OF_SERVICE_CATEGORIES +1 is used, because we will leave the first array entry 0
+ private static int mBSelected[] = new int[NO_OF_SERVICE_CATEGORIES + 1];
+ private static int mConfigDataComplete[] = new int[MAX_LENGTH_RESULT];
+
+ private static void setCbSmsConfig(int[] configData) {
+ if(configData == null) {
+ Log.e(LOG_TAG, "Error! No cell broadcast service categories returned.");
+ return;
+ }
+
+ if(configData[0] > MAX_LENGTH_RESULT) {
+ Log.e(LOG_TAG, "Error! Wrong number of service categories returned from RIL");
+ return;
+ }
+
+ //The required config values for broadcast SMS are stored in a C struct:
+ //
+ // typedef struct {
+ // int size;
+ // RIL_CDMA_BcServiceInfo *entries;
+ // } RIL_CDMA_BcSMSConfig;
+ //
+ // typedef struct {
+ // int uServiceCategory;
+ // int uLanguage;
+ // unsigned char bSelected;
+ // } RIL_CDMA_BcServiceInfo;
+ //
+ // This means, that we have to ignore the first value and check every
+ // 3rd value starting with the 2nd of all. This value indicates, where we
+ // will store the appropriate bSelected value, which is 2 values behind it.
+ for(int i = 1; i < configData.length; i += NO_OF_INTS_STRUCT_1) {
+ mBSelected[configData[i]] = configData[i +2];
+ }
+
+ //Store all values in an extra array
+ mConfigDataComplete = configData;
+ }
+
+ private static void setCbSmsBSelectedValue(boolean value, int pos) {
+ if(pos < mBSelected.length) {
+ mBSelected[pos] = (value == true ? 1 : 0);
+ } else {
+ Log.e(LOG_TAG,"Error! Invalid value position.");
+ }
+ }
+
+ private static int[] getCbSmsBselectedValues() {
+ return(mBSelected);
+ }
+
+ // TODO: Change the return value to a RIL_BroadcastSMSConfig
+ private static int[] getCbSmsAllValues() {
+ return(mConfigDataComplete);
+ }
+
+ private static void setCbSmsNoOfStructs(int value) {
+ //Sets the size parameter, which contains the number of structs
+ //that will be transmitted
+ mConfigDataComplete[0] = value;
+ }
+
+ private static void setConfigDataCompleteBSelected(boolean value, int serviceCategory) {
+ //Sets the bSelected value for a specific serviceCategory
+ for(int i = 1; i < mConfigDataComplete.length; i += NO_OF_INTS_STRUCT_1) {
+ if(mConfigDataComplete[i] == serviceCategory) {
+ mConfigDataComplete[i + 2] = value == true ? 1 : 0;
+ break;
+ }
+ }
+ }
+
+ private static void setConfigDataCompleteLanguage(int language) {
+ //It is only possible to set the same language for all entries
+ for(int i = 2; i < mConfigDataComplete.length; i += NO_OF_INTS_STRUCT_1) {
+ mConfigDataComplete[i] = language;
+ }
+ }
+
+ private static int getConfigDataLanguage() {
+ int language = mConfigDataComplete[2];
+ //2 is the language value of the first entry
+ //It is only possible to set the same language for all entries
+ if (language < 1 || language > 7) {
+ Log.e(LOG_TAG, "Error! Wrong language returned from RIL...defaulting to 1, english");
+ return 1;
+ }
+ else {
+ return language;
+ }
+ }
+ }
+}
diff --git a/src/com/android/phone/ChangeIccPinScreen.java b/src/com/android/phone/ChangeIccPinScreen.java
new file mode 100644
index 0000000..70bf431
--- /dev/null
+++ b/src/com/android/phone/ChangeIccPinScreen.java
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.method.DigitsKeyListener;
+import android.util.Log;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.IccCard;
+import com.android.internal.telephony.Phone;
+
+/**
+ * "Change ICC PIN" UI for the Phone app.
+ */
+public class ChangeIccPinScreen extends Activity {
+ private static final String LOG_TAG = PhoneGlobals.LOG_TAG;
+ private static final boolean DBG = false;
+
+ private static final int EVENT_PIN_CHANGED = 100;
+
+ private enum EntryState {
+ ES_PIN,
+ ES_PUK
+ }
+
+ private EntryState mState;
+
+ private static final int NO_ERROR = 0;
+ private static final int PIN_MISMATCH = 1;
+ private static final int PIN_INVALID_LENGTH = 2;
+
+ private static final int MIN_PIN_LENGTH = 4;
+ private static final int MAX_PIN_LENGTH = 8;
+
+ private Phone mPhone;
+ private boolean mChangePin2;
+ private TextView mBadPinError;
+ private TextView mMismatchError;
+ private EditText mOldPin;
+ private EditText mNewPin1;
+ private EditText mNewPin2;
+ private EditText mPUKCode;
+ private Button mButton;
+ private Button mPUKSubmit;
+ private ScrollView mScrollView;
+
+ private LinearLayout mIccPUKPanel;
+
+ private Handler mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_PIN_CHANGED:
+ AsyncResult ar = (AsyncResult) msg.obj;
+ handleResult(ar);
+ break;
+ }
+
+ return;
+ }
+ };
+
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mPhone = PhoneGlobals.getPhone();
+
+ resolveIntent();
+
+ setContentView(R.layout.change_sim_pin_screen);
+
+ mOldPin = (EditText) findViewById(R.id.old_pin);
+ mOldPin.setKeyListener(DigitsKeyListener.getInstance());
+ mOldPin.setMovementMethod(null);
+ mOldPin.setOnClickListener(mClicked);
+
+ mNewPin1 = (EditText) findViewById(R.id.new_pin1);
+ mNewPin1.setKeyListener(DigitsKeyListener.getInstance());
+ mNewPin1.setMovementMethod(null);
+ mNewPin1.setOnClickListener(mClicked);
+
+ mNewPin2 = (EditText) findViewById(R.id.new_pin2);
+ mNewPin2.setKeyListener(DigitsKeyListener.getInstance());
+ mNewPin2.setMovementMethod(null);
+ mNewPin2.setOnClickListener(mClicked);
+
+ mBadPinError = (TextView) findViewById(R.id.bad_pin);
+ mMismatchError = (TextView) findViewById(R.id.mismatch);
+
+ mButton = (Button) findViewById(R.id.button);
+ mButton.setOnClickListener(mClicked);
+
+ mScrollView = (ScrollView) findViewById(R.id.scroll);
+
+ mPUKCode = (EditText) findViewById(R.id.puk_code);
+ mPUKCode.setKeyListener(DigitsKeyListener.getInstance());
+ mPUKCode.setMovementMethod(null);
+ mPUKCode.setOnClickListener(mClicked);
+
+ mPUKSubmit = (Button) findViewById(R.id.puk_submit);
+ mPUKSubmit.setOnClickListener(mClicked);
+
+ mIccPUKPanel = (LinearLayout) findViewById(R.id.puk_panel);
+
+ int id = mChangePin2 ? R.string.change_pin2 : R.string.change_pin;
+ setTitle(getResources().getText(id));
+
+ mState = EntryState.ES_PIN;
+ }
+
+ private void resolveIntent() {
+ Intent intent = getIntent();
+ mChangePin2 = intent.getBooleanExtra("pin2", mChangePin2);
+ }
+
+ private void reset() {
+ mScrollView.scrollTo(0, 0);
+ mBadPinError.setVisibility(View.GONE);
+ mMismatchError.setVisibility(View.GONE);
+ }
+
+ private int validateNewPin(String p1, String p2) {
+ if (p1 == null) {
+ return PIN_INVALID_LENGTH;
+ }
+
+ if (!p1.equals(p2)) {
+ return PIN_MISMATCH;
+ }
+
+ int len1 = p1.length();
+
+ if (len1 < MIN_PIN_LENGTH || len1 > MAX_PIN_LENGTH) {
+ return PIN_INVALID_LENGTH;
+ }
+
+ return NO_ERROR;
+ }
+
+ private View.OnClickListener mClicked = new View.OnClickListener() {
+ public void onClick(View v) {
+ if (v == mOldPin) {
+ mNewPin1.requestFocus();
+ } else if (v == mNewPin1) {
+ mNewPin2.requestFocus();
+ } else if (v == mNewPin2) {
+ mButton.requestFocus();
+ } else if (v == mButton) {
+ IccCard iccCardInterface = mPhone.getIccCard();
+ if (iccCardInterface != null) {
+ String oldPin = mOldPin.getText().toString();
+ String newPin1 = mNewPin1.getText().toString();
+ String newPin2 = mNewPin2.getText().toString();
+
+ int error = validateNewPin(newPin1, newPin2);
+
+ switch (error) {
+ case PIN_INVALID_LENGTH:
+ case PIN_MISMATCH:
+ mNewPin1.getText().clear();
+ mNewPin2.getText().clear();
+ mMismatchError.setVisibility(View.VISIBLE);
+
+ Resources r = getResources();
+ CharSequence text;
+
+ if (error == PIN_MISMATCH) {
+ text = r.getString(R.string.mismatchPin);
+ } else {
+ text = r.getString(R.string.invalidPin);
+ }
+
+ mMismatchError.setText(text);
+ break;
+
+ default:
+ Message callBack = Message.obtain(mHandler,
+ EVENT_PIN_CHANGED);
+
+ if (DBG) log("change pin attempt: old=" + oldPin +
+ ", newPin=" + newPin1);
+
+ reset();
+
+ if (mChangePin2) {
+ iccCardInterface.changeIccFdnPassword(oldPin,
+ newPin1, callBack);
+ } else {
+ iccCardInterface.changeIccLockPassword(oldPin,
+ newPin1, callBack);
+ }
+
+ // TODO: show progress panel
+ }
+ }
+ } else if (v == mPUKCode) {
+ mPUKSubmit.requestFocus();
+ } else if (v == mPUKSubmit) {
+ mPhone.getIccCard().supplyPuk2(mPUKCode.getText().toString(),
+ mNewPin1.getText().toString(),
+ Message.obtain(mHandler, EVENT_PIN_CHANGED));
+ }
+ }
+ };
+
+ private void handleResult(AsyncResult ar) {
+ if (ar.exception == null) {
+ if (DBG) log("handleResult: success!");
+
+ if (mState == EntryState.ES_PUK) {
+ mScrollView.setVisibility(View.VISIBLE);
+ mIccPUKPanel.setVisibility(View.GONE);
+ }
+ // TODO: show success feedback
+ showConfirmation();
+
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ finish();
+ }
+ }, 3000);
+
+ } else if (ar.exception instanceof CommandException
+ /* && ((CommandException)ar.exception).getCommandError() ==
+ CommandException.Error.PASSWORD_INCORRECT */ ) {
+ if (mState == EntryState.ES_PIN) {
+ if (DBG) log("handleResult: pin failed!");
+ mOldPin.getText().clear();
+ mBadPinError.setVisibility(View.VISIBLE);
+ CommandException ce = (CommandException) ar.exception;
+ if (ce.getCommandError() == CommandException.Error.SIM_PUK2) {
+ if (DBG) log("handleResult: puk requested!");
+ mState = EntryState.ES_PUK;
+ displayPUKAlert();
+ mScrollView.setVisibility(View.GONE);
+ mIccPUKPanel.setVisibility(View.VISIBLE);
+ mPUKCode.requestFocus();
+ }
+ } else if (mState == EntryState.ES_PUK) {
+ //should really check to see if the error is CommandException.PASSWORD_INCORRECT...
+ if (DBG) log("handleResult: puk2 failed!");
+ displayPUKAlert();
+ mPUKCode.getText().clear();
+ mPUKCode.requestFocus();
+ }
+ }
+ }
+
+ private AlertDialog mPUKAlert;
+ private void displayPUKAlert () {
+ if (mPUKAlert == null) {
+ mPUKAlert = new AlertDialog.Builder(this)
+ .setMessage (R.string.puk_requested)
+ .setCancelable(false)
+ .show();
+ } else {
+ mPUKAlert.show();
+ }
+ //TODO: The 3 second delay here is somewhat arbitrary, reflecting the values
+ //used elsewhere for similar code. This should get revisited with the framework
+ //crew to see if there is some standard we should adhere to.
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ mPUKAlert.dismiss();
+ }
+ }, 3000);
+ }
+
+ private void showConfirmation() {
+ int id = mChangePin2 ? R.string.pin2_changed : R.string.pin_changed;
+ Toast.makeText(this, id, Toast.LENGTH_SHORT).show();
+ }
+
+ private void log(String msg) {
+ String prefix = mChangePin2 ? "[ChgPin2]" : "[ChgPin]";
+ Log.d(LOG_TAG, prefix + msg);
+ }
+}
diff --git a/src/com/android/phone/ClearMissedCallsService.java b/src/com/android/phone/ClearMissedCallsService.java
new file mode 100644
index 0000000..b882472
--- /dev/null
+++ b/src/com/android/phone/ClearMissedCallsService.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2011 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.phone;
+
+import android.app.IntentService;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.provider.CallLog.Calls;
+
+/**
+ * Handles the intent to clear the missed calls that is triggered when a notification is dismissed.
+ */
+public class ClearMissedCallsService extends IntentService {
+ /** This action is used to clear missed calls. */
+ public static final String ACTION_CLEAR_MISSED_CALLS =
+ "com.android.phone.intent.CLEAR_MISSED_CALLS";
+
+ private PhoneGlobals mApp;
+
+ public ClearMissedCallsService() {
+ super(ClearMissedCallsService.class.getSimpleName());
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mApp = PhoneGlobals.getInstance();
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (ACTION_CLEAR_MISSED_CALLS.equals(intent.getAction())) {
+ // Clear the list of new missed calls.
+ ContentValues values = new ContentValues();
+ values.put(Calls.NEW, 0);
+ values.put(Calls.IS_READ, 1);
+ StringBuilder where = new StringBuilder();
+ where.append(Calls.NEW);
+ where.append(" = 1 AND ");
+ where.append(Calls.TYPE);
+ where.append(" = ?");
+ getContentResolver().update(Calls.CONTENT_URI, values, where.toString(),
+ new String[]{ Integer.toString(Calls.MISSED_TYPE) });
+ mApp.notificationMgr.cancelMissedCallNotification();
+ }
+ }
+}
diff --git a/src/com/android/phone/Constants.java b/src/com/android/phone/Constants.java
new file mode 100644
index 0000000..3e10c3a
--- /dev/null
+++ b/src/com/android/phone/Constants.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2011 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.phone;
+
+/**
+ * App-wide constants and enums for the phone app.
+ *
+ * Any constants that need to be shared between two or more classes within
+ * the com.android.phone package should be defined here. (Constants that
+ * are private to only one class can go in that class's .java file.)
+ */
+public class Constants {
+
+ /**
+ * Complete list of error / diagnostic indications we might possibly
+ * need to present to the user.
+ *
+ * This enum is basically a high-level list of the kinds of failures
+ * or "exceptional conditions" that can occur when making a phone
+ * call. When an error occurs, the CallController stashes away one of
+ * these codes in the InCallUiState.pendingCallStatusCode flag and
+ * launches the InCallScreen; the InCallScreen will then display some
+ * kind of message to the user (usually an error dialog) explaining
+ * what happened.
+ *
+ * The enum values here cover all possible result status / error
+ * conditions that can happen when attempting to place an outgoing
+ * call (see CallController.placeCall() and placeCallInternal()), as
+ * well as some other conditions (like CDMA_CALL_LOST and EXITED_ECM)
+ * that don't technically result from the placeCall() sequence but
+ * still need to be communicated to the user.
+ */
+ public enum CallStatusCode {
+ /**
+ * No error or exceptional condition occurred.
+ * The InCallScreen does not need to display any kind of alert to the user.
+ */
+ SUCCESS,
+
+ /**
+ * Radio is explictly powered off, presumably because the
+ * device is in airplane mode.
+ */
+ POWER_OFF,
+
+ /**
+ * Only emergency numbers are allowed, but we tried to dial
+ * a non-emergency number.
+ */
+ EMERGENCY_ONLY,
+
+ /**
+ * No network connection.
+ */
+ OUT_OF_SERVICE,
+
+ /**
+ * The supplied CALL Intent didn't contain a valid phone number.
+ */
+ NO_PHONE_NUMBER_SUPPLIED,
+
+ /**
+ * Our initial phone number was actually an MMI sequence.
+ */
+ DIALED_MMI,
+
+ /**
+ * We couldn't successfully place the call due to an
+ * unknown failure in the telephony layer.
+ */
+ CALL_FAILED,
+
+ /**
+ * We tried to call a voicemail: URI but the device has no
+ * voicemail number configured.
+ *
+ * When InCallUiState.pendingCallStatusCode is set to this
+ * value, the InCallScreen will bring up a UI explaining what
+ * happened, and allowing the user to go into Settings to fix the
+ * problem.
+ */
+ VOICEMAIL_NUMBER_MISSING,
+
+ /**
+ * This status indicates that InCallScreen should display the
+ * CDMA-specific "call lost" dialog. (If an outgoing call fails,
+ * and the CDMA "auto-retry" feature is enabled, *and* the retried
+ * call fails too, we display this specific dialog.)
+ *
+ * TODO: this is currently unused, since the "call lost" dialog
+ * needs to be triggered by a *disconnect* event, rather than when
+ * the InCallScreen first comes to the foreground. For now we use
+ * the needToShowCallLostDialog field for this (see below.)
+ */
+ CDMA_CALL_LOST,
+
+ /**
+ * This status indicates that the call was placed successfully,
+ * but additionally, the InCallScreen needs to display the
+ * "Exiting ECM" dialog.
+ *
+ * (Details: "Emergency callback mode" is a CDMA-specific concept
+ * where the phone disallows data connections over the cell
+ * network for some period of time after you make an emergency
+ * call. If the phone is in ECM and you dial a non-emergency
+ * number, that automatically *cancels* ECM, but we additionally
+ * need to warn the user that ECM has been canceled (see bug
+ * 4207607.))
+ */
+ EXITED_ECM
+ }
+
+ //
+ // URI schemes
+ //
+
+ public static final String SCHEME_SIP = "sip";
+ public static final String SCHEME_SMS = "sms";
+ public static final String SCHEME_SMSTO = "smsto";
+ public static final String SCHEME_TEL = "tel";
+ public static final String SCHEME_VOICEMAIL = "voicemail";
+
+ //
+ // TODO: Move all the various EXTRA_* and intent action constants here too.
+ // (Currently they're all over the place: InCallScreen,
+ // OutgoingCallBroadcaster, OtaUtils, etc.)
+ //
+
+ // Dtmf tone type setting value for CDMA phone
+ public static final int DTMF_TONE_TYPE_NORMAL = 0;
+ public static final int DTMF_TONE_TYPE_LONG = 1;
+}
diff --git a/src/com/android/phone/ContactsAsyncHelper.java b/src/com/android/phone/ContactsAsyncHelper.java
new file mode 100644
index 0000000..10a6950
--- /dev/null
+++ b/src/com/android/phone/ContactsAsyncHelper.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.app.Notification;
+import android.content.ContentUris;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.ContactsContract.Contacts;
+import android.util.Log;
+
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.Connection;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Helper class for loading contacts photo asynchronously.
+ */
+public class ContactsAsyncHelper {
+
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "ContactsAsyncHelper";
+
+ /**
+ * Interface for a WorkerHandler result return.
+ */
+ public interface OnImageLoadCompleteListener {
+ /**
+ * Called when the image load is complete.
+ *
+ * @param token Integer passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
+ * Context, Uri, OnImageLoadCompleteListener, Object)}.
+ * @param photo Drawable object obtained by the async load.
+ * @param photoIcon Bitmap object obtained by the async load.
+ * @param cookie Object passed in {@link ContactsAsyncHelper#startObtainPhotoAsync(int,
+ * Context, Uri, OnImageLoadCompleteListener, Object)}. Can be null iff. the original
+ * cookie is null.
+ */
+ public void onImageLoadComplete(int token, Drawable photo, Bitmap photoIcon,
+ Object cookie);
+ }
+
+ // constants
+ private static final int EVENT_LOAD_IMAGE = 1;
+
+ private final Handler mResultHandler = new Handler() {
+ /** Called when loading is done. */
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ if (args.listener != null) {
+ if (DBG) {
+ Log.d(LOG_TAG, "Notifying listener: " + args.listener.toString() +
+ " image: " + args.uri + " completed");
+ }
+ args.listener.onImageLoadComplete(msg.what, args.photo, args.photoIcon,
+ args.cookie);
+ }
+ break;
+ default:
+ }
+ }
+ };
+
+ /** Handler run on a worker thread to load photo asynchronously. */
+ private static Handler sThreadHandler;
+
+ /** For forcing the system to call its constructor */
+ @SuppressWarnings("unused")
+ private static ContactsAsyncHelper sInstance;
+
+ static {
+ sInstance = new ContactsAsyncHelper();
+ }
+
+ private static final class WorkerArgs {
+ public Context context;
+ public Uri uri;
+ public Drawable photo;
+ public Bitmap photoIcon;
+ public Object cookie;
+ public OnImageLoadCompleteListener listener;
+ }
+
+ /**
+ * public inner class to help out the ContactsAsyncHelper callers
+ * with tracking the state of the CallerInfo Queries and image
+ * loading.
+ *
+ * Logic contained herein is used to remove the race conditions
+ * that exist as the CallerInfo queries run and mix with the image
+ * loads, which then mix with the Phone state changes.
+ */
+ public static class ImageTracker {
+
+ // Image display states
+ public static final int DISPLAY_UNDEFINED = 0;
+ public static final int DISPLAY_IMAGE = -1;
+ public static final int DISPLAY_DEFAULT = -2;
+
+ // State of the image on the imageview.
+ private CallerInfo mCurrentCallerInfo;
+ private int displayMode;
+
+ public ImageTracker() {
+ mCurrentCallerInfo = null;
+ displayMode = DISPLAY_UNDEFINED;
+ }
+
+ /**
+ * Used to see if the requested call / connection has a
+ * different caller attached to it than the one we currently
+ * have in the CallCard.
+ */
+ public boolean isDifferentImageRequest(CallerInfo ci) {
+ // note, since the connections are around for the lifetime of the
+ // call, and the CallerInfo-related items as well, we can
+ // definitely use a simple != comparison.
+ return (mCurrentCallerInfo != ci);
+ }
+
+ public boolean isDifferentImageRequest(Connection connection) {
+ // if the connection does not exist, see if the
+ // mCurrentCallerInfo is also null to match.
+ if (connection == null) {
+ if (DBG) Log.d(LOG_TAG, "isDifferentImageRequest: connection is null");
+ return (mCurrentCallerInfo != null);
+ }
+ Object o = connection.getUserData();
+
+ // if the call does NOT have a callerInfo attached
+ // then it is ok to query.
+ boolean runQuery = true;
+ if (o instanceof CallerInfo) {
+ runQuery = isDifferentImageRequest((CallerInfo) o);
+ }
+ return runQuery;
+ }
+
+ /**
+ * Simple setter for the CallerInfo object.
+ */
+ public void setPhotoRequest(CallerInfo ci) {
+ mCurrentCallerInfo = ci;
+ }
+
+ /**
+ * Convenience method used to retrieve the URI
+ * representing the Photo file recorded in the attached
+ * CallerInfo Object.
+ */
+ public Uri getPhotoUri() {
+ if (mCurrentCallerInfo != null) {
+ return ContentUris.withAppendedId(Contacts.CONTENT_URI,
+ mCurrentCallerInfo.person_id);
+ }
+ return null;
+ }
+
+ /**
+ * Simple setter for the Photo state.
+ */
+ public void setPhotoState(int state) {
+ displayMode = state;
+ }
+
+ /**
+ * Simple getter for the Photo state.
+ */
+ public int getPhotoState() {
+ return displayMode;
+ }
+ }
+
+ /**
+ * Thread worker class that handles the task of opening the stream and loading
+ * the images.
+ */
+ private class WorkerHandler extends Handler {
+ public WorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ WorkerArgs args = (WorkerArgs) msg.obj;
+
+ switch (msg.arg1) {
+ case EVENT_LOAD_IMAGE:
+ InputStream inputStream = null;
+ try {
+ try {
+ inputStream = Contacts.openContactPhotoInputStream(
+ args.context.getContentResolver(), args.uri, true);
+ } catch (Exception e) {
+ Log.e(LOG_TAG, "Error opening photo input stream", e);
+ }
+
+ if (inputStream != null) {
+ args.photo = Drawable.createFromStream(inputStream,
+ args.uri.toString());
+
+ // This assumes Drawable coming from contact database is usually
+ // BitmapDrawable and thus we can have (down)scaled version of it.
+ args.photoIcon = getPhotoIconWhenAppropriate(args.context, args.photo);
+
+ if (DBG) {
+ Log.d(LOG_TAG, "Loading image: " + msg.arg1 +
+ " token: " + msg.what + " image URI: " + args.uri);
+ }
+ } else {
+ args.photo = null;
+ args.photoIcon = null;
+ if (DBG) {
+ Log.d(LOG_TAG, "Problem with image: " + msg.arg1 +
+ " token: " + msg.what + " image URI: " + args.uri +
+ ", using default image.");
+ }
+ }
+ } finally {
+ if (inputStream != null) {
+ try {
+ inputStream.close();
+ } catch (IOException e) {
+ Log.e(LOG_TAG, "Unable to close input stream.", e);
+ }
+ }
+ }
+ break;
+ default:
+ }
+
+ // send the reply to the enclosing class.
+ Message reply = ContactsAsyncHelper.this.mResultHandler.obtainMessage(msg.what);
+ reply.arg1 = msg.arg1;
+ reply.obj = msg.obj;
+ reply.sendToTarget();
+ }
+
+ /**
+ * Returns a Bitmap object suitable for {@link Notification}'s large icon. This might
+ * return null when the given Drawable isn't BitmapDrawable, or if the system fails to
+ * create a scaled Bitmap for the Drawable.
+ */
+ private Bitmap getPhotoIconWhenAppropriate(Context context, Drawable photo) {
+ if (!(photo instanceof BitmapDrawable)) {
+ return null;
+ }
+ int iconSize = context.getResources()
+ .getDimensionPixelSize(R.dimen.notification_icon_size);
+ Bitmap orgBitmap = ((BitmapDrawable) photo).getBitmap();
+ int orgWidth = orgBitmap.getWidth();
+ int orgHeight = orgBitmap.getHeight();
+ int longerEdge = orgWidth > orgHeight ? orgWidth : orgHeight;
+ // We want downscaled one only when the original icon is too big.
+ if (longerEdge > iconSize) {
+ float ratio = ((float) longerEdge) / iconSize;
+ int newWidth = (int) (orgWidth / ratio);
+ int newHeight = (int) (orgHeight / ratio);
+ // If the longer edge is much longer than the shorter edge, the latter may
+ // become 0 which will cause a crash.
+ if (newWidth <= 0 || newHeight <= 0) {
+ Log.w(LOG_TAG, "Photo icon's width or height become 0.");
+ return null;
+ }
+
+ // It is sure ratio >= 1.0f in any case and thus the newly created Bitmap
+ // should be smaller than the original.
+ return Bitmap.createScaledBitmap(orgBitmap, newWidth, newHeight, true);
+ } else {
+ return orgBitmap;
+ }
+ }
+ }
+
+ /**
+ * Private constructor for static class
+ */
+ private ContactsAsyncHelper() {
+ HandlerThread thread = new HandlerThread("ContactsAsyncWorker");
+ thread.start();
+ sThreadHandler = new WorkerHandler(thread.getLooper());
+ }
+
+ /**
+ * Starts an asynchronous image load. After finishing the load,
+ * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
+ * will be called.
+ *
+ * @param token Arbitrary integer which will be returned as the first argument of
+ * {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable, Bitmap, Object)}
+ * @param context Context object used to do the time-consuming operation.
+ * @param personUri Uri to be used to fetch the photo
+ * @param listener Callback object which will be used when the asynchronous load is done.
+ * Can be null, which means only the asynchronous load is done while there's no way to
+ * obtain the loaded photos.
+ * @param cookie Arbitrary object the caller wants to remember, which will become the
+ * fourth argument of {@link OnImageLoadCompleteListener#onImageLoadComplete(int, Drawable,
+ * Bitmap, Object)}. Can be null, at which the callback will also has null for the argument.
+ */
+ public static final void startObtainPhotoAsync(int token, Context context, Uri personUri,
+ OnImageLoadCompleteListener listener, Object cookie) {
+ // in case the source caller info is null, the URI will be null as well.
+ // just update using the placeholder image in this case.
+ if (personUri == null) {
+ Log.wtf(LOG_TAG, "Uri is missing");
+ return;
+ }
+
+ // Added additional Cookie field in the callee to handle arguments
+ // sent to the callback function.
+
+ // setup arguments
+ WorkerArgs args = new WorkerArgs();
+ args.cookie = cookie;
+ args.context = context;
+ args.uri = personUri;
+ args.listener = listener;
+
+ // setup message arguments
+ Message msg = sThreadHandler.obtainMessage(token);
+ msg.arg1 = EVENT_LOAD_IMAGE;
+ msg.obj = args;
+
+ if (DBG) Log.d(LOG_TAG, "Begin loading image: " + args.uri +
+ ", displaying default image for now.");
+
+ // notify the thread to begin working
+ sThreadHandler.sendMessage(msg);
+ }
+
+
+}
diff --git a/src/com/android/phone/DTMFTwelveKeyDialer.java b/src/com/android/phone/DTMFTwelveKeyDialer.java
new file mode 100644
index 0000000..4afac55
--- /dev/null
+++ b/src/com/android/phone/DTMFTwelveKeyDialer.java
@@ -0,0 +1,1119 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.Settings;
+import android.telephony.PhoneNumberUtils;
+import android.text.Editable;
+import android.text.SpannableString;
+import android.text.method.DialerKeyListener;
+import android.text.style.RelativeSizeSpan;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.View.OnHoverListener;
+import android.view.accessibility.AccessibilityManager;
+import android.view.ViewStub;
+import android.widget.EditText;
+
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyCapabilities;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.Queue;
+
+
+/**
+ * Dialer class that encapsulates the DTMF twelve key behaviour.
+ * This model backs up the UI behaviour in DTMFTwelveKeyDialerView.java.
+ */
+public class DTMFTwelveKeyDialer implements View.OnTouchListener, View.OnKeyListener,
+ View.OnHoverListener, View.OnClickListener {
+ private static final String LOG_TAG = "DTMFTwelveKeyDialer";
+ private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ // events
+ private static final int PHONE_DISCONNECT = 100;
+ private static final int DTMF_SEND_CNF = 101;
+ private static final int DTMF_STOP = 102;
+
+ /** Accessibility manager instance used to check touch exploration state. */
+ private final AccessibilityManager mAccessibilityManager;
+
+ private CallManager mCM;
+ private ToneGenerator mToneGenerator;
+ private final Object mToneGeneratorLock = new Object();
+
+ // indicate if we want to enable the local tone playback.
+ private boolean mLocalToneEnabled;
+
+ // indicates that we are using automatically shortened DTMF tones
+ boolean mShortTone;
+
+ // indicate if the confirmation from TelephonyFW is pending.
+ private boolean mDTMFBurstCnfPending = false;
+
+ // Queue to queue the short dtmf characters.
+ private Queue<Character> mDTMFQueue = new LinkedList<Character>();
+
+ // Short Dtmf tone duration
+ private static final int DTMF_DURATION_MS = 120;
+
+
+ /** Hash Map to map a character to a tone*/
+ private static final HashMap<Character, Integer> mToneMap =
+ new HashMap<Character, Integer>();
+ /** Hash Map to map a view id to a character*/
+ private static final HashMap<Integer, Character> mDisplayMap =
+ new HashMap<Integer, Character>();
+ /** Set up the static maps*/
+ static {
+ // Map the key characters to tones
+ mToneMap.put('1', ToneGenerator.TONE_DTMF_1);
+ mToneMap.put('2', ToneGenerator.TONE_DTMF_2);
+ mToneMap.put('3', ToneGenerator.TONE_DTMF_3);
+ mToneMap.put('4', ToneGenerator.TONE_DTMF_4);
+ mToneMap.put('5', ToneGenerator.TONE_DTMF_5);
+ mToneMap.put('6', ToneGenerator.TONE_DTMF_6);
+ mToneMap.put('7', ToneGenerator.TONE_DTMF_7);
+ mToneMap.put('8', ToneGenerator.TONE_DTMF_8);
+ mToneMap.put('9', ToneGenerator.TONE_DTMF_9);
+ mToneMap.put('0', ToneGenerator.TONE_DTMF_0);
+ mToneMap.put('#', ToneGenerator.TONE_DTMF_P);
+ mToneMap.put('*', ToneGenerator.TONE_DTMF_S);
+
+ // Map the buttons to the display characters
+ mDisplayMap.put(R.id.one, '1');
+ mDisplayMap.put(R.id.two, '2');
+ mDisplayMap.put(R.id.three, '3');
+ mDisplayMap.put(R.id.four, '4');
+ mDisplayMap.put(R.id.five, '5');
+ mDisplayMap.put(R.id.six, '6');
+ mDisplayMap.put(R.id.seven, '7');
+ mDisplayMap.put(R.id.eight, '8');
+ mDisplayMap.put(R.id.nine, '9');
+ mDisplayMap.put(R.id.zero, '0');
+ mDisplayMap.put(R.id.pound, '#');
+ mDisplayMap.put(R.id.star, '*');
+ }
+
+ /** EditText field used to display the DTMF digits sent so far.
+ Note this is null in some modes (like during the CDMA OTA call,
+ where there's no onscreen "digits" display.) */
+ private EditText mDialpadDigits;
+
+ // InCallScreen reference.
+ private InCallScreen mInCallScreen;
+
+ /**
+ * The DTMFTwelveKeyDialerView we use to display the dialpad.
+ *
+ * Only one of mDialerView or mDialerStub will have a legitimate object; the other one will be
+ * null at that moment. Either of following scenarios will occur:
+ *
+ * - If the constructor with {@link DTMFTwelveKeyDialerView} is called, mDialerView will
+ * obtain that object, and mDialerStub will be null. mDialerStub won't be used in this case.
+ *
+ * - If the constructor with {@link ViewStub} is called, mDialerView will be null at that
+ * moment, and mDialerStub will obtain the ViewStub object.
+ * When the dialer is required by the user (i.e. until {@link #openDialer(boolean)} being
+ * called), mDialerStub will inflate the dialer, and make mDialerStub itself null.
+ * mDialerStub won't be used afterward.
+ */
+ private DTMFTwelveKeyDialerView mDialerView;
+
+ /**
+ * {@link ViewStub} holding {@link DTMFTwelveKeyDialerView}. See the comments for mDialerView.
+ */
+ private ViewStub mDialerStub;
+
+ // KeyListener used with the "dialpad digits" EditText widget.
+ private DTMFKeyListener mDialerKeyListener;
+
+ /**
+ * Our own key listener, specialized for dealing with DTMF codes.
+ * 1. Ignore the backspace since it is irrelevant.
+ * 2. Allow ONLY valid DTMF characters to generate a tone and be
+ * sent as a DTMF code.
+ * 3. All other remaining characters are handled by the superclass.
+ *
+ * This code is purely here to handle events from the hardware keyboard
+ * while the DTMF dialpad is up.
+ */
+ private class DTMFKeyListener extends DialerKeyListener {
+
+ private DTMFKeyListener() {
+ super();
+ }
+
+ /**
+ * Overriden to return correct DTMF-dialable characters.
+ */
+ @Override
+ protected char[] getAcceptedChars(){
+ return DTMF_CHARACTERS;
+ }
+
+ /** special key listener ignores backspace. */
+ @Override
+ public boolean backspace(View view, Editable content, int keyCode,
+ KeyEvent event) {
+ return false;
+ }
+
+ /**
+ * Return true if the keyCode is an accepted modifier key for the
+ * dialer (ALT or SHIFT).
+ */
+ private boolean isAcceptableModifierKey(int keyCode) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_ALT_LEFT:
+ case KeyEvent.KEYCODE_ALT_RIGHT:
+ case KeyEvent.KEYCODE_SHIFT_LEFT:
+ case KeyEvent.KEYCODE_SHIFT_RIGHT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Overriden so that with each valid button press, we start sending
+ * a dtmf code and play a local dtmf tone.
+ */
+ @Override
+ public boolean onKeyDown(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ // if (DBG) log("DTMFKeyListener.onKeyDown, keyCode " + keyCode + ", view " + view);
+
+ // find the character
+ char c = (char) lookup(event, content);
+
+ // if not a long press, and parent onKeyDown accepts the input
+ if (event.getRepeatCount() == 0 && super.onKeyDown(view, content, keyCode, event)) {
+
+ boolean keyOK = ok(getAcceptedChars(), c);
+
+ // if the character is a valid dtmf code, start playing the tone and send the
+ // code.
+ if (keyOK) {
+ if (DBG) log("DTMFKeyListener reading '" + c + "' from input.");
+ processDtmf(c);
+ } else if (DBG) {
+ log("DTMFKeyListener rejecting '" + c + "' from input.");
+ }
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Overriden so that with each valid button up, we stop sending
+ * a dtmf code and the dtmf tone.
+ */
+ @Override
+ public boolean onKeyUp(View view, Editable content,
+ int keyCode, KeyEvent event) {
+ // if (DBG) log("DTMFKeyListener.onKeyUp, keyCode " + keyCode + ", view " + view);
+
+ super.onKeyUp(view, content, keyCode, event);
+
+ // find the character
+ char c = (char) lookup(event, content);
+
+ boolean keyOK = ok(getAcceptedChars(), c);
+
+ if (keyOK) {
+ if (DBG) log("Stopping the tone for '" + c + "'");
+ stopTone();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handle individual keydown events when we DO NOT have an Editable handy.
+ */
+ public boolean onKeyDown(KeyEvent event) {
+ char c = lookup(event);
+ if (DBG) log("DTMFKeyListener.onKeyDown: event '" + c + "'");
+
+ // if not a long press, and parent onKeyDown accepts the input
+ if (event.getRepeatCount() == 0 && c != 0) {
+ // if the character is a valid dtmf code, start playing the tone and send the
+ // code.
+ if (ok(getAcceptedChars(), c)) {
+ if (DBG) log("DTMFKeyListener reading '" + c + "' from input.");
+ processDtmf(c);
+ return true;
+ } else if (DBG) {
+ log("DTMFKeyListener rejecting '" + c + "' from input.");
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Handle individual keyup events.
+ *
+ * @param event is the event we are trying to stop. If this is null,
+ * then we just force-stop the last tone without checking if the event
+ * is an acceptable dialer event.
+ */
+ public boolean onKeyUp(KeyEvent event) {
+ if (event == null) {
+ //the below piece of code sends stopDTMF event unnecessarily even when a null event
+ //is received, hence commenting it.
+ /*if (DBG) log("Stopping the last played tone.");
+ stopTone();*/
+ return true;
+ }
+
+ char c = lookup(event);
+ if (DBG) log("DTMFKeyListener.onKeyUp: event '" + c + "'");
+
+ // TODO: stopTone does not take in character input, we may want to
+ // consider checking for this ourselves.
+ if (ok(getAcceptedChars(), c)) {
+ if (DBG) log("Stopping the tone for '" + c + "'");
+ stopTone();
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Find the Dialer Key mapped to this event.
+ *
+ * @return The char value of the input event, otherwise
+ * 0 if no matching character was found.
+ */
+ private char lookup(KeyEvent event) {
+ // This code is similar to {@link DialerKeyListener#lookup(KeyEvent, Spannable) lookup}
+ int meta = event.getMetaState();
+ int number = event.getNumber();
+
+ if (!((meta & (KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON)) == 0) || (number == 0)) {
+ int match = event.getMatch(getAcceptedChars(), meta);
+ number = (match != 0) ? match : number;
+ }
+
+ return (char) number;
+ }
+
+ /**
+ * Check to see if the keyEvent is dialable.
+ */
+ boolean isKeyEventAcceptable (KeyEvent event) {
+ return (ok(getAcceptedChars(), lookup(event)));
+ }
+
+ /**
+ * Overrides the characters used in {@link DialerKeyListener#CHARACTERS}
+ * These are the valid dtmf characters.
+ */
+ public final char[] DTMF_CHARACTERS = new char[] {
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*'
+ };
+ }
+
+ /**
+ * Our own handler to take care of the messages from the phone state changes
+ */
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ // disconnect action
+ // make sure to close the dialer on ALL disconnect actions.
+ case PHONE_DISCONNECT:
+ if (DBG) log("disconnect message recieved, shutting down.");
+ // unregister since we are closing.
+ mCM.unregisterForDisconnect(this);
+ closeDialer(false);
+ break;
+ case DTMF_SEND_CNF:
+ if (DBG) log("dtmf confirmation received from FW.");
+ // handle burst dtmf confirmation
+ handleBurstDtmfConfirmation();
+ break;
+ case DTMF_STOP:
+ if (DBG) log("dtmf stop received");
+ stopTone();
+ break;
+ }
+ }
+ };
+
+
+ /**
+ * DTMFTwelveKeyDialer constructor with {@link DTMFTwelveKeyDialerView}
+ *
+ * @param parent the InCallScreen instance that owns us.
+ * @param dialerView the DTMFTwelveKeyDialerView we should use to display the dialpad.
+ */
+ public DTMFTwelveKeyDialer(InCallScreen parent,
+ DTMFTwelveKeyDialerView dialerView) {
+ this(parent);
+
+ // The passed-in DTMFTwelveKeyDialerView *should* always be
+ // non-null, now that the in-call UI uses only portrait mode.
+ if (dialerView == null) {
+ Log.e(LOG_TAG, "DTMFTwelveKeyDialer: null dialerView!", new IllegalStateException());
+ // ...continue as best we can, although things will
+ // be pretty broken without the mDialerView UI elements!
+ }
+ mDialerView = dialerView;
+ if (DBG) log("- Got passed-in mDialerView: " + mDialerView);
+
+ if (mDialerView != null) {
+ setupDialerView();
+ }
+ }
+
+ /**
+ * DTMFTwelveKeyDialer constructor with {@link ViewStub}.
+ *
+ * When the dialer is required for the first time (e.g. when {@link #openDialer(boolean)} is
+ * called), the object will inflate the ViewStub by itself, assuming the ViewStub will return
+ * {@link DTMFTwelveKeyDialerView} on {@link ViewStub#inflate()}.
+ *
+ * @param parent the InCallScreen instance that owns us.
+ * @param dialerStub ViewStub which will return {@link DTMFTwelveKeyDialerView} on
+ * {@link ViewStub#inflate()}.
+ */
+ public DTMFTwelveKeyDialer(InCallScreen parent, ViewStub dialerStub) {
+ this(parent);
+
+ mDialerStub = dialerStub;
+ if (DBG) log("- Got passed-in mDialerStub: " + mDialerStub);
+
+ // At this moment mDialerView is still null. We delay calling setupDialerView().
+ }
+
+ /**
+ * Private constructor used for initialization calls common to all public
+ * constructors.
+ *
+ * @param parent the InCallScreen instance that owns us.
+ */
+ private DTMFTwelveKeyDialer(InCallScreen parent) {
+ if (DBG) log("DTMFTwelveKeyDialer constructor... this = " + this);
+
+ mInCallScreen = parent;
+ mCM = PhoneGlobals.getInstance().mCM;
+ mAccessibilityManager = (AccessibilityManager) parent.getSystemService(
+ Context.ACCESSIBILITY_SERVICE);
+ }
+
+ /**
+ * Prepare the dialer view and relevant variables.
+ */
+ private void setupDialerView() {
+ if (DBG) log("setupDialerView()");
+ mDialerView.setDialer(this);
+
+ // In the normal in-call DTMF dialpad, mDialpadDigits is an
+ // EditText used to display the digits the user has typed so
+ // far. But some other modes (like the OTA call) have no
+ // "digits" display at all, in which case mDialpadDigits will
+ // be null.
+ mDialpadDigits = (EditText) mDialerView.findViewById(R.id.dtmfDialerField);
+ if (mDialpadDigits != null) {
+ mDialerKeyListener = new DTMFKeyListener();
+ mDialpadDigits.setKeyListener(mDialerKeyListener);
+
+ // remove the long-press context menus that support
+ // the edit (copy / paste / select) functions.
+ mDialpadDigits.setLongClickable(false);
+ }
+
+ // Hook up touch / key listeners for the buttons in the onscreen
+ // keypad.
+ setupKeypad(mDialerView);
+ }
+
+ /**
+ * Null out our reference to the InCallScreen activity.
+ * This indicates that the InCallScreen activity has been destroyed.
+ * At the same time, get rid of listeners since we're not going to
+ * be valid anymore.
+ */
+ /* package */ void clearInCallScreenReference() {
+ if (DBG) log("clearInCallScreenReference()...");
+ mInCallScreen = null;
+ mDialerKeyListener = null;
+ mHandler.removeMessages(DTMF_SEND_CNF);
+ synchronized (mDTMFQueue) {
+ mDTMFBurstCnfPending = false;
+ mDTMFQueue.clear();
+ }
+ closeDialer(false);
+ }
+
+ /**
+ * Dialer code that runs when the dialer is brought up.
+ * This includes layout changes, etc, and just prepares the dialer model for use.
+ */
+ private void onDialerOpen(boolean animate) {
+ if (DBG) log("onDialerOpen()...");
+
+ // Any time the dialer is open, listen for "disconnect" events (so
+ // we can close ourself.)
+ mCM.registerForDisconnect(mHandler, PHONE_DISCONNECT, null);
+
+ // On some devices the screen timeout is set to a special value
+ // while the dialpad is up.
+ PhoneGlobals.getInstance().updateWakeState();
+
+ // Give the InCallScreen a chance to do any necessary UI updates.
+ if (mInCallScreen != null) {
+ mInCallScreen.onDialerOpen(animate);
+ } else {
+ Log.e(LOG_TAG, "InCallScreen object was null during onDialerOpen()");
+ }
+ }
+
+ /**
+ * Allocates some resources we keep around during a "dialer session".
+ *
+ * (Currently, a "dialer session" just means any situation where we
+ * might need to play local DTMF tones, which means that we need to
+ * keep a ToneGenerator instance around. A ToneGenerator instance
+ * keeps an AudioTrack resource busy in AudioFlinger, so we don't want
+ * to keep it around forever.)
+ *
+ * Call {@link stopDialerSession} to release the dialer session
+ * resources.
+ */
+ public void startDialerSession() {
+ if (DBG) log("startDialerSession()... this = " + this);
+
+ // see if we need to play local tones.
+ if (PhoneGlobals.getInstance().getResources().getBoolean(R.bool.allow_local_dtmf_tones)) {
+ mLocalToneEnabled = Settings.System.getInt(mInCallScreen.getContentResolver(),
+ Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
+ } else {
+ mLocalToneEnabled = false;
+ }
+ if (DBG) log("- startDialerSession: mLocalToneEnabled = " + mLocalToneEnabled);
+
+ // create the tone generator
+ // if the mToneGenerator creation fails, just continue without it. It is
+ // a local audio signal, and is not as important as the dtmf tone itself.
+ if (mLocalToneEnabled) {
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator == null) {
+ try {
+ mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF, 80);
+ } catch (RuntimeException e) {
+ if (DBG) log("Exception caught while creating local tone generator: " + e);
+ mToneGenerator = null;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Dialer code that runs when the dialer is closed.
+ * This releases resources acquired when we start the dialer.
+ */
+ private void onDialerClose(boolean animate) {
+ if (DBG) log("onDialerClose()...");
+
+ // reset back to a short delay for the poke lock.
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ app.updateWakeState();
+
+ mCM.unregisterForDisconnect(mHandler);
+
+ // Give the InCallScreen a chance to do any necessary UI updates.
+ if (mInCallScreen != null) {
+ mInCallScreen.onDialerClose(animate);
+ } else {
+ Log.e(LOG_TAG, "InCallScreen object was null during onDialerClose()");
+ }
+ }
+
+ /**
+ * Releases resources we keep around during a "dialer session"
+ * (see {@link startDialerSession}).
+ *
+ * It's safe to call this even without a corresponding
+ * startDialerSession call.
+ */
+ public void stopDialerSession() {
+ // release the tone generator.
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator != null) {
+ mToneGenerator.release();
+ mToneGenerator = null;
+ }
+ }
+ }
+
+ /**
+ * Called externally (from InCallScreen) to play a DTMF Tone.
+ */
+ public boolean onDialerKeyDown(KeyEvent event) {
+ if (DBG) log("Notifying dtmf key down.");
+ if (mDialerKeyListener != null) {
+ return mDialerKeyListener.onKeyDown(event);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Called externally (from InCallScreen) to cancel the last DTMF Tone played.
+ */
+ public boolean onDialerKeyUp(KeyEvent event) {
+ if (DBG) log("Notifying dtmf key up.");
+ if (mDialerKeyListener != null) {
+ return mDialerKeyListener.onKeyUp(event);
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * setup the keys on the dialer activity, using the keymaps.
+ */
+ private void setupKeypad(DTMFTwelveKeyDialerView dialerView) {
+ // for each view id listed in the displaymap
+ View button;
+ for (int viewId : mDisplayMap.keySet()) {
+ // locate the view
+ button = dialerView.findViewById(viewId);
+ // Setup the listeners for the buttons
+ button.setOnTouchListener(this);
+ button.setClickable(true);
+ button.setOnKeyListener(this);
+ button.setOnHoverListener(this);
+ button.setOnClickListener(this);
+ }
+ }
+
+ /**
+ * catch the back and call buttons to return to the in call activity.
+ */
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // if (DBG) log("onKeyDown: keyCode " + keyCode);
+ switch (keyCode) {
+ // finish for these events
+ case KeyEvent.KEYCODE_BACK:
+ case KeyEvent.KEYCODE_CALL:
+ if (DBG) log("exit requested");
+ closeDialer(true); // do the "closing" animation
+ return true;
+ }
+ return mInCallScreen.onKeyDown(keyCode, event);
+ }
+
+ /**
+ * catch the back and call buttons to return to the in call activity.
+ */
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ // if (DBG) log("onKeyUp: keyCode " + keyCode);
+ return mInCallScreen.onKeyUp(keyCode, event);
+ }
+
+ /**
+ * Implemented for {@link android.view.View.OnHoverListener}. Handles touch
+ * events for accessibility when touch exploration is enabled.
+ */
+ @Override
+ public boolean onHover(View v, MotionEvent event) {
+ // When touch exploration is turned on, lifting a finger while inside
+ // the button's hover target bounds should perform a click action.
+ if (mAccessibilityManager.isEnabled()
+ && mAccessibilityManager.isTouchExplorationEnabled()) {
+ final int left = v.getPaddingLeft();
+ final int right = (v.getWidth() - v.getPaddingRight());
+ final int top = v.getPaddingTop();
+ final int bottom = (v.getHeight() - v.getPaddingBottom());
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ // Lift-to-type temporarily disables double-tap activation.
+ v.setClickable(false);
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+ if ((x > left) && (x < right) && (y > top) && (y < bottom)) {
+ v.performClick();
+ }
+ v.setClickable(true);
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onClick(View v) {
+ // When accessibility is on, simulate press and release to preserve the
+ // semantic meaning of performClick(). Required for Braille support.
+ if (mAccessibilityManager.isEnabled()) {
+ final int id = v.getId();
+ // Checking the press state prevents double activation.
+ if (!v.isPressed() && mDisplayMap.containsKey(id)) {
+ processDtmf(mDisplayMap.get(id), true /* timedShortTone */);
+ }
+ }
+ }
+
+ /**
+ * Implemented for the TouchListener, process the touch events.
+ */
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ int viewId = v.getId();
+
+ // if the button is recognized
+ if (mDisplayMap.containsKey(viewId)) {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ // Append the character mapped to this button, to the display.
+ // start the tone
+ processDtmf(mDisplayMap.get(viewId));
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ // stop the tone on ANY other event, except for MOVE.
+ stopTone();
+ break;
+ }
+ // do not return true [handled] here, since we want the
+ // press / click animation to be handled by the framework.
+ }
+ return false;
+ }
+
+ /**
+ * Implements View.OnKeyListener for the DTMF buttons. Enables dialing with trackball/dpad.
+ */
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ // if (DBG) log("onKey: keyCode " + keyCode + ", view " + v);
+
+ if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {
+ int viewId = v.getId();
+ if (mDisplayMap.containsKey(viewId)) {
+ switch (event.getAction()) {
+ case KeyEvent.ACTION_DOWN:
+ if (event.getRepeatCount() == 0) {
+ processDtmf(mDisplayMap.get(viewId));
+ }
+ break;
+ case KeyEvent.ACTION_UP:
+ stopTone();
+ break;
+ }
+ // do not return true [handled] here, since we want the
+ // press / click animation to be handled by the framework.
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the dialer is in "open" state, meaning it is already visible *and* it
+ * isn't fading out. Note that during fade-out animation the View will return VISIBLE but
+ * will become GONE soon later, so you would want to use this method instead of
+ * {@link View#getVisibility()}.
+ *
+ * Fade-in animation, on the other hand, will set the View's visibility VISIBLE soon after
+ * the request, so we don't need to take care much of it. In other words,
+ * {@link #openDialer(boolean)} soon makes the visibility VISIBLE and thus this method will
+ * return true just after the method call.
+ *
+ * Note: during the very early stage of "open" state, users may not see the dialpad yet because
+ * of its fading-in animation, while they will see it shortly anyway. Similarly, during the
+ * early stage of "closed" state (opposite of "open" state), users may still see the dialpad
+ * due to fading-out animation, but it will vanish shortly and thus we can treat it as "closed",
+ * or "not open". To make the transition clearer, we call the state "open", not "shown" nor
+ * "visible".
+ */
+ public boolean isOpened() {
+ // Return whether or not the dialer view is visible.
+ // (Note that if we're in the middle of a fade-out animation, that
+ // also counts as "not visible" even though mDialerView itself is
+ // technically still VISIBLE.)
+ return (mDialerView != null
+ &&(mDialerView.getVisibility() == View.VISIBLE)
+ && !AnimationUtils.Fade.isFadingOut(mDialerView));
+ }
+
+ /**
+ * Forces the dialer into the "open" state.
+ * Does nothing if the dialer is already open.
+ *
+ * The "open" state includes the state the dialer is fading in.
+ * {@link InCallScreen#onDialerOpen(boolean)} will change visibility state and do
+ * actual animation.
+ *
+ * @param animate if true, open the dialer with an animation.
+ *
+ * @see #isOpened
+ */
+ public void openDialer(boolean animate) {
+ if (DBG) log("openDialer()...");
+
+ if (mDialerView == null && mDialerStub != null) {
+ if (DBG) log("Dialer isn't ready. Inflate it from ViewStub.");
+ mDialerView = (DTMFTwelveKeyDialerView) mDialerStub.inflate();
+ setupDialerView();
+ mDialerStub = null;
+ }
+
+ if (!isOpened()) {
+ // Make the dialer view visible.
+ if (animate) {
+ AnimationUtils.Fade.show(mDialerView);
+ } else {
+ mDialerView.setVisibility(View.VISIBLE);
+ }
+ onDialerOpen(animate);
+ }
+ }
+
+ /**
+ * Forces the dialer into the "closed" state.
+ * Does nothing if the dialer is already closed.
+ *
+ * {@link InCallScreen#onDialerOpen(boolean)} will change visibility state and do
+ * actual animation.
+ *
+ * @param animate if true, close the dialer with an animation.
+ *
+ * @see #isOpened
+ */
+ public void closeDialer(boolean animate) {
+ if (DBG) log("closeDialer()...");
+
+ if (isOpened()) {
+ // Hide the dialer view.
+ if (animate) {
+ AnimationUtils.Fade.hide(mDialerView, View.GONE);
+ } else {
+ mDialerView.setVisibility(View.GONE);
+ }
+ onDialerClose(animate);
+ }
+ }
+
+ /**
+ * 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.
+ *
+ * @see #processDtmf(char, boolean)
+ */
+ private final void processDtmf(char c) {
+ processDtmf(c, false);
+ }
+
+ /**
+ * Processes the specified digit as a DTMF key, by playing the appropriate
+ * DTMF tone (or short tone if requested), and appending the digit to the
+ * EditText field that displays the DTMF digits sent so far.
+ */
+ private final void processDtmf(char c, boolean timedShortTone) {
+ // if it is a valid key, then update the display and send the dtmf tone.
+ if (PhoneNumberUtils.is12Key(c)) {
+ if (DBG) log("updating display and sending dtmf tone for '" + c + "'");
+
+ // Append this key to the "digits" widget.
+ if (mDialpadDigits != 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.)
+ mDialpadDigits.getText().append(c);
+ }
+
+ // Play the tone if it exists.
+ if (mToneMap.containsKey(c)) {
+ // begin tone playback.
+ startTone(c, timedShortTone);
+ }
+ } else if (DBG) {
+ log("ignoring dtmf request for '" + c + "'");
+ }
+
+ // Any DTMF keypress counts as explicit "user activity".
+ PhoneGlobals.getInstance().pokeUserActivity();
+ }
+
+ /**
+ * Clears out the display of "DTMF digits typed so far" that's kept in
+ * mDialpadDigits.
+ *
+ * The InCallScreen is responsible for calling this method any time a
+ * new call becomes active (or, more simply, any time a call ends).
+ * This is how we make sure that the "history" of DTMF digits you type
+ * doesn't persist from one call to the next.
+ *
+ * TODO: it might be more elegent if the dialpad itself could remember
+ * the call that we're associated with, and clear the digits if the
+ * "current call" has changed since last time. (This would require
+ * some unique identifier that's different for each call. We can't
+ * just use the foreground Call object, since that's a singleton that
+ * lasts the whole life of the phone process. Instead, maybe look at
+ * the Connection object that comes back from getEarliestConnection()?
+ * Or getEarliestConnectTime()?)
+ *
+ * Or to be even fancier, we could keep a mapping of *multiple*
+ * "active calls" to DTMF strings. That way you could have two lines
+ * in use and swap calls multiple times, and we'd still remember the
+ * digits for each call. (But that's such an obscure use case that
+ * it's probably not worth the extra complexity.)
+ */
+ public void clearDigits() {
+ if (DBG) log("clearDigits()...");
+
+ if (mDialpadDigits != null) {
+ mDialpadDigits.setText("");
+ }
+
+ setDialpadContext("");
+ }
+
+ /**
+ * Set the context text (hint) to show in the dialpad Digits EditText.
+ *
+ * This is currently only used for displaying a value for "Voice Mail"
+ * calls since they default to the dialpad and we want to give users better
+ * context when they dial voicemail.
+ *
+ * TODO: Is there value in extending this functionality for all contacts
+ * and not just Voice Mail calls?
+ * TODO: This should include setting the digits as well as the context
+ * once we start saving the digits properly...and properly in this case
+ * ideally means moving some of processDtmf() out of this class.
+ */
+ public void setDialpadContext(String contextValue) {
+ if (mDialpadDigits != null) {
+ if (contextValue == null) {
+ contextValue = "";
+ }
+ final SpannableString hint = new SpannableString(contextValue);
+ hint.setSpan(new RelativeSizeSpan(0.8f), 0, hint.length(), 0);
+ mDialpadDigits.setHint(hint);
+ }
+ }
+
+ /**
+ * Plays the local tone based the phone type.
+ */
+ public void startTone(char c, boolean timedShortTone) {
+ // Only play the tone if it exists.
+ if (!mToneMap.containsKey(c)) {
+ return;
+ }
+
+ if (!mInCallScreen.okToDialDTMFTones()) {
+ return;
+ }
+
+ // Read the settings as it may be changed by the user during the call
+ Phone phone = mCM.getFgPhone();
+ mShortTone = useShortDtmfTones(phone, phone.getContext());
+
+ // Before we go ahead and start a tone, we need to make sure that any pending
+ // stop-tone message is processed.
+ if (mHandler.hasMessages(DTMF_STOP)) {
+ mHandler.removeMessages(DTMF_STOP);
+ stopTone();
+ }
+
+ if (DBG) log("startDtmfTone()...");
+
+ // For Short DTMF we need to play the local tone for fixed duration
+ if (mShortTone) {
+ sendShortDtmfToNetwork(c);
+ } else {
+ // Pass as a char to be sent to network
+ if (DBG) log("send long dtmf for " + c);
+ mCM.startDtmf(c);
+
+ // If it is a timed tone, queue up the stop command in DTMF_DURATION_MS.
+ if (timedShortTone) {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(DTMF_STOP), DTMF_DURATION_MS);
+ }
+ }
+ startLocalToneIfNeeded(c);
+ }
+
+
+ /**
+ * Plays the local tone based the phone type, optionally forcing a short
+ * tone.
+ */
+ public void startLocalToneIfNeeded(char c) {
+ // if local tone playback is enabled, start it.
+ // Only play the tone if it exists.
+ if (!mToneMap.containsKey(c)) {
+ return;
+ }
+ if (mLocalToneEnabled) {
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator == null) {
+ if (DBG) log("startDtmfTone: mToneGenerator == null, tone: " + c);
+ } else {
+ if (DBG) log("starting local tone " + c);
+ int toneDuration = -1;
+ if (mShortTone) {
+ toneDuration = DTMF_DURATION_MS;
+ }
+ mToneGenerator.startTone(mToneMap.get(c), toneDuration);
+ }
+ }
+ }
+ }
+
+ /**
+ * Check to see if the keyEvent is dialable.
+ */
+ boolean isKeyEventAcceptable (KeyEvent event) {
+ return (mDialerKeyListener != null && mDialerKeyListener.isKeyEventAcceptable(event));
+ }
+
+ /**
+ * static logging method
+ */
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+
+ /**
+ * Stops the local tone based on the phone type.
+ */
+ public void stopTone() {
+ // We do not rely on InCallScreen#okToDialDTMFTones() here since it is ok to stop tones
+ // without starting them.
+
+ if (!mShortTone) {
+ if (DBG) log("stopping remote tone.");
+ mCM.stopDtmf();
+ stopLocalToneIfNeeded();
+ }
+ }
+
+ /**
+ * Stops the local tone based on the phone type.
+ */
+ public void stopLocalToneIfNeeded() {
+ if (!mShortTone) {
+ // if local tone playback is enabled, stop it.
+ if (DBG) log("trying to stop local tone...");
+ if (mLocalToneEnabled) {
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator == null) {
+ if (DBG) log("stopLocalTone: mToneGenerator == null");
+ } else {
+ if (DBG) log("stopping local tone.");
+ mToneGenerator.stopTone();
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Sends the dtmf character over the network for short DTMF settings
+ * When the characters are entered in quick succession,
+ * the characters are queued before sending over the network.
+ */
+ private void sendShortDtmfToNetwork(char dtmfDigit) {
+ synchronized (mDTMFQueue) {
+ if (mDTMFBurstCnfPending == true) {
+ // Insert the dtmf char to the queue
+ mDTMFQueue.add(new Character(dtmfDigit));
+ } else {
+ String dtmfStr = Character.toString(dtmfDigit);
+ mCM.sendBurstDtmf(dtmfStr, 0, 0, mHandler.obtainMessage(DTMF_SEND_CNF));
+ // Set flag to indicate wait for Telephony confirmation.
+ mDTMFBurstCnfPending = true;
+ }
+ }
+ }
+
+ /**
+ * Handles Burst Dtmf Confirmation from the Framework.
+ */
+ void handleBurstDtmfConfirmation() {
+ Character dtmfChar = null;
+ synchronized (mDTMFQueue) {
+ mDTMFBurstCnfPending = false;
+ if (!mDTMFQueue.isEmpty()) {
+ dtmfChar = mDTMFQueue.remove();
+ Log.i(LOG_TAG, "The dtmf character removed from queue" + dtmfChar);
+ }
+ }
+ if (dtmfChar != null) {
+ sendShortDtmfToNetwork(dtmfChar);
+ }
+ }
+
+ /**
+ * On GSM devices, we never use short tones.
+ * On CDMA devices, it depends upon the settings.
+ */
+ private static boolean useShortDtmfTones(Phone phone, Context context) {
+ int phoneType = phone.getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+ return false;
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ int toneType = android.provider.Settings.System.getInt(
+ context.getContentResolver(),
+ Settings.System.DTMF_TONE_TYPE_WHEN_DIALING,
+ Constants.DTMF_TONE_TYPE_NORMAL);
+ if (toneType == Constants.DTMF_TONE_TYPE_NORMAL) {
+ return true;
+ } else {
+ return false;
+ }
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_SIP) {
+ return false;
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ }
+
+}
diff --git a/src/com/android/phone/DTMFTwelveKeyDialerView.java b/src/com/android/phone/DTMFTwelveKeyDialerView.java
new file mode 100644
index 0000000..e0502b7
--- /dev/null
+++ b/src/com/android/phone/DTMFTwelveKeyDialerView.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.FocusFinder;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import java.util.ArrayList;
+
+/**
+ * DTMFTwelveKeyDialerView is the view logic that the DTMFDialer uses.
+ * This is really a thin wrapper around Linear Layout that intercepts
+ * some user interactions to provide the correct UI behaviour for the
+ * dialer.
+ *
+ * See dtmf_twelve_key_dialer_view.xml.
+ */
+class DTMFTwelveKeyDialerView extends LinearLayout {
+
+ private static final String LOG_TAG = "PHONE/DTMFTwelveKeyDialerView";
+ private static final boolean DBG = false;
+
+ private DTMFTwelveKeyDialer mDialer;
+
+
+ public DTMFTwelveKeyDialerView (Context context) {
+ super(context);
+ }
+
+ public DTMFTwelveKeyDialerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ void setDialer (DTMFTwelveKeyDialer dialer) {
+ mDialer = dialer;
+ }
+
+ /**
+ * Normally we ignore everything except for the BACK and CALL keys.
+ * For those, we pass them to the model (and then the InCallScreen).
+ */
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ if (DBG) log("dispatchKeyEvent(" + event + ")...");
+
+ int keyCode = event.getKeyCode();
+ if (mDialer != null) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ case KeyEvent.KEYCODE_CALL:
+ return event.isDown() ? mDialer.onKeyDown(keyCode, event) :
+ mDialer.onKeyUp(keyCode, event);
+ }
+ }
+
+ if (DBG) log("==> dispatchKeyEvent: forwarding event to the DTMFDialer");
+ return super.dispatchKeyEvent(event);
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/DefaultRingtonePreference.java b/src/com/android/phone/DefaultRingtonePreference.java
new file mode 100644
index 0000000..8205fd0
--- /dev/null
+++ b/src/com/android/phone/DefaultRingtonePreference.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.preference.RingtonePreference;
+import android.util.AttributeSet;
+
+/**
+ * RingtonePreference which doesn't show default ringtone setting.
+ *
+ * @see com.android.settings.DefaultRingtonePreference
+ */
+public class DefaultRingtonePreference extends RingtonePreference {
+ public DefaultRingtonePreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onPrepareRingtonePickerIntent(Intent ringtonePickerIntent) {
+ super.onPrepareRingtonePickerIntent(ringtonePickerIntent);
+
+ /*
+ * Since this preference is for choosing the default ringtone, it
+ * doesn't make sense to show a 'Default' item.
+ */
+ ringtonePickerIntent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false);
+ }
+
+ @Override
+ protected void onSaveRingtone(Uri ringtoneUri) {
+ RingtoneManager.setActualDefaultRingtoneUri(getContext(), getRingtoneType(), ringtoneUri);
+ }
+
+ @Override
+ protected Uri onRestoreRingtone() {
+ return RingtoneManager.getActualDefaultRingtoneUri(getContext(), getRingtoneType());
+ }
+}
diff --git a/src/com/android/phone/DeleteFdnContactScreen.java b/src/com/android/phone/DeleteFdnContactScreen.java
new file mode 100644
index 0000000..074078c
--- /dev/null
+++ b/src/com/android/phone/DeleteFdnContactScreen.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.Activity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Window;
+import android.widget.Toast;
+
+import static android.view.Window.PROGRESS_VISIBILITY_OFF;
+import static android.view.Window.PROGRESS_VISIBILITY_ON;
+
+/**
+ * Activity to let the user delete an FDN contact.
+ */
+public class DeleteFdnContactScreen extends Activity {
+ private static final String LOG_TAG = PhoneGlobals.LOG_TAG;
+ private static final boolean DBG = false;
+
+ private static final String INTENT_EXTRA_NAME = "name";
+ private static final String INTENT_EXTRA_NUMBER = "number";
+
+ private static final int PIN2_REQUEST_CODE = 100;
+
+ private String mName;
+ private String mNumber;
+ private String mPin2;
+
+ protected QueryHandler mQueryHandler;
+
+ private Handler mHandler = new Handler();
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ resolveIntent();
+
+ authenticatePin2();
+
+ getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ setContentView(R.layout.delete_fdn_contact_screen);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (DBG) log("onActivityResult");
+
+ switch (requestCode) {
+ case PIN2_REQUEST_CODE:
+ Bundle extras = (intent != null) ? intent.getExtras() : null;
+ if (extras != null) {
+ mPin2 = extras.getString("pin2");
+ showStatus(getResources().getText(
+ R.string.deleting_fdn_contact));
+ deleteContact();
+ } else {
+ // if they cancelled, then we just cancel too.
+ if (DBG) log("onActivityResult: CANCELLED");
+ displayProgress(false);
+ finish();
+ }
+ break;
+ }
+ }
+
+ private void resolveIntent() {
+ Intent intent = getIntent();
+
+ mName = intent.getStringExtra(INTENT_EXTRA_NAME);
+ mNumber = intent.getStringExtra(INTENT_EXTRA_NUMBER);
+
+ if (TextUtils.isEmpty(mNumber)) {
+ finish();
+ }
+ }
+
+ private void deleteContact() {
+ StringBuilder buf = new StringBuilder();
+ if (TextUtils.isEmpty(mName)) {
+ buf.append("number='");
+ } else {
+ buf.append("tag='");
+ buf.append(mName);
+ buf.append("' AND number='");
+ }
+ buf.append(mNumber);
+ buf.append("' AND pin2='");
+ buf.append(mPin2);
+ buf.append("'");
+
+ Uri uri = Uri.parse("content://icc/fdn");
+
+ mQueryHandler = new QueryHandler(getContentResolver());
+ mQueryHandler.startDelete(0, null, uri, buf.toString(), null);
+ displayProgress(true);
+ }
+
+ private void authenticatePin2() {
+ Intent intent = new Intent();
+ intent.setClass(this, GetPin2Screen.class);
+ startActivityForResult(intent, PIN2_REQUEST_CODE);
+ }
+
+ private void displayProgress(boolean flag) {
+ getWindow().setFeatureInt(
+ Window.FEATURE_INDETERMINATE_PROGRESS,
+ flag ? PROGRESS_VISIBILITY_ON : PROGRESS_VISIBILITY_OFF);
+ }
+
+ // Replace the status field with a toast to make things appear similar
+ // to the rest of the settings. Removed the useless status field.
+ private void showStatus(CharSequence statusMsg) {
+ if (statusMsg != null) {
+ Toast.makeText(this, statusMsg, Toast.LENGTH_SHORT)
+ .show();
+ }
+ }
+
+ private void handleResult(boolean success) {
+ if (success) {
+ if (DBG) log("handleResult: success!");
+ showStatus(getResources().getText(R.string.fdn_contact_deleted));
+ } else {
+ if (DBG) log("handleResult: failed!");
+ showStatus(getResources().getText(R.string.pin2_invalid));
+ }
+
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ finish();
+ }
+ }, 2000);
+
+ }
+
+ private class QueryHandler extends AsyncQueryHandler {
+ public QueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor c) {
+ }
+
+ @Override
+ protected void onInsertComplete(int token, Object cookie, Uri uri) {
+ }
+
+ @Override
+ protected void onUpdateComplete(int token, Object cookie, int result) {
+ }
+
+ @Override
+ protected void onDeleteComplete(int token, Object cookie, int result) {
+ if (DBG) log("onDeleteComplete");
+ displayProgress(false);
+ handleResult(result > 0);
+ }
+
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, "[DeleteFdnContact] " + msg);
+ }
+}
diff --git a/src/com/android/phone/EditFdnContactScreen.java b/src/com/android/phone/EditFdnContactScreen.java
new file mode 100644
index 0000000..2992b7d
--- /dev/null
+++ b/src/com/android/phone/EditFdnContactScreen.java
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import static android.view.Window.PROGRESS_VISIBILITY_OFF;
+import static android.view.Window.PROGRESS_VISIBILITY_ON;
+
+import android.app.Activity;
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.Contacts.PeopleColumns;
+import android.provider.Contacts.PhonesColumns;
+import android.telephony.PhoneNumberUtils;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.method.DialerKeyListener;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.Window;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+/**
+ * Activity to let the user add or edit an FDN contact.
+ */
+public class EditFdnContactScreen extends Activity {
+ private static final String LOG_TAG = PhoneGlobals.LOG_TAG;
+ private static final boolean DBG = false;
+
+ // Menu item codes
+ private static final int MENU_IMPORT = 1;
+ private static final int MENU_DELETE = 2;
+
+ private static final String INTENT_EXTRA_NAME = "name";
+ private static final String INTENT_EXTRA_NUMBER = "number";
+
+ private static final int PIN2_REQUEST_CODE = 100;
+
+ private String mName;
+ private String mNumber;
+ private String mPin2;
+ private boolean mAddContact;
+ private QueryHandler mQueryHandler;
+
+ private EditText mNameField;
+ private EditText mNumberField;
+ private LinearLayout mPinFieldContainer;
+ private Button mButton;
+
+ private Handler mHandler = new Handler();
+
+ /**
+ * Constants used in importing from contacts
+ */
+ /** request code when invoking subactivity */
+ private static final int CONTACTS_PICKER_CODE = 200;
+ /** projection for phone number query */
+ private static final String NUM_PROJECTION[] = {PeopleColumns.DISPLAY_NAME,
+ PhonesColumns.NUMBER};
+ /** static intent to invoke phone number picker */
+ private static final Intent CONTACT_IMPORT_INTENT;
+ static {
+ CONTACT_IMPORT_INTENT = new Intent(Intent.ACTION_GET_CONTENT);
+ CONTACT_IMPORT_INTENT.setType(android.provider.Contacts.Phones.CONTENT_ITEM_TYPE);
+ }
+ /** flag to track saving state */
+ private boolean mDataBusy;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ resolveIntent();
+
+ getWindow().requestFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ setContentView(R.layout.edit_fdn_contact_screen);
+ setupView();
+ setTitle(mAddContact ?
+ R.string.add_fdn_contact : R.string.edit_fdn_contact);
+
+ displayProgress(false);
+ }
+
+ /**
+ * We now want to bring up the pin request screen AFTER the
+ * contact information is displayed, to help with user
+ * experience.
+ *
+ * Also, process the results from the contact picker.
+ */
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (DBG) log("onActivityResult request:" + requestCode + " result:" + resultCode);
+
+ switch (requestCode) {
+ case PIN2_REQUEST_CODE:
+ Bundle extras = (intent != null) ? intent.getExtras() : null;
+ if (extras != null) {
+ mPin2 = extras.getString("pin2");
+ if (mAddContact) {
+ addContact();
+ } else {
+ updateContact();
+ }
+ } else if (resultCode != RESULT_OK) {
+ // if they cancelled, then we just cancel too.
+ if (DBG) log("onActivityResult: cancelled.");
+ finish();
+ }
+ break;
+
+ // look for the data associated with this number, and update
+ // the display with it.
+ case CONTACTS_PICKER_CODE:
+ if (resultCode != RESULT_OK) {
+ if (DBG) log("onActivityResult: cancelled.");
+ return;
+ }
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(intent.getData(),
+ NUM_PROJECTION, null, null, null);
+ if ((cursor == null) || (!cursor.moveToFirst())) {
+ Log.w(LOG_TAG,"onActivityResult: bad contact data, no results found.");
+ return;
+ }
+ mNameField.setText(cursor.getString(0));
+ mNumberField.setText(cursor.getString(1));
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ break;
+ }
+ }
+
+ /**
+ * Overridden to display the import and delete commands.
+ */
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ Resources r = getResources();
+
+ // Added the icons to the context menu
+ menu.add(0, MENU_IMPORT, 0, r.getString(R.string.importToFDNfromContacts))
+ .setIcon(R.drawable.ic_menu_contact);
+ menu.add(0, MENU_DELETE, 0, r.getString(R.string.menu_delete))
+ .setIcon(android.R.drawable.ic_menu_delete);
+ return true;
+ }
+
+ /**
+ * Allow the menu to be opened ONLY if we're not busy.
+ */
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ boolean result = super.onPrepareOptionsMenu(menu);
+ return mDataBusy ? false : result;
+ }
+
+ /**
+ * Overridden to allow for handling of delete and import.
+ */
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case MENU_IMPORT:
+ startActivityForResult(CONTACT_IMPORT_INTENT, CONTACTS_PICKER_CODE);
+ return true;
+
+ case MENU_DELETE:
+ deleteSelected();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void resolveIntent() {
+ Intent intent = getIntent();
+
+ mName = intent.getStringExtra(INTENT_EXTRA_NAME);
+ mNumber = intent.getStringExtra(INTENT_EXTRA_NUMBER);
+
+ mAddContact = TextUtils.isEmpty(mNumber);
+ }
+
+ /**
+ * We have multiple layouts, one to indicate that the user needs to
+ * open the keyboard to enter information (if the keybord is hidden).
+ * So, we need to make sure that the layout here matches that in the
+ * layout file.
+ */
+ private void setupView() {
+ mNameField = (EditText) findViewById(R.id.fdn_name);
+ if (mNameField != null) {
+ mNameField.setOnFocusChangeListener(mOnFocusChangeHandler);
+ mNameField.setOnClickListener(mClicked);
+ }
+
+ mNumberField = (EditText) findViewById(R.id.fdn_number);
+ if (mNumberField != null) {
+ mNumberField.setKeyListener(DialerKeyListener.getInstance());
+ mNumberField.setOnFocusChangeListener(mOnFocusChangeHandler);
+ mNumberField.setOnClickListener(mClicked);
+ }
+
+ if (!mAddContact) {
+ if (mNameField != null) {
+ mNameField.setText(mName);
+ }
+ if (mNumberField != null) {
+ mNumberField.setText(mNumber);
+ }
+ }
+
+ mButton = (Button) findViewById(R.id.button);
+ if (mButton != null) {
+ mButton.setOnClickListener(mClicked);
+ }
+
+ mPinFieldContainer = (LinearLayout) findViewById(R.id.pinc);
+
+ }
+
+ private String getNameFromTextField() {
+ return mNameField.getText().toString();
+ }
+
+ private String getNumberFromTextField() {
+ return mNumberField.getText().toString();
+ }
+
+ private Uri getContentURI() {
+ return Uri.parse("content://icc/fdn");
+ }
+
+ /**
+ * @param number is voice mail number
+ * @return true if number length is less than 20-digit limit
+ *
+ * TODO: Fix this logic.
+ */
+ private boolean isValidNumber(String number) {
+ return (number.length() <= 20);
+ }
+
+
+ private void addContact() {
+ if (DBG) log("addContact");
+
+ final String number = PhoneNumberUtils.convertAndStrip(getNumberFromTextField());
+
+ if (!isValidNumber(number)) {
+ handleResult(false, true);
+ return;
+ }
+
+ Uri uri = getContentURI();
+
+ ContentValues bundle = new ContentValues(3);
+ bundle.put("tag", getNameFromTextField());
+ bundle.put("number", number);
+ bundle.put("pin2", mPin2);
+
+ mQueryHandler = new QueryHandler(getContentResolver());
+ mQueryHandler.startInsert(0, null, uri, bundle);
+ displayProgress(true);
+ showStatus(getResources().getText(R.string.adding_fdn_contact));
+ }
+
+ private void updateContact() {
+ if (DBG) log("updateContact");
+
+ final String name = getNameFromTextField();
+ final String number = PhoneNumberUtils.convertAndStrip(getNumberFromTextField());
+
+ if (!isValidNumber(number)) {
+ handleResult(false, true);
+ return;
+ }
+ Uri uri = getContentURI();
+
+ ContentValues bundle = new ContentValues();
+ bundle.put("tag", mName);
+ bundle.put("number", mNumber);
+ bundle.put("newTag", name);
+ bundle.put("newNumber", number);
+ bundle.put("pin2", mPin2);
+
+ mQueryHandler = new QueryHandler(getContentResolver());
+ mQueryHandler.startUpdate(0, null, uri, bundle, null, null);
+ displayProgress(true);
+ showStatus(getResources().getText(R.string.updating_fdn_contact));
+ }
+
+ /**
+ * Handle the delete command, based upon the state of the Activity.
+ */
+ private void deleteSelected() {
+ // delete ONLY if this is NOT a new contact.
+ if (!mAddContact) {
+ Intent intent = new Intent();
+ intent.setClass(this, DeleteFdnContactScreen.class);
+ intent.putExtra(INTENT_EXTRA_NAME, mName);
+ intent.putExtra(INTENT_EXTRA_NUMBER, mNumber);
+ startActivity(intent);
+ }
+ finish();
+ }
+
+ private void authenticatePin2() {
+ Intent intent = new Intent();
+ intent.setClass(this, GetPin2Screen.class);
+ startActivityForResult(intent, PIN2_REQUEST_CODE);
+ }
+
+ private void displayProgress(boolean flag) {
+ // indicate we are busy.
+ mDataBusy = flag;
+ getWindow().setFeatureInt(
+ Window.FEATURE_INDETERMINATE_PROGRESS,
+ mDataBusy ? PROGRESS_VISIBILITY_ON : PROGRESS_VISIBILITY_OFF);
+ // make sure we don't allow calls to save when we're
+ // not ready for them.
+ mButton.setClickable(!mDataBusy);
+ }
+
+ /**
+ * Removed the status field, with preference to displaying a toast
+ * to match the rest of settings UI.
+ */
+ private void showStatus(CharSequence statusMsg) {
+ if (statusMsg != null) {
+ Toast.makeText(this, statusMsg, Toast.LENGTH_LONG)
+ .show();
+ }
+ }
+
+ private void handleResult(boolean success, boolean invalidNumber) {
+ if (success) {
+ if (DBG) log("handleResult: success!");
+ showStatus(getResources().getText(mAddContact ?
+ R.string.fdn_contact_added : R.string.fdn_contact_updated));
+ } else {
+ if (DBG) log("handleResult: failed!");
+ if (invalidNumber) {
+ showStatus(getResources().getText(R.string.fdn_invalid_number));
+ } else {
+ // There's no way to know whether the failure is due to incorrect PIN2 or
+ // an inappropriate phone number.
+ showStatus(getResources().getText(R.string.pin2_or_fdn_invalid));
+ }
+ }
+
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ finish();
+ }
+ }, 2000);
+
+ }
+
+ private final View.OnClickListener mClicked = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mPinFieldContainer.getVisibility() != View.VISIBLE) {
+ return;
+ }
+
+ if (v == mNameField) {
+ mNumberField.requestFocus();
+ } else if (v == mNumberField) {
+ mButton.requestFocus();
+ } else if (v == mButton) {
+ // Authenticate the pin AFTER the contact information
+ // is entered, and if we're not busy.
+ if (!mDataBusy) {
+ authenticatePin2();
+ }
+ }
+ }
+ };
+
+ private final View.OnFocusChangeListener mOnFocusChangeHandler =
+ new View.OnFocusChangeListener() {
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus) {
+ TextView textView = (TextView) v;
+ Selection.selectAll((Spannable) textView.getText());
+ }
+ }
+ };
+
+ private class QueryHandler extends AsyncQueryHandler {
+ public QueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor c) {
+ }
+
+ @Override
+ protected void onInsertComplete(int token, Object cookie, Uri uri) {
+ if (DBG) log("onInsertComplete");
+ displayProgress(false);
+ handleResult(uri != null, false);
+ }
+
+ @Override
+ protected void onUpdateComplete(int token, Object cookie, int result) {
+ if (DBG) log("onUpdateComplete");
+ displayProgress(false);
+ handleResult(result > 0, false);
+ }
+
+ @Override
+ protected void onDeleteComplete(int token, Object cookie, int result) {
+ }
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, "[EditFdnContact] " + msg);
+ }
+}
diff --git a/src/com/android/phone/EditPhoneNumberPreference.java b/src/com/android/phone/EditPhoneNumberPreference.java
new file mode 100644
index 0000000..86671a8
--- /dev/null
+++ b/src/com/android/phone/EditPhoneNumberPreference.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.preference.EditTextPreference;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.text.method.ArrowKeyMovementMethod;
+import android.text.method.DialerKeyListener;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.EditText;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+public class EditPhoneNumberPreference extends EditTextPreference {
+
+ //allowed modes for this preference.
+ /** simple confirmation (OK / CANCEL) */
+ private static final int CM_CONFIRM = 0;
+ /** toggle [(ENABLE / CANCEL) or (DISABLE / CANCEL)], use isToggled() to see requested state.*/
+ private static final int CM_ACTIVATION = 1;
+
+ private int mConfirmationMode;
+
+ //String constants used in storing the value of the preference
+ // The preference is backed by a string that holds the encoded value, which reads:
+ // <VALUE_ON | VALUE_OFF><VALUE_SEPARATOR><mPhoneNumber>
+ // for example, an enabled preference with a number of 6502345678 would read:
+ // "1:6502345678"
+ private static final String VALUE_SEPARATOR = ":";
+ private static final String VALUE_OFF = "0";
+ private static final String VALUE_ON = "1";
+
+ //UI layout
+ private ImageButton mContactPickButton;
+
+ //Listeners
+ /** Called when focus is changed between fields */
+ private View.OnFocusChangeListener mDialogFocusChangeListener;
+ /** Called when the Dialog is closed. */
+ private OnDialogClosedListener mDialogOnClosedListener;
+ /**
+ * Used to indicate that we are going to request for a
+ * default number. for the dialog.
+ */
+ private GetDefaultNumberListener mGetDefaultNumberListener;
+
+ //Activity values
+ private Activity mParentActivity;
+ private Intent mContactListIntent;
+ /** Arbitrary activity-assigned preference id value */
+ private int mPrefId;
+
+ //similar to toggle preference
+ private CharSequence mEnableText;
+ private CharSequence mDisableText;
+ private CharSequence mChangeNumberText;
+ private CharSequence mSummaryOn;
+ private CharSequence mSummaryOff;
+
+ // button that was clicked on dialog close.
+ private int mButtonClicked;
+
+ //relevant (parsed) value of the mText
+ private String mPhoneNumber;
+ private boolean mChecked;
+
+
+ /**
+ * Interface for the dialog closed listener, related to
+ * DialogPreference.onDialogClosed(), except we also pass in a buttonClicked
+ * value indicating which of the three possible buttons were pressed.
+ */
+ interface OnDialogClosedListener {
+ void onDialogClosed(EditPhoneNumberPreference preference, int buttonClicked);
+ }
+
+ /**
+ * Interface for the default number setting listener. Handles requests for
+ * the default display number for the dialog.
+ */
+ interface GetDefaultNumberListener {
+ /**
+ * Notify that we are looking for a default display value.
+ * @return null if there is no contribution from this interface,
+ * indicating that the orignal value of mPhoneNumber should be
+ * displayed unchanged.
+ */
+ String onGetDefaultNumber(EditPhoneNumberPreference preference);
+ }
+
+ /*
+ * Constructors
+ */
+ public EditPhoneNumberPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ setDialogLayoutResource(R.layout.pref_dialog_editphonenumber);
+
+ //create intent to bring up contact list
+ mContactListIntent = new Intent(Intent.ACTION_GET_CONTENT);
+ mContactListIntent.setType(Phone.CONTENT_ITEM_TYPE);
+
+ //get the edit phone number default settings
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ R.styleable.EditPhoneNumberPreference, 0, R.style.EditPhoneNumberPreference);
+ mEnableText = a.getString(R.styleable.EditPhoneNumberPreference_enableButtonText);
+ mDisableText = a.getString(R.styleable.EditPhoneNumberPreference_disableButtonText);
+ mChangeNumberText = a.getString(R.styleable.EditPhoneNumberPreference_changeNumButtonText);
+ mConfirmationMode = a.getInt(R.styleable.EditPhoneNumberPreference_confirmMode, 0);
+ a.recycle();
+
+ //get the summary settings, use CheckBoxPreference as the standard.
+ a = context.obtainStyledAttributes(attrs, android.R.styleable.CheckBoxPreference, 0, 0);
+ mSummaryOn = a.getString(android.R.styleable.CheckBoxPreference_summaryOn);
+ mSummaryOff = a.getString(android.R.styleable.CheckBoxPreference_summaryOff);
+ a.recycle();
+ }
+
+ public EditPhoneNumberPreference(Context context) {
+ this(context, null);
+ }
+
+
+ /*
+ * Methods called on UI bindings
+ */
+ @Override
+ //called when we're binding the view to the preference.
+ protected void onBindView(View view) {
+ super.onBindView(view);
+
+ // Sync the summary view
+ TextView summaryView = (TextView) view.findViewById(android.R.id.summary);
+ if (summaryView != null) {
+ CharSequence sum;
+ int vis;
+
+ //set summary depending upon mode
+ if (mConfirmationMode == CM_ACTIVATION) {
+ if (mChecked) {
+ sum = (mSummaryOn == null) ? getSummary() : mSummaryOn;
+ } else {
+ sum = (mSummaryOff == null) ? getSummary() : mSummaryOff;
+ }
+ } else {
+ sum = getSummary();
+ }
+
+ if (sum != null) {
+ summaryView.setText(sum);
+ vis = View.VISIBLE;
+ } else {
+ vis = View.GONE;
+ }
+
+ if (vis != summaryView.getVisibility()) {
+ summaryView.setVisibility(vis);
+ }
+ }
+ }
+
+ //called when we're binding the dialog to the preference's view.
+ @Override
+ protected void onBindDialogView(View view) {
+ // default the button clicked to be the cancel button.
+ mButtonClicked = DialogInterface.BUTTON_NEGATIVE;
+
+ super.onBindDialogView(view);
+
+ //get the edittext component within the number field
+ EditText editText = getEditText();
+ //get the contact pick button within the number field
+ mContactPickButton = (ImageButton) view.findViewById(R.id.select_contact);
+
+ //setup number entry
+ if (editText != null) {
+ // see if there is a means to get a default number,
+ // and set it accordingly.
+ if (mGetDefaultNumberListener != null) {
+ String defaultNumber = mGetDefaultNumberListener.onGetDefaultNumber(this);
+ if (defaultNumber != null) {
+ mPhoneNumber = defaultNumber;
+ }
+ }
+ editText.setText(mPhoneNumber);
+ editText.setMovementMethod(ArrowKeyMovementMethod.getInstance());
+ editText.setKeyListener(DialerKeyListener.getInstance());
+ editText.setOnFocusChangeListener(mDialogFocusChangeListener);
+ }
+
+ //set contact picker
+ if (mContactPickButton != null) {
+ mContactPickButton.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ if (mParentActivity != null) {
+ mParentActivity.startActivityForResult(mContactListIntent, mPrefId);
+ }
+ }
+ });
+ }
+ }
+
+ /**
+ * Overriding EditTextPreference's onAddEditTextToDialogView.
+ *
+ * This method attaches the EditText to the container specific to this
+ * preference's dialog layout.
+ */
+ @Override
+ protected void onAddEditTextToDialogView(View dialogView, EditText editText) {
+
+ // look for the container object
+ ViewGroup container = (ViewGroup) dialogView
+ .findViewById(R.id.edit_container);
+
+ // add the edittext to the container.
+ if (container != null) {
+ container.addView(editText, ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+ }
+
+ //control the appearance of the dialog depending upon the mode.
+ @Override
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ // modified so that we just worry about the buttons being
+ // displayed, since there is no need to hide the edittext
+ // field anymore.
+ if (mConfirmationMode == CM_ACTIVATION) {
+ if (mChecked) {
+ builder.setPositiveButton(mChangeNumberText, this);
+ builder.setNeutralButton(mDisableText, this);
+ } else {
+ builder.setPositiveButton(null, null);
+ builder.setNeutralButton(mEnableText, this);
+ }
+ }
+ // set the call icon on the title.
+ builder.setIcon(R.mipmap.ic_launcher_phone);
+ }
+
+
+ /*
+ * Listeners and other state setting methods
+ */
+ //set the on focus change listener to be assigned to the Dialog's edittext field.
+ public void setDialogOnFocusChangeListener(View.OnFocusChangeListener l) {
+ mDialogFocusChangeListener = l;
+ }
+
+ //set the listener to be called wht the dialog is closed.
+ public void setDialogOnClosedListener(OnDialogClosedListener l) {
+ mDialogOnClosedListener = l;
+ }
+
+ //set the link back to the parent activity, so that we may run the contact picker.
+ public void setParentActivity(Activity parent, int identifier) {
+ mParentActivity = parent;
+ mPrefId = identifier;
+ mGetDefaultNumberListener = null;
+ }
+
+ //set the link back to the parent activity, so that we may run the contact picker.
+ //also set the default number listener.
+ public void setParentActivity(Activity parent, int identifier, GetDefaultNumberListener l) {
+ mParentActivity = parent;
+ mPrefId = identifier;
+ mGetDefaultNumberListener = l;
+ }
+
+ /*
+ * Notification handlers
+ */
+ //Notify the preference that the pick activity is complete.
+ public void onPickActivityResult(String pickedValue) {
+ EditText editText = getEditText();
+ if (editText != null) {
+ editText.setText(pickedValue);
+ }
+ }
+
+ //called when the dialog is clicked.
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ // The neutral button (button3) is always the toggle.
+ if ((mConfirmationMode == CM_ACTIVATION) && (which == DialogInterface.BUTTON_NEUTRAL)) {
+ //flip the toggle if we are in the correct mode.
+ setToggled(!isToggled());
+ }
+ // record the button that was clicked.
+ mButtonClicked = which;
+ super.onClick(dialog, which);
+ }
+
+ @Override
+ //When the dialog is closed, perform the relevant actions, including setting
+ // phone numbers and calling the close action listener.
+ protected void onDialogClosed(boolean positiveResult) {
+ // A positive result is technically either button1 or button3.
+ if ((mButtonClicked == DialogInterface.BUTTON_POSITIVE) ||
+ (mButtonClicked == DialogInterface.BUTTON_NEUTRAL)){
+ setPhoneNumber(getEditText().getText().toString());
+ super.onDialogClosed(positiveResult);
+ setText(getStringValue());
+ } else {
+ super.onDialogClosed(positiveResult);
+ }
+
+ // send the clicked button over to the listener.
+ if (mDialogOnClosedListener != null) {
+ mDialogOnClosedListener.onDialogClosed(this, mButtonClicked);
+ }
+ }
+
+
+ /*
+ * Toggle handling code.
+ */
+ //return the toggle value.
+ public boolean isToggled() {
+ return mChecked;
+ }
+
+ //set the toggle value.
+ // return the current preference to allow for chaining preferences.
+ public EditPhoneNumberPreference setToggled(boolean checked) {
+ mChecked = checked;
+ setText(getStringValue());
+ notifyChanged();
+
+ return this;
+ }
+
+
+ /**
+ * Phone number handling code
+ */
+ public String getPhoneNumber() {
+ // return the phone number, after it has been stripped of all
+ // irrelevant text.
+ return PhoneNumberUtils.stripSeparators(mPhoneNumber);
+ }
+
+ /** The phone number including any formatting characters */
+ protected String getRawPhoneNumber() {
+ return mPhoneNumber;
+ }
+
+ //set the phone number value.
+ // return the current preference to allow for chaining preferences.
+ public EditPhoneNumberPreference setPhoneNumber(String number) {
+ mPhoneNumber = number;
+ setText(getStringValue());
+ notifyChanged();
+
+ return this;
+ }
+
+
+ /*
+ * Other code relevant to preference framework
+ */
+ //when setting default / initial values, make sure we're setting things correctly.
+ @Override
+ protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
+ setValueFromString(restoreValue ? getPersistedString(getStringValue())
+ : (String) defaultValue);
+ }
+
+ /**
+ * Decides how to disable dependents.
+ */
+ @Override
+ public boolean shouldDisableDependents() {
+ // There is really only one case we care about, but for consistency
+ // we fill out the dependency tree for all of the cases. If this
+ // is in activation mode (CF), we look for the encoded toggle value
+ // in the string. If this in confirm mode (VM), then we just
+ // examine the number field.
+ // Note: The toggle value is stored in the string in an encoded
+ // manner (refer to setValueFromString and getStringValue below).
+ boolean shouldDisable = false;
+ if ((mConfirmationMode == CM_ACTIVATION) && (mEncodedText != null)) {
+ String[] inValues = mEncodedText.split(":", 2);
+ shouldDisable = inValues[0].equals(VALUE_ON);
+ } else {
+ shouldDisable = (TextUtils.isEmpty(mPhoneNumber) && (mConfirmationMode == CM_CONFIRM));
+ }
+ return shouldDisable;
+ }
+
+ /**
+ * Override persistString so that we can get a hold of the EditTextPreference's
+ * text field.
+ */
+ private String mEncodedText = null;
+ @Override
+ protected boolean persistString(String value) {
+ mEncodedText = value;
+ return super.persistString(value);
+ }
+
+
+ /*
+ * Summary On handling code
+ */
+ //set the Summary for the on state (relevant only in CM_ACTIVATION mode)
+ public EditPhoneNumberPreference setSummaryOn(CharSequence summary) {
+ mSummaryOn = summary;
+ if (isToggled()) {
+ notifyChanged();
+ }
+ return this;
+ }
+
+ //set the Summary for the on state, given a string resource id
+ // (relevant only in CM_ACTIVATION mode)
+ public EditPhoneNumberPreference setSummaryOn(int summaryResId) {
+ return setSummaryOn(getContext().getString(summaryResId));
+ }
+
+ //get the summary string for the on state
+ public CharSequence getSummaryOn() {
+ return mSummaryOn;
+ }
+
+
+ /*
+ * Summary Off handling code
+ */
+ //set the Summary for the off state (relevant only in CM_ACTIVATION mode)
+ public EditPhoneNumberPreference setSummaryOff(CharSequence summary) {
+ mSummaryOff = summary;
+ if (!isToggled()) {
+ notifyChanged();
+ }
+ return this;
+ }
+
+ //set the Summary for the off state, given a string resource id
+ // (relevant only in CM_ACTIVATION mode)
+ public EditPhoneNumberPreference setSummaryOff(int summaryResId) {
+ return setSummaryOff(getContext().getString(summaryResId));
+ }
+
+ //get the summary string for the off state
+ public CharSequence getSummaryOff() {
+ return mSummaryOff;
+ }
+
+
+ /*
+ * Methods to get and set from encoded strings.
+ */
+ //set the values given an encoded string.
+ protected void setValueFromString(String value) {
+ String[] inValues = value.split(":", 2);
+ setToggled(inValues[0].equals(VALUE_ON));
+ setPhoneNumber(inValues[1]);
+ }
+
+ //retrieve the state of this preference in the form of an encoded string
+ protected String getStringValue() {
+ return ((isToggled() ? VALUE_ON : VALUE_OFF) + VALUE_SEPARATOR + getPhoneNumber());
+ }
+
+ /**
+ * Externally visible method to bring up the dialog.
+ *
+ * Generally used when we are navigating the user to this preference.
+ */
+ public void showPhoneNumberDialog() {
+ showDialog(null);
+ }
+}
diff --git a/src/com/android/phone/EditPinPreference.java b/src/com/android/phone/EditPinPreference.java
new file mode 100644
index 0000000..af0040d
--- /dev/null
+++ b/src/com/android/phone/EditPinPreference.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.preference.EditTextPreference;
+import android.text.InputType;
+import android.text.method.DigitsKeyListener;
+import android.text.method.PasswordTransformationMethod;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.EditText;
+
+import java.util.Map;
+
+/**
+ * Class similar to the com.android.settings.EditPinPreference
+ * class, with a couple of modifications, including a different layout
+ * for the dialog.
+ */
+public class EditPinPreference extends EditTextPreference {
+
+ private boolean shouldHideButtons;
+
+ interface OnPinEnteredListener {
+ void onPinEntered(EditPinPreference preference, boolean positiveResult);
+ }
+
+ private OnPinEnteredListener mPinListener;
+
+ public void setOnPinEnteredListener(OnPinEnteredListener listener) {
+ mPinListener = listener;
+ }
+
+ public EditPinPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public EditPinPreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ /**
+ * Overridden to setup the correct dialog layout, as well as setting up
+ * other properties for the pin / puk entry field.
+ */
+ @Override
+ protected View onCreateDialogView() {
+ // set the dialog layout
+ setDialogLayoutResource(R.layout.pref_dialog_editpin);
+
+ View dialog = super.onCreateDialogView();
+
+ getEditText().setInputType(InputType.TYPE_CLASS_NUMBER |
+ InputType.TYPE_NUMBER_VARIATION_PASSWORD);
+
+ return dialog;
+ }
+
+ @Override
+ protected void onBindDialogView(View view) {
+ super.onBindDialogView(view);
+
+ // If the layout does not contain an edittext, hide the buttons.
+ shouldHideButtons = (view.findViewById(android.R.id.edit) == null);
+ }
+
+ @Override
+ protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {
+ super.onPrepareDialogBuilder(builder);
+
+ // hide the buttons if we need to.
+ if (shouldHideButtons) {
+ builder.setPositiveButton(null, this);
+ builder.setNegativeButton(null, this);
+ }
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ super.onDialogClosed(positiveResult);
+ if (mPinListener != null) {
+ mPinListener.onPinEntered(this, positiveResult);
+ }
+ }
+
+ /**
+ * Externally visible method to bring up the dialog to
+ * for multi-step / multi-dialog requests (like changing
+ * the SIM pin).
+ */
+ public void showPinDialog() {
+ showDialog(null);
+ }
+}
diff --git a/src/com/android/phone/EmergencyCallHelper.java b/src/com/android/phone/EmergencyCallHelper.java
new file mode 100644
index 0000000..7f5b0d2
--- /dev/null
+++ b/src/com/android/phone/EmergencyCallHelper.java
@@ -0,0 +1,545 @@
+/*
+ * Copyright (C) 2011 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.phone;
+
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.phone.Constants.CallStatusCode;
+import com.android.phone.InCallUiState.ProgressIndicationType;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.telephony.ServiceState;
+import android.util.Log;
+
+
+/**
+ * Helper class for the {@link CallController} that implements special
+ * behavior related to emergency calls. Specifically, this class handles
+ * the case of the user trying to dial an emergency number while the radio
+ * is off (i.e. the device is in airplane mode), by forcibly turning the
+ * radio back on, waiting for it to come up, and then retrying the
+ * emergency call.
+ *
+ * This class is instantiated lazily (the first time the user attempts to
+ * make an emergency call from airplane mode) by the the
+ * {@link CallController} singleton.
+ */
+public class EmergencyCallHelper extends Handler {
+ private static final String TAG = "EmergencyCallHelper";
+ private static final boolean DBG = false;
+
+ // Number of times to retry the call, and time between retry attempts.
+ public static final int MAX_NUM_RETRIES = 6;
+ public static final long TIME_BETWEEN_RETRIES = 5000; // msec
+
+ // Timeout used with our wake lock (just as a safety valve to make
+ // sure we don't hold it forever).
+ public static final long WAKE_LOCK_TIMEOUT = 5 * 60 * 1000; // 5 minutes in msec
+
+ // Handler message codes; see handleMessage()
+ private static final int START_SEQUENCE = 1;
+ private static final int SERVICE_STATE_CHANGED = 2;
+ private static final int DISCONNECT = 3;
+ private static final int RETRY_TIMEOUT = 4;
+
+ private CallController mCallController;
+ private PhoneGlobals mApp;
+ private CallManager mCM;
+ private Phone mPhone;
+ private String mNumber; // The emergency number we're trying to dial
+ private int mNumRetriesSoFar;
+
+ // Wake lock we hold while running the whole sequence
+ private PowerManager.WakeLock mPartialWakeLock;
+
+ public EmergencyCallHelper(CallController callController) {
+ if (DBG) log("EmergencyCallHelper constructor...");
+ mCallController = callController;
+ mApp = PhoneGlobals.getInstance();
+ mCM = mApp.mCM;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case START_SEQUENCE:
+ startSequenceInternal(msg);
+ break;
+ case SERVICE_STATE_CHANGED:
+ onServiceStateChanged(msg);
+ break;
+ case DISCONNECT:
+ onDisconnect(msg);
+ break;
+ case RETRY_TIMEOUT:
+ onRetryTimeout();
+ break;
+ default:
+ Log.wtf(TAG, "handleMessage: unexpected message: " + msg);
+ break;
+ }
+ }
+
+ /**
+ * Starts the "emergency call from airplane mode" sequence.
+ *
+ * This is the (single) external API of the EmergencyCallHelper class.
+ * This method is called from the CallController placeCall() sequence
+ * if the user dials a valid emergency number, but the radio is
+ * powered-off (presumably due to airplane mode.)
+ *
+ * This method kicks off the following sequence:
+ * - Power on the radio
+ * - Listen for the service state change event telling us the radio has come up
+ * - Then launch the emergency call
+ * - Retry if the call fails with an OUT_OF_SERVICE error
+ * - Retry if we've gone 5 seconds without any response from the radio
+ * - Finally, clean up any leftover state (progress UI, wake locks, etc.)
+ *
+ * This method is safe to call from any thread, since it simply posts
+ * a message to the EmergencyCallHelper's handler (thus ensuring that
+ * the rest of the sequence is entirely serialized, and runs only on
+ * the handler thread.)
+ *
+ * This method does *not* force the in-call UI to come up; our caller
+ * is responsible for doing that (presumably by calling
+ * PhoneApp.displayCallScreen().)
+ */
+ public void startEmergencyCallFromAirplaneModeSequence(String number) {
+ if (DBG) log("startEmergencyCallFromAirplaneModeSequence('" + number + "')...");
+ Message msg = obtainMessage(START_SEQUENCE, number);
+ sendMessage(msg);
+ }
+
+ /**
+ * Actual implementation of startEmergencyCallFromAirplaneModeSequence(),
+ * guaranteed to run on the handler thread.
+ * @see startEmergencyCallFromAirplaneModeSequence()
+ */
+ private void startSequenceInternal(Message msg) {
+ if (DBG) log("startSequenceInternal(): msg = " + msg);
+
+ // First of all, clean up any state (including mPartialWakeLock!)
+ // left over from a prior emergency call sequence.
+ // This ensures that we'll behave sanely if another
+ // startEmergencyCallFromAirplaneModeSequence() comes in while
+ // we're already in the middle of the sequence.
+ cleanup();
+
+ mNumber = (String) msg.obj;
+ if (DBG) log("- startSequenceInternal: Got mNumber: '" + mNumber + "'");
+
+ mNumRetriesSoFar = 0;
+
+ // Reset mPhone to whatever the current default phone is right now.
+ mPhone = mApp.mCM.getDefaultPhone();
+
+ // Wake lock to make sure the processor doesn't go to sleep midway
+ // through the emergency call sequence.
+ PowerManager pm = (PowerManager) mApp.getSystemService(Context.POWER_SERVICE);
+ mPartialWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
+ // Acquire with a timeout, just to be sure we won't hold the wake
+ // lock forever even if a logic bug (in this class) causes us to
+ // somehow never call cleanup().
+ if (DBG) log("- startSequenceInternal: acquiring wake lock");
+ mPartialWakeLock.acquire(WAKE_LOCK_TIMEOUT);
+
+ // No need to check the current service state here, since the only
+ // reason the CallController would call this method in the first
+ // place is if the radio is powered-off.
+ //
+ // So just go ahead and turn the radio on.
+
+ powerOnRadio(); // We'll get an onServiceStateChanged() callback
+ // when the radio successfully comes up.
+
+ // Next step: when the SERVICE_STATE_CHANGED event comes in,
+ // we'll retry the call; see placeEmergencyCall();
+ // But also, just in case, start a timer to make sure we'll retry
+ // the call even if the SERVICE_STATE_CHANGED event never comes in
+ // for some reason.
+ startRetryTimer();
+
+ // And finally, let the in-call UI know that we need to
+ // display the "Turning on radio..." progress indication.
+ mApp.inCallUiState.setProgressIndication(ProgressIndicationType.TURNING_ON_RADIO);
+
+ // (Our caller is responsible for calling mApp.displayCallScreen().)
+ }
+
+ /**
+ * Handles the SERVICE_STATE_CHANGED event.
+ *
+ * (Normally this event tells us that the radio has finally come
+ * up. In that case, it's now safe to actually place the
+ * emergency call.)
+ */
+ private void onServiceStateChanged(Message msg) {
+ ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result;
+ if (DBG) log("onServiceStateChanged()... new state = " + state);
+
+ // Possible service states:
+ // - STATE_IN_SERVICE // Normal operation
+ // - STATE_OUT_OF_SERVICE // Still searching for an operator to register to,
+ // // or no radio signal
+ // - STATE_EMERGENCY_ONLY // Phone is locked; only emergency numbers are allowed
+ // - STATE_POWER_OFF // Radio is explicitly powered off (airplane mode)
+
+ // Once we reach either STATE_IN_SERVICE or STATE_EMERGENCY_ONLY,
+ // it's finally OK to place the emergency call.
+ boolean okToCall = (state.getState() == ServiceState.STATE_IN_SERVICE)
+ || (state.getState() == ServiceState.STATE_EMERGENCY_ONLY);
+
+ if (okToCall) {
+ // Woo hoo! It's OK to actually place the call.
+ if (DBG) log("onServiceStateChanged: ok to call!");
+
+ // Deregister for the service state change events.
+ unregisterForServiceStateChanged();
+
+ // Take down the "Turning on radio..." indication.
+ mApp.inCallUiState.clearProgressIndication();
+
+ placeEmergencyCall();
+
+ // The in-call UI is probably still up at this point,
+ // but make sure of that:
+ mApp.displayCallScreen();
+ } else {
+ // The service state changed, but we're still not ready to call yet.
+ // (This probably was the transition from STATE_POWER_OFF to
+ // STATE_OUT_OF_SERVICE, which happens immediately after powering-on
+ // the radio.)
+ //
+ // So just keep waiting; we'll probably get to either
+ // STATE_IN_SERVICE or STATE_EMERGENCY_ONLY very shortly.
+ // (Or even if that doesn't happen, we'll at least do another retry
+ // when the RETRY_TIMEOUT event fires.)
+ if (DBG) log("onServiceStateChanged: not ready to call yet, keep waiting...");
+ }
+ }
+
+ /**
+ * Handles a DISCONNECT event from the telephony layer.
+ *
+ * Even after we successfully place an emergency call (after powering
+ * on the radio), it's still possible for the call to fail with the
+ * disconnect cause OUT_OF_SERVICE. If so, schedule a retry.
+ */
+ private void onDisconnect(Message msg) {
+ Connection conn = (Connection) ((AsyncResult) msg.obj).result;
+ Connection.DisconnectCause cause = conn.getDisconnectCause();
+ if (DBG) log("onDisconnect: connection '" + conn
+ + "', addr '" + conn.getAddress() + "', cause = " + cause);
+
+ if (cause == Connection.DisconnectCause.OUT_OF_SERVICE) {
+ // Wait a bit more and try again (or just bail out totally if
+ // we've had too many failures.)
+ if (DBG) log("- onDisconnect: OUT_OF_SERVICE, need to retry...");
+ scheduleRetryOrBailOut();
+ } else {
+ // Any other disconnect cause means we're done.
+ // Either the emergency call succeeded *and* ended normally,
+ // or else there was some error that we can't retry. In either
+ // case, just clean up our internal state.)
+
+ if (DBG) log("==> Disconnect event; clean up...");
+ cleanup();
+
+ // Nothing else to do here. If the InCallScreen was visible,
+ // it would have received this disconnect event too (so it'll
+ // show the "Call ended" state and finish itself without any
+ // help from us.)
+ }
+ }
+
+ /**
+ * Handles the retry timer expiring.
+ */
+ private void onRetryTimeout() {
+ PhoneConstants.State phoneState = mCM.getState();
+ int serviceState = mPhone.getServiceState().getState();
+ if (DBG) log("onRetryTimeout(): phone state " + phoneState
+ + ", service state " + serviceState
+ + ", mNumRetriesSoFar = " + mNumRetriesSoFar);
+
+ // - If we're actually in a call, we've succeeded.
+ //
+ // - Otherwise, if the radio is now on, that means we successfully got
+ // out of airplane mode but somehow didn't get the service state
+ // change event. In that case, try to place the call.
+ //
+ // - If the radio is still powered off, try powering it on again.
+
+ if (phoneState == PhoneConstants.State.OFFHOOK) {
+ if (DBG) log("- onRetryTimeout: Call is active! Cleaning up...");
+ cleanup();
+ return;
+ }
+
+ if (serviceState != ServiceState.STATE_POWER_OFF) {
+ // Woo hoo -- we successfully got out of airplane mode.
+
+ // Deregister for the service state change events; we don't need
+ // these any more now that the radio is powered-on.
+ unregisterForServiceStateChanged();
+
+ // Take down the "Turning on radio..." indication.
+ mApp.inCallUiState.clearProgressIndication();
+
+ placeEmergencyCall(); // If the call fails, placeEmergencyCall()
+ // will schedule a retry.
+ } else {
+ // Uh oh; we've waited the full TIME_BETWEEN_RETRIES and the
+ // radio is still not powered-on. Try again...
+
+ if (DBG) log("- Trying (again) to turn on the radio...");
+ powerOnRadio(); // Again, we'll (hopefully) get an onServiceStateChanged()
+ // callback when the radio successfully comes up.
+
+ // ...and also set a fresh retry timer (or just bail out
+ // totally if we've had too many failures.)
+ scheduleRetryOrBailOut();
+ }
+
+ // Finally, the in-call UI is probably still up at this point,
+ // but make sure of that:
+ mApp.displayCallScreen();
+ }
+
+ /**
+ * Attempt to power on the radio (i.e. take the device out
+ * of airplane mode.)
+ *
+ * Additionally, start listening for service state changes;
+ * we'll eventually get an onServiceStateChanged() callback
+ * when the radio successfully comes up.
+ */
+ private void powerOnRadio() {
+ if (DBG) log("- powerOnRadio()...");
+
+ // We're about to turn on the radio, so arrange to be notified
+ // when the sequence is complete.
+ registerForServiceStateChanged();
+
+ // If airplane mode is on, we turn it off the same way that the
+ // Settings activity turns it off.
+ if (Settings.Global.getInt(mApp.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, 0) > 0) {
+ if (DBG) log("==> Turning off airplane mode...");
+
+ // Change the system setting
+ Settings.Global.putInt(mApp.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, 0);
+
+ // Post the intent
+ Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ intent.putExtra("state", false);
+ mApp.sendBroadcastAsUser(intent, UserHandle.ALL);
+ } else {
+ // Otherwise, for some strange reason the radio is off
+ // (even though the Settings database doesn't think we're
+ // in airplane mode.) In this case just turn the radio
+ // back on.
+ if (DBG) log("==> (Apparently) not in airplane mode; manually powering radio on...");
+ mPhone.setRadioPower(true);
+ }
+ }
+
+ /**
+ * Actually initiate the outgoing emergency call.
+ * (We do this once the radio has successfully been powered-up.)
+ *
+ * If the call succeeds, we're done.
+ * If the call fails, schedule a retry of the whole sequence.
+ */
+ private void placeEmergencyCall() {
+ if (DBG) log("placeEmergencyCall()...");
+
+ // Place an outgoing call to mNumber.
+ // Note we call PhoneUtils.placeCall() directly; we don't want any
+ // of the behavior from CallController.placeCallInternal() here.
+ // (Specifically, we don't want to start the "emergency call from
+ // airplane mode" sequence from the beginning again!)
+
+ registerForDisconnect(); // Get notified when this call disconnects
+
+ if (DBG) log("- placing call to '" + mNumber + "'...");
+ int callStatus = PhoneUtils.placeCall(mApp,
+ mPhone,
+ mNumber,
+ null, // contactUri
+ true, // isEmergencyCall
+ null); // gatewayUri
+ if (DBG) log("- PhoneUtils.placeCall() returned status = " + callStatus);
+
+ boolean success;
+ // Note PhoneUtils.placeCall() returns one of the CALL_STATUS_*
+ // constants, not a CallStatusCode enum value.
+ switch (callStatus) {
+ case PhoneUtils.CALL_STATUS_DIALED:
+ success = true;
+ break;
+
+ case PhoneUtils.CALL_STATUS_DIALED_MMI:
+ case PhoneUtils.CALL_STATUS_FAILED:
+ default:
+ // Anything else is a failure, and we'll need to retry.
+ Log.w(TAG, "placeEmergencyCall(): placeCall() failed: callStatus = " + callStatus);
+ success = false;
+ break;
+ }
+
+ if (success) {
+ if (DBG) log("==> Success from PhoneUtils.placeCall()!");
+ // Ok, the emergency call is (hopefully) under way.
+
+ // We're not done yet, though, so don't call cleanup() here.
+ // (It's still possible that this call will fail, and disconnect
+ // with cause==OUT_OF_SERVICE. If so, that will trigger a retry
+ // from the onDisconnect() method.)
+ } else {
+ if (DBG) log("==> Failure.");
+ // Wait a bit more and try again (or just bail out totally if
+ // we've had too many failures.)
+ scheduleRetryOrBailOut();
+ }
+ }
+
+ /**
+ * Schedules a retry in response to some failure (either the radio
+ * failing to power on, or a failure when trying to place the call.)
+ * Or, if we've hit the retry limit, bail out of this whole sequence
+ * and display a failure message to the user.
+ */
+ private void scheduleRetryOrBailOut() {
+ mNumRetriesSoFar++;
+ if (DBG) log("scheduleRetryOrBailOut()... mNumRetriesSoFar is now " + mNumRetriesSoFar);
+
+ if (mNumRetriesSoFar > MAX_NUM_RETRIES) {
+ Log.w(TAG, "scheduleRetryOrBailOut: hit MAX_NUM_RETRIES; giving up...");
+ cleanup();
+ // ...and have the InCallScreen display a generic failure
+ // message.
+ mApp.inCallUiState.setPendingCallStatusCode(CallStatusCode.CALL_FAILED);
+ } else {
+ if (DBG) log("- Scheduling another retry...");
+ startRetryTimer();
+ mApp.inCallUiState.setProgressIndication(ProgressIndicationType.RETRYING);
+ }
+ }
+
+ /**
+ * Clean up when done with the whole sequence: either after
+ * successfully placing *and* ending the emergency call, or after
+ * bailing out because of too many failures.
+ *
+ * The exact cleanup steps are:
+ * - Take down any progress UI (and also ask the in-call UI to refresh itself,
+ * if it's still visible)
+ * - Double-check that we're not still registered for any telephony events
+ * - Clean up any extraneous handler messages (like retry timeouts) still in the queue
+ * - Make sure we're not still holding any wake locks
+ *
+ * Basically this method guarantees that there will be no more
+ * activity from the EmergencyCallHelper until the CallController
+ * kicks off the whole sequence again with another call to
+ * startEmergencyCallFromAirplaneModeSequence().
+ *
+ * Note we don't call this method simply after a successful call to
+ * placeCall(), since it's still possible the call will disconnect
+ * very quickly with an OUT_OF_SERVICE error.
+ */
+ private void cleanup() {
+ if (DBG) log("cleanup()...");
+
+ // Take down the "Turning on radio..." indication.
+ mApp.inCallUiState.clearProgressIndication();
+
+ unregisterForServiceStateChanged();
+ unregisterForDisconnect();
+ cancelRetryTimer();
+
+ // Release / clean up the wake lock
+ if (mPartialWakeLock != null) {
+ if (mPartialWakeLock.isHeld()) {
+ if (DBG) log("- releasing wake lock");
+ mPartialWakeLock.release();
+ }
+ mPartialWakeLock = null;
+ }
+
+ // And finally, ask the in-call UI to refresh itself (to clean up the
+ // progress indication if necessary), if it's currently visible.
+ mApp.updateInCallScreen();
+ }
+
+ private void startRetryTimer() {
+ removeMessages(RETRY_TIMEOUT);
+ sendEmptyMessageDelayed(RETRY_TIMEOUT, TIME_BETWEEN_RETRIES);
+ }
+
+ private void cancelRetryTimer() {
+ removeMessages(RETRY_TIMEOUT);
+ }
+
+ private void registerForServiceStateChanged() {
+ // Unregister first, just to make sure we never register ourselves
+ // twice. (We need this because Phone.registerForServiceStateChanged()
+ // does not prevent multiple registration of the same handler.)
+ mPhone.unregisterForServiceStateChanged(this); // Safe even if not currently registered
+ mPhone.registerForServiceStateChanged(this, SERVICE_STATE_CHANGED, null);
+ }
+
+ private void unregisterForServiceStateChanged() {
+ // This method is safe to call even if we haven't set mPhone yet.
+ if (mPhone != null) {
+ mPhone.unregisterForServiceStateChanged(this); // Safe even if unnecessary
+ }
+ removeMessages(SERVICE_STATE_CHANGED); // Clean up any pending messages too
+ }
+
+ private void registerForDisconnect() {
+ // Note: no need to unregister first, since
+ // CallManager.registerForDisconnect() automatically prevents
+ // multiple registration of the same handler.
+ mCM.registerForDisconnect(this, DISCONNECT, null);
+ }
+
+ private void unregisterForDisconnect() {
+ mCM.unregisterForDisconnect(this); // Safe even if not currently registered
+ removeMessages(DISCONNECT); // Clean up any pending messages too
+ }
+
+
+ //
+ // Debugging
+ //
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/EmergencyCallbackModeExitDialog.java b/src/com/android/phone/EmergencyCallbackModeExitDialog.java
new file mode 100644
index 0000000..7758b23
--- /dev/null
+++ b/src/com/android/phone/EmergencyCallbackModeExitDialog.java
@@ -0,0 +1,347 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.res.Resources;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.CountDownTimer;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.TelephonyProperties;
+
+/**
+ * Displays dialog that enables users to exit Emergency Callback Mode
+ *
+ * @see EmergencyCallbackModeService
+ */
+public class EmergencyCallbackModeExitDialog extends Activity implements OnDismissListener {
+
+ /** Intent to trigger the Emergency Callback Mode exit dialog */
+ static final String ACTION_SHOW_ECM_EXIT_DIALOG =
+ "com.android.phone.action.ACTION_SHOW_ECM_EXIT_DIALOG";
+ /** Used to get the users choice from the return Intent's extra */
+ public static final String EXTRA_EXIT_ECM_RESULT = "exit_ecm_result";
+
+ public static final int EXIT_ECM_BLOCK_OTHERS = 1;
+ public static final int EXIT_ECM_DIALOG = 2;
+ public static final int EXIT_ECM_PROGRESS_DIALOG = 3;
+ public static final int EXIT_ECM_IN_EMERGENCY_CALL_DIALOG = 4;
+
+ AlertDialog mAlertDialog = null;
+ ProgressDialog mProgressDialog = null;
+ CountDownTimer mTimer = null;
+ EmergencyCallbackModeService mService = null;
+ Handler mHandler = null;
+ int mDialogType = 0;
+ long mEcmTimeout = 0;
+ private boolean mInEmergencyCall = false;
+ private static final int ECM_TIMER_RESET = 1;
+ private Phone mPhone = null;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Check if phone is in Emergency Callback Mode. If not, exit.
+ if (!Boolean.parseBoolean(
+ SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE))) {
+ finish();
+ }
+
+ mHandler = new Handler();
+
+ // Start thread that will wait for the connection completion so that it can get
+ // timeout value from the service
+ Thread waitForConnectionCompleteThread = new Thread(null, mTask,
+ "EcmExitDialogWaitThread");
+ waitForConnectionCompleteThread.start();
+
+ // Register ECM timer reset notfication
+ mPhone = PhoneGlobals.getPhone();
+ mPhone.registerForEcmTimerReset(mTimerResetHandler, ECM_TIMER_RESET, null);
+
+ // Register receiver for intent closing the dialog
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED);
+ registerReceiver(mEcmExitReceiver, filter);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ unregisterReceiver(mEcmExitReceiver);
+ // Unregister ECM timer reset notification
+ mPhone.unregisterForEcmTimerReset(mHandler);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ mDialogType = savedInstanceState.getInt("DIALOG_TYPE");
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt("DIALOG_TYPE", mDialogType);
+ }
+
+ /**
+ * Waits until bind to the service completes
+ */
+ private Runnable mTask = new Runnable() {
+ public void run() {
+ Looper.prepare();
+
+ // Bind to the remote service
+ bindService(new Intent(EmergencyCallbackModeExitDialog.this,
+ EmergencyCallbackModeService.class), mConnection, Context.BIND_AUTO_CREATE);
+
+ // Wait for bind to finish
+ synchronized (EmergencyCallbackModeExitDialog.this) {
+ try {
+ if (mService == null) {
+ EmergencyCallbackModeExitDialog.this.wait();
+ }
+ } catch (InterruptedException e) {
+ Log.d("ECM", "EmergencyCallbackModeExitDialog InterruptedException: "
+ + e.getMessage());
+ e.printStackTrace();
+ }
+ }
+
+ // Get timeout value and call state from the service
+ if (mService != null) {
+ mEcmTimeout = mService.getEmergencyCallbackModeTimeout();
+ mInEmergencyCall = mService.getEmergencyCallbackModeCallState();
+ }
+
+ // Unbind from remote service
+ unbindService(mConnection);
+
+ // Show dialog
+ mHandler.post(new Runnable() {
+ public void run() {
+ showEmergencyCallbackModeExitDialog();
+ }
+ });
+ }
+ };
+
+ /**
+ * Shows Emergency Callback Mode dialog and starts countdown timer
+ */
+ private void showEmergencyCallbackModeExitDialog() {
+
+ if(mInEmergencyCall) {
+ mDialogType = EXIT_ECM_IN_EMERGENCY_CALL_DIALOG;
+ showDialog(EXIT_ECM_IN_EMERGENCY_CALL_DIALOG);
+ } else {
+ if (getIntent().getAction().equals(
+ TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS)) {
+ mDialogType = EXIT_ECM_BLOCK_OTHERS;
+ showDialog(EXIT_ECM_BLOCK_OTHERS);
+ } else if (getIntent().getAction().equals(ACTION_SHOW_ECM_EXIT_DIALOG)) {
+ mDialogType = EXIT_ECM_DIALOG;
+ showDialog(EXIT_ECM_DIALOG);
+ }
+
+ mTimer = new CountDownTimer(mEcmTimeout, 1000) {
+ @Override
+ public void onTick(long millisUntilFinished) {
+ CharSequence text = getDialogText(millisUntilFinished);
+ mAlertDialog.setMessage(text);
+ }
+
+ @Override
+ public void onFinish() {
+ //Do nothing
+ }
+ }.start();
+ }
+ }
+
+ /**
+ * Creates dialog that enables users to exit Emergency Callback Mode
+ */
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ switch (id) {
+ case EXIT_ECM_BLOCK_OTHERS:
+ case EXIT_ECM_DIALOG:
+ CharSequence text = getDialogText(mEcmTimeout);
+ mAlertDialog = new AlertDialog.Builder(EmergencyCallbackModeExitDialog.this)
+ .setIcon(R.drawable.picture_emergency32x32)
+ .setTitle(R.string.phone_in_ecm_notification_title)
+ .setMessage(text)
+ .setPositiveButton(R.string.alert_dialog_yes,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,int whichButton) {
+ // User clicked Yes. Exit Emergency Callback Mode.
+ mPhone.exitEmergencyCallbackMode();
+
+ // Show progress dialog
+ showDialog(EXIT_ECM_PROGRESS_DIALOG);
+ mTimer.cancel();
+ }
+ })
+ .setNegativeButton(R.string.alert_dialog_no,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // User clicked No
+ setResult(RESULT_OK, (new Intent()).putExtra(
+ EXTRA_EXIT_ECM_RESULT, false));
+ finish();
+ }
+ }).create();
+ mAlertDialog.setOnDismissListener(this);
+ return mAlertDialog;
+
+ case EXIT_ECM_IN_EMERGENCY_CALL_DIALOG:
+ mAlertDialog = new AlertDialog.Builder(EmergencyCallbackModeExitDialog.this)
+ .setIcon(R.drawable.picture_emergency32x32)
+ .setTitle(R.string.phone_in_ecm_notification_title)
+ .setMessage(R.string.alert_dialog_in_ecm_call)
+ .setNeutralButton(R.string.alert_dialog_dismiss,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ // User clicked Dismiss
+ setResult(RESULT_OK, (new Intent()).putExtra(
+ EXTRA_EXIT_ECM_RESULT, false));
+ finish();
+ }
+ }).create();
+ mAlertDialog.setOnDismissListener(this);
+ return mAlertDialog;
+
+ case EXIT_ECM_PROGRESS_DIALOG:
+ mProgressDialog = new ProgressDialog(EmergencyCallbackModeExitDialog.this);
+ mProgressDialog.setMessage(getText(R.string.progress_dialog_exiting_ecm));
+ mProgressDialog.setIndeterminate(true);
+ mProgressDialog.setCancelable(false);
+ return mProgressDialog;
+
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Returns dialog box text with updated timeout value
+ */
+ private CharSequence getDialogText(long millisUntilFinished) {
+ // Format time
+ int minutes = (int)(millisUntilFinished / 60000);
+ String time = String.format("%d:%02d", minutes,
+ (millisUntilFinished % 60000) / 1000);
+
+ switch (mDialogType) {
+ case EXIT_ECM_BLOCK_OTHERS:
+ return String.format(getResources().getQuantityText(
+ R.plurals.alert_dialog_not_avaialble_in_ecm, minutes).toString(), time);
+ case EXIT_ECM_DIALOG:
+ return String.format(getResources().getQuantityText(R.plurals.alert_dialog_exit_ecm,
+ minutes).toString(), time);
+ }
+ return null;
+ }
+
+ /**
+ * Closes activity when dialog is dismissed
+ */
+ public void onDismiss(DialogInterface dialog) {
+ EmergencyCallbackModeExitDialog.this.setResult(RESULT_OK, (new Intent())
+ .putExtra(EXTRA_EXIT_ECM_RESULT, false));
+ finish();
+ }
+
+ /**
+ * Listens for Emergency Callback Mode state change intents
+ */
+ private BroadcastReceiver mEcmExitReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Received exit Emergency Callback Mode notification close all dialogs
+ if (intent.getAction().equals(
+ TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED)) {
+ if (intent.getBooleanExtra("phoneinECMState", false) == false) {
+ if (mAlertDialog != null)
+ mAlertDialog.dismiss();
+ if (mProgressDialog != null)
+ mProgressDialog.dismiss();
+ EmergencyCallbackModeExitDialog.this.setResult(RESULT_OK, (new Intent())
+ .putExtra(EXTRA_EXIT_ECM_RESULT, true));
+ finish();
+ }
+ }
+ }
+ };
+
+ /**
+ * Class for interacting with the interface of the service
+ */
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mService = ((EmergencyCallbackModeService.LocalBinder)service).getService();
+ // Notify thread that connection is ready
+ synchronized (EmergencyCallbackModeExitDialog.this) {
+ EmergencyCallbackModeExitDialog.this.notify();
+ }
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ mService = null;
+ }
+ };
+
+ /**
+ * Class for receiving framework timer reset notifications
+ */
+ private Handler mTimerResetHandler = new Handler () {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case ECM_TIMER_RESET:
+ if(!((Boolean)((AsyncResult) msg.obj).result).booleanValue()) {
+ EmergencyCallbackModeExitDialog.this.setResult(RESULT_OK, (new Intent())
+ .putExtra(EXTRA_EXIT_ECM_RESULT, false));
+ finish();
+ }
+ break;
+ }
+ }
+ };
+}
diff --git a/src/com/android/phone/EmergencyCallbackModeService.java b/src/com/android/phone/EmergencyCallbackModeService.java
new file mode 100644
index 0000000..b506598
--- /dev/null
+++ b/src/com/android/phone/EmergencyCallbackModeService.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.os.AsyncResult;
+import android.os.Binder;
+import android.os.CountDownTimer;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.telephony.cdma.CDMAPhone;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.TelephonyProperties;
+
+/**
+ * Application service that inserts/removes Emergency Callback Mode notification and
+ * updates Emergency Callback Mode countdown clock in the notification
+ *
+ * @see EmergencyCallbackModeExitDialog
+ */
+public class EmergencyCallbackModeService extends Service {
+
+ // Default Emergency Callback Mode timeout value
+ private static final int DEFAULT_ECM_EXIT_TIMER_VALUE = 300000;
+ private static final String LOG_TAG = "EmergencyCallbackModeService";
+
+ private NotificationManager mNotificationManager = null;
+ private CountDownTimer mTimer = null;
+ private long mTimeLeft = 0;
+ private Phone mPhone = null;
+ private boolean mInEmergencyCall = false;
+
+ private static final int ECM_TIMER_RESET = 1;
+
+ private Handler mHandler = new Handler () {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case ECM_TIMER_RESET:
+ resetEcmTimer((AsyncResult) msg.obj);
+ break;
+ }
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ // Check if it is CDMA phone
+ if (PhoneFactory.getDefaultPhone().getPhoneType() != PhoneConstants.PHONE_TYPE_CDMA) {
+ Log.e(LOG_TAG, "Error! Emergency Callback Mode not supported for " +
+ PhoneFactory.getDefaultPhone().getPhoneName() + " phones");
+ stopSelf();
+ }
+
+ // Register receiver for intents
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED);
+ filter.addAction(TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS);
+ registerReceiver(mEcmReceiver, filter);
+
+ mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+
+ // Register ECM timer reset notfication
+ mPhone = PhoneFactory.getDefaultPhone();
+ mPhone.registerForEcmTimerReset(mHandler, ECM_TIMER_RESET, null);
+
+ startTimerNotification();
+ }
+
+ @Override
+ public void onDestroy() {
+ // Unregister receiver
+ unregisterReceiver(mEcmReceiver);
+ // Unregister ECM timer reset notification
+ mPhone.unregisterForEcmTimerReset(mHandler);
+
+ // Cancel the notification and timer
+ mNotificationManager.cancel(R.string.phone_in_ecm_notification_title);
+ mTimer.cancel();
+ }
+
+ /**
+ * Listens for Emergency Callback Mode intents
+ */
+ private BroadcastReceiver mEcmReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ // Stop the service when phone exits Emergency Callback Mode
+ if (intent.getAction().equals(
+ TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED)) {
+ if (intent.getBooleanExtra("phoneinECMState", false) == false) {
+ stopSelf();
+ }
+ }
+ // Show dialog box
+ else if (intent.getAction().equals(
+ TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS)) {
+ context.startActivity(
+ new Intent(TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS)
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ }
+ }
+ };
+
+ /**
+ * Start timer notification for Emergency Callback Mode
+ */
+ private void startTimerNotification() {
+ // Get Emergency Callback Mode timeout value
+ long ecmTimeout = SystemProperties.getLong(
+ TelephonyProperties.PROPERTY_ECM_EXIT_TIMER, DEFAULT_ECM_EXIT_TIMER_VALUE);
+
+ // Show the notification
+ showNotification(ecmTimeout);
+
+ // Start countdown timer for the notification updates
+ mTimer = new CountDownTimer(ecmTimeout, 1000) {
+
+ @Override
+ public void onTick(long millisUntilFinished) {
+ mTimeLeft = millisUntilFinished;
+ EmergencyCallbackModeService.this.showNotification(millisUntilFinished);
+ }
+
+ @Override
+ public void onFinish() {
+ //Do nothing
+ }
+
+ }.start();
+ }
+
+ /**
+ * Shows notification for Emergency Callback Mode
+ */
+ private void showNotification(long millisUntilFinished) {
+
+ // Set the icon and text
+ Notification notification = new Notification(
+ R.drawable.picture_emergency25x25,
+ getText(R.string.phone_entered_ecm_text), 0);
+
+ // PendingIntent to launch Emergency Callback Mode Exit activity if the user selects
+ // this notification
+ PendingIntent contentIntent = PendingIntent.getActivity(this, 0,
+ new Intent(EmergencyCallbackModeExitDialog.ACTION_SHOW_ECM_EXIT_DIALOG), 0);
+
+ // Format notification string
+ String text = null;
+ if(mInEmergencyCall) {
+ text = getText(R.string.phone_in_ecm_call_notification_text).toString();
+ } else {
+ int minutes = (int)(millisUntilFinished / 60000);
+ String time = String.format("%d:%02d", minutes, (millisUntilFinished % 60000) / 1000);
+ text = String.format(getResources().getQuantityText(
+ R.plurals.phone_in_ecm_notification_time, minutes).toString(), time);
+ }
+ // Set the info in the notification
+ notification.setLatestEventInfo(this, getText(R.string.phone_in_ecm_notification_title),
+ text, contentIntent);
+
+ notification.flags = Notification.FLAG_ONGOING_EVENT;
+
+ // Show notification
+ mNotificationManager.notify(R.string.phone_in_ecm_notification_title, notification);
+ }
+
+ /**
+ * Handle ECM_TIMER_RESET notification
+ */
+ private void resetEcmTimer(AsyncResult r) {
+ boolean isTimerCanceled = ((Boolean)r.result).booleanValue();
+
+ if (isTimerCanceled) {
+ mInEmergencyCall = true;
+ mTimer.cancel();
+ showNotification(0);
+ } else {
+ mInEmergencyCall = false;
+ startTimerNotification();
+ }
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ // This is the object that receives interactions from clients.
+ private final IBinder mBinder = new LocalBinder();
+
+ /**
+ * Class for clients to access
+ */
+ public class LocalBinder extends Binder {
+ EmergencyCallbackModeService getService() {
+ return EmergencyCallbackModeService.this;
+ }
+ }
+
+ /**
+ * Returns Emergency Callback Mode timeout value
+ */
+ public long getEmergencyCallbackModeTimeout() {
+ return mTimeLeft;
+ }
+
+ /**
+ * Returns Emergency Callback Mode call state
+ */
+ public boolean getEmergencyCallbackModeCallState() {
+ return mInEmergencyCall;
+ }
+}
diff --git a/src/com/android/phone/EmergencyDialer.java b/src/com/android/phone/EmergencyDialer.java
new file mode 100644
index 0000000..6900183
--- /dev/null
+++ b/src/com/android/phone/EmergencyDialer.java
@@ -0,0 +1,639 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.StatusBarManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.media.ToneGenerator;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.Settings;
+import android.telephony.PhoneNumberUtils;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.method.DialerKeyListener;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.EditText;
+
+import com.android.phone.common.HapticFeedback;
+
+
+/**
+ * EmergencyDialer is a special dialer that is used ONLY for dialing emergency calls.
+ *
+ * It's a simplified version of the regular dialer (i.e. the TwelveKeyDialer
+ * activity from apps/Contacts) that:
+ * 1. Allows ONLY emergency calls to be dialed
+ * 2. Disallows voicemail functionality
+ * 3. Uses the FLAG_SHOW_WHEN_LOCKED window manager flag to allow this
+ * activity to stay in front of the keyguard.
+ *
+ * TODO: Even though this is an ultra-simplified version of the normal
+ * dialer, there's still lots of code duplication between this class and
+ * the TwelveKeyDialer class from apps/Contacts. Could the common code be
+ * moved into a shared base class that would live in the framework?
+ * Or could we figure out some way to move *this* class into apps/Contacts
+ * also?
+ */
+public class EmergencyDialer extends Activity implements View.OnClickListener,
+ View.OnLongClickListener, View.OnHoverListener, View.OnKeyListener, TextWatcher {
+ // Keys used with onSaveInstanceState().
+ private static final String LAST_NUMBER = "lastNumber";
+
+ // Intent action for this activity.
+ public static final String ACTION_DIAL = "com.android.phone.EmergencyDialer.DIAL";
+
+ // List of dialer button IDs.
+ private static final int[] DIALER_KEYS = new int[] {
+ 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.zero, R.id.pound };
+
+ // Debug constants.
+ private static final boolean DBG = false;
+ private static final String LOG_TAG = "EmergencyDialer";
+
+ private PhoneGlobals mApp;
+ private StatusBarManager mStatusBarManager;
+ private AccessibilityManager mAccessibilityManager;
+
+ /** The length of DTMF tones in milliseconds */
+ private static final int TONE_LENGTH_MS = 150;
+
+ /** The DTMF tone volume relative to other sounds in the stream */
+ private static final int TONE_RELATIVE_VOLUME = 80;
+
+ /** Stream type used to play the DTMF tones off call, and mapped to the volume control keys */
+ private static final int DIAL_TONE_STREAM_TYPE = AudioManager.STREAM_DTMF;
+
+ private static final int BAD_EMERGENCY_NUMBER_DIALOG = 0;
+
+ private static final int USER_ACTIVITY_TIMEOUT_WHEN_NO_PROX_SENSOR = 15000; // millis
+
+ EditText mDigits;
+ private View mDialButton;
+ private View mDelete;
+
+ private ToneGenerator mToneGenerator;
+ private Object mToneGeneratorLock = new Object();
+
+ // determines if we want to playback local DTMF tones.
+ private boolean mDTMFToneEnabled;
+
+ // Haptic feedback (vibration) for dialer key presses.
+ private HapticFeedback mHaptic = new HapticFeedback();
+
+ // close activity when screen turns off
+ private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) {
+ finish();
+ }
+ }
+ };
+
+ private String mLastNumber; // last number we tried to dial. Used to restore error dialog.
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ // Do nothing
+ }
+
+ @Override
+ public void onTextChanged(CharSequence input, int start, int before, int changeCount) {
+ // Do nothing
+ }
+
+ @Override
+ public void afterTextChanged(Editable input) {
+ // Check for special sequences, in particular the "**04" or "**05"
+ // sequences that allow you to enter PIN or PUK-related codes.
+ //
+ // But note we *don't* allow most other special sequences here,
+ // like "secret codes" (*#*#<code>#*#*) or IMEI display ("*#06#"),
+ // since those shouldn't be available if the device is locked.
+ //
+ // So we call SpecialCharSequenceMgr.handleCharsForLockedDevice()
+ // here, not the regular handleChars() method.
+ if (SpecialCharSequenceMgr.handleCharsForLockedDevice(this, input.toString(), this)) {
+ // A special sequence was entered, clear the digits
+ mDigits.getText().clear();
+ }
+
+ updateDialAndDeleteButtonStateEnabledAttr();
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ mApp = PhoneGlobals.getInstance();
+ mStatusBarManager = (StatusBarManager) getSystemService(Context.STATUS_BAR_SERVICE);
+ mAccessibilityManager = (AccessibilityManager) getSystemService(ACCESSIBILITY_SERVICE);
+
+ // Allow this activity to be displayed in front of the keyguard / lockscreen.
+ WindowManager.LayoutParams lp = getWindow().getAttributes();
+ lp.flags |= WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
+ if (!mApp.proximitySensorModeEnabled()) {
+ // When no proximity sensor is available, use a shorter timeout.
+ lp.userActivityTimeout = USER_ACTIVITY_TIMEOUT_WHEN_NO_PROX_SENSOR;
+ }
+ getWindow().setAttributes(lp);
+
+ setContentView(R.layout.emergency_dialer);
+
+ mDigits = (EditText) findViewById(R.id.digits);
+ mDigits.setKeyListener(DialerKeyListener.getInstance());
+ mDigits.setOnClickListener(this);
+ mDigits.setOnKeyListener(this);
+ mDigits.setLongClickable(false);
+ if (mAccessibilityManager.isEnabled()) {
+ // The text view must be selected to send accessibility events.
+ mDigits.setSelected(true);
+ }
+ maybeAddNumberFormatting();
+
+ // Check for the presence of the keypad
+ View view = findViewById(R.id.one);
+ if (view != null) {
+ setupKeypad();
+ }
+
+ mDelete = findViewById(R.id.deleteButton);
+ mDelete.setOnClickListener(this);
+ mDelete.setOnLongClickListener(this);
+
+ mDialButton = findViewById(R.id.dialButton);
+
+ // Check whether we should show the onscreen "Dial" button and co.
+ Resources res = getResources();
+ if (res.getBoolean(R.bool.config_show_onscreen_dial_button)) {
+ mDialButton.setOnClickListener(this);
+ } else {
+ mDialButton.setVisibility(View.GONE);
+ }
+
+ if (icicle != null) {
+ super.onRestoreInstanceState(icicle);
+ }
+
+ // Extract phone number from intent
+ Uri data = getIntent().getData();
+ if (data != null && (Constants.SCHEME_TEL.equals(data.getScheme()))) {
+ String number = PhoneNumberUtils.getNumberFromIntent(getIntent(), this);
+ if (number != null) {
+ mDigits.setText(number);
+ }
+ }
+
+ // if the mToneGenerator creation fails, just continue without it. It is
+ // a local audio signal, and is not as important as the dtmf tone itself.
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator == null) {
+ try {
+ mToneGenerator = new ToneGenerator(DIAL_TONE_STREAM_TYPE, TONE_RELATIVE_VOLUME);
+ } catch (RuntimeException e) {
+ Log.w(LOG_TAG, "Exception caught while creating local tone generator: " + e);
+ mToneGenerator = null;
+ }
+ }
+ }
+
+ final IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ registerReceiver(mBroadcastReceiver, intentFilter);
+
+ try {
+ mHaptic.init(this, res.getBoolean(R.bool.config_enable_dialer_key_vibration));
+ } catch (Resources.NotFoundException nfe) {
+ Log.e(LOG_TAG, "Vibrate control bool missing.", nfe);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator != null) {
+ mToneGenerator.release();
+ mToneGenerator = null;
+ }
+ }
+ unregisterReceiver(mBroadcastReceiver);
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle icicle) {
+ mLastNumber = icicle.getString(LAST_NUMBER);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putString(LAST_NUMBER, mLastNumber);
+ }
+
+ /**
+ * Explicitly turn off number formatting, since it gets in the way of the emergency
+ * number detector
+ */
+ protected void maybeAddNumberFormatting() {
+ // Do nothing.
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+
+ // This can't be done in onCreate(), since the auto-restoring of the digits
+ // will play DTMF tones for all the old digits if it is when onRestoreSavedInstanceState()
+ // is called. This method will be called every time the activity is created, and
+ // will always happen after onRestoreSavedInstanceState().
+ mDigits.addTextChangedListener(this);
+ }
+
+ private void setupKeypad() {
+ // Setup the listeners for the buttons
+ for (int id : DIALER_KEYS) {
+ final View key = findViewById(id);
+ key.setOnClickListener(this);
+ key.setOnHoverListener(this);
+ }
+
+ View view = findViewById(R.id.zero);
+ view.setOnLongClickListener(this);
+ }
+
+ /**
+ * handle key events
+ */
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ // Happen when there's a "Call" hard button.
+ case KeyEvent.KEYCODE_CALL: {
+ if (TextUtils.isEmpty(mDigits.getText().toString())) {
+ // if we are adding a call from the InCallScreen and the phone
+ // number entered is empty, we just close the dialer to expose
+ // the InCallScreen under it.
+ finish();
+ } else {
+ // otherwise, we place the call.
+ placeCall();
+ }
+ return true;
+ }
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void keyPressed(int keyCode) {
+ mHaptic.vibrate();
+ KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, keyCode);
+ mDigits.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKey(View view, int keyCode, KeyEvent event) {
+ switch (view.getId()) {
+ case R.id.digits:
+ // Happen when "Done" button of the IME is pressed. This can happen when this
+ // Activity is forced into landscape mode due to a desk dock.
+ if (keyCode == KeyEvent.KEYCODE_ENTER
+ && event.getAction() == KeyEvent.ACTION_UP) {
+ placeCall();
+ return true;
+ }
+ break;
+ }
+ return false;
+ }
+
+ @Override
+ public void onClick(View view) {
+ switch (view.getId()) {
+ case R.id.one: {
+ playTone(ToneGenerator.TONE_DTMF_1);
+ keyPressed(KeyEvent.KEYCODE_1);
+ return;
+ }
+ case R.id.two: {
+ playTone(ToneGenerator.TONE_DTMF_2);
+ keyPressed(KeyEvent.KEYCODE_2);
+ return;
+ }
+ case R.id.three: {
+ playTone(ToneGenerator.TONE_DTMF_3);
+ keyPressed(KeyEvent.KEYCODE_3);
+ return;
+ }
+ case R.id.four: {
+ playTone(ToneGenerator.TONE_DTMF_4);
+ keyPressed(KeyEvent.KEYCODE_4);
+ return;
+ }
+ case R.id.five: {
+ playTone(ToneGenerator.TONE_DTMF_5);
+ keyPressed(KeyEvent.KEYCODE_5);
+ return;
+ }
+ case R.id.six: {
+ playTone(ToneGenerator.TONE_DTMF_6);
+ keyPressed(KeyEvent.KEYCODE_6);
+ return;
+ }
+ case R.id.seven: {
+ playTone(ToneGenerator.TONE_DTMF_7);
+ keyPressed(KeyEvent.KEYCODE_7);
+ return;
+ }
+ case R.id.eight: {
+ playTone(ToneGenerator.TONE_DTMF_8);
+ keyPressed(KeyEvent.KEYCODE_8);
+ return;
+ }
+ case R.id.nine: {
+ playTone(ToneGenerator.TONE_DTMF_9);
+ keyPressed(KeyEvent.KEYCODE_9);
+ return;
+ }
+ case R.id.zero: {
+ playTone(ToneGenerator.TONE_DTMF_0);
+ keyPressed(KeyEvent.KEYCODE_0);
+ return;
+ }
+ case R.id.pound: {
+ playTone(ToneGenerator.TONE_DTMF_P);
+ keyPressed(KeyEvent.KEYCODE_POUND);
+ return;
+ }
+ case R.id.star: {
+ playTone(ToneGenerator.TONE_DTMF_S);
+ keyPressed(KeyEvent.KEYCODE_STAR);
+ return;
+ }
+ case R.id.deleteButton: {
+ keyPressed(KeyEvent.KEYCODE_DEL);
+ return;
+ }
+ case R.id.dialButton: {
+ mHaptic.vibrate(); // Vibrate here too, just like we do for the regular keys
+ placeCall();
+ return;
+ }
+ case R.id.digits: {
+ if (mDigits.length() != 0) {
+ mDigits.setCursorVisible(true);
+ }
+ return;
+ }
+ }
+ }
+
+ /**
+ * Implemented for {@link android.view.View.OnHoverListener}. Handles touch
+ * events for accessibility when touch exploration is enabled.
+ */
+ @Override
+ public boolean onHover(View v, MotionEvent event) {
+ // When touch exploration is turned on, lifting a finger while inside
+ // the button's hover target bounds should perform a click action.
+ if (mAccessibilityManager.isEnabled()
+ && mAccessibilityManager.isTouchExplorationEnabled()) {
+
+ switch (event.getActionMasked()) {
+ case MotionEvent.ACTION_HOVER_ENTER:
+ // Lift-to-type temporarily disables double-tap activation.
+ v.setClickable(false);
+ break;
+ case MotionEvent.ACTION_HOVER_EXIT:
+ final int left = v.getPaddingLeft();
+ final int right = (v.getWidth() - v.getPaddingRight());
+ final int top = v.getPaddingTop();
+ final int bottom = (v.getHeight() - v.getPaddingBottom());
+ final int x = (int) event.getX();
+ final int y = (int) event.getY();
+ if ((x > left) && (x < right) && (y > top) && (y < bottom)) {
+ v.performClick();
+ }
+ v.setClickable(true);
+ break;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * called for long touch events
+ */
+ @Override
+ public boolean onLongClick(View view) {
+ int id = view.getId();
+ switch (id) {
+ case R.id.deleteButton: {
+ mDigits.getText().clear();
+ // TODO: The framework forgets to clear the pressed
+ // status of disabled button. Until this is fixed,
+ // clear manually the pressed status. b/2133127
+ mDelete.setPressed(false);
+ return true;
+ }
+ case R.id.zero: {
+ keyPressed(KeyEvent.KEYCODE_PLUS);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ // retrieve the DTMF tone play back setting.
+ mDTMFToneEnabled = Settings.System.getInt(getContentResolver(),
+ Settings.System.DTMF_TONE_WHEN_DIALING, 1) == 1;
+
+ // Retrieve the haptic feedback setting.
+ mHaptic.checkSystemSetting();
+
+ // if the mToneGenerator creation fails, just continue without it. It is
+ // a local audio signal, and is not as important as the dtmf tone itself.
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator == null) {
+ try {
+ mToneGenerator = new ToneGenerator(AudioManager.STREAM_DTMF,
+ TONE_RELATIVE_VOLUME);
+ } catch (RuntimeException e) {
+ Log.w(LOG_TAG, "Exception caught while creating local tone generator: " + e);
+ mToneGenerator = null;
+ }
+ }
+ }
+
+ // Disable the status bar and set the poke lock timeout to medium.
+ // There is no need to do anything with the wake lock.
+ if (DBG) Log.d(LOG_TAG, "disabling status bar, set to long timeout");
+ mStatusBarManager.disable(StatusBarManager.DISABLE_EXPAND);
+
+ updateDialAndDeleteButtonStateEnabledAttr();
+ }
+
+ @Override
+ public void onPause() {
+ // Reenable the status bar and set the poke lock timeout to default.
+ // There is no need to do anything with the wake lock.
+ if (DBG) Log.d(LOG_TAG, "reenabling status bar and closing the dialer");
+ mStatusBarManager.disable(StatusBarManager.DISABLE_NONE);
+
+ super.onPause();
+
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator != null) {
+ mToneGenerator.release();
+ mToneGenerator = null;
+ }
+ }
+ }
+
+ /**
+ * place the call, but check to make sure it is a viable number.
+ */
+ private void placeCall() {
+ mLastNumber = mDigits.getText().toString();
+ if (PhoneNumberUtils.isLocalEmergencyNumber(mLastNumber, this)) {
+ if (DBG) Log.d(LOG_TAG, "placing call to " + mLastNumber);
+
+ // place the call if it is a valid number
+ if (mLastNumber == null || !TextUtils.isGraphic(mLastNumber)) {
+ // There is no number entered.
+ playTone(ToneGenerator.TONE_PROP_NACK);
+ return;
+ }
+ Intent intent = new Intent(Intent.ACTION_CALL_EMERGENCY);
+ intent.setData(Uri.fromParts(Constants.SCHEME_TEL, mLastNumber, null));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ finish();
+ } else {
+ if (DBG) Log.d(LOG_TAG, "rejecting bad requested number " + mLastNumber);
+
+ // erase the number and throw up an alert dialog.
+ mDigits.getText().delete(0, mDigits.getText().length());
+ showDialog(BAD_EMERGENCY_NUMBER_DIALOG);
+ }
+ }
+
+ /**
+ * Plays the specified tone for TONE_LENGTH_MS milliseconds.
+ *
+ * The tone is played locally, using the audio stream for phone calls.
+ * Tones are played only if the "Audible touch tones" user preference
+ * is checked, and are NOT played if the device is in silent mode.
+ *
+ * @param tone a tone code from {@link ToneGenerator}
+ */
+ void playTone(int tone) {
+ // if local tone playback is disabled, just return.
+ if (!mDTMFToneEnabled) {
+ return;
+ }
+
+ // Also do nothing if the phone is in silent mode.
+ // We need to re-check the ringer mode for *every* playTone()
+ // call, rather than keeping a local flag that's updated in
+ // onResume(), since it's possible to toggle silent mode without
+ // leaving the current activity (via the ENDCALL-longpress menu.)
+ AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ int ringerMode = audioManager.getRingerMode();
+ if ((ringerMode == AudioManager.RINGER_MODE_SILENT)
+ || (ringerMode == AudioManager.RINGER_MODE_VIBRATE)) {
+ return;
+ }
+
+ synchronized (mToneGeneratorLock) {
+ if (mToneGenerator == null) {
+ Log.w(LOG_TAG, "playTone: mToneGenerator == null, tone: " + tone);
+ return;
+ }
+
+ // Start the new tone (will stop any playing tone)
+ mToneGenerator.startTone(tone, TONE_LENGTH_MS);
+ }
+ }
+
+ private CharSequence createErrorMessage(String number) {
+ if (!TextUtils.isEmpty(number)) {
+ return getString(R.string.dial_emergency_error, mLastNumber);
+ } else {
+ return getText(R.string.dial_emergency_empty_error).toString();
+ }
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ AlertDialog dialog = null;
+ if (id == BAD_EMERGENCY_NUMBER_DIALOG) {
+ // construct dialog
+ dialog = new AlertDialog.Builder(this)
+ .setTitle(getText(R.string.emergency_enable_radio_dialog_title))
+ .setMessage(createErrorMessage(mLastNumber))
+ .setPositiveButton(R.string.ok, null)
+ .setCancelable(true).create();
+
+ // blur stuff behind the dialog
+ dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+ }
+ return dialog;
+ }
+
+ @Override
+ protected void onPrepareDialog(int id, Dialog dialog) {
+ super.onPrepareDialog(id, dialog);
+ if (id == BAD_EMERGENCY_NUMBER_DIALOG) {
+ AlertDialog alert = (AlertDialog) dialog;
+ alert.setMessage(createErrorMessage(mLastNumber));
+ }
+ }
+
+ /**
+ * Update the enabledness of the "Dial" and "Backspace" buttons if applicable.
+ */
+ private void updateDialAndDeleteButtonStateEnabledAttr() {
+ final boolean notEmpty = mDigits.length() != 0;
+
+ mDialButton.setEnabled(notEmpty);
+ mDelete.setEnabled(notEmpty);
+ }
+}
diff --git a/src/com/android/phone/EnableFdnScreen.java b/src/com/android/phone/EnableFdnScreen.java
new file mode 100644
index 0000000..0db47c3
--- /dev/null
+++ b/src/com/android/phone/EnableFdnScreen.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.Activity;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+import android.text.method.DigitsKeyListener;
+import android.util.Log;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.Phone;
+
+/**
+ * UI to enable/disable FDN.
+ */
+public class EnableFdnScreen extends Activity {
+ private static final String LOG_TAG = PhoneGlobals.LOG_TAG;
+ private static final boolean DBG = false;
+
+ private static final int ENABLE_FDN_COMPLETE = 100;
+
+ private LinearLayout mPinFieldContainer;
+ private EditText mPin2Field;
+ private TextView mStatusField;
+ private boolean mEnable;
+ private Phone mPhone;
+
+ private Handler mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case ENABLE_FDN_COMPLETE:
+ AsyncResult ar = (AsyncResult) msg.obj;
+ handleResult(ar);
+ break;
+ }
+
+ return;
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.enable_fdn_screen);
+ setupView();
+
+ mPhone = PhoneGlobals.getPhone();
+ mEnable = !mPhone.getIccCard().getIccFdnEnabled();
+
+ int id = mEnable ? R.string.enable_fdn : R.string.disable_fdn;
+ setTitle(getResources().getText(id));
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mPhone = PhoneGlobals.getPhone();
+ }
+
+ private void setupView() {
+ mPin2Field = (EditText) findViewById(R.id.pin);
+ mPin2Field.setKeyListener(DigitsKeyListener.getInstance());
+ mPin2Field.setMovementMethod(null);
+ mPin2Field.setOnClickListener(mClicked);
+
+ mPinFieldContainer = (LinearLayout) findViewById(R.id.pinc);
+ mStatusField = (TextView) findViewById(R.id.status);
+ }
+
+ private void showStatus(CharSequence statusMsg) {
+ if (statusMsg != null) {
+ mStatusField.setText(statusMsg);
+ mStatusField.setVisibility(View.VISIBLE);
+ mPinFieldContainer.setVisibility(View.GONE);
+ } else {
+ mPinFieldContainer.setVisibility(View.VISIBLE);
+ mStatusField.setVisibility(View.GONE);
+ }
+ }
+
+ private String getPin2() {
+ return mPin2Field.getText().toString();
+ }
+
+ private void enableFdn() {
+ Message callback = Message.obtain(mHandler, ENABLE_FDN_COMPLETE);
+ mPhone.getIccCard().setIccFdnEnabled(mEnable, getPin2(), callback);
+ if (DBG) log("enableFdn: please wait...");
+ }
+
+ private void handleResult(AsyncResult ar) {
+ if (ar.exception == null) {
+ if (DBG) log("handleResult: success!");
+ showStatus(getResources().getText(mEnable ?
+ R.string.enable_fdn_ok : R.string.disable_fdn_ok));
+ } else if (ar.exception instanceof CommandException
+ /* && ((CommandException)ar.exception).getCommandError() ==
+ CommandException.Error.GENERIC_FAILURE */ ) {
+ if (DBG) log("handleResult: failed!");
+ showStatus(getResources().getText(
+ R.string.pin_failed));
+ }
+
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ finish();
+ }
+ }, 3000);
+ }
+
+ private View.OnClickListener mClicked = new View.OnClickListener() {
+ public void onClick(View v) {
+ if (TextUtils.isEmpty(mPin2Field.getText())) {
+ return;
+ }
+
+ showStatus(getResources().getText(
+ R.string.enable_in_progress));
+
+ enableFdn();
+ }
+ };
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, "[EnableSimPin] " + msg);
+ }
+}
diff --git a/src/com/android/phone/EnableIccPinScreen.java b/src/com/android/phone/EnableIccPinScreen.java
new file mode 100644
index 0000000..160978f
--- /dev/null
+++ b/src/com/android/phone/EnableIccPinScreen.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.Activity;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.TextUtils;
+import android.text.method.DigitsKeyListener;
+import android.util.Log;
+import android.view.View;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.Phone;
+
+/**
+ * UI to enable/disable the ICC PIN.
+ */
+public class EnableIccPinScreen extends Activity {
+ private static final String LOG_TAG = PhoneGlobals.LOG_TAG;
+
+ private static final int ENABLE_ICC_PIN_COMPLETE = 100;
+ private static final boolean DBG = false;
+
+ private LinearLayout mPinFieldContainer;
+ private EditText mPinField;
+ private TextView mStatusField;
+ private boolean mEnable;
+ private Phone mPhone;
+
+ private Handler mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case ENABLE_ICC_PIN_COMPLETE:
+ AsyncResult ar = (AsyncResult) msg.obj;
+ handleResult(ar);
+ break;
+ }
+
+ return;
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.enable_sim_pin_screen);
+ setupView();
+
+ mPhone = PhoneGlobals.getPhone();
+ mEnable = !mPhone.getIccCard().getIccLockEnabled();
+
+ int id = mEnable ? R.string.enable_sim_pin : R.string.disable_sim_pin;
+ setTitle(getResources().getText(id));
+ }
+
+ private void setupView() {
+ mPinField = (EditText) findViewById(R.id.pin);
+ mPinField.setKeyListener(DigitsKeyListener.getInstance());
+ mPinField.setMovementMethod(null);
+ mPinField.setOnClickListener(mClicked);
+
+ mPinFieldContainer = (LinearLayout) findViewById(R.id.pinc);
+ mStatusField = (TextView) findViewById(R.id.status);
+ }
+
+ private void showStatus(CharSequence statusMsg) {
+ if (statusMsg != null) {
+ mStatusField.setText(statusMsg);
+ mStatusField.setVisibility(View.VISIBLE);
+ mPinFieldContainer.setVisibility(View.GONE);
+ } else {
+ mPinFieldContainer.setVisibility(View.VISIBLE);
+ mStatusField.setVisibility(View.GONE);
+ }
+ }
+
+ private String getPin() {
+ return mPinField.getText().toString();
+ }
+
+ private void enableIccPin() {
+ Message callback = Message.obtain(mHandler, ENABLE_ICC_PIN_COMPLETE);
+ if (DBG) log("enableIccPin:");
+ mPhone.getIccCard().setIccLockEnabled(mEnable, getPin(), callback);
+ if (DBG) log("enableIccPin: please wait...");
+ }
+
+ private void handleResult(AsyncResult ar) {
+ if (ar.exception == null) {
+ if (DBG) log("handleResult: success!");
+ showStatus(getResources().getText(
+ mEnable ? R.string.enable_pin_ok : R.string.disable_pin_ok));
+ } else if (ar.exception instanceof CommandException
+ /* && ((CommandException)ar.exception).getCommandError() ==
+ CommandException.Error.GENERIC_FAILURE */ ) {
+ if (DBG) log("handleResult: failed!");
+ showStatus(getResources().getText(
+ R.string.pin_failed));
+ }
+
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ finish();
+ }
+ }, 3000);
+ }
+
+ private View.OnClickListener mClicked = new View.OnClickListener() {
+ public void onClick(View v) {
+ if (TextUtils.isEmpty(mPinField.getText())) {
+ return;
+ }
+
+ showStatus(getResources().getText(
+ R.string.enable_in_progress));
+
+ enableIccPin();
+ }
+ };
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, "[EnableIccPin] " + msg);
+ }
+}
diff --git a/src/com/android/phone/EventLogTags.logtags b/src/com/android/phone/EventLogTags.logtags
new file mode 100644
index 0000000..474a01c
--- /dev/null
+++ b/src/com/android/phone/EventLogTags.logtags
@@ -0,0 +1,9 @@
+# See system/core/logcat/event.logtags for a description of the format of this file.
+
+option java_package com.android.phone;
+
+70301 phone_ui_enter
+70302 phone_ui_exit
+70303 phone_ui_button_click (text|3)
+70304 phone_ui_ringer_query_elapsed
+70305 phone_ui_multiple_query
diff --git a/src/com/android/phone/FakePhoneActivity.java b/src/com/android/phone/FakePhoneActivity.java
new file mode 100644
index 0000000..686a766
--- /dev/null
+++ b/src/com/android/phone/FakePhoneActivity.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2007 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.phone;
+
+import android.app.Activity;
+import android.app.NotificationManager;
+import android.os.Bundle;
+import com.android.internal.telephony.test.SimulatedRadioControl;
+import android.util.Log;
+import android.view.View.OnClickListener;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.Toast;
+
+/**
+ * A simple activity that presents you with a UI for faking incoming phone operations.
+ */
+public class FakePhoneActivity extends Activity {
+ private static final String TAG = "FakePhoneActivity";
+
+ private Button mPlaceCall;
+ private EditText mPhoneNumber;
+ SimulatedRadioControl mRadioControl;
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.fake_phone_activity);
+
+ mPlaceCall = (Button) findViewById(R.id.placeCall);
+ mPlaceCall.setOnClickListener(new ButtonListener());
+
+ mPhoneNumber = (EditText) findViewById(R.id.phoneNumber);
+ mPhoneNumber.setOnClickListener(
+ new View.OnClickListener() {
+ public void onClick(View v) {
+ mPlaceCall.requestFocus();
+ }
+ });
+
+ mRadioControl = PhoneGlobals.getPhone().getSimulatedRadioControl();
+
+ Log.i(TAG, "- PhoneApp.getInstance(): " + PhoneGlobals.getInstance());
+ Log.i(TAG, "- PhoneApp.getPhone(): " + PhoneGlobals.getPhone());
+ Log.i(TAG, "- mRadioControl: " + mRadioControl);
+ }
+
+ private class ButtonListener implements OnClickListener {
+ public void onClick(View v) {
+ if (mRadioControl == null) {
+ Log.e("Phone", "SimulatedRadioControl not available, abort!");
+ NotificationManager nm =
+ (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ Toast.makeText(FakePhoneActivity.this, "null mRadioControl!",
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ mRadioControl.triggerRing(mPhoneNumber.getText().toString());
+ }
+ }
+}
diff --git a/src/com/android/phone/FdnList.java b/src/com/android/phone/FdnList.java
new file mode 100644
index 0000000..bd9680c
--- /dev/null
+++ b/src/com/android/phone/FdnList.java
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2007 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.phone;
+
+import android.app.ActionBar;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Bundle;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ListView;
+
+/**
+ * FDN List UI for the Phone app.
+ */
+public class FdnList extends ADNList {
+ private static final int MENU_ADD = 1;
+ private static final int MENU_EDIT = 2;
+ private static final int MENU_DELETE = 3;
+
+ private static final String INTENT_EXTRA_NAME = "name";
+ private static final String INTENT_EXTRA_NUMBER = "number";
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ // android.R.id.home will be triggered in onOptionsItemSelected()
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ @Override
+ protected Uri resolveIntent() {
+ Intent intent = getIntent();
+ intent.setData(Uri.parse("content://icc/fdn"));
+ return intent.getData();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ Resources r = getResources();
+
+ // Added the icons to the context menu
+ menu.add(0, MENU_ADD, 0, r.getString(R.string.menu_add))
+ .setIcon(android.R.drawable.ic_menu_add);
+ menu.add(0, MENU_EDIT, 0, r.getString(R.string.menu_edit))
+ .setIcon(android.R.drawable.ic_menu_edit);
+ menu.add(0, MENU_DELETE, 0, r.getString(R.string.menu_delete))
+ .setIcon(android.R.drawable.ic_menu_delete);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ boolean hasSelection = (getSelectedItemPosition() >= 0);
+
+ menu.findItem(MENU_ADD).setVisible(true);
+ menu.findItem(MENU_EDIT).setVisible(hasSelection);
+ menu.findItem(MENU_DELETE).setVisible(hasSelection);
+
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home: // See ActionBar#setDisplayHomeAsUpEnabled()
+ Intent intent = new Intent(this, FdnSetting.class);
+ intent.setAction(Intent.ACTION_MAIN);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ finish();
+ return true;
+
+ case MENU_ADD:
+ addContact();
+ return true;
+
+ case MENU_EDIT:
+ editSelected();
+ return true;
+
+ case MENU_DELETE:
+ deleteSelected();
+ return true;
+ }
+
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ // TODO: is this what we really want?
+ editSelected(position);
+ }
+
+ private void addContact() {
+ // if we don't put extras "name" when starting this activity, then
+ // EditFdnContactScreen treats it like add contact.
+ Intent intent = new Intent();
+ intent.setClass(this, EditFdnContactScreen.class);
+ startActivity(intent);
+ }
+
+ /**
+ * Overloaded to call editSelected with the current selection
+ * by default. This method may have a problem with touch UI
+ * since touch UI does not really have a concept of "selected"
+ * items.
+ */
+ private void editSelected() {
+ editSelected(getSelectedItemPosition());
+ }
+
+ /**
+ * Edit the item at the selected position in the list.
+ */
+ private void editSelected(int position) {
+ if (mCursor.moveToPosition(position)) {
+ String name = mCursor.getString(NAME_COLUMN);
+ String number = mCursor.getString(NUMBER_COLUMN);
+
+ Intent intent = new Intent();
+ intent.setClass(this, EditFdnContactScreen.class);
+ intent.putExtra(INTENT_EXTRA_NAME, name);
+ intent.putExtra(INTENT_EXTRA_NUMBER, number);
+ startActivity(intent);
+ }
+ }
+
+ private void deleteSelected() {
+ if (mCursor.moveToPosition(getSelectedItemPosition())) {
+ String name = mCursor.getString(NAME_COLUMN);
+ String number = mCursor.getString(NUMBER_COLUMN);
+
+ Intent intent = new Intent();
+ intent.setClass(this, DeleteFdnContactScreen.class);
+ intent.putExtra(INTENT_EXTRA_NAME, name);
+ intent.putExtra(INTENT_EXTRA_NUMBER, number);
+ startActivity(intent);
+ }
+ }
+}
diff --git a/src/com/android/phone/FdnSetting.java b/src/com/android/phone/FdnSetting.java
new file mode 100644
index 0000000..283d612
--- /dev/null
+++ b/src/com/android/phone/FdnSetting.java
@@ -0,0 +1,493 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+import android.view.MenuItem;
+import android.view.WindowManager;
+import android.widget.Toast;
+
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.Phone;
+
+/**
+ * FDN settings UI for the Phone app.
+ * Rewritten to look and behave closer to the other preferences.
+ */
+public class FdnSetting extends PreferenceActivity
+ implements EditPinPreference.OnPinEnteredListener, DialogInterface.OnCancelListener {
+
+ private static final String LOG_TAG = PhoneGlobals.LOG_TAG;
+ private static final boolean DBG = false;
+
+ private Phone mPhone;
+
+ /**
+ * Events we handle.
+ * The first is used for toggling FDN enable, the second for the PIN change.
+ */
+ private static final int EVENT_PIN2_ENTRY_COMPLETE = 100;
+ private static final int EVENT_PIN2_CHANGE_COMPLETE = 200;
+
+ // String keys for preference lookup
+ // We only care about the pin preferences here, the manage FDN contacts
+ // Preference is handled solely in xml.
+ private static final String BUTTON_FDN_ENABLE_KEY = "button_fdn_enable_key";
+ private static final String BUTTON_CHANGE_PIN2_KEY = "button_change_pin2_key";
+
+ private EditPinPreference mButtonEnableFDN;
+ private EditPinPreference mButtonChangePin2;
+
+ // State variables
+ private String mOldPin;
+ private String mNewPin;
+ private String mPuk2;
+ private static final int PIN_CHANGE_OLD = 0;
+ private static final int PIN_CHANGE_NEW = 1;
+ private static final int PIN_CHANGE_REENTER = 2;
+ private static final int PIN_CHANGE_PUK = 3;
+ private static final int PIN_CHANGE_NEW_PIN_FOR_PUK = 4;
+ private static final int PIN_CHANGE_REENTER_PIN_FOR_PUK = 5;
+ private int mPinChangeState;
+ private boolean mIsPuk2Locked; // Indicates we know that we are PUK2 blocked.
+
+ private static final String SKIP_OLD_PIN_KEY = "skip_old_pin_key";
+ private static final String PIN_CHANGE_STATE_KEY = "pin_change_state_key";
+ private static final String OLD_PIN_KEY = "old_pin_key";
+ private static final String NEW_PIN_KEY = "new_pin_key";
+ private static final String DIALOG_MESSAGE_KEY = "dialog_message_key";
+ private static final String DIALOG_PIN_ENTRY_KEY = "dialog_pin_entry_key";
+
+ // size limits for the pin.
+ private static final int MIN_PIN_LENGTH = 4;
+ private static final int MAX_PIN_LENGTH = 8;
+
+ /**
+ * Delegate to the respective handlers.
+ */
+ @Override
+ public void onPinEntered(EditPinPreference preference, boolean positiveResult) {
+ if (preference == mButtonEnableFDN) {
+ toggleFDNEnable(positiveResult);
+ } else if (preference == mButtonChangePin2){
+ updatePINChangeState(positiveResult);
+ }
+ }
+
+ /**
+ * Attempt to toggle FDN activation.
+ */
+ private void toggleFDNEnable(boolean positiveResult) {
+ if (!positiveResult) {
+ return;
+ }
+
+ // validate the pin first, before submitting it to the RIL for FDN enable.
+ String password = mButtonEnableFDN.getText();
+ if (validatePin (password, false)) {
+ // get the relevant data for the icc call
+ boolean isEnabled = mPhone.getIccCard().getIccFdnEnabled();
+ Message onComplete = mFDNHandler.obtainMessage(EVENT_PIN2_ENTRY_COMPLETE);
+
+ // make fdn request
+ mPhone.getIccCard().setIccFdnEnabled(!isEnabled, password, onComplete);
+ } else {
+ // throw up error if the pin is invalid.
+ displayMessage(R.string.invalidPin2);
+ }
+
+ mButtonEnableFDN.setText("");
+ }
+
+ /**
+ * Attempt to change the pin.
+ */
+ private void updatePINChangeState(boolean positiveResult) {
+ if (DBG) log("updatePINChangeState positive=" + positiveResult
+ + " mPinChangeState=" + mPinChangeState
+ + " mSkipOldPin=" + mIsPuk2Locked);
+
+ if (!positiveResult) {
+ // reset the state on cancel, either to expect PUK2 or PIN2
+ if (!mIsPuk2Locked) {
+ resetPinChangeState();
+ } else {
+ resetPinChangeStateForPUK2();
+ }
+ return;
+ }
+
+ // Progress through the dialog states, generally in this order:
+ // 1. Enter old pin
+ // 2. Enter new pin
+ // 3. Re-Enter new pin
+ // While handling any error conditions that may show up in between.
+ // Also handle the PUK2 entry, if it is requested.
+ //
+ // In general, if any invalid entries are made, the dialog re-
+ // appears with text to indicate what the issue is.
+ switch (mPinChangeState) {
+ case PIN_CHANGE_OLD:
+ mOldPin = mButtonChangePin2.getText();
+ mButtonChangePin2.setText("");
+ // if the pin is not valid, display a message and reset the state.
+ if (validatePin (mOldPin, false)) {
+ mPinChangeState = PIN_CHANGE_NEW;
+ displayPinChangeDialog();
+ } else {
+ displayPinChangeDialog(R.string.invalidPin2, true);
+ }
+ break;
+ case PIN_CHANGE_NEW:
+ mNewPin = mButtonChangePin2.getText();
+ mButtonChangePin2.setText("");
+ // if the new pin is not valid, display a message and reset the state.
+ if (validatePin (mNewPin, false)) {
+ mPinChangeState = PIN_CHANGE_REENTER;
+ displayPinChangeDialog();
+ } else {
+ displayPinChangeDialog(R.string.invalidPin2, true);
+ }
+ break;
+ case PIN_CHANGE_REENTER:
+ // if the re-entered pin is not valid, display a message and reset the state.
+ if (!mNewPin.equals(mButtonChangePin2.getText())) {
+ mPinChangeState = PIN_CHANGE_NEW;
+ mButtonChangePin2.setText("");
+ displayPinChangeDialog(R.string.mismatchPin2, true);
+ } else {
+ // If the PIN is valid, then we submit the change PIN request.
+ mButtonChangePin2.setText("");
+ Message onComplete = mFDNHandler.obtainMessage(
+ EVENT_PIN2_CHANGE_COMPLETE);
+ mPhone.getIccCard().changeIccFdnPassword(
+ mOldPin, mNewPin, onComplete);
+ }
+ break;
+ case PIN_CHANGE_PUK: {
+ // Doh! too many incorrect requests, PUK requested.
+ mPuk2 = mButtonChangePin2.getText();
+ mButtonChangePin2.setText("");
+ // if the puk is not valid, display
+ // a message and reset the state.
+ if (validatePin (mPuk2, true)) {
+ mPinChangeState = PIN_CHANGE_NEW_PIN_FOR_PUK;
+ displayPinChangeDialog();
+ } else {
+ displayPinChangeDialog(R.string.invalidPuk2, true);
+ }
+ }
+ break;
+ case PIN_CHANGE_NEW_PIN_FOR_PUK:
+ mNewPin = mButtonChangePin2.getText();
+ mButtonChangePin2.setText("");
+ // if the new pin is not valid, display
+ // a message and reset the state.
+ if (validatePin (mNewPin, false)) {
+ mPinChangeState = PIN_CHANGE_REENTER_PIN_FOR_PUK;
+ displayPinChangeDialog();
+ } else {
+ displayPinChangeDialog(R.string.invalidPin2, true);
+ }
+ break;
+ case PIN_CHANGE_REENTER_PIN_FOR_PUK:
+ // if the re-entered pin is not valid, display
+ // a message and reset the state.
+ if (!mNewPin.equals(mButtonChangePin2.getText())) {
+ mPinChangeState = PIN_CHANGE_NEW_PIN_FOR_PUK;
+ mButtonChangePin2.setText("");
+ displayPinChangeDialog(R.string.mismatchPin2, true);
+ } else {
+ // Both puk2 and new pin2 are ready to submit
+ mButtonChangePin2.setText("");
+ Message onComplete = mFDNHandler.obtainMessage(
+ EVENT_PIN2_CHANGE_COMPLETE);
+ mPhone.getIccCard().supplyPuk2(mPuk2, mNewPin, onComplete);
+ }
+ break;
+ }
+ }
+
+ /**
+ * Handler for asynchronous replies from the sim.
+ */
+ private final Handler mFDNHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+
+ // when we are enabling FDN, either we are unsuccessful and display
+ // a toast, or just update the UI.
+ case EVENT_PIN2_ENTRY_COMPLETE: {
+ AsyncResult ar = (AsyncResult) msg.obj;
+ if (ar.exception != null) {
+ // see if PUK2 is requested and alert the user accordingly.
+ CommandException ce = (CommandException) ar.exception;
+ if (ce.getCommandError() == CommandException.Error.SIM_PUK2) {
+ // make sure we set the PUK2 state so that we can skip
+ // some redundant behaviour.
+ displayMessage(R.string.fdn_enable_puk2_requested);
+ resetPinChangeStateForPUK2();
+ } else {
+ displayMessage(R.string.pin2_invalid);
+ }
+ }
+ updateEnableFDN();
+ }
+ break;
+
+ // when changing the pin we need to pay attention to whether or not
+ // the error requests a PUK (usually after too many incorrect tries)
+ // Set the state accordingly.
+ case EVENT_PIN2_CHANGE_COMPLETE: {
+ if (DBG)
+ log("Handle EVENT_PIN2_CHANGE_COMPLETE");
+ AsyncResult ar = (AsyncResult) msg.obj;
+ if (ar.exception != null) {
+ CommandException ce = (CommandException) ar.exception;
+ if (ce.getCommandError() == CommandException.Error.SIM_PUK2) {
+ // throw an alert dialog on the screen, displaying the
+ // request for a PUK2. set the cancel listener to
+ // FdnSetting.onCancel().
+ AlertDialog a = new AlertDialog.Builder(FdnSetting.this)
+ .setMessage(R.string.puk2_requested)
+ .setCancelable(true)
+ .setOnCancelListener(FdnSetting.this)
+ .create();
+ a.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ a.show();
+ } else {
+ // set the correct error message depending upon the state.
+ // Reset the state depending upon or knowledge of the PUK state.
+ if (!mIsPuk2Locked) {
+ displayMessage(R.string.badPin2);
+ resetPinChangeState();
+ } else {
+ displayMessage(R.string.badPuk2);
+ resetPinChangeStateForPUK2();
+ }
+ }
+ } else {
+ // reset to normal behaviour on successful change.
+ displayMessage(R.string.pin2_changed);
+ resetPinChangeState();
+ }
+ }
+ break;
+ }
+ }
+ };
+
+ /**
+ * Cancel listener for the PUK2 request alert dialog.
+ */
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ // set the state of the preference and then display the dialog.
+ resetPinChangeStateForPUK2();
+ displayPinChangeDialog(0, true);
+ }
+
+ /**
+ * Display a toast for message, like the rest of the settings.
+ */
+ private final void displayMessage(int strId) {
+ Toast.makeText(this, getString(strId), Toast.LENGTH_SHORT)
+ .show();
+ }
+
+ /**
+ * The next two functions are for updating the message field on the dialog.
+ */
+ private final void displayPinChangeDialog() {
+ displayPinChangeDialog(0, true);
+ }
+
+ private final void displayPinChangeDialog(int strId, boolean shouldDisplay) {
+ int msgId;
+ switch (mPinChangeState) {
+ case PIN_CHANGE_OLD:
+ msgId = R.string.oldPin2Label;
+ break;
+ case PIN_CHANGE_NEW:
+ case PIN_CHANGE_NEW_PIN_FOR_PUK:
+ msgId = R.string.newPin2Label;
+ break;
+ case PIN_CHANGE_REENTER:
+ case PIN_CHANGE_REENTER_PIN_FOR_PUK:
+ msgId = R.string.confirmPin2Label;
+ break;
+ case PIN_CHANGE_PUK:
+ default:
+ msgId = R.string.label_puk2_code;
+ break;
+ }
+
+ // append the note / additional message, if needed.
+ if (strId != 0) {
+ mButtonChangePin2.setDialogMessage(getText(msgId) + "\n" + getText(strId));
+ } else {
+ mButtonChangePin2.setDialogMessage(msgId);
+ }
+
+ // only display if requested.
+ if (shouldDisplay) {
+ mButtonChangePin2.showPinDialog();
+ }
+ }
+
+ /**
+ * Reset the state of the pin change dialog.
+ */
+ private final void resetPinChangeState() {
+ if (DBG) log("resetPinChangeState");
+ mPinChangeState = PIN_CHANGE_OLD;
+ displayPinChangeDialog(0, false);
+ mOldPin = mNewPin = "";
+ mIsPuk2Locked = false;
+ }
+
+ /**
+ * Reset the state of the pin change dialog solely for PUK2 use.
+ */
+ private final void resetPinChangeStateForPUK2() {
+ if (DBG) log("resetPinChangeStateForPUK2");
+ mPinChangeState = PIN_CHANGE_PUK;
+ displayPinChangeDialog(0, false);
+ mOldPin = mNewPin = mPuk2 = "";
+ mIsPuk2Locked = true;
+ }
+
+ /**
+ * Validate the pin entry.
+ *
+ * @param pin This is the pin to validate
+ * @param isPuk Boolean indicating whether we are to treat
+ * the pin input as a puk.
+ */
+ private boolean validatePin(String pin, boolean isPuk) {
+
+ // for pin, we have 4-8 numbers, or puk, we use only 8.
+ int pinMinimum = isPuk ? MAX_PIN_LENGTH : MIN_PIN_LENGTH;
+
+ // check validity
+ if (pin == null || pin.length() < pinMinimum || pin.length() > MAX_PIN_LENGTH) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ * Reflect the updated FDN state in the UI.
+ */
+ private void updateEnableFDN() {
+ if (mPhone.getIccCard().getIccFdnEnabled()) {
+ mButtonEnableFDN.setTitle(R.string.enable_fdn_ok);
+ mButtonEnableFDN.setSummary(R.string.fdn_enabled);
+ mButtonEnableFDN.setDialogTitle(R.string.disable_fdn);
+ } else {
+ mButtonEnableFDN.setTitle(R.string.disable_fdn_ok);
+ mButtonEnableFDN.setSummary(R.string.fdn_disabled);
+ mButtonEnableFDN.setDialogTitle(R.string.enable_fdn);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ addPreferencesFromResource(R.xml.fdn_setting);
+
+ mPhone = PhoneGlobals.getPhone();
+
+ //get UI object references
+ PreferenceScreen prefSet = getPreferenceScreen();
+ mButtonEnableFDN = (EditPinPreference) prefSet.findPreference(BUTTON_FDN_ENABLE_KEY);
+ mButtonChangePin2 = (EditPinPreference) prefSet.findPreference(BUTTON_CHANGE_PIN2_KEY);
+
+ //assign click listener and update state
+ mButtonEnableFDN.setOnPinEnteredListener(this);
+ updateEnableFDN();
+
+ mButtonChangePin2.setOnPinEnteredListener(this);
+
+ // Only reset the pin change dialog if we're not in the middle of changing it.
+ if (icicle == null) {
+ resetPinChangeState();
+ } else {
+ mIsPuk2Locked = icicle.getBoolean(SKIP_OLD_PIN_KEY);
+ mPinChangeState = icicle.getInt(PIN_CHANGE_STATE_KEY);
+ mOldPin = icicle.getString(OLD_PIN_KEY);
+ mNewPin = icicle.getString(NEW_PIN_KEY);
+ mButtonChangePin2.setDialogMessage(icicle.getString(DIALOG_MESSAGE_KEY));
+ mButtonChangePin2.setText(icicle.getString(DIALOG_PIN_ENTRY_KEY));
+ }
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ // android.R.id.home will be triggered in onOptionsItemSelected()
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mPhone = PhoneGlobals.getPhone();
+ updateEnableFDN();
+ }
+
+ /**
+ * Save the state of the pin change.
+ */
+ @Override
+ protected void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+ out.putBoolean(SKIP_OLD_PIN_KEY, mIsPuk2Locked);
+ out.putInt(PIN_CHANGE_STATE_KEY, mPinChangeState);
+ out.putString(OLD_PIN_KEY, mOldPin);
+ out.putString(NEW_PIN_KEY, mNewPin);
+ out.putString(DIALOG_MESSAGE_KEY, mButtonChangePin2.getDialogMessage().toString());
+ out.putString(DIALOG_PIN_ENTRY_KEY, mButtonChangePin2.getText());
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled()
+ CallFeaturesSetting.goUpToTopLevelSetting(this);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, "FdnSetting: " + msg);
+ }
+}
+
diff --git a/src/com/android/phone/GetPin2Screen.java b/src/com/android/phone/GetPin2Screen.java
new file mode 100644
index 0000000..a06b0cf
--- /dev/null
+++ b/src/com/android/phone/GetPin2Screen.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+import android.text.method.DigitsKeyListener;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.inputmethod.EditorInfo;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+/**
+ * Pin2 entry screen.
+ */
+public class GetPin2Screen extends Activity implements TextView.OnEditorActionListener {
+ private static final String LOG_TAG = PhoneGlobals.LOG_TAG;
+
+ private EditText mPin2Field;
+ private Button mOkButton;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.get_pin2_screen);
+
+ mPin2Field = (EditText) findViewById(R.id.pin);
+ mPin2Field.setKeyListener(DigitsKeyListener.getInstance());
+ mPin2Field.setMovementMethod(null);
+ mPin2Field.setOnEditorActionListener(this);
+
+ mOkButton = (Button) findViewById(R.id.ok);
+ mOkButton.setOnClickListener(mClicked);
+ }
+
+ private String getPin2() {
+ return mPin2Field.getText().toString();
+ }
+
+ private void returnResult() {
+ Bundle map = new Bundle();
+ map.putString("pin2", getPin2());
+
+ Intent intent = getIntent();
+ Uri uri = intent.getData();
+
+ Intent action = new Intent();
+ if (uri != null) action.setAction(uri.toString());
+ setResult(RESULT_OK, action.putExtras(map));
+ finish();
+ }
+
+ @Override
+ public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+ if (actionId == EditorInfo.IME_ACTION_DONE) {
+ mOkButton.performClick();
+ return true;
+ }
+ return false;
+ }
+
+ private final View.OnClickListener mClicked = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (TextUtils.isEmpty(mPin2Field.getText())) {
+ return;
+ }
+
+ returnResult();
+ }
+ };
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, "[GetPin2] " + msg);
+ }
+}
diff --git a/src/com/android/phone/GsmUmtsAdditionalCallOptions.java b/src/com/android/phone/GsmUmtsAdditionalCallOptions.java
new file mode 100644
index 0000000..cd400f9
--- /dev/null
+++ b/src/com/android/phone/GsmUmtsAdditionalCallOptions.java
@@ -0,0 +1,95 @@
+package com.android.phone;
+
+import android.app.ActionBar;
+import android.content.Intent;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceScreen;
+import android.util.Log;
+import android.view.MenuItem;
+
+import java.util.ArrayList;
+
+public class GsmUmtsAdditionalCallOptions extends
+ TimeConsumingPreferenceActivity {
+ private static final String LOG_TAG = "GsmUmtsAdditionalCallOptions";
+ private final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ private static final String BUTTON_CLIR_KEY = "button_clir_key";
+ private static final String BUTTON_CW_KEY = "button_cw_key";
+
+ private CLIRListPreference mCLIRButton;
+ private CallWaitingCheckBoxPreference mCWButton;
+
+ private final ArrayList<Preference> mPreferences = new ArrayList<Preference>();
+ private int mInitIndex= 0;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ addPreferencesFromResource(R.xml.gsm_umts_additional_options);
+
+ PreferenceScreen prefSet = getPreferenceScreen();
+ mCLIRButton = (CLIRListPreference) prefSet.findPreference(BUTTON_CLIR_KEY);
+ mCWButton = (CallWaitingCheckBoxPreference) prefSet.findPreference(BUTTON_CW_KEY);
+
+ mPreferences.add(mCLIRButton);
+ mPreferences.add(mCWButton);
+
+ if (icicle == null) {
+ if (DBG) Log.d(LOG_TAG, "start to init ");
+ mCLIRButton.init(this, false);
+ } else {
+ if (DBG) Log.d(LOG_TAG, "restore stored states");
+ mInitIndex = mPreferences.size();
+ mCLIRButton.init(this, true);
+ mCWButton.init(this, true);
+ int[] clirArray = icicle.getIntArray(mCLIRButton.getKey());
+ if (clirArray != null) {
+ if (DBG) Log.d(LOG_TAG, "onCreate: clirArray[0]="
+ + clirArray[0] + ", clirArray[1]=" + clirArray[1]);
+ mCLIRButton.handleGetCLIRResult(clirArray);
+ } else {
+ mCLIRButton.init(this, false);
+ }
+ }
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ // android.R.id.home will be triggered in onOptionsItemSelected()
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ if (mCLIRButton.clirArray != null) {
+ outState.putIntArray(mCLIRButton.getKey(), mCLIRButton.clirArray);
+ }
+ }
+
+ @Override
+ public void onFinished(Preference preference, boolean reading) {
+ if (mInitIndex < mPreferences.size()-1 && !isFinishing()) {
+ mInitIndex++;
+ Preference pref = mPreferences.get(mInitIndex);
+ if (pref instanceof CallWaitingCheckBoxPreference) {
+ ((CallWaitingCheckBoxPreference) pref).init(this, false);
+ }
+ }
+ super.onFinished(preference, reading);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled()
+ CallFeaturesSetting.goUpToTopLevelSetting(this);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/com/android/phone/GsmUmtsCallForwardOptions.java b/src/com/android/phone/GsmUmtsCallForwardOptions.java
new file mode 100644
index 0000000..8ecb1bf
--- /dev/null
+++ b/src/com/android/phone/GsmUmtsCallForwardOptions.java
@@ -0,0 +1,181 @@
+package com.android.phone;
+
+import com.android.internal.telephony.CallForwardInfo;
+import com.android.internal.telephony.CommandsInterface;
+
+import android.app.ActionBar;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.PreferenceScreen;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.util.Log;
+import android.view.MenuItem;
+
+import java.util.ArrayList;
+
+
+public class GsmUmtsCallForwardOptions extends TimeConsumingPreferenceActivity {
+ private static final String LOG_TAG = "GsmUmtsCallForwardOptions";
+ private final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ private static final String NUM_PROJECTION[] = {Phone.NUMBER};
+
+ private static final String BUTTON_CFU_KEY = "button_cfu_key";
+ private static final String BUTTON_CFB_KEY = "button_cfb_key";
+ private static final String BUTTON_CFNRY_KEY = "button_cfnry_key";
+ private static final String BUTTON_CFNRC_KEY = "button_cfnrc_key";
+
+ private static final String KEY_TOGGLE = "toggle";
+ private static final String KEY_STATUS = "status";
+ private static final String KEY_NUMBER = "number";
+
+ private CallForwardEditPreference mButtonCFU;
+ private CallForwardEditPreference mButtonCFB;
+ private CallForwardEditPreference mButtonCFNRy;
+ private CallForwardEditPreference mButtonCFNRc;
+
+ private final ArrayList<CallForwardEditPreference> mPreferences =
+ new ArrayList<CallForwardEditPreference> ();
+ private int mInitIndex= 0;
+
+ private boolean mFirstResume;
+ private Bundle mIcicle;
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ addPreferencesFromResource(R.xml.callforward_options);
+
+ PreferenceScreen prefSet = getPreferenceScreen();
+ mButtonCFU = (CallForwardEditPreference) prefSet.findPreference(BUTTON_CFU_KEY);
+ mButtonCFB = (CallForwardEditPreference) prefSet.findPreference(BUTTON_CFB_KEY);
+ mButtonCFNRy = (CallForwardEditPreference) prefSet.findPreference(BUTTON_CFNRY_KEY);
+ mButtonCFNRc = (CallForwardEditPreference) prefSet.findPreference(BUTTON_CFNRC_KEY);
+
+ mButtonCFU.setParentActivity(this, mButtonCFU.reason);
+ mButtonCFB.setParentActivity(this, mButtonCFB.reason);
+ mButtonCFNRy.setParentActivity(this, mButtonCFNRy.reason);
+ mButtonCFNRc.setParentActivity(this, mButtonCFNRc.reason);
+
+ mPreferences.add(mButtonCFU);
+ mPreferences.add(mButtonCFB);
+ mPreferences.add(mButtonCFNRy);
+ mPreferences.add(mButtonCFNRc);
+
+ // we wait to do the initialization until onResume so that the
+ // TimeConsumingPreferenceActivity dialog can display as it
+ // relies on onResume / onPause to maintain its foreground state.
+
+ mFirstResume = true;
+ mIcicle = icicle;
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ // android.R.id.home will be triggered in onOptionsItemSelected()
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (mFirstResume) {
+ if (mIcicle == null) {
+ if (DBG) Log.d(LOG_TAG, "start to init ");
+ mPreferences.get(mInitIndex).init(this, false);
+ } else {
+ mInitIndex = mPreferences.size();
+
+ for (CallForwardEditPreference pref : mPreferences) {
+ Bundle bundle = mIcicle.getParcelable(pref.getKey());
+ pref.setToggled(bundle.getBoolean(KEY_TOGGLE));
+ CallForwardInfo cf = new CallForwardInfo();
+ cf.number = bundle.getString(KEY_NUMBER);
+ cf.status = bundle.getInt(KEY_STATUS);
+ pref.handleCallForwardResult(cf);
+ pref.init(this, true);
+ }
+ }
+ mFirstResume = false;
+ mIcicle=null;
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ for (CallForwardEditPreference pref : mPreferences) {
+ Bundle bundle = new Bundle();
+ bundle.putBoolean(KEY_TOGGLE, pref.isToggled());
+ if (pref.callForwardInfo != null) {
+ bundle.putString(KEY_NUMBER, pref.callForwardInfo.number);
+ bundle.putInt(KEY_STATUS, pref.callForwardInfo.status);
+ }
+ outState.putParcelable(pref.getKey(), bundle);
+ }
+ }
+
+ @Override
+ public void onFinished(Preference preference, boolean reading) {
+ if (mInitIndex < mPreferences.size()-1 && !isFinishing()) {
+ mInitIndex++;
+ mPreferences.get(mInitIndex).init(this, false);
+ }
+
+ super.onFinished(preference, reading);
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (DBG) Log.d(LOG_TAG, "onActivityResult: done");
+ if (resultCode != RESULT_OK) {
+ if (DBG) Log.d(LOG_TAG, "onActivityResult: contact picker result not OK.");
+ return;
+ }
+ Cursor cursor = null;
+ try {
+ cursor = getContentResolver().query(data.getData(),
+ NUM_PROJECTION, null, null, null);
+ if ((cursor == null) || (!cursor.moveToFirst())) {
+ if (DBG) Log.d(LOG_TAG, "onActivityResult: bad contact data, no results found.");
+ return;
+ }
+
+ switch (requestCode) {
+ case CommandsInterface.CF_REASON_UNCONDITIONAL:
+ mButtonCFU.onPickActivityResult(cursor.getString(0));
+ break;
+ case CommandsInterface.CF_REASON_BUSY:
+ mButtonCFB.onPickActivityResult(cursor.getString(0));
+ break;
+ case CommandsInterface.CF_REASON_NO_REPLY:
+ mButtonCFNRy.onPickActivityResult(cursor.getString(0));
+ break;
+ case CommandsInterface.CF_REASON_NOT_REACHABLE:
+ mButtonCFNRc.onPickActivityResult(cursor.getString(0));
+ break;
+ default:
+ // TODO: may need exception here.
+ }
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled()
+ CallFeaturesSetting.goUpToTopLevelSetting(this);
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/com/android/phone/GsmUmtsCallOptions.java b/src/com/android/phone/GsmUmtsCallOptions.java
new file mode 100644
index 0000000..a9a1940
--- /dev/null
+++ b/src/com/android/phone/GsmUmtsCallOptions.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.os.Bundle;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+
+public class GsmUmtsCallOptions extends PreferenceActivity {
+ private static final String LOG_TAG = "GsmUmtsCallOptions";
+ private final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ addPreferencesFromResource(R.xml.gsm_umts_call_options);
+
+ if (PhoneGlobals.getPhone().getPhoneType() != PhoneConstants.PHONE_TYPE_GSM) {
+ //disable the entire screen
+ getPreferenceScreen().setEnabled(false);
+ }
+ }
+}
diff --git a/src/com/android/phone/GsmUmtsOptions.java b/src/com/android/phone/GsmUmtsOptions.java
new file mode 100644
index 0000000..1f1ac4a
--- /dev/null
+++ b/src/com/android/phone/GsmUmtsOptions.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+import android.content.res.Resources;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneFactory;
+
+/**
+ * List of Network-specific settings screens.
+ */
+public class GsmUmtsOptions {
+ private static final String LOG_TAG = "GsmUmtsOptions";
+
+ private PreferenceScreen mButtonAPNExpand;
+ private PreferenceScreen mButtonOperatorSelectionExpand;
+ private CheckBoxPreference mButtonPrefer2g;
+
+ private static final String BUTTON_APN_EXPAND_KEY = "button_apn_key";
+ private static final String BUTTON_OPERATOR_SELECTION_EXPAND_KEY = "button_carrier_sel_key";
+ private static final String BUTTON_PREFER_2G_KEY = "button_prefer_2g_key";
+ private PreferenceActivity mPrefActivity;
+ private PreferenceScreen mPrefScreen;
+
+ public GsmUmtsOptions(PreferenceActivity prefActivity, PreferenceScreen prefScreen) {
+ mPrefActivity = prefActivity;
+ mPrefScreen = prefScreen;
+ create();
+ }
+
+ protected void create() {
+ mPrefActivity.addPreferencesFromResource(R.xml.gsm_umts_options);
+ mButtonAPNExpand = (PreferenceScreen) mPrefScreen.findPreference(BUTTON_APN_EXPAND_KEY);
+ mButtonOperatorSelectionExpand =
+ (PreferenceScreen) mPrefScreen.findPreference(BUTTON_OPERATOR_SELECTION_EXPAND_KEY);
+ mButtonPrefer2g = (CheckBoxPreference) mPrefScreen.findPreference(BUTTON_PREFER_2G_KEY);
+ if (PhoneFactory.getDefaultPhone().getPhoneType() != PhoneConstants.PHONE_TYPE_GSM) {
+ log("Not a GSM phone");
+ mButtonAPNExpand.setEnabled(false);
+ mButtonOperatorSelectionExpand.setEnabled(false);
+ mButtonPrefer2g.setEnabled(false);
+ } else {
+ log("Not a CDMA phone");
+ Resources res = mPrefActivity.getResources();
+
+ // Determine which options to display, for GSM these are defaulted
+ // are defaulted to true in Phone/res/values/config.xml. But for
+ // some operators like verizon they maybe overriden in operator
+ // specific resources or device specifc overlays.
+ if (!res.getBoolean(R.bool.config_apn_expand)) {
+ mPrefScreen.removePreference(mPrefScreen.findPreference(BUTTON_APN_EXPAND_KEY));
+ }
+ if (!res.getBoolean(R.bool.config_operator_selection_expand)) {
+ mPrefScreen.removePreference(mPrefScreen
+ .findPreference(BUTTON_OPERATOR_SELECTION_EXPAND_KEY));
+ }
+ if (!res.getBoolean(R.bool.config_prefer_2g)) {
+ mPrefScreen.removePreference(mPrefScreen.findPreference(BUTTON_PREFER_2G_KEY));
+ }
+
+ if (res.getBoolean(R.bool.csp_enabled)) {
+ if (PhoneFactory.getDefaultPhone().isCspPlmnEnabled()) {
+ log("[CSP] Enabling Operator Selection menu.");
+ mButtonOperatorSelectionExpand.setEnabled(true);
+ } else {
+ log("[CSP] Disabling Operator Selection menu.");
+ mPrefScreen.removePreference(mPrefScreen
+ .findPreference(BUTTON_OPERATOR_SELECTION_EXPAND_KEY));
+ }
+ }
+ }
+ }
+
+ public boolean preferenceTreeClick(Preference preference) {
+ if (preference.getKey().equals(BUTTON_PREFER_2G_KEY)) {
+ log("preferenceTreeClick: return true");
+ return true;
+ }
+ log("preferenceTreeClick: return false");
+ return false;
+ }
+
+ protected void log(String s) {
+ android.util.Log.d(LOG_TAG, s);
+ }
+}
diff --git a/src/com/android/phone/INetworkQueryService.aidl b/src/com/android/phone/INetworkQueryService.aidl
new file mode 100644
index 0000000..749163c
--- /dev/null
+++ b/src/com/android/phone/INetworkQueryService.aidl
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import com.android.phone.INetworkQueryServiceCallback;
+
+/**
+ * Service interface to handle queries for available networks. The
+ * Phone application lets this service interface handle carrier
+ * availability queries instead of making direct calls to the
+ * GSMPhone layer.
+ */
+oneway interface INetworkQueryService {
+
+ /**
+ * Starts a network query if it has not been started yet, and
+ * request a callback through the INetworkQueryServiceCallback
+ * object on query completion. If there is an existing request,
+ * then just add the callback to the list of notifications
+ * that will be sent upon query completion.
+ */
+ void startNetworkQuery(in INetworkQueryServiceCallback cb);
+
+ /**
+ * Tells the service that the requested query is to be ignored.
+ * This may not do anything for the Query request in the
+ * underlying RIL, but it ensures that the callback is removed
+ * from the list of notifications.
+ */
+ void stopNetworkQuery(in INetworkQueryServiceCallback cb);
+}
diff --git a/src/com/android/phone/INetworkQueryServiceCallback.aidl b/src/com/android/phone/INetworkQueryServiceCallback.aidl
new file mode 100644
index 0000000..4c32883
--- /dev/null
+++ b/src/com/android/phone/INetworkQueryServiceCallback.aidl
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import com.android.internal.telephony.OperatorInfo;
+
+/**
+ * Service interface to handle callbacks into the activity from the
+ * NetworkQueryService. These objects are used to notify that a
+ * query is complete and that the results are ready to process.
+ */
+oneway interface INetworkQueryServiceCallback {
+
+ /**
+ * Called upon query completion, handing a status value and an
+ * array of OperatorInfo objects.
+ *
+ * @param networkInfoArray is the list of OperatorInfo. Can be
+ * null, indicating no results were found, or an error.
+ * @param status the status indicating if there were any
+ * problems with the request.
+ */
+ void onQueryComplete(in List<OperatorInfo> networkInfoArray, int status);
+
+}
diff --git a/src/com/android/phone/IccNetworkDepersonalizationPanel.java b/src/com/android/phone/IccNetworkDepersonalizationPanel.java
new file mode 100644
index 0000000..aa582a1
--- /dev/null
+++ b/src/com/android/phone/IccNetworkDepersonalizationPanel.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.text.Editable;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.method.DialerKeyListener;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.internal.telephony.Phone;
+
+/**
+ * "SIM network unlock" PIN entry screen.
+ *
+ * @see PhoneGlobals.EVENT_SIM_NETWORK_LOCKED
+ *
+ * TODO: This UI should be part of the lock screen, not the
+ * phone app (see bug 1804111).
+ */
+public class IccNetworkDepersonalizationPanel extends IccPanel {
+
+ //debug constants
+ private static final boolean DBG = false;
+
+ //events
+ private static final int EVENT_ICC_NTWRK_DEPERSONALIZATION_RESULT = 100;
+
+ private Phone mPhone;
+
+ //UI elements
+ private EditText mPinEntry;
+ private LinearLayout mEntryPanel;
+ private LinearLayout mStatusPanel;
+ private TextView mStatusText;
+
+ private Button mUnlockButton;
+ private Button mDismissButton;
+
+ //private textwatcher to control text entry.
+ private TextWatcher mPinEntryWatcher = new TextWatcher() {
+ public void beforeTextChanged(CharSequence buffer, int start, int olen, int nlen) {
+ }
+
+ public void onTextChanged(CharSequence buffer, int start, int olen, int nlen) {
+ }
+
+ public void afterTextChanged(Editable buffer) {
+ if (SpecialCharSequenceMgr.handleChars(
+ getContext(), buffer.toString())) {
+ mPinEntry.getText().clear();
+ }
+ }
+ };
+
+ //handler for unlock function results
+ private Handler mHandler = new Handler() {
+ public void handleMessage(Message msg) {
+ if (msg.what == EVENT_ICC_NTWRK_DEPERSONALIZATION_RESULT) {
+ AsyncResult res = (AsyncResult) msg.obj;
+ if (res.exception != null) {
+ if (DBG) log("network depersonalization request failure.");
+ indicateError();
+ postDelayed(new Runnable() {
+ public void run() {
+ hideAlert();
+ mPinEntry.getText().clear();
+ mPinEntry.requestFocus();
+ }
+ }, 3000);
+ } else {
+ if (DBG) log("network depersonalization success.");
+ indicateSuccess();
+ postDelayed(new Runnable() {
+ public void run() {
+ dismiss();
+ }
+ }, 3000);
+ }
+ }
+ }
+ };
+
+ //constructor
+ public IccNetworkDepersonalizationPanel(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.sim_ndp);
+
+ // PIN entry text field
+ mPinEntry = (EditText) findViewById(R.id.pin_entry);
+ mPinEntry.setKeyListener(DialerKeyListener.getInstance());
+ mPinEntry.setOnClickListener(mUnlockListener);
+
+ // Attach the textwatcher
+ CharSequence text = mPinEntry.getText();
+ Spannable span = (Spannable) text;
+ span.setSpan(mPinEntryWatcher, 0, text.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
+
+ mEntryPanel = (LinearLayout) findViewById(R.id.entry_panel);
+
+ mUnlockButton = (Button) findViewById(R.id.ndp_unlock);
+ mUnlockButton.setOnClickListener(mUnlockListener);
+
+ // The "Dismiss" button is present in some (but not all) products,
+ // based on the "sim_network_unlock_allow_dismiss" resource.
+ mDismissButton = (Button) findViewById(R.id.ndp_dismiss);
+ if (getContext().getResources().getBoolean(R.bool.sim_network_unlock_allow_dismiss)) {
+ if (DBG) log("Enabling 'Dismiss' button...");
+ mDismissButton.setVisibility(View.VISIBLE);
+ mDismissButton.setOnClickListener(mDismissListener);
+ } else {
+ if (DBG) log("Removing 'Dismiss' button...");
+ mDismissButton.setVisibility(View.GONE);
+ }
+
+ //status panel is used since we're having problems with the alert dialog.
+ mStatusPanel = (LinearLayout) findViewById(R.id.status_panel);
+ mStatusText = (TextView) findViewById(R.id.status_text);
+
+ mPhone = PhoneGlobals.getPhone();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ }
+
+ //Mirrors IccPinUnlockPanel.onKeyDown().
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ View.OnClickListener mUnlockListener = new View.OnClickListener() {
+ public void onClick(View v) {
+ String pin = mPinEntry.getText().toString();
+
+ if (TextUtils.isEmpty(pin)) {
+ return;
+ }
+
+ if (DBG) log("requesting network depersonalization with code " + pin);
+ mPhone.getIccCard().supplyNetworkDepersonalization(pin,
+ Message.obtain(mHandler, EVENT_ICC_NTWRK_DEPERSONALIZATION_RESULT));
+ indicateBusy();
+ }
+ };
+
+ private void indicateBusy() {
+ mStatusText.setText(R.string.requesting_unlock);
+ mEntryPanel.setVisibility(View.GONE);
+ mStatusPanel.setVisibility(View.VISIBLE);
+ }
+
+ private void indicateError() {
+ mStatusText.setText(R.string.unlock_failed);
+ mEntryPanel.setVisibility(View.GONE);
+ mStatusPanel.setVisibility(View.VISIBLE);
+ }
+
+ private void indicateSuccess() {
+ mStatusText.setText(R.string.unlock_success);
+ mEntryPanel.setVisibility(View.GONE);
+ mStatusPanel.setVisibility(View.VISIBLE);
+ }
+
+ private void hideAlert() {
+ mEntryPanel.setVisibility(View.VISIBLE);
+ mStatusPanel.setVisibility(View.GONE);
+ }
+
+ View.OnClickListener mDismissListener = new View.OnClickListener() {
+ public void onClick(View v) {
+ if (DBG) log("mDismissListener: skipping depersonalization...");
+ dismiss();
+ }
+ };
+
+ private void log(String msg) {
+ Log.v(TAG, "[IccNetworkDepersonalizationPanel] " + msg);
+ }
+}
diff --git a/src/com/android/phone/IccPanel.java b/src/com/android/phone/IccPanel.java
new file mode 100644
index 0000000..e603a06
--- /dev/null
+++ b/src/com/android/phone/IccPanel.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.Dialog;
+import android.app.StatusBarManager;
+import android.content.Context;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.WindowManager;
+import android.view.Window;
+import android.os.Bundle;
+
+/**
+ * Base class for ICC-related panels in the Phone UI.
+ */
+public class IccPanel extends Dialog {
+ protected static final String TAG = PhoneGlobals.LOG_TAG;
+
+ private StatusBarManager mStatusBarManager;
+
+ public IccPanel(Context context) {
+ super(context, R.style.IccPanel);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Window winP = getWindow();
+ winP.setType(WindowManager.LayoutParams.TYPE_PRIORITY_PHONE);
+ winP.setLayout(WindowManager.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.MATCH_PARENT);
+ winP.setGravity(Gravity.CENTER);
+
+ // TODO: Ideally, we'd like this dialog to be visible in front of the
+ // keyguard, so the user will see it immediately after boot (without
+ // needing to enter the lock pattern or dismiss the keyguard first.)
+ //
+ // However that's not easy to do. It's not just a matter of setting
+ // the FLAG_SHOW_WHEN_LOCKED and FLAG_DISMISS_KEYGUARD flags on our
+ // window, since we're a Dialog (not an Activity), and the framework
+ // won't ever let a dialog hide the keyguard (because there could
+ // possibly be stuff behind it that shouldn't be seen.)
+ //
+ // So for now, we'll live with the fact that the user has to enter the
+ // lock pattern (or dismiss the keyguard) *before* being able to type
+ // a SIM network unlock PIN. That's not ideal, but still OK.
+ // (And eventually this will be a moot point once this UI moves
+ // from the phone app to the framework; see bug 1804111).
+
+ // TODO: we shouldn't need the mStatusBarManager calls here either,
+ // once this dialog gets moved into the framework and becomes a truly
+ // full-screen UI.
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ mStatusBarManager = (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE);
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mStatusBarManager.disable(StatusBarManager.DISABLE_EXPAND);
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mStatusBarManager.disable(StatusBarManager.DISABLE_NONE);
+ }
+
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK) {
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+}
diff --git a/src/com/android/phone/IccProvider.java b/src/com/android/phone/IccProvider.java
new file mode 100644
index 0000000..827d500
--- /dev/null
+++ b/src/com/android/phone/IccProvider.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+/**
+ * ICC address book content provider.
+ */
+public class IccProvider extends com.android.internal.telephony.IccProvider {
+ public IccProvider() {
+ super();
+ }
+}
diff --git a/src/com/android/phone/InCallControlState.java b/src/com/android/phone/InCallControlState.java
new file mode 100644
index 0000000..e5c7f20
--- /dev/null
+++ b/src/com/android/phone/InCallControlState.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyCapabilities;
+
+/**
+ * Helper class to keep track of enabledness, visibility, and "on/off"
+ * or "checked" state of the various controls available in the in-call
+ * UI, based on the current telephony state.
+ *
+ * This class is independent of the exact UI controls used on any given
+ * device. To avoid cluttering up the "view" code (i.e. InCallTouchUi)
+ * with logic about which functions are available right now, we instead
+ * have that logic here, and provide simple boolean flags to indicate the
+ * state and/or enabledness of all possible in-call user operations.
+ *
+ * (In other words, this is the "model" that corresponds to the "view"
+ * implemented by InCallTouchUi.)
+ */
+public class InCallControlState {
+ private static final String LOG_TAG = "InCallControlState";
+ private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ private InCallScreen mInCallScreen;
+ private CallManager mCM;
+
+ //
+ // Our "public API": Boolean flags to indicate the state and/or
+ // enabledness of all possible in-call user operations:
+ //
+
+ public boolean manageConferenceVisible;
+ public boolean manageConferenceEnabled;
+ //
+ public boolean canAddCall;
+ //
+ public boolean canEndCall;
+ //
+ public boolean canSwap;
+ public boolean canMerge;
+ //
+ public boolean bluetoothEnabled;
+ public boolean bluetoothIndicatorOn;
+ //
+ public boolean speakerEnabled;
+ public boolean speakerOn;
+ //
+ public boolean canMute;
+ public boolean muteIndicatorOn;
+ //
+ public boolean dialpadEnabled;
+ public boolean dialpadVisible;
+ //
+ /** True if the "Hold" function is *ever* available on this device */
+ public boolean supportsHold;
+ /** True if the call is currently on hold */
+ public boolean onHold;
+ /** True if the "Hold" or "Unhold" function should be available right now */
+ // TODO: this name is misleading. Let's break this apart into
+ // separate canHold and canUnhold flags, and have the caller look at
+ // "canHold || canUnhold" to decide whether the hold/unhold UI element
+ // should be visible.
+ public boolean canHold;
+
+
+ public InCallControlState(InCallScreen inCallScreen, CallManager cm) {
+ if (DBG) log("InCallControlState constructor...");
+ mInCallScreen = inCallScreen;
+ mCM = cm;
+ }
+
+ /**
+ * Updates all our public boolean flags based on the current state of
+ * the Phone.
+ */
+ public void update() {
+ final PhoneConstants.State state = mCM.getState(); // coarse-grained voice call state
+ final Call fgCall = mCM.getActiveFgCall();
+ final Call.State fgCallState = fgCall.getState();
+ final boolean hasActiveForegroundCall = (fgCallState == Call.State.ACTIVE);
+ final boolean hasHoldingCall = mCM.hasActiveBgCall();
+
+ // Manage conference:
+ if (TelephonyCapabilities.supportsConferenceCallManagement(fgCall.getPhone())) {
+ // This item is visible only if the foreground call is a
+ // conference call, and it's enabled unless the "Manage
+ // conference" UI is already up.
+ manageConferenceVisible = PhoneUtils.isConferenceCall(fgCall);
+ manageConferenceEnabled =
+ manageConferenceVisible && !mInCallScreen.isManageConferenceMode();
+ } else {
+ // This device has no concept of managing a conference call.
+ manageConferenceVisible = false;
+ manageConferenceEnabled = false;
+ }
+
+ // "Add call":
+ canAddCall = PhoneUtils.okToAddCall(mCM);
+
+ // "End call": always enabled unless the phone is totally idle.
+ // Note that while the phone is ringing, the InCallTouchUi widget isn't
+ // visible at all, so the state of the End button doesn't matter. However
+ // we *do* still set canEndCall to true in this case, purely to prevent a
+ // UI glitch when the InCallTouchUi widget first appears, immediately after
+ // answering an incoming call.
+ canEndCall = (mCM.hasActiveFgCall() || mCM.hasActiveRingingCall() || mCM.hasActiveBgCall());
+
+ // Swap / merge calls
+ canSwap = PhoneUtils.okToSwapCalls(mCM);
+ canMerge = PhoneUtils.okToMergeCalls(mCM);
+
+ // "Bluetooth":
+ if (mInCallScreen.isBluetoothAvailable()) {
+ bluetoothEnabled = true;
+ bluetoothIndicatorOn = mInCallScreen.isBluetoothAudioConnectedOrPending();
+ } else {
+ bluetoothEnabled = false;
+ bluetoothIndicatorOn = false;
+ }
+
+ // "Speaker": always enabled unless the phone is totally idle.
+ // The current speaker state comes from the AudioManager.
+ speakerEnabled = (state != PhoneConstants.State.IDLE);
+ speakerOn = PhoneUtils.isSpeakerOn(mInCallScreen);
+
+ // "Mute": only enabled when the foreground call is ACTIVE.
+ // (It's meaningless while on hold, or while DIALING/ALERTING.)
+ // It's also explicitly disabled during emergency calls or if
+ // emergency callback mode (ECM) is active.
+ Connection c = fgCall.getLatestConnection();
+ boolean isEmergencyCall = false;
+ if (c != null) isEmergencyCall =
+ PhoneNumberUtils.isLocalEmergencyNumber(c.getAddress(),
+ fgCall.getPhone().getContext());
+ boolean isECM = PhoneUtils.isPhoneInEcm(fgCall.getPhone());
+ if (isEmergencyCall || isECM) { // disable "Mute" item
+ canMute = false;
+ muteIndicatorOn = false;
+ } else {
+ canMute = hasActiveForegroundCall;
+ muteIndicatorOn = PhoneUtils.getMute();
+ }
+
+ // "Dialpad": Enabled only when it's OK to use the dialpad in the
+ // first place.
+ dialpadEnabled = mInCallScreen.okToShowDialpad();
+
+ // Also keep track of whether the dialpad is currently "opened"
+ // (i.e. visible).
+ dialpadVisible = mInCallScreen.isDialerOpened();
+
+ // "Hold:
+ if (TelephonyCapabilities.supportsHoldAndUnhold(fgCall.getPhone())) {
+ // This phone has the concept of explicit "Hold" and "Unhold" actions.
+ supportsHold = true;
+ // "On hold" means that there's a holding call and
+ // *no* foreground call. (If there *is* a foreground call,
+ // that's "two lines in use".)
+ onHold = hasHoldingCall && (fgCallState == Call.State.IDLE);
+ // The "Hold" control is disabled entirely if there's
+ // no way to either hold or unhold in the current state.
+ boolean okToHold = hasActiveForegroundCall && !hasHoldingCall;
+ boolean okToUnhold = onHold;
+ canHold = okToHold || okToUnhold;
+ } else if (hasHoldingCall && (fgCallState == Call.State.IDLE)) {
+ // Even when foreground phone device doesn't support hold/unhold, phone devices
+ // for background holding calls may do.
+ //
+ // If the foreground call is ACTIVE, we should turn on "swap" button instead.
+ final Call bgCall = mCM.getFirstActiveBgCall();
+ if (bgCall != null &&
+ TelephonyCapabilities.supportsHoldAndUnhold(bgCall.getPhone())) {
+ supportsHold = true;
+ onHold = true;
+ canHold = true;
+ }
+ } else {
+ // This device has no concept of "putting a call on hold."
+ supportsHold = false;
+ onHold = false;
+ canHold = false;
+ }
+
+ if (DBG) dumpState();
+ }
+
+ public void dumpState() {
+ log("InCallControlState:");
+ log(" manageConferenceVisible: " + manageConferenceVisible);
+ log(" manageConferenceEnabled: " + manageConferenceEnabled);
+ log(" canAddCall: " + canAddCall);
+ log(" canEndCall: " + canEndCall);
+ log(" canSwap: " + canSwap);
+ log(" canMerge: " + canMerge);
+ log(" bluetoothEnabled: " + bluetoothEnabled);
+ log(" bluetoothIndicatorOn: " + bluetoothIndicatorOn);
+ log(" speakerEnabled: " + speakerEnabled);
+ log(" speakerOn: " + speakerOn);
+ log(" canMute: " + canMute);
+ log(" muteIndicatorOn: " + muteIndicatorOn);
+ log(" dialpadEnabled: " + dialpadEnabled);
+ log(" dialpadVisible: " + dialpadVisible);
+ log(" onHold: " + onHold);
+ log(" canHold: " + canHold);
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/InCallScreen.java b/src/com/android/phone/InCallScreen.java
new file mode 100644
index 0000000..d5db689
--- /dev/null
+++ b/src/com/android/phone/InCallScreen.java
@@ -0,0 +1,4612 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.IBluetoothHeadsetPhone;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.Typeface;
+import android.media.AudioManager;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.telephony.ServiceState;
+import android.text.TextUtils;
+import android.text.method.DialerKeyListener;
+import android.util.EventLog;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.EditText;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.MmiCode;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyCapabilities;
+import com.android.phone.Constants.CallStatusCode;
+import com.android.phone.InCallUiState.InCallScreenMode;
+import com.android.phone.OtaUtils.CdmaOtaScreenState;
+
+import java.util.List;
+
+
+/**
+ * Phone app "in call" screen.
+ */
+public class InCallScreen extends Activity
+ implements View.OnClickListener {
+ private static final String LOG_TAG = "InCallScreen";
+
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ /**
+ * Intent extra used to specify whether the DTMF dialpad should be
+ * initially visible when bringing up the InCallScreen. (If this
+ * extra is present, the dialpad will be initially shown if the extra
+ * has the boolean value true, and initially hidden otherwise.)
+ */
+ // TODO: Should be EXTRA_SHOW_DIALPAD for consistency.
+ static final String SHOW_DIALPAD_EXTRA = "com.android.phone.ShowDialpad";
+
+ /**
+ * Intent extra to specify the package name of the gateway
+ * provider. Used to get the name displayed in the in-call screen
+ * during the call setup. The value is a string.
+ */
+ // TODO: This extra is currently set by the gateway application as
+ // a temporary measure. Ultimately, the framework will securely
+ // set it.
+ /* package */ static final String EXTRA_GATEWAY_PROVIDER_PACKAGE =
+ "com.android.phone.extra.GATEWAY_PROVIDER_PACKAGE";
+
+ /**
+ * Intent extra to specify the URI of the provider to place the
+ * call. The value is a string. It holds the gateway address
+ * (phone gateway URL should start with the 'tel:' scheme) that
+ * will actually be contacted to call the number passed in the
+ * intent URL or in the EXTRA_PHONE_NUMBER extra.
+ */
+ // TODO: Should the value be a Uri (Parcelable)? Need to make sure
+ // MMI code '#' don't get confused as URI fragments.
+ /* package */ static final String EXTRA_GATEWAY_URI =
+ "com.android.phone.extra.GATEWAY_URI";
+
+ // Amount of time (in msec) that we display the "Call ended" state.
+ // The "short" value is for calls ended by the local user, and the
+ // "long" value is for calls ended by the remote caller.
+ private static final int CALL_ENDED_SHORT_DELAY = 200; // msec
+ private static final int CALL_ENDED_LONG_DELAY = 2000; // msec
+ private static final int CALL_ENDED_EXTRA_LONG_DELAY = 5000; // msec
+
+ // Amount of time that we display the PAUSE alert Dialog showing the
+ // post dial string yet to be send out to the n/w
+ private static final int PAUSE_PROMPT_DIALOG_TIMEOUT = 2000; //msec
+
+ // Amount of time that we display the provider info if applicable.
+ private static final int PROVIDER_INFO_TIMEOUT = 5000; // msec
+
+ // These are values for the settings of the auto retry mode:
+ // 0 = disabled
+ // 1 = enabled
+ // TODO (Moto):These constants don't really belong here,
+ // they should be moved to Settings where the value is being looked up in the first place
+ static final int AUTO_RETRY_OFF = 0;
+ static final int AUTO_RETRY_ON = 1;
+
+ // Message codes; see mHandler below.
+ // Note message codes < 100 are reserved for the PhoneApp.
+ private static final int PHONE_STATE_CHANGED = 101;
+ private static final int PHONE_DISCONNECT = 102;
+ private static final int EVENT_HEADSET_PLUG_STATE_CHANGED = 103;
+ private static final int POST_ON_DIAL_CHARS = 104;
+ private static final int WILD_PROMPT_CHAR_ENTERED = 105;
+ private static final int ADD_VOICEMAIL_NUMBER = 106;
+ private static final int DONT_ADD_VOICEMAIL_NUMBER = 107;
+ private static final int DELAYED_CLEANUP_AFTER_DISCONNECT = 108;
+ private static final int SUPP_SERVICE_FAILED = 110;
+ private static final int REQUEST_UPDATE_BLUETOOTH_INDICATION = 114;
+ private static final int PHONE_CDMA_CALL_WAITING = 115;
+ private static final int REQUEST_CLOSE_SPC_ERROR_NOTICE = 118;
+ private static final int REQUEST_CLOSE_OTA_FAILURE_NOTICE = 119;
+ private static final int EVENT_PAUSE_DIALOG_COMPLETE = 120;
+ private static final int EVENT_HIDE_PROVIDER_INFO = 121; // Time to remove the info.
+ private static final int REQUEST_UPDATE_SCREEN = 122;
+ private static final int PHONE_INCOMING_RING = 123;
+ private static final int PHONE_NEW_RINGING_CONNECTION = 124;
+
+ // When InCallScreenMode is UNDEFINED set the default action
+ // to ACTION_UNDEFINED so if we are resumed the activity will
+ // know its undefined. In particular checkIsOtaCall will return
+ // false.
+ public static final String ACTION_UNDEFINED = "com.android.phone.InCallScreen.UNDEFINED";
+
+ /** Status codes returned from syncWithPhoneState(). */
+ private enum SyncWithPhoneStateStatus {
+ /**
+ * Successfully updated our internal state based on the telephony state.
+ */
+ SUCCESS,
+
+ /**
+ * There was no phone state to sync with (i.e. the phone was
+ * completely idle). In most cases this means that the
+ * in-call UI shouldn't be visible in the first place, unless
+ * we need to remain in the foreground while displaying an
+ * error message.
+ */
+ PHONE_NOT_IN_USE
+ }
+
+ private boolean mRegisteredForPhoneStates;
+
+ private PhoneGlobals mApp;
+ private CallManager mCM;
+
+ // TODO: need to clean up all remaining uses of mPhone.
+ // (There may be more than one Phone instance on the device, so it's wrong
+ // to just keep a single mPhone field. Instead, any time we need a Phone
+ // reference we should get it dynamically from the CallManager, probably
+ // based on the current foreground Call.)
+ private Phone mPhone;
+
+ private BluetoothHeadset mBluetoothHeadset;
+ private BluetoothAdapter mBluetoothAdapter;
+ private boolean mBluetoothConnectionPending;
+ private long mBluetoothConnectionRequestTime;
+
+ /** Main in-call UI elements. */
+ private CallCard mCallCard;
+
+ // UI controls:
+ private InCallControlState mInCallControlState;
+ private InCallTouchUi mInCallTouchUi;
+ private RespondViaSmsManager mRespondViaSmsManager; // see internalRespondViaSms()
+ private ManageConferenceUtils mManageConferenceUtils;
+
+ // DTMF Dialer controller and its view:
+ private DTMFTwelveKeyDialer mDialer;
+
+ private EditText mWildPromptText;
+
+ // Various dialogs we bring up (see dismissAllDialogs()).
+ // TODO: convert these all to use the "managed dialogs" framework.
+ //
+ // The MMI started dialog can actually be one of 2 items:
+ // 1. An alert dialog if the MMI code is a normal MMI
+ // 2. A progress dialog if the user requested a USSD
+ private Dialog mMmiStartedDialog;
+ private AlertDialog mMissingVoicemailDialog;
+ private AlertDialog mGenericErrorDialog;
+ private AlertDialog mSuppServiceFailureDialog;
+ private AlertDialog mWaitPromptDialog;
+ private AlertDialog mWildPromptDialog;
+ private AlertDialog mCallLostDialog;
+ private AlertDialog mPausePromptDialog;
+ private AlertDialog mExitingECMDialog;
+ // NOTE: if you add a new dialog here, be sure to add it to dismissAllDialogs() also.
+
+ // ProgressDialog created by showProgressIndication()
+ private ProgressDialog mProgressDialog;
+
+ // TODO: If the Activity class ever provides an easy way to get the
+ // current "activity lifecycle" state, we can remove these flags.
+ private boolean mIsDestroyed = false;
+ private boolean mIsForegroundActivity = false;
+ private boolean mIsForegroundActivityForProximity = false;
+ private PowerManager mPowerManager;
+
+ // For use with Pause/Wait dialogs
+ private String mPostDialStrAfterPause;
+ private boolean mPauseInProgress = false;
+
+ // Info about the most-recently-disconnected Connection, which is used
+ // to determine what should happen when exiting the InCallScreen after a
+ // call. (This info is set by onDisconnect(), and used by
+ // delayedCleanupAfterDisconnect().)
+ private Connection.DisconnectCause mLastDisconnectCause;
+
+ /** In-call audio routing options; see switchInCallAudio(). */
+ public enum InCallAudioMode {
+ SPEAKER, // Speakerphone
+ BLUETOOTH, // Bluetooth headset (if available)
+ EARPIECE, // Handset earpiece (or wired headset, if connected)
+ }
+
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (mIsDestroyed) {
+ if (DBG) log("Handler: ignoring message " + msg + "; we're destroyed!");
+ return;
+ }
+ if (!mIsForegroundActivity) {
+ if (DBG) log("Handler: handling message " + msg + " while not in foreground");
+ // Continue anyway; some of the messages below *want* to
+ // be handled even if we're not the foreground activity
+ // (like DELAYED_CLEANUP_AFTER_DISCONNECT), and they all
+ // should at least be safe to handle if we're not in the
+ // foreground...
+ }
+
+ switch (msg.what) {
+ case SUPP_SERVICE_FAILED:
+ onSuppServiceFailed((AsyncResult) msg.obj);
+ break;
+
+ case PHONE_STATE_CHANGED:
+ onPhoneStateChanged((AsyncResult) msg.obj);
+ break;
+
+ case PHONE_DISCONNECT:
+ onDisconnect((AsyncResult) msg.obj);
+ break;
+
+ case EVENT_HEADSET_PLUG_STATE_CHANGED:
+ // Update the in-call UI, since some UI elements (such
+ // as the "Speaker" button) may change state depending on
+ // whether a headset is plugged in.
+ // TODO: A full updateScreen() is overkill here, since
+ // the value of PhoneApp.isHeadsetPlugged() only affects a
+ // single onscreen UI element. (But even a full updateScreen()
+ // is still pretty cheap, so let's keep this simple
+ // for now.)
+ updateScreen();
+
+ // Also, force the "audio mode" popup to refresh itself if
+ // it's visible, since one of its items is either "Wired
+ // headset" or "Handset earpiece" depending on whether the
+ // headset is plugged in or not.
+ mInCallTouchUi.refreshAudioModePopup(); // safe even if the popup's not active
+
+ break;
+
+ // TODO: sort out MMI code (probably we should remove this method entirely).
+ // See also MMI handling code in onResume()
+ // case PhoneApp.MMI_INITIATE:
+ // onMMIInitiate((AsyncResult) msg.obj);
+ // break;
+
+ case PhoneGlobals.MMI_CANCEL:
+ onMMICancel();
+ break;
+
+ // handle the mmi complete message.
+ // since the message display class has been replaced with
+ // a system dialog in PhoneUtils.displayMMIComplete(), we
+ // should finish the activity here to close the window.
+ case PhoneGlobals.MMI_COMPLETE:
+ onMMIComplete((MmiCode) ((AsyncResult) msg.obj).result);
+ break;
+
+ case POST_ON_DIAL_CHARS:
+ handlePostOnDialChars((AsyncResult) msg.obj, (char) msg.arg1);
+ break;
+
+ case ADD_VOICEMAIL_NUMBER:
+ addVoiceMailNumberPanel();
+ break;
+
+ case DONT_ADD_VOICEMAIL_NUMBER:
+ dontAddVoiceMailNumber();
+ break;
+
+ case DELAYED_CLEANUP_AFTER_DISCONNECT:
+ delayedCleanupAfterDisconnect();
+ break;
+
+ case REQUEST_UPDATE_BLUETOOTH_INDICATION:
+ if (VDBG) log("REQUEST_UPDATE_BLUETOOTH_INDICATION...");
+ // The bluetooth headset state changed, so some UI
+ // elements may need to update. (There's no need to
+ // look up the current state here, since any UI
+ // elements that care about the bluetooth state get it
+ // directly from PhoneApp.showBluetoothIndication().)
+ updateScreen();
+ break;
+
+ case PHONE_CDMA_CALL_WAITING:
+ if (DBG) log("Received PHONE_CDMA_CALL_WAITING event ...");
+ Connection cn = mCM.getFirstActiveRingingCall().getLatestConnection();
+
+ // Only proceed if we get a valid connection object
+ if (cn != null) {
+ // Finally update screen with Call waiting info and request
+ // screen to wake up
+ updateScreen();
+ mApp.updateWakeState();
+ }
+ break;
+
+ case REQUEST_CLOSE_SPC_ERROR_NOTICE:
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.onOtaCloseSpcNotice();
+ }
+ break;
+
+ case REQUEST_CLOSE_OTA_FAILURE_NOTICE:
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.onOtaCloseFailureNotice();
+ }
+ break;
+
+ case EVENT_PAUSE_DIALOG_COMPLETE:
+ if (mPausePromptDialog != null) {
+ if (DBG) log("- DISMISSING mPausePromptDialog.");
+ mPausePromptDialog.dismiss(); // safe even if already dismissed
+ mPausePromptDialog = null;
+ }
+ break;
+
+ case EVENT_HIDE_PROVIDER_INFO:
+ mApp.inCallUiState.providerInfoVisible = false;
+ if (mCallCard != null) {
+ mCallCard.updateState(mCM);
+ }
+ break;
+ case REQUEST_UPDATE_SCREEN:
+ updateScreen();
+ break;
+
+ case PHONE_INCOMING_RING:
+ onIncomingRing();
+ break;
+
+ case PHONE_NEW_RINGING_CONNECTION:
+ onNewRingingConnection();
+ break;
+
+ default:
+ Log.wtf(LOG_TAG, "mHandler: unexpected message: " + msg);
+ break;
+ }
+ }
+ };
+
+ private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_HEADSET_PLUG)) {
+ // Listen for ACTION_HEADSET_PLUG broadcasts so that we
+ // can update the onscreen UI when the headset state changes.
+ // if (DBG) log("mReceiver: ACTION_HEADSET_PLUG");
+ // if (DBG) log("==> intent: " + intent);
+ // if (DBG) log(" state: " + intent.getIntExtra("state", 0));
+ // if (DBG) log(" name: " + intent.getStringExtra("name"));
+ // send the event and add the state as an argument.
+ Message message = Message.obtain(mHandler, EVENT_HEADSET_PLUG_STATE_CHANGED,
+ intent.getIntExtra("state", 0), 0);
+ mHandler.sendMessage(message);
+ }
+ }
+ };
+
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ Log.i(LOG_TAG, "onCreate()... this = " + this);
+ Profiler.callScreenOnCreate();
+ super.onCreate(icicle);
+
+ // Make sure this is a voice-capable device.
+ if (!PhoneGlobals.sVoiceCapable) {
+ // There should be no way to ever reach the InCallScreen on a
+ // non-voice-capable device, since this activity is not exported by
+ // our manifest, and we explicitly disable any other external APIs
+ // like the CALL intent and ITelephony.showCallScreen().
+ // So the fact that we got here indicates a phone app bug.
+ Log.wtf(LOG_TAG, "onCreate() reached on non-voice-capable device");
+ finish();
+ return;
+ }
+
+ mApp = PhoneGlobals.getInstance();
+ mApp.setInCallScreenInstance(this);
+
+ // set this flag so this activity will stay in front of the keyguard
+ int flags = WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
+ if (mApp.getPhoneState() == PhoneConstants.State.OFFHOOK) {
+ // While we are in call, the in-call screen should dismiss the keyguard.
+ // This allows the user to press Home to go directly home without going through
+ // an insecure lock screen.
+ // But we do not want to do this if there is no active call so we do not
+ // bypass the keyguard if the call is not answered or declined.
+ flags |= WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
+ }
+
+ WindowManager.LayoutParams lp = getWindow().getAttributes();
+ lp.flags |= flags;
+ if (!mApp.proximitySensorModeEnabled()) {
+ // If we don't have a proximity sensor, then the in-call screen explicitly
+ // controls user activity. This is to prevent spurious touches from waking
+ // the display.
+ lp.inputFeatures |= WindowManager.LayoutParams.INPUT_FEATURE_DISABLE_USER_ACTIVITY;
+ }
+ getWindow().setAttributes(lp);
+
+ setPhone(mApp.phone); // Sets mPhone
+
+ mCM = mApp.mCM;
+ log("- onCreate: phone state = " + mCM.getState());
+
+ mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
+ if (mBluetoothAdapter != null) {
+ mBluetoothAdapter.getProfileProxy(getApplicationContext(), mBluetoothProfileServiceListener,
+ BluetoothProfile.HEADSET);
+ }
+
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+
+ // Inflate everything in incall_screen.xml and add it to the screen.
+ setContentView(R.layout.incall_screen);
+
+ // If in landscape, then one of the ViewStubs (instead of <include>) is used for the
+ // incall_touch_ui, because CDMA and GSM button layouts are noticeably different.
+ final ViewStub touchUiStub = (ViewStub) findViewById(
+ mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA
+ ? R.id.inCallTouchUiCdmaStub : R.id.inCallTouchUiStub);
+ if (touchUiStub != null) touchUiStub.inflate();
+
+ initInCallScreen();
+
+ registerForPhoneStates();
+
+ // No need to change wake state here; that happens in onResume() when we
+ // are actually displayed.
+
+ // Handle the Intent we were launched with, but only if this is the
+ // the very first time we're being launched (ie. NOT if we're being
+ // re-initialized after previously being shut down.)
+ // Once we're up and running, any future Intents we need
+ // to handle will come in via the onNewIntent() method.
+ if (icicle == null) {
+ if (DBG) log("onCreate(): this is our very first launch, checking intent...");
+ internalResolveIntent(getIntent());
+ }
+
+ Profiler.callScreenCreated();
+ if (DBG) log("onCreate(): exit");
+ }
+
+ private BluetoothProfile.ServiceListener mBluetoothProfileServiceListener =
+ new BluetoothProfile.ServiceListener() {
+ @Override
+ public void onServiceConnected(int profile, BluetoothProfile proxy) {
+ mBluetoothHeadset = (BluetoothHeadset) proxy;
+ if (VDBG) log("- Got BluetoothHeadset: " + mBluetoothHeadset);
+ }
+
+ @Override
+ public void onServiceDisconnected(int profile) {
+ mBluetoothHeadset = null;
+ }
+ };
+
+ /**
+ * Sets the Phone object used internally by the InCallScreen.
+ *
+ * In normal operation this is called from onCreate(), and the
+ * passed-in Phone object comes from the PhoneApp.
+ * For testing, test classes can use this method to
+ * inject a test Phone instance.
+ */
+ /* package */ void setPhone(Phone phone) {
+ mPhone = phone;
+ }
+
+ @Override
+ protected void onResume() {
+ if (DBG) log("onResume()...");
+ super.onResume();
+
+ mIsForegroundActivity = true;
+ mIsForegroundActivityForProximity = true;
+
+ // The flag shouldn't be turned on when there are actual phone calls.
+ if (mCM.hasActiveFgCall() || mCM.hasActiveBgCall() || mCM.hasActiveRingingCall()) {
+ mApp.inCallUiState.showAlreadyDisconnectedState = false;
+ }
+
+ final InCallUiState inCallUiState = mApp.inCallUiState;
+ if (VDBG) inCallUiState.dumpState();
+
+ updateExpandedViewState();
+
+ // ...and update the in-call notification too, since the status bar
+ // icon needs to be hidden while we're the foreground activity:
+ mApp.notificationMgr.updateInCallNotification();
+
+ // Listen for broadcast intents that might affect the onscreen UI.
+ registerReceiver(mReceiver, new IntentFilter(Intent.ACTION_HEADSET_PLUG));
+
+ // Keep a "dialer session" active when we're in the foreground.
+ // (This is needed to play DTMF tones.)
+ mDialer.startDialerSession();
+
+ // Restore various other state from the InCallUiState object:
+
+ // Update the onscreen dialpad state to match the InCallUiState.
+ if (inCallUiState.showDialpad) {
+ openDialpadInternal(false); // no "opening" animation
+ } else {
+ closeDialpadInternal(false); // no "closing" animation
+ }
+
+ // Reset the dialpad context
+ // TODO: Dialpad digits should be set here as well (once they are saved)
+ mDialer.setDialpadContext(inCallUiState.dialpadContextText);
+
+ // If there's a "Respond via SMS" popup still around since the
+ // last time we were the foreground activity, make sure it's not
+ // still active!
+ // (The popup should *never* be visible initially when we first
+ // come to the foreground; it only ever comes up in response to
+ // the user selecting the "SMS" option from the incoming call
+ // widget.)
+ mRespondViaSmsManager.dismissPopup(); // safe even if already dismissed
+
+ // Display an error / diagnostic indication if necessary.
+ //
+ // When the InCallScreen comes to the foreground, we normally we
+ // display the in-call UI in whatever state is appropriate based on
+ // the state of the telephony framework (e.g. an outgoing call in
+ // DIALING state, an incoming call, etc.)
+ //
+ // But if the InCallUiState has a "pending call status code" set,
+ // that means we need to display some kind of status or error
+ // indication to the user instead of the regular in-call UI. (The
+ // most common example of this is when there's some kind of
+ // failure while initiating an outgoing call; see
+ // CallController.placeCall().)
+ //
+ boolean handledStartupError = false;
+ if (inCallUiState.hasPendingCallStatusCode()) {
+ if (DBG) log("- onResume: need to show status indication!");
+ showStatusIndication(inCallUiState.getPendingCallStatusCode());
+
+ // Set handledStartupError to ensure that we won't bail out below.
+ // (We need to stay here in the InCallScreen so that the user
+ // is able to see the error dialog!)
+ handledStartupError = true;
+ }
+
+ // Set the volume control handler while we are in the foreground.
+ final boolean bluetoothConnected = isBluetoothAudioConnected();
+
+ if (bluetoothConnected) {
+ setVolumeControlStream(AudioManager.STREAM_BLUETOOTH_SCO);
+ } else {
+ setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
+ }
+
+ takeKeyEvents(true);
+
+ // If an OTASP call is in progress, use the special OTASP-specific UI.
+ boolean inOtaCall = false;
+ if (TelephonyCapabilities.supportsOtasp(mPhone)) {
+ inOtaCall = checkOtaspStateOnResume();
+ }
+ if (!inOtaCall) {
+ // Always start off in NORMAL mode
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ }
+
+ // Before checking the state of the CallManager, clean up any
+ // connections in the DISCONNECTED state.
+ // (The DISCONNECTED state is used only to drive the "call ended"
+ // UI; it's totally useless when *entering* the InCallScreen.)
+ mCM.clearDisconnected();
+
+ // Update the onscreen UI to reflect the current telephony state.
+ SyncWithPhoneStateStatus status = syncWithPhoneState();
+
+ // Note there's no need to call updateScreen() here;
+ // syncWithPhoneState() already did that if necessary.
+
+ if (status != SyncWithPhoneStateStatus.SUCCESS) {
+ if (DBG) log("- onResume: syncWithPhoneState failed! status = " + status);
+ // Couldn't update the UI, presumably because the phone is totally
+ // idle.
+
+ // Even though the phone is idle, though, we do still need to
+ // stay here on the InCallScreen if we're displaying an
+ // error dialog (see "showStatusIndication()" above).
+
+ if (handledStartupError) {
+ // Stay here for now. We'll eventually leave the
+ // InCallScreen when the user presses the dialog's OK
+ // button (see bailOutAfterErrorDialog()), or when the
+ // progress indicator goes away.
+ Log.i(LOG_TAG, " ==> syncWithPhoneState failed, but staying here anyway.");
+ } else {
+ // The phone is idle, and we did NOT handle a
+ // startup error during this pass thru onResume.
+ //
+ // This basically means that we're being resumed because of
+ // some action *other* than a new intent. (For example,
+ // the user pressing POWER to wake up the device, causing
+ // the InCallScreen to come back to the foreground.)
+ //
+ // In this scenario we do NOT want to stay here on the
+ // InCallScreen: we're not showing any useful info to the
+ // user (like a dialog), and the in-call UI itself is
+ // useless if there's no active call. So bail out.
+
+ Log.i(LOG_TAG, " ==> syncWithPhoneState failed; bailing out!");
+ dismissAllDialogs();
+
+ // Force the InCallScreen to truly finish(), rather than just
+ // moving it to the back of the activity stack (which is what
+ // our finish() method usually does.)
+ // This is necessary to avoid an obscure scenario where the
+ // InCallScreen can get stuck in an inconsistent state, somehow
+ // causing a *subsequent* outgoing call to fail (bug 4172599).
+ endInCallScreenSession(true /* force a real finish() call */);
+ return;
+ }
+ } else if (TelephonyCapabilities.supportsOtasp(mPhone)) {
+ if (inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL ||
+ inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED) {
+ if (mCallCard != null) mCallCard.setVisibility(View.GONE);
+ updateScreen();
+ return;
+ }
+ }
+
+ // InCallScreen is now active.
+ EventLog.writeEvent(EventLogTags.PHONE_UI_ENTER);
+
+ // Update the poke lock and wake lock when we move to the foreground.
+ // This will be no-op when prox sensor is effective.
+ mApp.updateWakeState();
+
+ // Restore the mute state if the last mute state change was NOT
+ // done by the user.
+ if (mApp.getRestoreMuteOnInCallResume()) {
+ // Mute state is based on the foreground call
+ PhoneUtils.restoreMuteState();
+ mApp.setRestoreMuteOnInCallResume(false);
+ }
+
+ Profiler.profileViewCreate(getWindow(), InCallScreen.class.getName());
+
+ // If there's a pending MMI code, we'll show a dialog here.
+ //
+ // Note: previously we had shown the dialog when MMI_INITIATE event's coming
+ // from telephony layer, while right now we don't because the event comes
+ // too early (before in-call screen is prepared).
+ // Now we instead check pending MMI code and show the dialog here.
+ //
+ // This *may* cause some problem, e.g. when the user really quickly starts
+ // MMI sequence and calls an actual phone number before the MMI request
+ // being completed, which is rather rare.
+ //
+ // TODO: streamline this logic and have a UX in a better manner.
+ // Right now syncWithPhoneState() above will return SUCCESS based on
+ // mPhone.getPendingMmiCodes().isEmpty(), while we check it again here.
+ // Also we show pre-populated in-call UI under the dialog, which looks
+ // not great. (issue 5210375, 5545506)
+ // After cleaning them, remove commented-out MMI handling code elsewhere.
+ if (!mPhone.getPendingMmiCodes().isEmpty()) {
+ if (mMmiStartedDialog == null) {
+ MmiCode mmiCode = mPhone.getPendingMmiCodes().get(0);
+ Message message = Message.obtain(mHandler, PhoneGlobals.MMI_CANCEL);
+ mMmiStartedDialog = PhoneUtils.displayMMIInitiate(this, mmiCode,
+ message, mMmiStartedDialog);
+ // mInCallScreen needs to receive MMI_COMPLETE/MMI_CANCEL event from telephony,
+ // which will dismiss the entire screen.
+ }
+ }
+
+ // This means the screen is shown even though there's no connection, which only happens
+ // when the phone call has hung up while the screen is turned off at that moment.
+ // We want to show "disconnected" state with photos with appropriate elapsed time for
+ // the finished phone call.
+ if (mApp.inCallUiState.showAlreadyDisconnectedState) {
+ // if (DBG) {
+ log("onResume(): detected \"show already disconnected state\" situation."
+ + " set up DELAYED_CLEANUP_AFTER_DISCONNECT message with "
+ + CALL_ENDED_LONG_DELAY + " msec delay.");
+ //}
+ mHandler.removeMessages(DELAYED_CLEANUP_AFTER_DISCONNECT);
+ mHandler.sendEmptyMessageDelayed(DELAYED_CLEANUP_AFTER_DISCONNECT,
+ CALL_ENDED_LONG_DELAY);
+ }
+
+ if (VDBG) log("onResume() done.");
+ }
+
+ // onPause is guaranteed to be called when the InCallScreen goes
+ // in the background.
+ @Override
+ protected void onPause() {
+ if (DBG) log("onPause()...");
+ super.onPause();
+
+ if (mPowerManager.isScreenOn()) {
+ // Set to false when the screen went background *not* by screen turned off. Probably
+ // the user bailed out of the in-call screen (by pressing BACK, HOME, etc.)
+ mIsForegroundActivityForProximity = false;
+ }
+ mIsForegroundActivity = false;
+
+ // Force a clear of the provider info frame. Since the
+ // frame is removed using a timed message, it is
+ // possible we missed it if the prev call was interrupted.
+ mApp.inCallUiState.providerInfoVisible = false;
+
+ // "show-already-disconnected-state" should be effective just during the first wake-up.
+ // We should never allow it to stay true after that.
+ mApp.inCallUiState.showAlreadyDisconnectedState = false;
+
+ // A safety measure to disable proximity sensor in case call failed
+ // and the telephony state did not change.
+ mApp.setBeginningCall(false);
+
+ // Make sure the "Manage conference" chronometer is stopped when
+ // we move away from the foreground.
+ mManageConferenceUtils.stopConferenceTime();
+
+ // as a catch-all, make sure that any dtmf tones are stopped
+ // when the UI is no longer in the foreground.
+ mDialer.onDialerKeyUp(null);
+
+ // Release any "dialer session" resources, now that we're no
+ // longer in the foreground.
+ mDialer.stopDialerSession();
+
+ // If the device is put to sleep as the phone call is ending,
+ // we may see cases where the DELAYED_CLEANUP_AFTER_DISCONNECT
+ // event gets handled AFTER the device goes to sleep and wakes
+ // up again.
+
+ // This is because it is possible for a sleep command
+ // (executed with the End Call key) to come during the 2
+ // seconds that the "Call Ended" screen is up. Sleep then
+ // pauses the device (including the cleanup event) and
+ // resumes the event when it wakes up.
+
+ // To fix this, we introduce a bit of code that pushes the UI
+ // to the background if we pause and see a request to
+ // DELAYED_CLEANUP_AFTER_DISCONNECT.
+
+ // Note: We can try to finish directly, by:
+ // 1. Removing the DELAYED_CLEANUP_AFTER_DISCONNECT messages
+ // 2. Calling delayedCleanupAfterDisconnect directly
+
+ // However, doing so can cause problems between the phone
+ // app and the keyguard - the keyguard is trying to sleep at
+ // the same time that the phone state is changing. This can
+ // end up causing the sleep request to be ignored.
+ if (mHandler.hasMessages(DELAYED_CLEANUP_AFTER_DISCONNECT)
+ && mCM.getState() != PhoneConstants.State.RINGING) {
+ if (DBG) log("DELAYED_CLEANUP_AFTER_DISCONNECT detected, moving UI to background.");
+ endInCallScreenSession();
+ }
+
+ EventLog.writeEvent(EventLogTags.PHONE_UI_EXIT);
+
+ // Dismiss any dialogs we may have brought up, just to be 100%
+ // sure they won't still be around when we get back here.
+ dismissAllDialogs();
+
+ updateExpandedViewState();
+
+ // ...and the in-call notification too:
+ mApp.notificationMgr.updateInCallNotification();
+ // ...and *always* reset the system bar back to its normal state
+ // when leaving the in-call UI.
+ // (While we're the foreground activity, we disable navigation in
+ // some call states; see InCallTouchUi.updateState().)
+ mApp.notificationMgr.statusBarHelper.enableSystemBarNavigation(true);
+
+ // Unregister for broadcast intents. (These affect the visible UI
+ // of the InCallScreen, so we only care about them while we're in the
+ // foreground.)
+ unregisterReceiver(mReceiver);
+
+ // Make sure we revert the poke lock and wake lock when we move to
+ // the background.
+ mApp.updateWakeState();
+
+ // clear the dismiss keyguard flag so we are back to the default state
+ // when we next resume
+ updateKeyguardPolicy(false);
+
+ // See also PhoneApp#updatePhoneState(), which takes care of all the other release() calls.
+ if (mApp.getUpdateLock().isHeld() && mApp.getPhoneState() == PhoneConstants.State.IDLE) {
+ if (DBG) {
+ log("Release UpdateLock on onPause() because there's no active phone call.");
+ }
+ mApp.getUpdateLock().release();
+ }
+ }
+
+ @Override
+ protected void onStop() {
+ if (DBG) log("onStop()...");
+ super.onStop();
+
+ stopTimer();
+
+ PhoneConstants.State state = mCM.getState();
+ if (DBG) log("onStop: state = " + state);
+
+ if (state == PhoneConstants.State.IDLE) {
+ if (mRespondViaSmsManager.isShowingPopup()) {
+ // This means that the user has been opening the "Respond via SMS" dialog even
+ // after the incoming call hanging up, and the screen finally went background.
+ // In that case we just close the dialog and exit the whole in-call screen.
+ mRespondViaSmsManager.dismissPopup();
+ }
+
+ // when OTA Activation, OTA Success/Failure dialog or OTA SPC
+ // failure dialog is running, do not destroy inCallScreen. Because call
+ // is already ended and dialog will not get redrawn on slider event.
+ if ((mApp.cdmaOtaProvisionData != null) && (mApp.cdmaOtaScreenState != null)
+ && ((mApp.cdmaOtaScreenState.otaScreenState !=
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION)
+ && (mApp.cdmaOtaScreenState.otaScreenState !=
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_SUCCESS_FAILURE_DLG)
+ && (!mApp.cdmaOtaProvisionData.inOtaSpcState))) {
+ // we don't want the call screen to remain in the activity history
+ // if there are not active or ringing calls.
+ if (DBG) log("- onStop: calling finish() to clear activity history...");
+ moveTaskToBack(true);
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.cleanOtaScreen(true);
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ Log.i(LOG_TAG, "onDestroy()... this = " + this);
+ super.onDestroy();
+
+ // Set the magic flag that tells us NOT to handle any handler
+ // messages that come in asynchronously after we get destroyed.
+ mIsDestroyed = true;
+
+ mApp.setInCallScreenInstance(null);
+
+ // Clear out the InCallScreen references in various helper objects
+ // (to let them know we've been destroyed).
+ if (mCallCard != null) {
+ mCallCard.setInCallScreenInstance(null);
+ }
+ if (mInCallTouchUi != null) {
+ mInCallTouchUi.setInCallScreenInstance(null);
+ }
+ mRespondViaSmsManager.setInCallScreenInstance(null);
+
+ mDialer.clearInCallScreenReference();
+ mDialer = null;
+
+ unregisterForPhoneStates();
+ // No need to change wake state here; that happens in onPause() when we
+ // are moving out of the foreground.
+
+ if (mBluetoothHeadset != null) {
+ mBluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, mBluetoothHeadset);
+ mBluetoothHeadset = null;
+ }
+
+ // Dismiss all dialogs, to be absolutely sure we won't leak any of
+ // them while changing orientation.
+ dismissAllDialogs();
+
+ // If there's an OtaUtils instance around, clear out its
+ // references to our internal widgets.
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.clearUiWidgets();
+ }
+ }
+
+ /**
+ * Dismisses the in-call screen.
+ *
+ * We never *really* finish() the InCallScreen, since we don't want to
+ * get destroyed and then have to be re-created from scratch for the
+ * next call. Instead, we just move ourselves to the back of the
+ * activity stack.
+ *
+ * This also means that we'll no longer be reachable via the BACK
+ * button (since moveTaskToBack() puts us behind the Home app, but the
+ * home app doesn't allow the BACK key to move you any farther down in
+ * the history stack.)
+ *
+ * (Since the Phone app itself is never killed, this basically means
+ * that we'll keep a single InCallScreen instance around for the
+ * entire uptime of the device. This noticeably improves the UI
+ * responsiveness for incoming calls.)
+ */
+ @Override
+ public void finish() {
+ if (DBG) log("finish()...");
+ moveTaskToBack(true);
+ }
+
+ /**
+ * End the current in call screen session.
+ *
+ * This must be called when an InCallScreen session has
+ * complete so that the next invocation via an onResume will
+ * not be in an old state.
+ */
+ public void endInCallScreenSession() {
+ if (DBG) log("endInCallScreenSession()... phone state = " + mCM.getState());
+ endInCallScreenSession(false);
+ }
+
+ /**
+ * Internal version of endInCallScreenSession().
+ *
+ * @param forceFinish If true, force the InCallScreen to
+ * truly finish() rather than just calling moveTaskToBack().
+ * @see finish()
+ */
+ private void endInCallScreenSession(boolean forceFinish) {
+ if (DBG) {
+ log("endInCallScreenSession(" + forceFinish + ")... phone state = " + mCM.getState());
+ }
+ if (forceFinish) {
+ Log.i(LOG_TAG, "endInCallScreenSession(): FORCING a call to super.finish()!");
+ super.finish(); // Call super.finish() rather than our own finish() method,
+ // which actually just calls moveTaskToBack().
+ } else {
+ moveTaskToBack(true);
+ }
+ setInCallScreenMode(InCallScreenMode.UNDEFINED);
+
+ // Call update screen so that the in-call screen goes back to a normal state.
+ // This avoids bugs where a previous state will filcker the next time phone is
+ // opened.
+ updateScreen();
+
+ if (mCallCard != null) {
+ mCallCard.clear();
+ }
+ }
+
+ /**
+ * True when this Activity is in foreground (between onResume() and onPause()).
+ */
+ /* package */ boolean isForegroundActivity() {
+ return mIsForegroundActivity;
+ }
+
+ /**
+ * Returns true when the Activity is in foreground (between onResume() and onPause()),
+ * or, is in background due to user's bailing out of the screen, not by screen turning off.
+ *
+ * @see #isForegroundActivity()
+ */
+ /* package */ boolean isForegroundActivityForProximity() {
+ return mIsForegroundActivityForProximity;
+ }
+
+ /* package */ void updateKeyguardPolicy(boolean dismissKeyguard) {
+ if (dismissKeyguard) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ } else {
+ getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
+ }
+ }
+
+ private void registerForPhoneStates() {
+ if (!mRegisteredForPhoneStates) {
+ mCM.registerForPreciseCallStateChanged(mHandler, PHONE_STATE_CHANGED, null);
+ mCM.registerForDisconnect(mHandler, PHONE_DISCONNECT, null);
+ // TODO: sort out MMI code (probably we should remove this method entirely).
+ // See also MMI handling code in onResume()
+ // mCM.registerForMmiInitiate(mHandler, PhoneApp.MMI_INITIATE, null);
+
+ // register for the MMI complete message. Upon completion,
+ // PhoneUtils will bring up a system dialog instead of the
+ // message display class in PhoneUtils.displayMMIComplete().
+ // We'll listen for that message too, so that we can finish
+ // the activity at the same time.
+ mCM.registerForMmiComplete(mHandler, PhoneGlobals.MMI_COMPLETE, null);
+ mCM.registerForCallWaiting(mHandler, PHONE_CDMA_CALL_WAITING, null);
+ mCM.registerForPostDialCharacter(mHandler, POST_ON_DIAL_CHARS, null);
+ mCM.registerForSuppServiceFailed(mHandler, SUPP_SERVICE_FAILED, null);
+ mCM.registerForIncomingRing(mHandler, PHONE_INCOMING_RING, null);
+ mCM.registerForNewRingingConnection(mHandler, PHONE_NEW_RINGING_CONNECTION, null);
+ mRegisteredForPhoneStates = true;
+ }
+ }
+
+ private void unregisterForPhoneStates() {
+ mCM.unregisterForPreciseCallStateChanged(mHandler);
+ mCM.unregisterForDisconnect(mHandler);
+ mCM.unregisterForMmiInitiate(mHandler);
+ mCM.unregisterForMmiComplete(mHandler);
+ mCM.unregisterForCallWaiting(mHandler);
+ mCM.unregisterForPostDialCharacter(mHandler);
+ mCM.unregisterForSuppServiceFailed(mHandler);
+ mCM.unregisterForIncomingRing(mHandler);
+ mCM.unregisterForNewRingingConnection(mHandler);
+ mRegisteredForPhoneStates = false;
+ }
+
+ /* package */ void updateAfterRadioTechnologyChange() {
+ if (DBG) Log.d(LOG_TAG, "updateAfterRadioTechnologyChange()...");
+
+ // Reset the call screen since the calls cannot be transferred
+ // across radio technologies.
+ resetInCallScreenMode();
+
+ // Unregister for all events from the old obsolete phone
+ unregisterForPhoneStates();
+
+ // (Re)register for all events relevant to the new active phone
+ registerForPhoneStates();
+
+ // And finally, refresh the onscreen UI. (Note that it's safe
+ // to call requestUpdateScreen() even if the radio change ended up
+ // causing us to exit the InCallScreen.)
+ requestUpdateScreen();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ log("onNewIntent: intent = " + intent + ", phone state = " + mCM.getState());
+
+ // We're being re-launched with a new Intent. Since it's possible for a
+ // single InCallScreen instance to persist indefinitely (even if we
+ // finish() ourselves), this sequence can potentially happen any time
+ // the InCallScreen needs to be displayed.
+
+ // Stash away the new intent so that we can get it in the future
+ // by calling getIntent(). (Otherwise getIntent() will return the
+ // original Intent from when we first got created!)
+ setIntent(intent);
+
+ // Activities are always paused before receiving a new intent, so
+ // we can count on our onResume() method being called next.
+
+ // Just like in onCreate(), handle the intent.
+ internalResolveIntent(intent);
+ }
+
+ private void internalResolveIntent(Intent intent) {
+ if (intent == null || intent.getAction() == null) {
+ return;
+ }
+ String action = intent.getAction();
+ if (DBG) log("internalResolveIntent: action=" + action);
+
+ // In gingerbread and earlier releases, the InCallScreen used to
+ // directly handle certain intent actions that could initiate phone
+ // calls, namely ACTION_CALL and ACTION_CALL_EMERGENCY, and also
+ // OtaUtils.ACTION_PERFORM_CDMA_PROVISIONING.
+ //
+ // But it doesn't make sense to tie those actions to the InCallScreen
+ // (or especially to the *activity lifecycle* of the InCallScreen).
+ // Instead, the InCallScreen should only be concerned with running the
+ // onscreen UI while in a call. So we've now offloaded the call-control
+ // functionality to a new module called CallController, and OTASP calls
+ // are now launched from the OtaUtils startInteractiveOtasp() or
+ // startNonInteractiveOtasp() methods.
+ //
+ // So now, the InCallScreen is only ever launched using the ACTION_MAIN
+ // action, and (upon launch) performs no functionality other than
+ // displaying the UI in a state that matches the current telephony
+ // state.
+
+ if (action.equals(intent.ACTION_MAIN)) {
+ // This action is the normal way to bring up the in-call UI.
+ //
+ // Most of the interesting work of updating the onscreen UI (to
+ // match the current telephony state) happens in the
+ // syncWithPhoneState() => updateScreen() sequence that happens in
+ // onResume().
+ //
+ // But we do check here for one extra that can come along with the
+ // ACTION_MAIN intent:
+
+ if (intent.hasExtra(SHOW_DIALPAD_EXTRA)) {
+ // SHOW_DIALPAD_EXTRA can be used here to specify whether the DTMF
+ // dialpad should be initially visible. If the extra isn't
+ // present at all, we just leave the dialpad in its previous state.
+
+ boolean showDialpad = intent.getBooleanExtra(SHOW_DIALPAD_EXTRA, false);
+ if (VDBG) log("- internalResolveIntent: SHOW_DIALPAD_EXTRA: " + showDialpad);
+
+ // If SHOW_DIALPAD_EXTRA is specified, that overrides whatever
+ // the previous state of inCallUiState.showDialpad was.
+ mApp.inCallUiState.showDialpad = showDialpad;
+
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ final boolean hasHoldingCall = mCM.hasActiveBgCall();
+
+ // There's only one line in use, AND it's on hold, at which we're sure the user
+ // wants to use the dialpad toward the exact line, so un-hold the holding line.
+ if (showDialpad && !hasActiveCall && hasHoldingCall) {
+ PhoneUtils.switchHoldingAndActive(mCM.getFirstActiveBgCall());
+ }
+ }
+ // ...and in onResume() we'll update the onscreen dialpad state to
+ // match the InCallUiState.
+
+ return;
+ }
+
+ if (action.equals(OtaUtils.ACTION_DISPLAY_ACTIVATION_SCREEN)) {
+ // Bring up the in-call UI in the OTASP-specific "activate" state;
+ // see OtaUtils.startInteractiveOtasp(). Note that at this point
+ // the OTASP call has not been started yet; we won't actually make
+ // the call until the user presses the "Activate" button.
+
+ if (!TelephonyCapabilities.supportsOtasp(mPhone)) {
+ throw new IllegalStateException(
+ "Received ACTION_DISPLAY_ACTIVATION_SCREEN intent on non-OTASP-capable device: "
+ + intent);
+ }
+
+ setInCallScreenMode(InCallScreenMode.OTA_NORMAL);
+ if ((mApp.cdmaOtaProvisionData != null)
+ && (!mApp.cdmaOtaProvisionData.isOtaCallIntentProcessed)) {
+ mApp.cdmaOtaProvisionData.isOtaCallIntentProcessed = true;
+ mApp.cdmaOtaScreenState.otaScreenState =
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION;
+ }
+ return;
+ }
+
+ // Various intent actions that should no longer come here directly:
+ if (action.equals(OtaUtils.ACTION_PERFORM_CDMA_PROVISIONING)) {
+ // This intent is now handled by the InCallScreenShowActivation
+ // activity, which translates it into a call to
+ // OtaUtils.startInteractiveOtasp().
+ throw new IllegalStateException(
+ "Unexpected ACTION_PERFORM_CDMA_PROVISIONING received by InCallScreen: "
+ + intent);
+ } else if (action.equals(Intent.ACTION_CALL)
+ || action.equals(Intent.ACTION_CALL_EMERGENCY)) {
+ // ACTION_CALL* intents go to the OutgoingCallBroadcaster, which now
+ // translates them into CallController.placeCall() calls rather than
+ // launching the InCallScreen directly.
+ throw new IllegalStateException("Unexpected CALL action received by InCallScreen: "
+ + intent);
+ } else if (action.equals(ACTION_UNDEFINED)) {
+ // This action is only used for internal bookkeeping; we should
+ // never actually get launched with it.
+ Log.wtf(LOG_TAG, "internalResolveIntent: got launched with ACTION_UNDEFINED");
+ return;
+ } else {
+ Log.wtf(LOG_TAG, "internalResolveIntent: unexpected intent action: " + action);
+ // But continue the best we can (basically treating this case
+ // like ACTION_MAIN...)
+ return;
+ }
+ }
+
+ private void stopTimer() {
+ if (mCallCard != null) mCallCard.stopTimer();
+ }
+
+ private void initInCallScreen() {
+ if (VDBG) log("initInCallScreen()...");
+
+ // Have the WindowManager filter out touch events that are "too fat".
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES);
+
+ // Initialize the CallCard.
+ mCallCard = (CallCard) findViewById(R.id.callCard);
+ if (VDBG) log(" - mCallCard = " + mCallCard);
+ mCallCard.setInCallScreenInstance(this);
+
+ // Initialize the onscreen UI elements.
+ initInCallTouchUi();
+
+ // Helper class to keep track of enabledness/state of UI controls
+ mInCallControlState = new InCallControlState(this, mCM);
+
+ // Helper class to run the "Manage conference" UI
+ mManageConferenceUtils = new ManageConferenceUtils(this, mCM);
+
+ // The DTMF Dialpad.
+ ViewStub stub = (ViewStub) findViewById(R.id.dtmf_twelve_key_dialer_stub);
+ mDialer = new DTMFTwelveKeyDialer(this, stub);
+ mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ }
+
+ /**
+ * Returns true if the phone is "in use", meaning that at least one line
+ * is active (ie. off hook or ringing or dialing). Conversely, a return
+ * value of false means there's currently no phone activity at all.
+ */
+ private boolean phoneIsInUse() {
+ return mCM.getState() != PhoneConstants.State.IDLE;
+ }
+
+ private boolean handleDialerKeyDown(int keyCode, KeyEvent event) {
+ if (VDBG) log("handleDialerKeyDown: keyCode " + keyCode + ", event " + event + "...");
+
+ // As soon as the user starts typing valid dialable keys on the
+ // keyboard (presumably to type DTMF tones) we start passing the
+ // key events to the DTMFDialer's onDialerKeyDown. We do so
+ // only if the okToDialDTMFTones() conditions pass.
+ if (okToDialDTMFTones()) {
+ return mDialer.onDialerKeyDown(event);
+
+ // TODO: If the dialpad isn't currently visible, maybe
+ // consider automatically bringing it up right now?
+ // (Just to make sure the user sees the digits widget...)
+ // But this probably isn't too critical since it's awkward to
+ // use the hard keyboard while in-call in the first place,
+ // especially now that the in-call UI is portrait-only...
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (DBG) log("onBackPressed()...");
+
+ // To consume this BACK press, the code here should just do
+ // something and return. Otherwise, call super.onBackPressed() to
+ // get the default implementation (which simply finishes the
+ // current activity.)
+
+ if (mCM.hasActiveRingingCall()) {
+ // The Back key, just like the Home key, is always disabled
+ // while an incoming call is ringing. (The user *must* either
+ // answer or reject the call before leaving the incoming-call
+ // screen.)
+ if (DBG) log("BACK key while ringing: ignored");
+
+ // And consume this event; *don't* call super.onBackPressed().
+ return;
+ }
+
+ // BACK is also used to exit out of any "special modes" of the
+ // in-call UI:
+
+ if (mDialer.isOpened()) {
+ closeDialpadInternal(true); // do the "closing" animation
+ return;
+ }
+
+ if (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.MANAGE_CONFERENCE) {
+ // Hide the Manage Conference panel, return to NORMAL mode.
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ requestUpdateScreen();
+ return;
+ }
+
+ // Nothing special to do. Fall back to the default behavior.
+ super.onBackPressed();
+ }
+
+ /**
+ * Handles the green CALL key while in-call.
+ * @return true if we consumed the event.
+ */
+ private boolean handleCallKey() {
+ // The green CALL button means either "Answer", "Unhold", or
+ // "Swap calls", or can be a no-op, depending on the current state
+ // of the Phone.
+
+ final boolean hasRingingCall = mCM.hasActiveRingingCall();
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ final boolean hasHoldingCall = mCM.hasActiveBgCall();
+
+ int phoneType = mPhone.getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ // The green CALL button means either "Answer", "Swap calls/On Hold", or
+ // "Add to 3WC", depending on the current state of the Phone.
+
+ CdmaPhoneCallState.PhoneCallState currCallState =
+ mApp.cdmaPhoneCallState.getCurrentCallState();
+ if (hasRingingCall) {
+ //Scenario 1: Accepting the First Incoming and Call Waiting call
+ if (DBG) log("answerCall: First Incoming and Call Waiting scenario");
+ internalAnswerCall(); // Automatically holds the current active call,
+ // if there is one
+ } else if ((currCallState == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE)
+ && (hasActiveCall)) {
+ //Scenario 2: Merging 3Way calls
+ if (DBG) log("answerCall: Merge 3-way call scenario");
+ // Merge calls
+ PhoneUtils.mergeCalls(mCM);
+ } else if (currCallState == CdmaPhoneCallState.PhoneCallState.CONF_CALL) {
+ //Scenario 3: Switching between two Call waiting calls or drop the latest
+ // connection if in a 3Way merge scenario
+ if (DBG) log("answerCall: Switch btwn 2 calls scenario");
+ internalSwapCalls();
+ }
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ if (hasRingingCall) {
+ // If an incoming call is ringing, the CALL button is actually
+ // handled by the PhoneWindowManager. (We do this to make
+ // sure that we'll respond to the key even if the InCallScreen
+ // hasn't come to the foreground yet.)
+ //
+ // We'd only ever get here in the extremely rare case that the
+ // incoming call started ringing *after*
+ // PhoneWindowManager.interceptKeyTq() but before the event
+ // got here, or else if the PhoneWindowManager had some
+ // problem connecting to the ITelephony service.
+ Log.w(LOG_TAG, "handleCallKey: incoming call is ringing!"
+ + " (PhoneWindowManager should have handled this key.)");
+ // But go ahead and handle the key as normal, since the
+ // PhoneWindowManager presumably did NOT handle it:
+
+ // There's an incoming ringing call: CALL means "Answer".
+ internalAnswerCall();
+ } else if (hasActiveCall && hasHoldingCall) {
+ // Two lines are in use: CALL means "Swap calls".
+ if (DBG) log("handleCallKey: both lines in use ==> swap calls.");
+ internalSwapCalls();
+ } else if (hasHoldingCall) {
+ // There's only one line in use, AND it's on hold.
+ // In this case CALL is a shortcut for "unhold".
+ if (DBG) log("handleCallKey: call on hold ==> unhold.");
+ PhoneUtils.switchHoldingAndActive(
+ mCM.getFirstActiveBgCall()); // Really means "unhold" in this state
+ } else {
+ // The most common case: there's only one line in use, and
+ // it's an active call (i.e. it's not on hold.)
+ // In this case CALL is a no-op.
+ // (This used to be a shortcut for "add call", but that was a
+ // bad idea because "Add call" is so infrequently-used, and
+ // because the user experience is pretty confusing if you
+ // inadvertently trigger it.)
+ if (VDBG) log("handleCallKey: call in foregound ==> ignoring.");
+ // But note we still consume this key event; see below.
+ }
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+
+ // We *always* consume the CALL key, since the system-wide default
+ // action ("go to the in-call screen") is useless here.
+ return true;
+ }
+
+ boolean isKeyEventAcceptableDTMF (KeyEvent event) {
+ return (mDialer != null && mDialer.isKeyEventAcceptable(event));
+ }
+
+ /**
+ * Overriden to track relevant focus changes.
+ *
+ * If a key is down and some time later the focus changes, we may
+ * NOT recieve the keyup event; logically the keyup event has not
+ * occured in this window. This issue is fixed by treating a focus
+ * changed event as an interruption to the keydown, making sure
+ * that any code that needs to be run in onKeyUp is ALSO run here.
+ */
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ // the dtmf tones should no longer be played
+ if (VDBG) log("onWindowFocusChanged(" + hasFocus + ")...");
+ if (!hasFocus && mDialer != null) {
+ if (VDBG) log("- onWindowFocusChanged: faking onDialerKeyUp()...");
+ mDialer.onDialerKeyUp(null);
+ }
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ // if (DBG) log("onKeyUp(keycode " + keyCode + ")...");
+
+ // push input to the dialer.
+ if ((mDialer != null) && (mDialer.onDialerKeyUp(event))){
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_CALL) {
+ // Always consume CALL to be sure the PhoneWindow won't do anything with it
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ // if (DBG) log("onKeyDown(keycode " + keyCode + ")...");
+
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CALL:
+ boolean handled = handleCallKey();
+ if (!handled) {
+ Log.w(LOG_TAG, "InCallScreen should always handle KEYCODE_CALL in onKeyDown");
+ }
+ // Always consume CALL to be sure the PhoneWindow won't do anything with it
+ return true;
+
+ // Note there's no KeyEvent.KEYCODE_ENDCALL case here.
+ // The standard system-wide handling of the ENDCALL key
+ // (see PhoneWindowManager's handling of KEYCODE_ENDCALL)
+ // already implements exactly what the UI spec wants,
+ // namely (1) "hang up" if there's a current active call,
+ // or (2) "don't answer" if there's a current ringing call.
+
+ case KeyEvent.KEYCODE_CAMERA:
+ // Disable the CAMERA button while in-call since it's too
+ // easy to press accidentally.
+ return true;
+
+ case KeyEvent.KEYCODE_VOLUME_UP:
+ case KeyEvent.KEYCODE_VOLUME_DOWN:
+ case KeyEvent.KEYCODE_VOLUME_MUTE:
+ if (mCM.getState() == PhoneConstants.State.RINGING) {
+ // If an incoming call is ringing, the VOLUME buttons are
+ // actually handled by the PhoneWindowManager. (We do
+ // this to make sure that we'll respond to them even if
+ // the InCallScreen hasn't come to the foreground yet.)
+ //
+ // We'd only ever get here in the extremely rare case that the
+ // incoming call started ringing *after*
+ // PhoneWindowManager.interceptKeyTq() but before the event
+ // got here, or else if the PhoneWindowManager had some
+ // problem connecting to the ITelephony service.
+ Log.w(LOG_TAG, "VOLUME key: incoming call is ringing!"
+ + " (PhoneWindowManager should have handled this key.)");
+ // But go ahead and handle the key as normal, since the
+ // PhoneWindowManager presumably did NOT handle it:
+ internalSilenceRinger();
+
+ // As long as an incoming call is ringing, we always
+ // consume the VOLUME keys.
+ return true;
+ }
+ break;
+
+ case KeyEvent.KEYCODE_MUTE:
+ onMuteClick();
+ return true;
+
+ // Various testing/debugging features, enabled ONLY when VDBG == true.
+ case KeyEvent.KEYCODE_SLASH:
+ if (VDBG) {
+ log("----------- InCallScreen View dump --------------");
+ // Dump starting from the top-level view of the entire activity:
+ Window w = this.getWindow();
+ View decorView = w.getDecorView();
+ decorView.debug();
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_EQUALS:
+ if (VDBG) {
+ log("----------- InCallScreen call state dump --------------");
+ PhoneUtils.dumpCallState(mPhone);
+ PhoneUtils.dumpCallManager();
+ return true;
+ }
+ break;
+ case KeyEvent.KEYCODE_GRAVE:
+ if (VDBG) {
+ // Placeholder for other misc temp testing
+ log("------------ Temp testing -----------------");
+ return true;
+ }
+ break;
+ }
+
+ if (event.getRepeatCount() == 0 && handleDialerKeyDown(keyCode, event)) {
+ return true;
+ }
+
+ return super.onKeyDown(keyCode, event);
+ }
+
+ /**
+ * Handle a failure notification for a supplementary service
+ * (i.e. conference, switch, separate, transfer, etc.).
+ */
+ void onSuppServiceFailed(AsyncResult r) {
+ Phone.SuppService service = (Phone.SuppService) r.result;
+ if (DBG) log("onSuppServiceFailed: " + service);
+
+ int errorMessageResId;
+ switch (service) {
+ case SWITCH:
+ // Attempt to switch foreground and background/incoming calls failed
+ // ("Failed to switch calls")
+ errorMessageResId = R.string.incall_error_supp_service_switch;
+ break;
+
+ case SEPARATE:
+ // Attempt to separate a call from a conference call
+ // failed ("Failed to separate out call")
+ errorMessageResId = R.string.incall_error_supp_service_separate;
+ break;
+
+ case TRANSFER:
+ // Attempt to connect foreground and background calls to
+ // each other (and hanging up user's line) failed ("Call
+ // transfer failed")
+ errorMessageResId = R.string.incall_error_supp_service_transfer;
+ break;
+
+ case CONFERENCE:
+ // Attempt to add a call to conference call failed
+ // ("Conference call failed")
+ errorMessageResId = R.string.incall_error_supp_service_conference;
+ break;
+
+ case REJECT:
+ // Attempt to reject an incoming call failed
+ // ("Call rejection failed")
+ errorMessageResId = R.string.incall_error_supp_service_reject;
+ break;
+
+ case HANGUP:
+ // Attempt to release a call failed ("Failed to release call(s)")
+ errorMessageResId = R.string.incall_error_supp_service_hangup;
+ break;
+
+ case UNKNOWN:
+ default:
+ // Attempt to use a service we don't recognize or support
+ // ("Unsupported service" or "Selected service failed")
+ errorMessageResId = R.string.incall_error_supp_service_unknown;
+ break;
+ }
+
+ // mSuppServiceFailureDialog is a generic dialog used for any
+ // supp service failure, and there's only ever have one
+ // instance at a time. So just in case a previous dialog is
+ // still around, dismiss it.
+ if (mSuppServiceFailureDialog != null) {
+ if (DBG) log("- DISMISSING mSuppServiceFailureDialog.");
+ mSuppServiceFailureDialog.dismiss(); // It's safe to dismiss() a dialog
+ // that's already dismissed.
+ mSuppServiceFailureDialog = null;
+ }
+
+ mSuppServiceFailureDialog = new AlertDialog.Builder(this)
+ .setMessage(errorMessageResId)
+ .setPositiveButton(R.string.ok, null)
+ .create();
+ mSuppServiceFailureDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+ mSuppServiceFailureDialog.show();
+ }
+
+ /**
+ * Something has changed in the phone's state. Update the UI.
+ */
+ private void onPhoneStateChanged(AsyncResult r) {
+ PhoneConstants.State state = mCM.getState();
+ if (DBG) log("onPhoneStateChanged: current state = " + state);
+
+ // There's nothing to do here if we're not the foreground activity.
+ // (When we *do* eventually come to the foreground, we'll do a
+ // full update then.)
+ if (!mIsForegroundActivity) {
+ if (DBG) log("onPhoneStateChanged: Activity not in foreground! Bailing out...");
+ return;
+ }
+
+ updateExpandedViewState();
+
+ // Update the onscreen UI.
+ // We use requestUpdateScreen() here (which posts a handler message)
+ // instead of calling updateScreen() directly, which allows us to avoid
+ // unnecessary work if multiple onPhoneStateChanged() events come in all
+ // at the same time.
+
+ requestUpdateScreen();
+
+ // Make sure we update the poke lock and wake lock when certain
+ // phone state changes occur.
+ mApp.updateWakeState();
+ }
+
+ /**
+ * Updates the UI after a phone connection is disconnected, as follows:
+ *
+ * - If this was a missed or rejected incoming call, and no other
+ * calls are active, dismiss the in-call UI immediately. (The
+ * CallNotifier will still create a "missed call" notification if
+ * necessary.)
+ *
+ * - With any other disconnect cause, if the phone is now totally
+ * idle, display the "Call ended" state for a couple of seconds.
+ *
+ * - Or, if the phone is still in use, stay on the in-call screen
+ * (and update the UI to reflect the current state of the Phone.)
+ *
+ * @param r r.result contains the connection that just ended
+ */
+ private void onDisconnect(AsyncResult r) {
+ Connection c = (Connection) r.result;
+ Connection.DisconnectCause cause = c.getDisconnectCause();
+ if (DBG) log("onDisconnect: connection '" + c + "', cause = " + cause
+ + ", showing screen: " + mApp.isShowingCallScreen());
+
+ boolean currentlyIdle = !phoneIsInUse();
+ int autoretrySetting = AUTO_RETRY_OFF;
+ boolean phoneIsCdma = (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA);
+ if (phoneIsCdma) {
+ // Get the Auto-retry setting only if Phone State is IDLE,
+ // else let it stay as AUTO_RETRY_OFF
+ if (currentlyIdle) {
+ autoretrySetting = android.provider.Settings.Global.getInt(mPhone.getContext().
+ getContentResolver(), android.provider.Settings.Global.CALL_AUTO_RETRY, 0);
+ }
+ }
+
+ // for OTA Call, only if in OTA NORMAL mode, handle OTA END scenario
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL)
+ && ((mApp.cdmaOtaProvisionData != null)
+ && (!mApp.cdmaOtaProvisionData.inOtaSpcState))) {
+ setInCallScreenMode(InCallScreenMode.OTA_ENDED);
+ updateScreen();
+ return;
+ } else if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)
+ || ((mApp.cdmaOtaProvisionData != null)
+ && mApp.cdmaOtaProvisionData.inOtaSpcState)) {
+ if (DBG) log("onDisconnect: OTA Call end already handled");
+ return;
+ }
+
+ // Any time a call disconnects, clear out the "history" of DTMF
+ // digits you typed (to make sure it doesn't persist from one call
+ // to the next.)
+ mDialer.clearDigits();
+
+ // Under certain call disconnected states, we want to alert the user
+ // with a dialog instead of going through the normal disconnect
+ // routine.
+ if (cause == Connection.DisconnectCause.CALL_BARRED) {
+ showGenericErrorDialog(R.string.callFailed_cb_enabled, false);
+ return;
+ } else if (cause == Connection.DisconnectCause.FDN_BLOCKED) {
+ showGenericErrorDialog(R.string.callFailed_fdn_only, false);
+ return;
+ } else if (cause == Connection.DisconnectCause.CS_RESTRICTED) {
+ showGenericErrorDialog(R.string.callFailed_dsac_restricted, false);
+ return;
+ } else if (cause == Connection.DisconnectCause.CS_RESTRICTED_EMERGENCY) {
+ showGenericErrorDialog(R.string.callFailed_dsac_restricted_emergency, false);
+ return;
+ } else if (cause == Connection.DisconnectCause.CS_RESTRICTED_NORMAL) {
+ showGenericErrorDialog(R.string.callFailed_dsac_restricted_normal, false);
+ return;
+ }
+
+ if (phoneIsCdma) {
+ Call.State callState = mApp.notifier.getPreviousCdmaCallState();
+ if ((callState == Call.State.ACTIVE)
+ && (cause != Connection.DisconnectCause.INCOMING_MISSED)
+ && (cause != Connection.DisconnectCause.NORMAL)
+ && (cause != Connection.DisconnectCause.LOCAL)
+ && (cause != Connection.DisconnectCause.INCOMING_REJECTED)) {
+ showCallLostDialog();
+ } else if ((callState == Call.State.DIALING || callState == Call.State.ALERTING)
+ && (cause != Connection.DisconnectCause.INCOMING_MISSED)
+ && (cause != Connection.DisconnectCause.NORMAL)
+ && (cause != Connection.DisconnectCause.LOCAL)
+ && (cause != Connection.DisconnectCause.INCOMING_REJECTED)) {
+
+ if (mApp.inCallUiState.needToShowCallLostDialog) {
+ // Show the dialog now since the call that just failed was a retry.
+ showCallLostDialog();
+ mApp.inCallUiState.needToShowCallLostDialog = false;
+ } else {
+ if (autoretrySetting == AUTO_RETRY_OFF) {
+ // Show the dialog for failed call if Auto Retry is OFF in Settings.
+ showCallLostDialog();
+ mApp.inCallUiState.needToShowCallLostDialog = false;
+ } else {
+ // Set the needToShowCallLostDialog flag now, so we'll know to show
+ // the dialog if *this* call fails.
+ mApp.inCallUiState.needToShowCallLostDialog = true;
+ }
+ }
+ }
+ }
+
+ // Explicitly clean up up any DISCONNECTED connections
+ // in a conference call.
+ // [Background: Even after a connection gets disconnected, its
+ // Connection object still stays around for a few seconds, in the
+ // DISCONNECTED state. With regular calls, this state drives the
+ // "call ended" UI. But when a single person disconnects from a
+ // conference call there's no "call ended" state at all; in that
+ // case we blow away any DISCONNECTED connections right now to make sure
+ // the UI updates instantly to reflect the current state.]
+ final Call call = c.getCall();
+ if (call != null) {
+ // We only care about situation of a single caller
+ // disconnecting from a conference call. In that case, the
+ // call will have more than one Connection (including the one
+ // that just disconnected, which will be in the DISCONNECTED
+ // state) *and* at least one ACTIVE connection. (If the Call
+ // has *no* ACTIVE connections, that means that the entire
+ // conference call just ended, so we *do* want to show the
+ // "Call ended" state.)
+ List<Connection> connections = call.getConnections();
+ if (connections != null && connections.size() > 1) {
+ for (Connection conn : connections) {
+ if (conn.getState() == Call.State.ACTIVE) {
+ // This call still has at least one ACTIVE connection!
+ // So blow away any DISCONNECTED connections
+ // (including, presumably, the one that just
+ // disconnected from this conference call.)
+
+ // We also force the wake state to refresh, just in
+ // case the disconnected connections are removed
+ // before the phone state change.
+ if (VDBG) log("- Still-active conf call; clearing DISCONNECTED...");
+ mApp.updateWakeState();
+ mCM.clearDisconnected(); // This happens synchronously.
+ break;
+ }
+ }
+ }
+ }
+
+ // Note: see CallNotifier.onDisconnect() for some other behavior
+ // that might be triggered by a disconnect event, like playing the
+ // busy/congestion tone.
+
+ // Stash away some info about the call that just disconnected.
+ // (This might affect what happens after we exit the InCallScreen; see
+ // delayedCleanupAfterDisconnect().)
+ // TODO: rather than stashing this away now and then reading it in
+ // delayedCleanupAfterDisconnect(), it would be cleaner to just pass
+ // this as an argument to delayedCleanupAfterDisconnect() (if we call
+ // it directly) or else pass it as a Message argument when we post the
+ // DELAYED_CLEANUP_AFTER_DISCONNECT message.
+ mLastDisconnectCause = cause;
+
+ // We bail out immediately (and *don't* display the "call ended"
+ // state at all) if this was an incoming call.
+ boolean bailOutImmediately =
+ ((cause == Connection.DisconnectCause.INCOMING_MISSED)
+ || (cause == Connection.DisconnectCause.INCOMING_REJECTED))
+ && currentlyIdle;
+
+ boolean showingQuickResponseDialog =
+ mRespondViaSmsManager != null && mRespondViaSmsManager.isShowingPopup();
+
+ // Note: we also do some special handling for the case when a call
+ // disconnects with cause==OUT_OF_SERVICE while making an
+ // emergency call from airplane mode. That's handled by
+ // EmergencyCallHelper.onDisconnect().
+
+ if (bailOutImmediately && showingQuickResponseDialog) {
+ if (DBG) log("- onDisconnect: Respond-via-SMS dialog is still being displayed...");
+
+ // Do *not* exit the in-call UI yet!
+ // If the call was an incoming call that was missed *and* the user is using
+ // quick response screen, we keep showing the screen for a moment, assuming the
+ // user wants to reply the call anyway.
+ //
+ // For this case, we will exit the screen when:
+ // - the message is sent (RespondViaSmsManager)
+ // - the message is canceled (RespondViaSmsManager), or
+ // - when the whole in-call UI becomes background (onPause())
+ } else if (bailOutImmediately) {
+ if (DBG) log("- onDisconnect: bailOutImmediately...");
+
+ // Exit the in-call UI!
+ // (This is basically the same "delayed cleanup" we do below,
+ // just with zero delay. Since the Phone is currently idle,
+ // this call is guaranteed to immediately finish this activity.)
+ delayedCleanupAfterDisconnect();
+ } else {
+ if (DBG) log("- onDisconnect: delayed bailout...");
+ // Stay on the in-call screen for now. (Either the phone is
+ // still in use, or the phone is idle but we want to display
+ // the "call ended" state for a couple of seconds.)
+
+ // Switch to the special "Call ended" state when the phone is idle
+ // but there's still a call in the DISCONNECTED state:
+ if (currentlyIdle
+ && (mCM.hasDisconnectedFgCall() || mCM.hasDisconnectedBgCall())) {
+ if (DBG) log("- onDisconnect: switching to 'Call ended' state...");
+ setInCallScreenMode(InCallScreenMode.CALL_ENDED);
+ }
+
+ // Force a UI update in case we need to display anything
+ // special based on this connection's DisconnectCause
+ // (see CallCard.getCallFailedString()).
+ updateScreen();
+
+ // Some other misc cleanup that we do if the call that just
+ // disconnected was the foreground call.
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ if (!hasActiveCall) {
+ if (DBG) log("- onDisconnect: cleaning up after FG call disconnect...");
+
+ // Dismiss any dialogs which are only meaningful for an
+ // active call *and* which become moot if the call ends.
+ if (mWaitPromptDialog != null) {
+ if (VDBG) log("- DISMISSING mWaitPromptDialog.");
+ mWaitPromptDialog.dismiss(); // safe even if already dismissed
+ mWaitPromptDialog = null;
+ }
+ if (mWildPromptDialog != null) {
+ if (VDBG) log("- DISMISSING mWildPromptDialog.");
+ mWildPromptDialog.dismiss(); // safe even if already dismissed
+ mWildPromptDialog = null;
+ }
+ if (mPausePromptDialog != null) {
+ if (DBG) log("- DISMISSING mPausePromptDialog.");
+ mPausePromptDialog.dismiss(); // safe even if already dismissed
+ mPausePromptDialog = null;
+ }
+ }
+
+ // Updating the screen wake state is done in onPhoneStateChanged().
+
+
+ // CDMA: We only clean up if the Phone state is IDLE as we might receive an
+ // onDisconnect for a Call Collision case (rare but possible).
+ // For Call collision cases i.e. when the user makes an out going call
+ // and at the same time receives an Incoming Call, the Incoming Call is given
+ // higher preference. At this time framework sends a disconnect for the Out going
+ // call connection hence we should *not* bring down the InCallScreen as the Phone
+ // State would be RINGING
+ if (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ if (!currentlyIdle) {
+ // Clean up any connections in the DISCONNECTED state.
+ // This is necessary cause in CallCollision the foreground call might have
+ // connections in DISCONNECTED state which needs to be cleared.
+ mCM.clearDisconnected();
+
+ // The phone is still in use. Stay here in this activity.
+ // But we don't need to keep the screen on.
+ if (DBG) log("onDisconnect: Call Collision case - staying on InCallScreen.");
+ if (DBG) PhoneUtils.dumpCallState(mPhone);
+ return;
+ }
+ }
+
+ // This is onDisconnect() request from the last phone call; no available call anymore.
+ //
+ // When the in-call UI is in background *because* the screen is turned off (unlike the
+ // other case where the other activity is being shown), we wake up the screen and
+ // show "DISCONNECTED" state once, with appropriate elapsed time. After showing that
+ // we *must* bail out of the screen again, showing screen lock if needed.
+ //
+ // See also comments for isForegroundActivityForProximity()
+ //
+ // TODO: Consider moving this to CallNotifier. This code assumes the InCallScreen
+ // never gets destroyed. For this exact case, it works (since InCallScreen won't be
+ // destroyed), while technically this isn't right; Activity may be destroyed when
+ // in background.
+ if (currentlyIdle && !isForegroundActivity() && isForegroundActivityForProximity()) {
+ log("Force waking up the screen to let users see \"disconnected\" state");
+ if (call != null) {
+ mCallCard.updateElapsedTimeWidget(call);
+ }
+ // This variable will be kept true until the next InCallScreen#onPause(), which
+ // forcibly turns it off regardless of the situation (for avoiding unnecessary
+ // confusion around this special case).
+ mApp.inCallUiState.showAlreadyDisconnectedState = true;
+
+ // Finally request wake-up..
+ mApp.wakeUpScreen();
+
+ // InCallScreen#onResume() will set DELAYED_CLEANUP_AFTER_DISCONNECT message,
+ // so skip the following section.
+ return;
+ }
+
+ // Finally, arrange for delayedCleanupAfterDisconnect() to get
+ // called after a short interval (during which we display the
+ // "call ended" state.) At that point, if the
+ // Phone is idle, we'll finish out of this activity.
+ final int callEndedDisplayDelay;
+ switch (cause) {
+ // When the local user hanged up the ongoing call, it is ok to dismiss the screen
+ // soon. In other cases, we show the "hung up" screen longer.
+ //
+ // - For expected reasons we will use CALL_ENDED_LONG_DELAY.
+ // -- when the peer hanged up the call
+ // -- when the local user rejects the incoming call during the other ongoing call
+ // (TODO: there may be other cases which should be in this category)
+ //
+ // - For other unexpected reasons, we will use CALL_ENDED_EXTRA_LONG_DELAY,
+ // assuming the local user wants to confirm the disconnect reason.
+ case LOCAL:
+ callEndedDisplayDelay = CALL_ENDED_SHORT_DELAY;
+ break;
+ case NORMAL:
+ case INCOMING_REJECTED:
+ callEndedDisplayDelay = CALL_ENDED_LONG_DELAY;
+ break;
+ default:
+ callEndedDisplayDelay = CALL_ENDED_EXTRA_LONG_DELAY;
+ break;
+ }
+ mHandler.removeMessages(DELAYED_CLEANUP_AFTER_DISCONNECT);
+ mHandler.sendEmptyMessageDelayed(DELAYED_CLEANUP_AFTER_DISCONNECT,
+ callEndedDisplayDelay);
+ }
+
+ // Remove 3way timer (only meaningful for CDMA)
+ // TODO: this call needs to happen in the CallController, not here.
+ // (It should probably be triggered by the CallNotifier's onDisconnect method.)
+ // mHandler.removeMessages(THREEWAY_CALLERINFO_DISPLAY_DONE);
+ }
+
+ /**
+ * Brings up the "MMI Started" dialog.
+ */
+ /* TODO: sort out MMI code (probably we should remove this method entirely). See also
+ MMI handling code in onResume()
+ private void onMMIInitiate(AsyncResult r) {
+ if (VDBG) log("onMMIInitiate()... AsyncResult r = " + r);
+
+ // Watch out: don't do this if we're not the foreground activity,
+ // mainly since in the Dialog.show() might fail if we don't have a
+ // valid window token any more...
+ // (Note that this exact sequence can happen if you try to start
+ // an MMI code while the radio is off or out of service.)
+ if (!mIsForegroundActivity) {
+ if (VDBG) log("Activity not in foreground! Bailing out...");
+ return;
+ }
+
+ // Also, if any other dialog is up right now (presumably the
+ // generic error dialog displaying the "Starting MMI..." message)
+ // take it down before bringing up the real "MMI Started" dialog
+ // in its place.
+ dismissAllDialogs();
+
+ MmiCode mmiCode = (MmiCode) r.result;
+ if (VDBG) log(" - MmiCode: " + mmiCode);
+
+ Message message = Message.obtain(mHandler, PhoneApp.MMI_CANCEL);
+ mMmiStartedDialog = PhoneUtils.displayMMIInitiate(this, mmiCode,
+ message, mMmiStartedDialog);
+ }*/
+
+ /**
+ * Handles an MMI_CANCEL event, which is triggered by the button
+ * (labeled either "OK" or "Cancel") on the "MMI Started" dialog.
+ * @see PhoneUtils#cancelMmiCode(Phone)
+ */
+ private void onMMICancel() {
+ if (VDBG) log("onMMICancel()...");
+
+ // First of all, cancel the outstanding MMI code (if possible.)
+ PhoneUtils.cancelMmiCode(mPhone);
+
+ // Regardless of whether the current MMI code was cancelable, the
+ // PhoneApp will get an MMI_COMPLETE event very soon, which will
+ // take us to the MMI Complete dialog (see
+ // PhoneUtils.displayMMIComplete().)
+ //
+ // But until that event comes in, we *don't* want to stay here on
+ // the in-call screen, since we'll be visible in a
+ // partially-constructed state as soon as the "MMI Started" dialog
+ // gets dismissed. So let's forcibly bail out right now.
+ if (DBG) log("onMMICancel: finishing InCallScreen...");
+ dismissAllDialogs();
+ endInCallScreenSession();
+ }
+
+ /**
+ * Handles an MMI_COMPLETE event, which is triggered by telephony,
+ * implying MMI
+ */
+ private void onMMIComplete(MmiCode mmiCode) {
+ // Check the code to see if the request is ready to
+ // finish, this includes any MMI state that is not
+ // PENDING.
+
+ // if phone is a CDMA phone display feature code completed message
+ int phoneType = mPhone.getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ PhoneUtils.displayMMIComplete(mPhone, mApp, mmiCode, null, null);
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+ if (mmiCode.getState() != MmiCode.State.PENDING) {
+ if (DBG) log("Got MMI_COMPLETE, finishing InCallScreen...");
+ dismissAllDialogs();
+ endInCallScreenSession();
+ }
+ }
+ }
+
+ /**
+ * Handles the POST_ON_DIAL_CHARS message from the Phone
+ * (see our call to mPhone.setOnPostDialCharacter() above.)
+ *
+ * TODO: NEED TO TEST THIS SEQUENCE now that we no longer handle
+ * "dialable" key events here in the InCallScreen: we do directly to the
+ * Dialer UI instead. Similarly, we may now need to go directly to the
+ * Dialer to handle POST_ON_DIAL_CHARS too.
+ */
+ private void handlePostOnDialChars(AsyncResult r, char ch) {
+ Connection c = (Connection) r.result;
+
+ if (c != null) {
+ Connection.PostDialState state =
+ (Connection.PostDialState) r.userObj;
+
+ if (VDBG) log("handlePostOnDialChar: state = " +
+ state + ", ch = " + ch);
+
+ switch (state) {
+ case STARTED:
+ mDialer.stopLocalToneIfNeeded();
+ if (mPauseInProgress) {
+ /**
+ * Note that on some devices, this will never happen,
+ * because we will not ever enter the PAUSE state.
+ */
+ showPausePromptDialog(c, mPostDialStrAfterPause);
+ }
+ mPauseInProgress = false;
+ mDialer.startLocalToneIfNeeded(ch);
+
+ // TODO: is this needed, now that you can't actually
+ // type DTMF chars or dial directly from here?
+ // If so, we'd need to yank you out of the in-call screen
+ // here too (and take you to the 12-key dialer in "in-call" mode.)
+ // displayPostDialedChar(ch);
+ break;
+
+ case WAIT:
+ // wait shows a prompt.
+ if (DBG) log("handlePostOnDialChars: show WAIT prompt...");
+ mDialer.stopLocalToneIfNeeded();
+ String postDialStr = c.getRemainingPostDialString();
+ showWaitPromptDialog(c, postDialStr);
+ break;
+
+ case WILD:
+ if (DBG) log("handlePostOnDialChars: show WILD prompt");
+ mDialer.stopLocalToneIfNeeded();
+ showWildPromptDialog(c);
+ break;
+
+ case COMPLETE:
+ mDialer.stopLocalToneIfNeeded();
+ break;
+
+ case PAUSE:
+ // pauses for a brief period of time then continue dialing.
+ mDialer.stopLocalToneIfNeeded();
+ mPostDialStrAfterPause = c.getRemainingPostDialString();
+ mPauseInProgress = true;
+ break;
+
+ default:
+ break;
+ }
+ }
+ }
+
+ /**
+ * Pop up an alert dialog with OK and Cancel buttons to allow user to
+ * Accept or Reject the WAIT inserted as part of the Dial string.
+ */
+ private void showWaitPromptDialog(final Connection c, String postDialStr) {
+ if (DBG) log("showWaitPromptDialogChoice: '" + postDialStr + "'...");
+
+ Resources r = getResources();
+ StringBuilder buf = new StringBuilder();
+ buf.append(r.getText(R.string.wait_prompt_str));
+ buf.append(postDialStr);
+
+ // if (DBG) log("- mWaitPromptDialog = " + mWaitPromptDialog);
+ if (mWaitPromptDialog != null) {
+ if (DBG) log("- DISMISSING mWaitPromptDialog.");
+ mWaitPromptDialog.dismiss(); // safe even if already dismissed
+ mWaitPromptDialog = null;
+ }
+
+ mWaitPromptDialog = new AlertDialog.Builder(this)
+ .setMessage(buf.toString())
+ .setPositiveButton(R.string.pause_prompt_yes,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (DBG) log("handle WAIT_PROMPT_CONFIRMED, proceed...");
+ c.proceedAfterWaitChar();
+ }
+ })
+ .setNegativeButton(R.string.pause_prompt_no,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (DBG) log("handle POST_DIAL_CANCELED!");
+ c.cancelPostDial();
+ }
+ })
+ .create();
+ mWaitPromptDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+
+ mWaitPromptDialog.show();
+ }
+
+ /**
+ * Pop up an alert dialog which waits for 2 seconds for each P (Pause) Character entered
+ * as part of the Dial String.
+ */
+ private void showPausePromptDialog(final Connection c, String postDialStrAfterPause) {
+ Resources r = getResources();
+ StringBuilder buf = new StringBuilder();
+ buf.append(r.getText(R.string.pause_prompt_str));
+ buf.append(postDialStrAfterPause);
+
+ if (mPausePromptDialog != null) {
+ if (DBG) log("- DISMISSING mPausePromptDialog.");
+ mPausePromptDialog.dismiss(); // safe even if already dismissed
+ mPausePromptDialog = null;
+ }
+
+ mPausePromptDialog = new AlertDialog.Builder(this)
+ .setMessage(buf.toString())
+ .create();
+ mPausePromptDialog.show();
+ // 2 second timer
+ Message msg = Message.obtain(mHandler, EVENT_PAUSE_DIALOG_COMPLETE);
+ mHandler.sendMessageDelayed(msg, PAUSE_PROMPT_DIALOG_TIMEOUT);
+ }
+
+ private View createWildPromptView() {
+ LinearLayout result = new LinearLayout(this);
+ result.setOrientation(LinearLayout.VERTICAL);
+ result.setPadding(5, 5, 5, 5);
+
+ LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ TextView promptMsg = new TextView(this);
+ promptMsg.setTextSize(14);
+ promptMsg.setTypeface(Typeface.DEFAULT_BOLD);
+ promptMsg.setText(getResources().getText(R.string.wild_prompt_str));
+
+ result.addView(promptMsg, lp);
+
+ mWildPromptText = new EditText(this);
+ mWildPromptText.setKeyListener(DialerKeyListener.getInstance());
+ mWildPromptText.setMovementMethod(null);
+ mWildPromptText.setTextSize(14);
+ mWildPromptText.setMaxLines(1);
+ mWildPromptText.setHorizontallyScrolling(true);
+ mWildPromptText.setBackgroundResource(android.R.drawable.editbox_background);
+
+ LinearLayout.LayoutParams lp2 = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ lp2.setMargins(0, 3, 0, 0);
+
+ result.addView(mWildPromptText, lp2);
+
+ return result;
+ }
+
+ private void showWildPromptDialog(final Connection c) {
+ View v = createWildPromptView();
+
+ if (mWildPromptDialog != null) {
+ if (VDBG) log("- DISMISSING mWildPromptDialog.");
+ mWildPromptDialog.dismiss(); // safe even if already dismissed
+ mWildPromptDialog = null;
+ }
+
+ mWildPromptDialog = new AlertDialog.Builder(this)
+ .setView(v)
+ .setPositiveButton(
+ R.string.send_button,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int whichButton) {
+ if (VDBG) log("handle WILD_PROMPT_CHAR_ENTERED, proceed...");
+ String replacement = null;
+ if (mWildPromptText != null) {
+ replacement = mWildPromptText.getText().toString();
+ mWildPromptText = null;
+ }
+ c.proceedAfterWildChar(replacement);
+ mApp.pokeUserActivity();
+ }
+ })
+ .setOnCancelListener(
+ new DialogInterface.OnCancelListener() {
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (VDBG) log("handle POST_DIAL_CANCELED!");
+ c.cancelPostDial();
+ mApp.pokeUserActivity();
+ }
+ })
+ .create();
+ mWildPromptDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+ mWildPromptDialog.show();
+
+ mWildPromptText.requestFocus();
+ }
+
+ /**
+ * Updates the state of the in-call UI based on the current state of
+ * the Phone. This call has no effect if we're not currently the
+ * foreground activity.
+ *
+ * This method is only allowed to be called from the UI thread (since it
+ * manipulates our View hierarchy). If you need to update the screen from
+ * some other thread, or if you just want to "post a request" for the screen
+ * to be updated (rather than doing it synchronously), call
+ * requestUpdateScreen() instead.
+ *
+ * Right now this method will update UI visibility immediately, with no animation.
+ * TODO: have animate flag here and use it anywhere possible.
+ */
+ private void updateScreen() {
+ if (DBG) log("updateScreen()...");
+ final InCallScreenMode inCallScreenMode = mApp.inCallUiState.inCallScreenMode;
+ if (VDBG) {
+ PhoneConstants.State state = mCM.getState();
+ log(" - phone state = " + state);
+ log(" - inCallScreenMode = " + inCallScreenMode);
+ }
+
+ // Don't update anything if we're not in the foreground (there's
+ // no point updating our UI widgets since we're not visible!)
+ // Also note this check also ensures we won't update while we're
+ // in the middle of pausing, which could cause a visible glitch in
+ // the "activity ending" transition.
+ if (!mIsForegroundActivity) {
+ if (DBG) log("- updateScreen: not the foreground Activity! Bailing out...");
+ return;
+ }
+
+ if (inCallScreenMode == InCallScreenMode.OTA_NORMAL) {
+ if (DBG) log("- updateScreen: OTA call state NORMAL (NOT updating in-call UI)...");
+ mCallCard.setVisibility(View.GONE);
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.otaShowProperScreen();
+ } else {
+ Log.w(LOG_TAG, "OtaUtils object is null, not showing any screen for that.");
+ }
+ return; // Return without updating in-call UI.
+ } else if (inCallScreenMode == InCallScreenMode.OTA_ENDED) {
+ if (DBG) log("- updateScreen: OTA call ended state (NOT updating in-call UI)...");
+ mCallCard.setVisibility(View.GONE);
+ // Wake up the screen when we get notification, good or bad.
+ mApp.wakeUpScreen();
+ if (mApp.cdmaOtaScreenState.otaScreenState
+ == CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION) {
+ if (DBG) log("- updateScreen: OTA_STATUS_ACTIVATION");
+ if (mApp.otaUtils != null) {
+ if (DBG) log("- updateScreen: mApp.otaUtils is not null, "
+ + "call otaShowActivationScreen");
+ mApp.otaUtils.otaShowActivateScreen();
+ }
+ } else {
+ if (DBG) log("- updateScreen: OTA Call end state for Dialogs");
+ if (mApp.otaUtils != null) {
+ if (DBG) log("- updateScreen: Show OTA Success Failure dialog");
+ mApp.otaUtils.otaShowSuccessFailure();
+ }
+ }
+ return; // Return without updating in-call UI.
+ } else if (inCallScreenMode == InCallScreenMode.MANAGE_CONFERENCE) {
+ if (DBG) log("- updateScreen: manage conference mode (NOT updating in-call UI)...");
+ mCallCard.setVisibility(View.GONE);
+ updateManageConferencePanelIfNecessary();
+ return; // Return without updating in-call UI.
+ } else if (inCallScreenMode == InCallScreenMode.CALL_ENDED) {
+ if (DBG) log("- updateScreen: call ended state...");
+ // Continue with the rest of updateScreen() as usual, since we do
+ // need to update the background (to the special "call ended" color)
+ // and the CallCard (to show the "Call ended" label.)
+ }
+
+ if (DBG) log("- updateScreen: updating the in-call UI...");
+ // Note we update the InCallTouchUi widget before the CallCard,
+ // since the CallCard adjusts its size based on how much vertical
+ // space the InCallTouchUi widget needs.
+ updateInCallTouchUi();
+ mCallCard.updateState(mCM);
+
+ // If an incoming call is ringing, make sure the dialpad is
+ // closed. (We do this to make sure we're not covering up the
+ // "incoming call" UI.)
+ if (mCM.getState() == PhoneConstants.State.RINGING) {
+ if (mDialer.isOpened()) {
+ Log.i(LOG_TAG, "During RINGING state we force hiding dialpad.");
+ closeDialpadInternal(false); // don't do the "closing" animation
+ }
+
+ // At this point, we are guranteed that the dialer is closed.
+ // This means that it is safe to clear out the "history" of DTMF digits
+ // you may have typed into the previous call (so you don't see the
+ // previous call's digits if you answer this call and then bring up the
+ // dialpad.)
+ //
+ // TODO: it would be more precise to do this when you *answer* the
+ // incoming call, rather than as soon as it starts ringing, but
+ // the InCallScreen doesn't keep enough state right now to notice
+ // that specific transition in onPhoneStateChanged().
+ // TODO: This clears out the dialpad context as well so when a second
+ // call comes in while a voicemail call is happening, the voicemail
+ // dialpad will no longer have the "Voice Mail" context. It's a small
+ // case so not terribly bad, but we need to maintain a better
+ // call-to-callstate mapping before we can fix this.
+ mDialer.clearDigits();
+ }
+
+
+ // Now that we're sure DTMF dialpad is in an appropriate state, reflect
+ // the dialpad state into CallCard
+ updateCallCardVisibilityPerDialerState(false);
+
+ updateProgressIndication();
+
+ // Forcibly take down all dialog if an incoming call is ringing.
+ if (mCM.hasActiveRingingCall()) {
+ dismissAllDialogs();
+ } else {
+ // Wait prompt dialog is not currently up. But it *should* be
+ // up if the FG call has a connection in the WAIT state and
+ // the phone isn't ringing.
+ String postDialStr = null;
+ List<Connection> fgConnections = mCM.getFgCallConnections();
+ int phoneType = mCM.getFgPhone().getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ Connection fgLatestConnection = mCM.getFgCallLatestConnection();
+ if (mApp.cdmaPhoneCallState.getCurrentCallState() ==
+ CdmaPhoneCallState.PhoneCallState.CONF_CALL) {
+ for (Connection cn : fgConnections) {
+ if ((cn != null) && (cn.getPostDialState() ==
+ Connection.PostDialState.WAIT)) {
+ cn.cancelPostDial();
+ }
+ }
+ } else if ((fgLatestConnection != null)
+ && (fgLatestConnection.getPostDialState() == Connection.PostDialState.WAIT)) {
+ if(DBG) log("show the Wait dialog for CDMA");
+ postDialStr = fgLatestConnection.getRemainingPostDialString();
+ showWaitPromptDialog(fgLatestConnection, postDialStr);
+ }
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ for (Connection cn : fgConnections) {
+ if ((cn != null) && (cn.getPostDialState() == Connection.PostDialState.WAIT)) {
+ postDialStr = cn.getRemainingPostDialString();
+ showWaitPromptDialog(cn, postDialStr);
+ }
+ }
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ }
+ }
+
+ /**
+ * (Re)synchronizes the onscreen UI with the current state of the
+ * telephony framework.
+ *
+ * @return SyncWithPhoneStateStatus.SUCCESS if we successfully updated the UI, or
+ * SyncWithPhoneStateStatus.PHONE_NOT_IN_USE if there was no phone state to sync
+ * with (ie. the phone was completely idle). In the latter case, we
+ * shouldn't even be in the in-call UI in the first place, and it's
+ * the caller's responsibility to bail out of this activity by
+ * calling endInCallScreenSession if appropriate.
+ *
+ * This method directly calls updateScreen() in the normal "phone is
+ * in use" case, so there's no need for the caller to do so.
+ */
+ private SyncWithPhoneStateStatus syncWithPhoneState() {
+ boolean updateSuccessful = false;
+ if (DBG) log("syncWithPhoneState()...");
+ if (DBG) PhoneUtils.dumpCallState(mPhone);
+ if (VDBG) dumpBluetoothState();
+
+ // Make sure the Phone is "in use". (If not, we shouldn't be on
+ // this screen in the first place.)
+
+ // An active or just-ended OTA call counts as "in use".
+ if (TelephonyCapabilities.supportsOtasp(mCM.getFgPhone())
+ && ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL)
+ || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED))) {
+ // Even when OTA Call ends, need to show OTA End UI,
+ // so return Success to allow UI update.
+ return SyncWithPhoneStateStatus.SUCCESS;
+ }
+
+ // If an MMI code is running that also counts as "in use".
+ //
+ // TODO: We currently only call getPendingMmiCodes() for GSM
+ // phones. (The code's been that way all along.) But CDMAPhone
+ // does in fact implement getPendingMmiCodes(), so should we
+ // check that here regardless of the phone type?
+ boolean hasPendingMmiCodes =
+ (mPhone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM)
+ && !mPhone.getPendingMmiCodes().isEmpty();
+
+ // Finally, it's also OK to stay here on the InCallScreen if we
+ // need to display a progress indicator while something's
+ // happening in the background.
+ boolean showProgressIndication = mApp.inCallUiState.isProgressIndicationActive();
+
+ boolean showScreenEvenAfterDisconnect = mApp.inCallUiState.showAlreadyDisconnectedState;
+
+ if (mCM.hasActiveFgCall() || mCM.hasActiveBgCall() || mCM.hasActiveRingingCall()
+ || hasPendingMmiCodes || showProgressIndication || showScreenEvenAfterDisconnect) {
+ if (VDBG) log("syncWithPhoneState: it's ok to be here; update the screen...");
+ updateScreen();
+ return SyncWithPhoneStateStatus.SUCCESS;
+ }
+
+ Log.i(LOG_TAG, "syncWithPhoneState: phone is idle (shouldn't be here)");
+ return SyncWithPhoneStateStatus.PHONE_NOT_IN_USE;
+ }
+
+
+
+ private void handleMissingVoiceMailNumber() {
+ if (DBG) log("handleMissingVoiceMailNumber");
+
+ final Message msg = Message.obtain(mHandler);
+ msg.what = DONT_ADD_VOICEMAIL_NUMBER;
+
+ final Message msg2 = Message.obtain(mHandler);
+ msg2.what = ADD_VOICEMAIL_NUMBER;
+
+ mMissingVoicemailDialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.no_vm_number)
+ .setMessage(R.string.no_vm_number_msg)
+ .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (VDBG) log("Missing voicemail AlertDialog: POSITIVE click...");
+ msg.sendToTarget(); // see dontAddVoiceMailNumber()
+ mApp.pokeUserActivity();
+ }})
+ .setNegativeButton(R.string.add_vm_number_str,
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ if (VDBG) log("Missing voicemail AlertDialog: NEGATIVE click...");
+ msg2.sendToTarget(); // see addVoiceMailNumber()
+ mApp.pokeUserActivity();
+ }})
+ .setOnCancelListener(new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ if (VDBG) log("Missing voicemail AlertDialog: CANCEL handler...");
+ msg.sendToTarget(); // see dontAddVoiceMailNumber()
+ mApp.pokeUserActivity();
+ }})
+ .create();
+
+ // When the dialog is up, completely hide the in-call UI
+ // underneath (which is in a partially-constructed state).
+ mMissingVoicemailDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+
+ mMissingVoicemailDialog.show();
+ }
+
+ private void addVoiceMailNumberPanel() {
+ if (mMissingVoicemailDialog != null) {
+ mMissingVoicemailDialog.dismiss();
+ mMissingVoicemailDialog = null;
+ }
+ if (DBG) log("addVoiceMailNumberPanel: finishing InCallScreen...");
+ endInCallScreenSession();
+
+ if (DBG) log("show vm setting");
+
+ // navigate to the Voicemail setting in the Call Settings activity.
+ Intent intent = new Intent(CallFeaturesSetting.ACTION_ADD_VOICEMAIL);
+ intent.setClass(this, CallFeaturesSetting.class);
+ startActivity(intent);
+ }
+
+ private void dontAddVoiceMailNumber() {
+ if (mMissingVoicemailDialog != null) {
+ mMissingVoicemailDialog.dismiss();
+ mMissingVoicemailDialog = null;
+ }
+ if (DBG) log("dontAddVoiceMailNumber: finishing InCallScreen...");
+ endInCallScreenSession();
+ }
+
+ /**
+ * Do some delayed cleanup after a Phone call gets disconnected.
+ *
+ * This method gets called a couple of seconds after any DISCONNECT
+ * event from the Phone; it's triggered by the
+ * DELAYED_CLEANUP_AFTER_DISCONNECT message we send in onDisconnect().
+ *
+ * If the Phone is totally idle right now, that means we've already
+ * shown the "call ended" state for a couple of seconds, and it's now
+ * time to endInCallScreenSession this activity.
+ *
+ * If the Phone is *not* idle right now, that probably means that one
+ * call ended but the other line is still in use. In that case, do
+ * nothing, and instead stay here on the InCallScreen.
+ */
+ private void delayedCleanupAfterDisconnect() {
+ if (VDBG) log("delayedCleanupAfterDisconnect()... Phone state = " + mCM.getState());
+
+ // Clean up any connections in the DISCONNECTED state.
+ //
+ // [Background: Even after a connection gets disconnected, its
+ // Connection object still stays around, in the special
+ // DISCONNECTED state. This is necessary because we we need the
+ // caller-id information from that Connection to properly draw the
+ // "Call ended" state of the CallCard.
+ // But at this point we truly don't need that connection any
+ // more, so tell the Phone that it's now OK to to clean up any
+ // connections still in that state.]
+ mCM.clearDisconnected();
+
+ // There are two cases where we should *not* exit the InCallScreen:
+ // (1) Phone is still in use
+ // or
+ // (2) There's an active progress indication (i.e. the "Retrying..."
+ // progress dialog) that we need to continue to display.
+
+ boolean stayHere = phoneIsInUse() || mApp.inCallUiState.isProgressIndicationActive();
+
+ if (stayHere) {
+ if (DBG) log("- delayedCleanupAfterDisconnect: staying on the InCallScreen...");
+ } else {
+ // Phone is idle! We should exit the in-call UI now.
+ if (DBG) log("- delayedCleanupAfterDisconnect: phone is idle...");
+
+ // And (finally!) exit from the in-call screen
+ // (but not if we're already in the process of pausing...)
+ if (mIsForegroundActivity) {
+ if (DBG) log("- delayedCleanupAfterDisconnect: finishing InCallScreen...");
+
+ // In some cases we finish the call by taking the user to the
+ // Call Log. Otherwise, we simply call endInCallScreenSession,
+ // which will take us back to wherever we came from.
+ //
+ // UI note: In eclair and earlier, we went to the Call Log
+ // after outgoing calls initiated on the device, but never for
+ // incoming calls. Now we do it for incoming calls too, as
+ // long as the call was answered by the user. (We always go
+ // back where you came from after a rejected or missed incoming
+ // call.)
+ //
+ // And in any case, *never* go to the call log if we're in
+ // emergency mode (i.e. if the screen is locked and a lock
+ // pattern or PIN/password is set), or if we somehow got here
+ // on a non-voice-capable device.
+
+ if (VDBG) log("- Post-call behavior:");
+ if (VDBG) log(" - mLastDisconnectCause = " + mLastDisconnectCause);
+ if (VDBG) log(" - isPhoneStateRestricted() = " + isPhoneStateRestricted());
+
+ // DisconnectCause values in the most common scenarios:
+ // - INCOMING_MISSED: incoming ringing call times out, or the
+ // other end hangs up while still ringing
+ // - INCOMING_REJECTED: user rejects the call while ringing
+ // - LOCAL: user hung up while a call was active (after
+ // answering an incoming call, or after making an
+ // outgoing call)
+ // - NORMAL: the other end hung up (after answering an incoming
+ // call, or after making an outgoing call)
+
+ if ((mLastDisconnectCause != Connection.DisconnectCause.INCOMING_MISSED)
+ && (mLastDisconnectCause != Connection.DisconnectCause.INCOMING_REJECTED)
+ && !isPhoneStateRestricted()
+ && PhoneGlobals.sVoiceCapable) {
+ final Intent intent = mApp.createPhoneEndIntentUsingCallOrigin();
+ ActivityOptions opts = ActivityOptions.makeCustomAnimation(this,
+ R.anim.activity_close_enter, R.anim.activity_close_exit);
+ if (VDBG) {
+ log("- Show Call Log (or Dialtacts) after disconnect. Current intent: "
+ + intent);
+ }
+ try {
+ startActivity(intent, opts.toBundle());
+ } catch (ActivityNotFoundException e) {
+ // Don't crash if there's somehow no "Call log" at
+ // all on this device.
+ // (This should never happen, though, since we already
+ // checked PhoneApp.sVoiceCapable above, and any
+ // voice-capable device surely *should* have a call
+ // log activity....)
+ Log.w(LOG_TAG, "delayedCleanupAfterDisconnect: "
+ + "transition to call log failed; intent = " + intent);
+ // ...so just return back where we came from....
+ }
+ // Even if we did go to the call log, note that we still
+ // call endInCallScreenSession (below) to make sure we don't
+ // stay in the activity history.
+ }
+
+ }
+ endInCallScreenSession();
+
+ // Reset the call origin when the session ends and this in-call UI is being finished.
+ mApp.setLatestActiveCallOrigin(null);
+ }
+ }
+
+
+ /**
+ * View.OnClickListener implementation.
+ *
+ * This method handles clicks from UI elements that use the
+ * InCallScreen itself as their OnClickListener.
+ *
+ * Note: Currently this method is used only for a few special buttons:
+ * - the mButtonManageConferenceDone "Back to call" button
+ * - the "dim" effect for the secondary call photo in CallCard as the second "swap" button
+ * - other OTASP-specific buttons managed by OtaUtils.java.
+ *
+ * *Most* in-call controls are handled by the handleOnscreenButtonClick() method, via the
+ * InCallTouchUi widget.
+ */
+ @Override
+ public void onClick(View view) {
+ int id = view.getId();
+ if (VDBG) log("onClick(View " + view + ", id " + id + ")...");
+
+ switch (id) {
+ case R.id.manage_done: // mButtonManageConferenceDone
+ if (VDBG) log("onClick: mButtonManageConferenceDone...");
+ // Hide the Manage Conference panel, return to NORMAL mode.
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ requestUpdateScreen();
+ break;
+
+ case R.id.dim_effect_for_secondary_photo:
+ if (mInCallControlState.canSwap) {
+ internalSwapCalls();
+ }
+ break;
+
+ default:
+ // Presumably one of the OTASP-specific buttons managed by
+ // OtaUtils.java.
+ // (TODO: It would be cleaner for the OtaUtils instance itself to
+ // be the OnClickListener for its own buttons.)
+
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL
+ || mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)
+ && mApp.otaUtils != null) {
+ mApp.otaUtils.onClickHandler(id);
+ } else {
+ // Uh oh: we *should* only receive clicks here from the
+ // buttons managed by OtaUtils.java, but if we're not in one
+ // of the special OTASP modes, those buttons shouldn't have
+ // been visible in the first place.
+ Log.w(LOG_TAG,
+ "onClick: unexpected click from ID " + id + " (View = " + view + ")");
+ }
+ break;
+ }
+
+ EventLog.writeEvent(EventLogTags.PHONE_UI_BUTTON_CLICK,
+ (view instanceof TextView) ? ((TextView) view).getText() : "");
+
+ // Clicking any onscreen UI element counts as explicit "user activity".
+ mApp.pokeUserActivity();
+ }
+
+ private void onHoldClick() {
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ final boolean hasHoldingCall = mCM.hasActiveBgCall();
+ log("onHoldClick: hasActiveCall = " + hasActiveCall
+ + ", hasHoldingCall = " + hasHoldingCall);
+ boolean newHoldState;
+ boolean holdButtonEnabled;
+ if (hasActiveCall && !hasHoldingCall) {
+ // There's only one line in use, and that line is active.
+ PhoneUtils.switchHoldingAndActive(
+ mCM.getFirstActiveBgCall()); // Really means "hold" in this state
+ newHoldState = true;
+ holdButtonEnabled = true;
+ } else if (!hasActiveCall && hasHoldingCall) {
+ // There's only one line in use, and that line is on hold.
+ PhoneUtils.switchHoldingAndActive(
+ mCM.getFirstActiveBgCall()); // Really means "unhold" in this state
+ newHoldState = false;
+ holdButtonEnabled = true;
+ } else {
+ // Either zero or 2 lines are in use; "hold/unhold" is meaningless.
+ newHoldState = false;
+ holdButtonEnabled = false;
+ }
+ // No need to forcibly update the onscreen UI; just wait for the
+ // onPhoneStateChanged() callback. (This seems to be responsive
+ // enough.)
+
+ // Also, any time we hold or unhold, force the DTMF dialpad to close.
+ closeDialpadInternal(true); // do the "closing" animation
+ }
+
+ /**
+ * Toggles in-call audio between speaker and the built-in earpiece (or
+ * wired headset.)
+ */
+ public void toggleSpeaker() {
+ // TODO: Turning on the speaker seems to enable the mic
+ // whether or not the "mute" feature is active!
+ // Not sure if this is an feature of the telephony API
+ // that I need to handle specially, or just a bug.
+ boolean newSpeakerState = !PhoneUtils.isSpeakerOn(this);
+ log("toggleSpeaker(): newSpeakerState = " + newSpeakerState);
+
+ if (newSpeakerState && isBluetoothAvailable() && isBluetoothAudioConnected()) {
+ disconnectBluetoothAudio();
+ }
+ PhoneUtils.turnOnSpeaker(this, newSpeakerState, true);
+
+ // And update the InCallTouchUi widget (since the "audio mode"
+ // button might need to change its appearance based on the new
+ // audio state.)
+ updateInCallTouchUi();
+ }
+
+ /*
+ * onMuteClick is called only when there is a foreground call
+ */
+ private void onMuteClick() {
+ boolean newMuteState = !PhoneUtils.getMute();
+ log("onMuteClick(): newMuteState = " + newMuteState);
+ PhoneUtils.setMute(newMuteState);
+ }
+
+ /**
+ * Toggles whether or not to route in-call audio to the bluetooth
+ * headset, or do nothing (but log a warning) if no bluetooth device
+ * is actually connected.
+ *
+ * TODO: this method is currently unused, but the "audio mode" UI
+ * design is still in flux so let's keep it around for now.
+ * (But if we ultimately end up *not* providing any way for the UI to
+ * simply "toggle bluetooth", we can get rid of this method.)
+ */
+ public void toggleBluetooth() {
+ if (VDBG) log("toggleBluetooth()...");
+
+ if (isBluetoothAvailable()) {
+ // Toggle the bluetooth audio connection state:
+ if (isBluetoothAudioConnected()) {
+ disconnectBluetoothAudio();
+ } else {
+ // Manually turn the speaker phone off, instead of allowing the
+ // Bluetooth audio routing to handle it, since there's other
+ // important state-updating that needs to happen in the
+ // PhoneUtils.turnOnSpeaker() method.
+ // (Similarly, whenever the user turns *on* the speaker, we
+ // manually disconnect the active bluetooth headset;
+ // see toggleSpeaker() and/or switchInCallAudio().)
+ if (PhoneUtils.isSpeakerOn(this)) {
+ PhoneUtils.turnOnSpeaker(this, false, true);
+ }
+
+ connectBluetoothAudio();
+ }
+ } else {
+ // Bluetooth isn't available; the onscreen UI shouldn't have
+ // allowed this request in the first place!
+ Log.w(LOG_TAG, "toggleBluetooth(): bluetooth is unavailable");
+ }
+
+ // And update the InCallTouchUi widget (since the "audio mode"
+ // button might need to change its appearance based on the new
+ // audio state.)
+ updateInCallTouchUi();
+ }
+
+ /**
+ * Switches the current routing of in-call audio between speaker,
+ * bluetooth, and the built-in earpiece (or wired headset.)
+ *
+ * This method is used on devices that provide a single 3-way switch
+ * for audio routing. For devices that provide separate toggles for
+ * Speaker and Bluetooth, see toggleBluetooth() and toggleSpeaker().
+ *
+ * TODO: UI design is still in flux. If we end up totally
+ * eliminating the concept of Speaker and Bluetooth toggle buttons,
+ * we can get rid of toggleBluetooth() and toggleSpeaker().
+ */
+ public void switchInCallAudio(InCallAudioMode newMode) {
+ log("switchInCallAudio: new mode = " + newMode);
+ switch (newMode) {
+ case SPEAKER:
+ if (!PhoneUtils.isSpeakerOn(this)) {
+ // Switch away from Bluetooth, if it was active.
+ if (isBluetoothAvailable() && isBluetoothAudioConnected()) {
+ disconnectBluetoothAudio();
+ }
+ PhoneUtils.turnOnSpeaker(this, true, true);
+ }
+ break;
+
+ case BLUETOOTH:
+ // If already connected to BT, there's nothing to do here.
+ if (isBluetoothAvailable() && !isBluetoothAudioConnected()) {
+ // Manually turn the speaker phone off, instead of allowing the
+ // Bluetooth audio routing to handle it, since there's other
+ // important state-updating that needs to happen in the
+ // PhoneUtils.turnOnSpeaker() method.
+ // (Similarly, whenever the user turns *on* the speaker, we
+ // manually disconnect the active bluetooth headset;
+ // see toggleSpeaker() and/or switchInCallAudio().)
+ if (PhoneUtils.isSpeakerOn(this)) {
+ PhoneUtils.turnOnSpeaker(this, false, true);
+ }
+ connectBluetoothAudio();
+ }
+ break;
+
+ case EARPIECE:
+ // Switch to either the handset earpiece, or the wired headset (if connected.)
+ // (Do this by simply making sure both speaker and bluetooth are off.)
+ if (isBluetoothAvailable() && isBluetoothAudioConnected()) {
+ disconnectBluetoothAudio();
+ }
+ if (PhoneUtils.isSpeakerOn(this)) {
+ PhoneUtils.turnOnSpeaker(this, false, true);
+ }
+ break;
+
+ default:
+ Log.wtf(LOG_TAG, "switchInCallAudio: unexpected mode " + newMode);
+ break;
+ }
+
+ // And finally, update the InCallTouchUi widget (since the "audio
+ // mode" button might need to change its appearance based on the
+ // new audio state.)
+ updateInCallTouchUi();
+ }
+
+ /**
+ * Handle a click on the "Open/Close dialpad" button.
+ *
+ * @see DTMFTwelveKeyDialer#openDialer(boolean)
+ * @see DTMFTwelveKeyDialer#closeDialer(boolean)
+ */
+ private void onOpenCloseDialpad() {
+ if (VDBG) log("onOpenCloseDialpad()...");
+ if (mDialer.isOpened()) {
+ closeDialpadInternal(true); // do the "closing" animation
+ } else {
+ openDialpadInternal(true); // do the "opening" animation
+ }
+ mApp.updateProximitySensorMode(mCM.getState());
+ }
+
+ /** Internal wrapper around {@link DTMFTwelveKeyDialer#openDialer(boolean)} */
+ private void openDialpadInternal(boolean animate) {
+ mDialer.openDialer(animate);
+ // And update the InCallUiState (so that we'll restore the dialpad
+ // to the correct state if we get paused/resumed).
+ mApp.inCallUiState.showDialpad = true;
+ }
+
+ // Internal wrapper around DTMFTwelveKeyDialer.closeDialer()
+ private void closeDialpadInternal(boolean animate) {
+ mDialer.closeDialer(animate);
+ // And update the InCallUiState (so that we'll restore the dialpad
+ // to the correct state if we get paused/resumed).
+ mApp.inCallUiState.showDialpad = false;
+ }
+
+ /**
+ * Handles button clicks from the InCallTouchUi widget.
+ */
+ /* package */ void handleOnscreenButtonClick(int id) {
+ if (DBG) log("handleOnscreenButtonClick(id " + id + ")...");
+
+ switch (id) {
+ // Actions while an incoming call is ringing:
+ case R.id.incomingCallAnswer:
+ internalAnswerCall();
+ break;
+ case R.id.incomingCallReject:
+ hangupRingingCall();
+ break;
+ case R.id.incomingCallRespondViaSms:
+ internalRespondViaSms();
+ break;
+
+ // The other regular (single-tap) buttons used while in-call:
+ case R.id.holdButton:
+ onHoldClick();
+ break;
+ case R.id.swapButton:
+ internalSwapCalls();
+ break;
+ case R.id.endButton:
+ internalHangup();
+ break;
+ case R.id.dialpadButton:
+ onOpenCloseDialpad();
+ break;
+ case R.id.muteButton:
+ onMuteClick();
+ break;
+ case R.id.addButton:
+ PhoneUtils.startNewCall(mCM); // Fires off an ACTION_DIAL intent
+ break;
+ case R.id.mergeButton:
+ case R.id.cdmaMergeButton:
+ PhoneUtils.mergeCalls(mCM);
+ break;
+ case R.id.manageConferenceButton:
+ // Show the Manage Conference panel.
+ setInCallScreenMode(InCallScreenMode.MANAGE_CONFERENCE);
+ requestUpdateScreen();
+ break;
+
+ default:
+ Log.w(LOG_TAG, "handleOnscreenButtonClick: unexpected ID " + id);
+ break;
+ }
+
+ // Clicking any onscreen UI element counts as explicit "user activity".
+ mApp.pokeUserActivity();
+
+ // Just in case the user clicked a "stateful" UI element (like one
+ // of the toggle buttons), we force the in-call buttons to update,
+ // to make sure the user sees the *new* current state.
+ //
+ // Note that some in-call buttons will *not* immediately change the
+ // state of the UI, namely those that send a request to the telephony
+ // layer (like "Hold" or "End call".) For those buttons, the
+ // updateInCallTouchUi() call here won't have any visible effect.
+ // Instead, the UI will be updated eventually when the next
+ // onPhoneStateChanged() event comes in and triggers an updateScreen()
+ // call.
+ //
+ // TODO: updateInCallTouchUi() is overkill here; it would be
+ // more efficient to update *only* the affected button(s).
+ // (But this isn't a big deal since updateInCallTouchUi() is pretty
+ // cheap anyway...)
+ updateInCallTouchUi();
+ }
+
+ /**
+ * Display a status or error indication to the user according to the
+ * specified InCallUiState.CallStatusCode value.
+ */
+ private void showStatusIndication(CallStatusCode status) {
+ switch (status) {
+ case SUCCESS:
+ // The InCallScreen does not need to display any kind of error indication,
+ // so we shouldn't have gotten here in the first place.
+ Log.wtf(LOG_TAG, "showStatusIndication: nothing to display");
+ break;
+
+ case POWER_OFF:
+ // Radio is explictly powered off, presumably because the
+ // device is in airplane mode.
+ //
+ // TODO: For now this UI is ultra-simple: we simply display
+ // a message telling the user to turn off airplane mode.
+ // But it might be nicer for the dialog to offer the option
+ // to turn the radio on right there (and automatically retry
+ // the call once network registration is complete.)
+ showGenericErrorDialog(R.string.incall_error_power_off,
+ true /* isStartupError */);
+ break;
+
+ case EMERGENCY_ONLY:
+ // Only emergency numbers are allowed, but we tried to dial
+ // a non-emergency number.
+ // (This state is currently unused; see comments above.)
+ showGenericErrorDialog(R.string.incall_error_emergency_only,
+ true /* isStartupError */);
+ break;
+
+ case OUT_OF_SERVICE:
+ // No network connection.
+ showGenericErrorDialog(R.string.incall_error_out_of_service,
+ true /* isStartupError */);
+ break;
+
+ case NO_PHONE_NUMBER_SUPPLIED:
+ // The supplied Intent didn't contain a valid phone number.
+ // (This is rare and should only ever happen with broken
+ // 3rd-party apps.) For now just show a generic error.
+ showGenericErrorDialog(R.string.incall_error_no_phone_number_supplied,
+ true /* isStartupError */);
+ break;
+
+ case DIALED_MMI:
+ // Our initial phone number was actually an MMI sequence.
+ // There's no real "error" here, but we do bring up the
+ // a Toast (as requested of the New UI paradigm).
+ //
+ // In-call MMIs do not trigger the normal MMI Initiate
+ // Notifications, so we should notify the user here.
+ // Otherwise, the code in PhoneUtils.java should handle
+ // user notifications in the form of Toasts or Dialogs.
+ if (mCM.getState() == PhoneConstants.State.OFFHOOK) {
+ Toast.makeText(mApp, R.string.incall_status_dialed_mmi, Toast.LENGTH_SHORT)
+ .show();
+ }
+ break;
+
+ case CALL_FAILED:
+ // We couldn't successfully place the call; there was some
+ // failure in the telephony layer.
+ // TODO: Need UI spec for this failure case; for now just
+ // show a generic error.
+ showGenericErrorDialog(R.string.incall_error_call_failed,
+ true /* isStartupError */);
+ break;
+
+ case VOICEMAIL_NUMBER_MISSING:
+ // We tried to call a voicemail: URI but the device has no
+ // voicemail number configured.
+ handleMissingVoiceMailNumber();
+ break;
+
+ case CDMA_CALL_LOST:
+ // This status indicates that InCallScreen should display the
+ // CDMA-specific "call lost" dialog. (If an outgoing call fails,
+ // and the CDMA "auto-retry" feature is enabled, *and* the retried
+ // call fails too, we display this specific dialog.)
+ //
+ // TODO: currently unused; see InCallUiState.needToShowCallLostDialog
+ break;
+
+ case EXITED_ECM:
+ // This status indicates that InCallScreen needs to display a
+ // warning that we're exiting ECM (emergency callback mode).
+ showExitingECMDialog();
+ break;
+
+ default:
+ throw new IllegalStateException(
+ "showStatusIndication: unexpected status code: " + status);
+ }
+
+ // TODO: still need to make sure that pressing OK or BACK from
+ // *any* of the dialogs we launch here ends up calling
+ // inCallUiState.clearPendingCallStatusCode()
+ // *and*
+ // make sure the Dialog handles both OK *and* cancel by calling
+ // endInCallScreenSession. (See showGenericErrorDialog() for an
+ // example.)
+ //
+ // (showGenericErrorDialog() currently does this correctly,
+ // but handleMissingVoiceMailNumber() probably needs to be fixed too.)
+ //
+ // Also need to make sure that bailing out of any of these dialogs by
+ // pressing Home clears out the pending status code too. (If you do
+ // that, neither the dialog's clickListener *or* cancelListener seems
+ // to run...)
+ }
+
+ /**
+ * Utility function to bring up a generic "error" dialog, and then bail
+ * out of the in-call UI when the user hits OK (or the BACK button.)
+ */
+ private void showGenericErrorDialog(int resid, boolean isStartupError) {
+ CharSequence msg = getResources().getText(resid);
+ if (DBG) log("showGenericErrorDialog('" + msg + "')...");
+
+ // create the clicklistener and cancel listener as needed.
+ DialogInterface.OnClickListener clickListener;
+ OnCancelListener cancelListener;
+ if (isStartupError) {
+ clickListener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ bailOutAfterErrorDialog();
+ }};
+ cancelListener = new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ bailOutAfterErrorDialog();
+ }};
+ } else {
+ clickListener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ delayedCleanupAfterDisconnect();
+ }};
+ cancelListener = new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ delayedCleanupAfterDisconnect();
+ }};
+ }
+
+ // TODO: Consider adding a setTitle() call here (with some generic
+ // "failure" title?)
+ mGenericErrorDialog = new AlertDialog.Builder(this)
+ .setMessage(msg)
+ .setPositiveButton(R.string.ok, clickListener)
+ .setOnCancelListener(cancelListener)
+ .create();
+
+ // When the dialog is up, completely hide the in-call UI
+ // underneath (which is in a partially-constructed state).
+ mGenericErrorDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+
+ mGenericErrorDialog.show();
+ }
+
+ private void showCallLostDialog() {
+ if (DBG) log("showCallLostDialog()...");
+
+ // Don't need to show the dialog if InCallScreen isn't in the forgeround
+ if (!mIsForegroundActivity) {
+ if (DBG) log("showCallLostDialog: not the foreground Activity! Bailing out...");
+ return;
+ }
+
+ // Don't need to show the dialog again, if there is one already.
+ if (mCallLostDialog != null) {
+ if (DBG) log("showCallLostDialog: There is a mCallLostDialog already.");
+ return;
+ }
+
+ mCallLostDialog = new AlertDialog.Builder(this)
+ .setMessage(R.string.call_lost)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .create();
+ mCallLostDialog.show();
+ }
+
+ /**
+ * Displays the "Exiting ECM" warning dialog.
+ *
+ * Background: If the phone is currently in ECM (Emergency callback
+ * mode) and we dial a non-emergency number, that automatically
+ * *cancels* ECM. (That behavior comes from CdmaCallTracker.dial().)
+ * When that happens, we need to warn the user that they're no longer
+ * in ECM (bug 4207607.)
+ *
+ * So bring up a dialog explaining what's happening. There's nothing
+ * for the user to do, by the way; we're simply providing an
+ * indication that they're exiting ECM. We *could* use a Toast for
+ * this, but toasts are pretty easy to miss, so instead use a dialog
+ * with a single "OK" button.
+ *
+ * TODO: it's ugly that the code here has to make assumptions about
+ * the behavior of the telephony layer (namely that dialing a
+ * non-emergency number while in ECM causes us to exit ECM.)
+ *
+ * Instead, this warning dialog should really be triggered by our
+ * handler for the
+ * TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED intent in
+ * PhoneApp.java. But that won't work until that intent also
+ * includes a *reason* why we're exiting ECM, since we need to
+ * display this dialog when exiting ECM because of an outgoing call,
+ * but NOT if we're exiting ECM because the user manually turned it
+ * off via the EmergencyCallbackModeExitDialog.
+ *
+ * Or, it might be simpler to just have outgoing non-emergency calls
+ * *not* cancel ECM. That way the UI wouldn't have to do anything
+ * special here.
+ */
+ private void showExitingECMDialog() {
+ Log.i(LOG_TAG, "showExitingECMDialog()...");
+
+ if (mExitingECMDialog != null) {
+ if (DBG) log("- DISMISSING mExitingECMDialog.");
+ mExitingECMDialog.dismiss(); // safe even if already dismissed
+ mExitingECMDialog = null;
+ }
+
+ // When the user dismisses the "Exiting ECM" dialog, we clear out
+ // the pending call status code field (since we're done with this
+ // dialog), but do *not* bail out of the InCallScreen.
+
+ final InCallUiState inCallUiState = mApp.inCallUiState;
+ DialogInterface.OnClickListener clickListener = new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ inCallUiState.clearPendingCallStatusCode();
+ }};
+ OnCancelListener cancelListener = new OnCancelListener() {
+ public void onCancel(DialogInterface dialog) {
+ inCallUiState.clearPendingCallStatusCode();
+ }};
+
+ // Ultra-simple AlertDialog with only an OK button:
+ mExitingECMDialog = new AlertDialog.Builder(this)
+ .setMessage(R.string.progress_dialog_exiting_ecm)
+ .setPositiveButton(R.string.ok, clickListener)
+ .setOnCancelListener(cancelListener)
+ .create();
+ mExitingECMDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+ mExitingECMDialog.show();
+ }
+
+ private void bailOutAfterErrorDialog() {
+ if (mGenericErrorDialog != null) {
+ if (DBG) log("bailOutAfterErrorDialog: DISMISSING mGenericErrorDialog.");
+ mGenericErrorDialog.dismiss();
+ mGenericErrorDialog = null;
+ }
+ if (DBG) log("bailOutAfterErrorDialog(): end InCallScreen session...");
+
+ // Now that the user has dismissed the error dialog (presumably by
+ // either hitting the OK button or pressing Back, we can now reset
+ // the pending call status code field.
+ //
+ // (Note that the pending call status is NOT cleared simply
+ // by the InCallScreen being paused or finished, since the resulting
+ // dialog is supposed to persist across orientation changes or if the
+ // screen turns off.)
+ //
+ // See the "Error / diagnostic indications" section of
+ // InCallUiState.java for more detailed info about the
+ // pending call status code field.
+ final InCallUiState inCallUiState = mApp.inCallUiState;
+ inCallUiState.clearPendingCallStatusCode();
+
+ // Force the InCallScreen to truly finish(), rather than just
+ // moving it to the back of the activity stack (which is what
+ // our finish() method usually does.)
+ // This is necessary to avoid an obscure scenario where the
+ // InCallScreen can get stuck in an inconsistent state, somehow
+ // causing a *subsequent* outgoing call to fail (bug 4172599).
+ endInCallScreenSession(true /* force a real finish() call */);
+ }
+
+ /**
+ * Dismisses (and nulls out) all persistent Dialogs managed
+ * by the InCallScreen. Useful if (a) we're about to bring up
+ * a dialog and want to pre-empt any currently visible dialogs,
+ * or (b) as a cleanup step when the Activity is going away.
+ */
+ private void dismissAllDialogs() {
+ if (DBG) log("dismissAllDialogs()...");
+
+ // Note it's safe to dismiss() a dialog that's already dismissed.
+ // (Even if the AlertDialog object(s) below are still around, it's
+ // possible that the actual dialog(s) may have already been
+ // dismissed by the user.)
+
+ if (mMissingVoicemailDialog != null) {
+ if (VDBG) log("- DISMISSING mMissingVoicemailDialog.");
+ mMissingVoicemailDialog.dismiss();
+ mMissingVoicemailDialog = null;
+ }
+ if (mMmiStartedDialog != null) {
+ if (VDBG) log("- DISMISSING mMmiStartedDialog.");
+ mMmiStartedDialog.dismiss();
+ mMmiStartedDialog = null;
+ }
+ if (mGenericErrorDialog != null) {
+ if (VDBG) log("- DISMISSING mGenericErrorDialog.");
+ mGenericErrorDialog.dismiss();
+ mGenericErrorDialog = null;
+ }
+ if (mSuppServiceFailureDialog != null) {
+ if (VDBG) log("- DISMISSING mSuppServiceFailureDialog.");
+ mSuppServiceFailureDialog.dismiss();
+ mSuppServiceFailureDialog = null;
+ }
+ if (mWaitPromptDialog != null) {
+ if (VDBG) log("- DISMISSING mWaitPromptDialog.");
+ mWaitPromptDialog.dismiss();
+ mWaitPromptDialog = null;
+ }
+ if (mWildPromptDialog != null) {
+ if (VDBG) log("- DISMISSING mWildPromptDialog.");
+ mWildPromptDialog.dismiss();
+ mWildPromptDialog = null;
+ }
+ if (mCallLostDialog != null) {
+ if (VDBG) log("- DISMISSING mCallLostDialog.");
+ mCallLostDialog.dismiss();
+ mCallLostDialog = null;
+ }
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL
+ || mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)
+ && mApp.otaUtils != null) {
+ mApp.otaUtils.dismissAllOtaDialogs();
+ }
+ if (mPausePromptDialog != null) {
+ if (DBG) log("- DISMISSING mPausePromptDialog.");
+ mPausePromptDialog.dismiss();
+ mPausePromptDialog = null;
+ }
+ if (mExitingECMDialog != null) {
+ if (DBG) log("- DISMISSING mExitingECMDialog.");
+ mExitingECMDialog.dismiss();
+ mExitingECMDialog = null;
+ }
+ }
+
+ /**
+ * Updates the state of the onscreen "progress indication" used in
+ * some (relatively rare) scenarios where we need to wait for
+ * something to happen before enabling the in-call UI.
+ *
+ * If necessary, this method will cause a ProgressDialog (i.e. a
+ * spinning wait cursor) to be drawn *on top of* whatever the current
+ * state of the in-call UI is.
+ *
+ * @see InCallUiState.ProgressIndicationType
+ */
+ private void updateProgressIndication() {
+ // If an incoming call is ringing, that takes priority over any
+ // possible value of inCallUiState.progressIndication.
+ if (mCM.hasActiveRingingCall()) {
+ dismissProgressIndication();
+ return;
+ }
+
+ // Otherwise, put up a progress indication if indicated by the
+ // inCallUiState.progressIndication field.
+ final InCallUiState inCallUiState = mApp.inCallUiState;
+ switch (inCallUiState.getProgressIndication()) {
+ case NONE:
+ // No progress indication necessary, so make sure it's dismissed.
+ dismissProgressIndication();
+ break;
+
+ case TURNING_ON_RADIO:
+ showProgressIndication(
+ R.string.emergency_enable_radio_dialog_title,
+ R.string.emergency_enable_radio_dialog_message);
+ break;
+
+ case RETRYING:
+ showProgressIndication(
+ R.string.emergency_enable_radio_dialog_title,
+ R.string.emergency_enable_radio_dialog_retry);
+ break;
+
+ default:
+ Log.wtf(LOG_TAG, "updateProgressIndication: unexpected value: "
+ + inCallUiState.getProgressIndication());
+ dismissProgressIndication();
+ break;
+ }
+ }
+
+ /**
+ * Show an onscreen "progress indication" with the specified title and message.
+ */
+ private void showProgressIndication(int titleResId, int messageResId) {
+ if (DBG) log("showProgressIndication(message " + messageResId + ")...");
+
+ // TODO: make this be a no-op if the progress indication is
+ // already visible with the exact same title and message.
+
+ dismissProgressIndication(); // Clean up any prior progress indication
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setTitle(getText(titleResId));
+ mProgressDialog.setMessage(getText(messageResId));
+ mProgressDialog.setIndeterminate(true);
+ mProgressDialog.setCancelable(false);
+ mProgressDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
+ mProgressDialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+ mProgressDialog.show();
+ }
+
+ /**
+ * Dismiss the onscreen "progress indication" (if present).
+ */
+ private void dismissProgressIndication() {
+ if (DBG) log("dismissProgressIndication()...");
+ if (mProgressDialog != null) {
+ mProgressDialog.dismiss(); // safe even if already dismissed
+ mProgressDialog = null;
+ }
+ }
+
+
+ //
+ // Helper functions for answering incoming calls.
+ //
+
+ /**
+ * Answer a ringing call. This method does nothing if there's no
+ * ringing or waiting call.
+ */
+ private void internalAnswerCall() {
+ if (DBG) log("internalAnswerCall()...");
+ // if (DBG) PhoneUtils.dumpCallState(mPhone);
+
+ final boolean hasRingingCall = mCM.hasActiveRingingCall();
+
+ if (hasRingingCall) {
+ Phone phone = mCM.getRingingPhone();
+ Call ringing = mCM.getFirstActiveRingingCall();
+ int phoneType = phone.getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ if (DBG) log("internalAnswerCall: answering (CDMA)...");
+ if (mCM.hasActiveFgCall()
+ && mCM.getFgPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_SIP) {
+ // The incoming call is CDMA call and the ongoing
+ // call is a SIP call. The CDMA network does not
+ // support holding an active call, so there's no
+ // way to swap between a CDMA call and a SIP call.
+ // So for now, we just don't allow a CDMA call and
+ // a SIP call to be active at the same time.We'll
+ // "answer incoming, end ongoing" in this case.
+ if (DBG) log("internalAnswerCall: answer "
+ + "CDMA incoming and end SIP ongoing");
+ PhoneUtils.answerAndEndActive(mCM, ringing);
+ } else {
+ PhoneUtils.answerCall(ringing);
+ }
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_SIP) {
+ if (DBG) log("internalAnswerCall: answering (SIP)...");
+ if (mCM.hasActiveFgCall()
+ && mCM.getFgPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ // Similar to the PHONE_TYPE_CDMA handling.
+ // The incoming call is SIP call and the ongoing
+ // call is a CDMA call. The CDMA network does not
+ // support holding an active call, so there's no
+ // way to swap between a CDMA call and a SIP call.
+ // So for now, we just don't allow a CDMA call and
+ // a SIP call to be active at the same time.We'll
+ // "answer incoming, end ongoing" in this case.
+ if (DBG) log("internalAnswerCall: answer "
+ + "SIP incoming and end CDMA ongoing");
+ PhoneUtils.answerAndEndActive(mCM, ringing);
+ } else {
+ PhoneUtils.answerCall(ringing);
+ }
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+ if (DBG) log("internalAnswerCall: answering (GSM)...");
+ // GSM: this is usually just a wrapper around
+ // PhoneUtils.answerCall(), *but* we also need to do
+ // something special for the "both lines in use" case.
+
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ final boolean hasHoldingCall = mCM.hasActiveBgCall();
+
+ if (hasActiveCall && hasHoldingCall) {
+ if (DBG) log("internalAnswerCall: answering (both lines in use!)...");
+ // The relatively rare case where both lines are
+ // already in use. We "answer incoming, end ongoing"
+ // in this case, according to the current UI spec.
+ PhoneUtils.answerAndEndActive(mCM, ringing);
+
+ // Alternatively, we could use
+ // PhoneUtils.answerAndEndHolding(mPhone);
+ // here to end the on-hold call instead.
+ } else {
+ if (DBG) log("internalAnswerCall: answering...");
+ PhoneUtils.answerCall(ringing); // Automatically holds the current active call,
+ // if there is one
+ }
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+
+ // Call origin is valid only with outgoing calls. Disable it on incoming calls.
+ mApp.setLatestActiveCallOrigin(null);
+ }
+ }
+
+ /**
+ * Hang up the ringing call (aka "Don't answer").
+ */
+ /* package */ void hangupRingingCall() {
+ if (DBG) log("hangupRingingCall()...");
+ if (VDBG) PhoneUtils.dumpCallManager();
+ // In the rare case when multiple calls are ringing, the UI policy
+ // it to always act on the first ringing call.
+ PhoneUtils.hangupRingingCall(mCM.getFirstActiveRingingCall());
+ }
+
+ /**
+ * Silence the ringer (if an incoming call is ringing.)
+ */
+ private void internalSilenceRinger() {
+ if (DBG) log("internalSilenceRinger()...");
+ final CallNotifier notifier = mApp.notifier;
+ if (notifier.isRinging()) {
+ // ringer is actually playing, so silence it.
+ notifier.silenceRinger();
+ }
+ }
+
+ /**
+ * Respond via SMS to the ringing call.
+ * @see RespondViaSmsManager
+ */
+ private void internalRespondViaSms() {
+ log("internalRespondViaSms()...");
+ if (VDBG) PhoneUtils.dumpCallManager();
+
+ // In the rare case when multiple calls are ringing, the UI policy
+ // it to always act on the first ringing call.
+ Call ringingCall = mCM.getFirstActiveRingingCall();
+
+ mRespondViaSmsManager.showRespondViaSmsPopup(ringingCall);
+
+ // Silence the ringer, since it would be distracting while you're trying
+ // to pick a response. (Note that we'll restart the ringer if you bail
+ // out of the popup, though; see RespondViaSmsCancelListener.)
+ internalSilenceRinger();
+ }
+
+ /**
+ * Hang up the current active call.
+ */
+ private void internalHangup() {
+ PhoneConstants.State state = mCM.getState();
+ log("internalHangup()... phone state = " + state);
+
+ // Regardless of the phone state, issue a hangup request.
+ // (If the phone is already idle, this call will presumably have no
+ // effect (but also see the note below.))
+ PhoneUtils.hangup(mCM);
+
+ // If the user just hung up the only active call, we'll eventually exit
+ // the in-call UI after the following sequence:
+ // - When the hangup() succeeds, we'll get a DISCONNECT event from
+ // the telephony layer (see onDisconnect()).
+ // - We immediately switch to the "Call ended" state (see the "delayed
+ // bailout" code path in onDisconnect()) and also post a delayed
+ // DELAYED_CLEANUP_AFTER_DISCONNECT message.
+ // - When the DELAYED_CLEANUP_AFTER_DISCONNECT message comes in (see
+ // delayedCleanupAfterDisconnect()) we do some final cleanup, and exit
+ // this activity unless the phone is still in use (i.e. if there's
+ // another call, or something else going on like an active MMI
+ // sequence.)
+
+ if (state == PhoneConstants.State.IDLE) {
+ // The user asked us to hang up, but the phone was (already) idle!
+ Log.w(LOG_TAG, "internalHangup(): phone is already IDLE!");
+
+ // This is rare, but can happen in a few cases:
+ // (a) If the user quickly double-taps the "End" button. In this case
+ // we'll see that 2nd press event during the brief "Call ended"
+ // state (where the phone is IDLE), or possibly even before the
+ // radio has been able to respond to the initial hangup request.
+ // (b) More rarely, this can happen if the user presses "End" at the
+ // exact moment that the call ends on its own (like because of the
+ // other person hanging up.)
+ // (c) Finally, this could also happen if we somehow get stuck here on
+ // the InCallScreen with the phone truly idle, perhaps due to a
+ // bug where we somehow *didn't* exit when the phone became idle
+ // in the first place.
+
+ // TODO: as a "safety valve" for case (c), consider immediately
+ // bailing out of the in-call UI right here. (The user can always
+ // bail out by pressing Home, of course, but they'll probably try
+ // pressing End first.)
+ //
+ // Log.i(LOG_TAG, "internalHangup(): phone is already IDLE! Bailing out...");
+ // endInCallScreenSession();
+ }
+ }
+
+ /**
+ * InCallScreen-specific wrapper around PhoneUtils.switchHoldingAndActive().
+ */
+ private void internalSwapCalls() {
+ if (DBG) log("internalSwapCalls()...");
+
+ // Any time we swap calls, force the DTMF dialpad to close.
+ // (We want the regular in-call UI to be visible right now, so the
+ // user can clearly see which call is now in the foreground.)
+ closeDialpadInternal(true); // do the "closing" animation
+
+ // Also, clear out the "history" of DTMF digits you typed, to make
+ // sure you don't see digits from call #1 while call #2 is active.
+ // (Yes, this does mean that swapping calls twice will cause you
+ // to lose any previous digits from the current call; see the TODO
+ // comment on DTMFTwelvKeyDialer.clearDigits() for more info.)
+ mDialer.clearDigits();
+
+ // Swap the fg and bg calls.
+ // In the future we may provides some way for user to choose among
+ // multiple background calls, for now, always act on the first background calll.
+ PhoneUtils.switchHoldingAndActive(mCM.getFirstActiveBgCall());
+
+ // If we have a valid BluetoothPhoneService then since CDMA network or
+ // Telephony FW does not send us information on which caller got swapped
+ // we need to update the second call active state in BluetoothPhoneService internally
+ if (mCM.getBgPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ IBluetoothHeadsetPhone btPhone = mApp.getBluetoothPhoneService();
+ if (btPhone != null) {
+ try {
+ btPhone.cdmaSwapSecondCallState();
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Sets the current high-level "mode" of the in-call UI.
+ *
+ * NOTE: if newMode is CALL_ENDED, the caller is responsible for
+ * posting a delayed DELAYED_CLEANUP_AFTER_DISCONNECT message, to make
+ * sure the "call ended" state goes away after a couple of seconds.
+ *
+ * Note this method does NOT refresh of the onscreen UI; the caller is
+ * responsible for calling updateScreen() or requestUpdateScreen() if
+ * necessary.
+ */
+ private void setInCallScreenMode(InCallScreenMode newMode) {
+ if (DBG) log("setInCallScreenMode: " + newMode);
+ mApp.inCallUiState.inCallScreenMode = newMode;
+
+ switch (newMode) {
+ case MANAGE_CONFERENCE:
+ if (!PhoneUtils.isConferenceCall(mCM.getActiveFgCall())) {
+ Log.w(LOG_TAG, "MANAGE_CONFERENCE: no active conference call!");
+ // Hide the Manage Conference panel, return to NORMAL mode.
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ return;
+ }
+ List<Connection> connections = mCM.getFgCallConnections();
+ // There almost certainly will be > 1 connection,
+ // since isConferenceCall() just returned true.
+ if ((connections == null) || (connections.size() <= 1)) {
+ Log.w(LOG_TAG,
+ "MANAGE_CONFERENCE: Bogus TRUE from isConferenceCall(); connections = "
+ + connections);
+ // Hide the Manage Conference panel, return to NORMAL mode.
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ return;
+ }
+
+ // TODO: Don't do this here. The call to
+ // initManageConferencePanel() should instead happen
+ // automagically in ManageConferenceUtils the very first
+ // time you call updateManageConferencePanel() or
+ // setPanelVisible(true).
+ mManageConferenceUtils.initManageConferencePanel(); // if necessary
+
+ mManageConferenceUtils.updateManageConferencePanel(connections);
+
+ // The "Manage conference" UI takes up the full main frame,
+ // replacing the CallCard PopupWindow.
+ mManageConferenceUtils.setPanelVisible(true);
+
+ // Start the chronometer.
+ // TODO: Similarly, we shouldn't expose startConferenceTime()
+ // and stopConferenceTime(); the ManageConferenceUtils
+ // class ought to manage the conferenceTime widget itself
+ // based on setPanelVisible() calls.
+
+ // Note: there is active Fg call since we are in conference call
+ long callDuration =
+ mCM.getActiveFgCall().getEarliestConnection().getDurationMillis();
+ mManageConferenceUtils.startConferenceTime(
+ SystemClock.elapsedRealtime() - callDuration);
+
+ // No need to close the dialer here, since the Manage
+ // Conference UI will just cover it up anyway.
+
+ break;
+
+ case CALL_ENDED:
+ case NORMAL:
+ mManageConferenceUtils.setPanelVisible(false);
+ mManageConferenceUtils.stopConferenceTime();
+ break;
+
+ case OTA_NORMAL:
+ mApp.otaUtils.setCdmaOtaInCallScreenUiState(
+ OtaUtils.CdmaOtaInCallScreenUiState.State.NORMAL);
+ break;
+
+ case OTA_ENDED:
+ mApp.otaUtils.setCdmaOtaInCallScreenUiState(
+ OtaUtils.CdmaOtaInCallScreenUiState.State.ENDED);
+ break;
+
+ case UNDEFINED:
+ // Set our Activities intent to ACTION_UNDEFINED so
+ // that if we get resumed after we've completed a call
+ // the next call will not cause checkIsOtaCall to
+ // return true.
+ //
+ // TODO(OTASP): update these comments
+ //
+ // With the framework as of October 2009 the sequence below
+ // causes the framework to call onResume, onPause, onNewIntent,
+ // onResume. If we don't call setIntent below then when the
+ // first onResume calls checkIsOtaCall via checkOtaspStateOnResume it will
+ // return true and the Activity will be confused.
+ //
+ // 1) Power up Phone A
+ // 2) Place *22899 call and activate Phone A
+ // 3) Press the power key on Phone A to turn off the display
+ // 4) Call Phone A from Phone B answering Phone A
+ // 5) The screen will be blank (Should be normal InCallScreen)
+ // 6) Hang up the Phone B
+ // 7) Phone A displays the activation screen.
+ //
+ // Step 3 is the critical step to cause the onResume, onPause
+ // onNewIntent, onResume sequence. If step 3 is skipped the
+ // sequence will be onNewIntent, onResume and all will be well.
+ setIntent(new Intent(ACTION_UNDEFINED));
+
+ // Cleanup Ota Screen if necessary and set the panel
+ // to VISIBLE.
+ if (mCM.getState() != PhoneConstants.State.OFFHOOK) {
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.cleanOtaScreen(true);
+ }
+ } else {
+ log("WARNING: Setting mode to UNDEFINED but phone is OFFHOOK,"
+ + " skip cleanOtaScreen.");
+ }
+ break;
+ }
+ }
+
+ /**
+ * @return true if the "Manage conference" UI is currently visible.
+ */
+ /* package */ boolean isManageConferenceMode() {
+ return (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.MANAGE_CONFERENCE);
+ }
+
+ /**
+ * Checks if the "Manage conference" UI needs to be updated.
+ * If the state of the current conference call has changed
+ * since our previous call to updateManageConferencePanel()),
+ * do a fresh update. Also, if the current call is no longer a
+ * conference call at all, bail out of the "Manage conference" UI and
+ * return to InCallScreenMode.NORMAL mode.
+ */
+ private void updateManageConferencePanelIfNecessary() {
+ if (VDBG) log("updateManageConferencePanelIfNecessary: " + mCM.getActiveFgCall() + "...");
+
+ List<Connection> connections = mCM.getFgCallConnections();
+ if (connections == null) {
+ if (VDBG) log("==> no connections on foreground call!");
+ // Hide the Manage Conference panel, return to NORMAL mode.
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ SyncWithPhoneStateStatus status = syncWithPhoneState();
+ if (status != SyncWithPhoneStateStatus.SUCCESS) {
+ Log.w(LOG_TAG, "- syncWithPhoneState failed! status = " + status);
+ // We shouldn't even be in the in-call UI in the first
+ // place, so bail out:
+ if (DBG) log("updateManageConferencePanelIfNecessary: endInCallScreenSession... 1");
+ endInCallScreenSession();
+ return;
+ }
+ return;
+ }
+
+ int numConnections = connections.size();
+ if (numConnections <= 1) {
+ if (VDBG) log("==> foreground call no longer a conference!");
+ // Hide the Manage Conference panel, return to NORMAL mode.
+ setInCallScreenMode(InCallScreenMode.NORMAL);
+ SyncWithPhoneStateStatus status = syncWithPhoneState();
+ if (status != SyncWithPhoneStateStatus.SUCCESS) {
+ Log.w(LOG_TAG, "- syncWithPhoneState failed! status = " + status);
+ // We shouldn't even be in the in-call UI in the first
+ // place, so bail out:
+ if (DBG) log("updateManageConferencePanelIfNecessary: endInCallScreenSession... 2");
+ endInCallScreenSession();
+ return;
+ }
+ return;
+ }
+
+ // TODO: the test to see if numConnections has changed can go in
+ // updateManageConferencePanel(), rather than here.
+ if (numConnections != mManageConferenceUtils.getNumCallersInConference()) {
+ if (VDBG) log("==> Conference size has changed; need to rebuild UI!");
+ mManageConferenceUtils.updateManageConferencePanel(connections);
+ }
+ }
+
+ /**
+ * Updates {@link #mCallCard}'s visibility state per DTMF dialpad visibility. They
+ * cannot be shown simultaneously and thus we should reflect DTMF dialpad visibility into
+ * another.
+ *
+ * Note: During OTA calls or users' managing conference calls, we should *not* call this method
+ * but manually manage both visibility.
+ *
+ * @see #updateScreen()
+ */
+ private void updateCallCardVisibilityPerDialerState(boolean animate) {
+ // We need to hide the CallCard while the dialpad is visible.
+ if (isDialerOpened()) {
+ if (VDBG) {
+ log("- updateCallCardVisibilityPerDialerState(animate="
+ + animate + "): dialpad open, hide mCallCard...");
+ }
+ if (animate) {
+ AnimationUtils.Fade.hide(mCallCard, View.GONE);
+ } else {
+ mCallCard.setVisibility(View.GONE);
+ }
+ } else {
+ // Dialpad is dismissed; bring back the CallCard if it's supposed to be visible.
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.NORMAL)
+ || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.CALL_ENDED)) {
+ if (VDBG) {
+ log("- updateCallCardVisibilityPerDialerState(animate="
+ + animate + "): dialpad dismissed, show mCallCard...");
+ }
+ if (animate) {
+ AnimationUtils.Fade.show(mCallCard);
+ } else {
+ mCallCard.setVisibility(View.VISIBLE);
+ }
+ }
+ }
+ }
+
+ /**
+ * @see DTMFTwelveKeyDialer#isOpened()
+ */
+ /* package */ boolean isDialerOpened() {
+ return (mDialer != null && mDialer.isOpened());
+ }
+
+ /**
+ * Called any time the DTMF dialpad is opened.
+ * @see DTMFTwelveKeyDialer#openDialer(boolean)
+ */
+ /* package */ void onDialerOpen(boolean animate) {
+ if (DBG) log("onDialerOpen()...");
+
+ // Update the in-call touch UI.
+ updateInCallTouchUi();
+
+ // Update CallCard UI, which depends on the dialpad.
+ updateCallCardVisibilityPerDialerState(animate);
+
+ // This counts as explicit "user activity".
+ mApp.pokeUserActivity();
+
+ //If on OTA Call, hide OTA Screen
+ // TODO: This may not be necessary, now that the dialpad is
+ // always visible in OTA mode.
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL
+ || mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)
+ && mApp.otaUtils != null) {
+ mApp.otaUtils.hideOtaScreen();
+ }
+ }
+
+ /**
+ * Called any time the DTMF dialpad is closed.
+ * @see DTMFTwelveKeyDialer#closeDialer(boolean)
+ */
+ /* package */ void onDialerClose(boolean animate) {
+ if (DBG) log("onDialerClose()...");
+
+ // OTA-specific cleanup upon closing the dialpad.
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL)
+ || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)
+ || ((mApp.cdmaOtaScreenState != null)
+ && (mApp.cdmaOtaScreenState.otaScreenState ==
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION))) {
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.otaShowProperScreen();
+ }
+ }
+
+ // Update the in-call touch UI.
+ updateInCallTouchUi();
+
+ // Update CallCard UI, which depends on the dialpad.
+ updateCallCardVisibilityPerDialerState(animate);
+
+ // This counts as explicit "user activity".
+ mApp.pokeUserActivity();
+ }
+
+ /**
+ * Determines when we can dial DTMF tones.
+ */
+ /* package */ boolean okToDialDTMFTones() {
+ final boolean hasRingingCall = mCM.hasActiveRingingCall();
+ final Call.State fgCallState = mCM.getActiveFgCallState();
+
+ // We're allowed to send DTMF tones when there's an ACTIVE
+ // foreground call, and not when an incoming call is ringing
+ // (since DTMF tones are useless in that state), or if the
+ // Manage Conference UI is visible (since the tab interferes
+ // with the "Back to call" button.)
+
+ // We can also dial while in ALERTING state because there are
+ // some connections that never update to an ACTIVE state (no
+ // indication from the network).
+ boolean canDial =
+ (fgCallState == Call.State.ACTIVE || fgCallState == Call.State.ALERTING)
+ && !hasRingingCall
+ && (mApp.inCallUiState.inCallScreenMode != InCallScreenMode.MANAGE_CONFERENCE);
+
+ if (VDBG) log ("[okToDialDTMFTones] foreground state: " + fgCallState +
+ ", ringing state: " + hasRingingCall +
+ ", call screen mode: " + mApp.inCallUiState.inCallScreenMode +
+ ", result: " + canDial);
+
+ return canDial;
+ }
+
+ /**
+ * @return true if the in-call DTMF dialpad should be available to the
+ * user, given the current state of the phone and the in-call UI.
+ * (This is used to control the enabledness of the "Show
+ * dialpad" onscreen button; see InCallControlState.dialpadEnabled.)
+ */
+ /* package */ boolean okToShowDialpad() {
+ // Very similar to okToDialDTMFTones(), but allow DIALING here.
+ final Call.State fgCallState = mCM.getActiveFgCallState();
+ return okToDialDTMFTones() || (fgCallState == Call.State.DIALING);
+ }
+
+ /**
+ * Initializes the in-call touch UI on devices that need it.
+ */
+ private void initInCallTouchUi() {
+ if (DBG) log("initInCallTouchUi()...");
+ // TODO: we currently use the InCallTouchUi widget in at least
+ // some states on ALL platforms. But if some devices ultimately
+ // end up not using *any* onscreen touch UI, we should make sure
+ // to not even inflate the InCallTouchUi widget on those devices.
+ mInCallTouchUi = (InCallTouchUi) findViewById(R.id.inCallTouchUi);
+ mInCallTouchUi.setInCallScreenInstance(this);
+
+ // RespondViaSmsManager implements the "Respond via SMS"
+ // feature that's triggered from the incoming call widget.
+ mRespondViaSmsManager = new RespondViaSmsManager();
+ mRespondViaSmsManager.setInCallScreenInstance(this);
+ }
+
+ /**
+ * Updates the state of the in-call touch UI.
+ */
+ private void updateInCallTouchUi() {
+ if (mInCallTouchUi != null) {
+ mInCallTouchUi.updateState(mCM);
+ }
+ }
+
+ /**
+ * @return the InCallTouchUi widget
+ */
+ /* package */ InCallTouchUi getInCallTouchUi() {
+ return mInCallTouchUi;
+ }
+
+ /**
+ * Posts a handler message telling the InCallScreen to refresh the
+ * onscreen in-call UI.
+ *
+ * This is just a wrapper around updateScreen(), for use by the
+ * rest of the phone app or from a thread other than the UI thread.
+ *
+ * updateScreen() is a no-op if the InCallScreen is not the foreground
+ * activity, so it's safe to call this whether or not the InCallScreen
+ * is currently visible.
+ */
+ /* package */ void requestUpdateScreen() {
+ if (DBG) log("requestUpdateScreen()...");
+ mHandler.removeMessages(REQUEST_UPDATE_SCREEN);
+ mHandler.sendEmptyMessage(REQUEST_UPDATE_SCREEN);
+ }
+
+ /**
+ * @return true if we're in restricted / emergency dialing only mode.
+ */
+ public boolean isPhoneStateRestricted() {
+ // TODO: This needs to work IN TANDEM with the KeyGuardViewMediator Code.
+ // Right now, it looks like the mInputRestricted flag is INTERNAL to the
+ // KeyGuardViewMediator and SPECIFICALLY set to be FALSE while the emergency
+ // phone call is being made, to allow for input into the InCallScreen.
+ // Having the InCallScreen judge the state of the device from this flag
+ // becomes meaningless since it is always false for us. The mediator should
+ // have an additional API to let this app know that it should be restricted.
+ int serviceState = mCM.getServiceState();
+ return ((serviceState == ServiceState.STATE_EMERGENCY_ONLY) ||
+ (serviceState == ServiceState.STATE_OUT_OF_SERVICE) ||
+ (mApp.getKeyguardManager().inKeyguardRestrictedInputMode()));
+ }
+
+
+ //
+ // Bluetooth helper methods.
+ //
+ // - BluetoothAdapter is the Bluetooth system service. If
+ // getDefaultAdapter() returns null
+ // then the device is not BT capable. Use BluetoothDevice.isEnabled()
+ // to see if BT is enabled on the device.
+ //
+ // - BluetoothHeadset is the API for the control connection to a
+ // Bluetooth Headset. This lets you completely connect/disconnect a
+ // headset (which we don't do from the Phone UI!) but also lets you
+ // get the address of the currently active headset and see whether
+ // it's currently connected.
+
+ /**
+ * @return true if the Bluetooth on/off switch in the UI should be
+ * available to the user (i.e. if the device is BT-capable
+ * and a headset is connected.)
+ */
+ /* package */ boolean isBluetoothAvailable() {
+ if (VDBG) log("isBluetoothAvailable()...");
+
+ // There's no need to ask the Bluetooth system service if BT is enabled:
+ //
+ // BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
+ // if ((adapter == null) || !adapter.isEnabled()) {
+ // if (DBG) log(" ==> FALSE (BT not enabled)");
+ // return false;
+ // }
+ // if (DBG) log(" - BT enabled! device name " + adapter.getName()
+ // + ", address " + adapter.getAddress());
+ //
+ // ...since we already have a BluetoothHeadset instance. We can just
+ // call isConnected() on that, and assume it'll be false if BT isn't
+ // enabled at all.
+
+ // Check if there's a connected headset, using the BluetoothHeadset API.
+ boolean isConnected = false;
+ if (mBluetoothHeadset != null) {
+ List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+ if (deviceList.size() > 0) {
+ BluetoothDevice device = deviceList.get(0);
+ isConnected = true;
+
+ if (VDBG) log(" - headset state = " +
+ mBluetoothHeadset.getConnectionState(device));
+ if (VDBG) log(" - headset address: " + device);
+ if (VDBG) log(" - isConnected: " + isConnected);
+ }
+ }
+
+ if (VDBG) log(" ==> " + isConnected);
+ return isConnected;
+ }
+
+ /**
+ * @return true if a BT Headset is available, and its audio is currently connected.
+ */
+ /* package */ boolean isBluetoothAudioConnected() {
+ if (mBluetoothHeadset == null) {
+ if (VDBG) log("isBluetoothAudioConnected: ==> FALSE (null mBluetoothHeadset)");
+ return false;
+ }
+ List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+ if (deviceList.isEmpty()) {
+ return false;
+ }
+ BluetoothDevice device = deviceList.get(0);
+ boolean isAudioOn = mBluetoothHeadset.isAudioConnected(device);
+ if (VDBG) log("isBluetoothAudioConnected: ==> isAudioOn = " + isAudioOn);
+ return isAudioOn;
+ }
+
+ /**
+ * Helper method used to control the onscreen "Bluetooth" indication;
+ * see InCallControlState.bluetoothIndicatorOn.
+ *
+ * @return true if a BT device is available and its audio is currently connected,
+ * <b>or</b> if we issued a BluetoothHeadset.connectAudio()
+ * call within the last 5 seconds (which presumably means
+ * that the BT audio connection is currently being set
+ * up, and will be connected soon.)
+ */
+ /* package */ boolean isBluetoothAudioConnectedOrPending() {
+ if (isBluetoothAudioConnected()) {
+ if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> TRUE (really connected)");
+ return true;
+ }
+
+ // If we issued a connectAudio() call "recently enough", even
+ // if BT isn't actually connected yet, let's still pretend BT is
+ // on. This makes the onscreen indication more responsive.
+ if (mBluetoothConnectionPending) {
+ long timeSinceRequest =
+ SystemClock.elapsedRealtime() - mBluetoothConnectionRequestTime;
+ if (timeSinceRequest < 5000 /* 5 seconds */) {
+ if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> TRUE (requested "
+ + timeSinceRequest + " msec ago)");
+ return true;
+ } else {
+ if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> FALSE (request too old: "
+ + timeSinceRequest + " msec ago)");
+ mBluetoothConnectionPending = false;
+ return false;
+ }
+ }
+
+ if (VDBG) log("isBluetoothAudioConnectedOrPending: ==> FALSE");
+ return false;
+ }
+
+ /**
+ * Posts a message to our handler saying to update the onscreen UI
+ * based on a bluetooth headset state change.
+ */
+ /* package */ void requestUpdateBluetoothIndication() {
+ if (VDBG) log("requestUpdateBluetoothIndication()...");
+ // No need to look at the current state here; any UI elements that
+ // care about the bluetooth state (i.e. the CallCard) get
+ // the necessary state directly from PhoneApp.showBluetoothIndication().
+ mHandler.removeMessages(REQUEST_UPDATE_BLUETOOTH_INDICATION);
+ mHandler.sendEmptyMessage(REQUEST_UPDATE_BLUETOOTH_INDICATION);
+ }
+
+ private void dumpBluetoothState() {
+ log("============== dumpBluetoothState() =============");
+ log("= isBluetoothAvailable: " + isBluetoothAvailable());
+ log("= isBluetoothAudioConnected: " + isBluetoothAudioConnected());
+ log("= isBluetoothAudioConnectedOrPending: " + isBluetoothAudioConnectedOrPending());
+ log("= PhoneApp.showBluetoothIndication: "
+ + mApp.showBluetoothIndication());
+ log("=");
+ if (mBluetoothAdapter != null) {
+ if (mBluetoothHeadset != null) {
+ List<BluetoothDevice> deviceList = mBluetoothHeadset.getConnectedDevices();
+
+ if (deviceList.size() > 0) {
+ BluetoothDevice device = deviceList.get(0);
+ log("= BluetoothHeadset.getCurrentDevice: " + device);
+ log("= BluetoothHeadset.State: "
+ + mBluetoothHeadset.getConnectionState(device));
+ log("= BluetoothHeadset audio connected: " +
+ mBluetoothHeadset.isAudioConnected(device));
+ }
+ } else {
+ log("= mBluetoothHeadset is null");
+ }
+ } else {
+ log("= mBluetoothAdapter is null; device is not BT capable");
+ }
+ }
+
+ /* package */ void connectBluetoothAudio() {
+ if (VDBG) log("connectBluetoothAudio()...");
+ if (mBluetoothHeadset != null) {
+ // TODO(BT) check return
+ mBluetoothHeadset.connectAudio();
+ }
+
+ // Watch out: The bluetooth connection doesn't happen instantly;
+ // the connectAudio() call returns instantly but does its real
+ // work in another thread. The mBluetoothConnectionPending flag
+ // is just a little trickery to ensure that the onscreen UI updates
+ // instantly. (See isBluetoothAudioConnectedOrPending() above.)
+ mBluetoothConnectionPending = true;
+ mBluetoothConnectionRequestTime = SystemClock.elapsedRealtime();
+ }
+
+ /* package */ void disconnectBluetoothAudio() {
+ if (VDBG) log("disconnectBluetoothAudio()...");
+ if (mBluetoothHeadset != null) {
+ mBluetoothHeadset.disconnectAudio();
+ }
+ mBluetoothConnectionPending = false;
+ }
+
+ /**
+ * Posts a handler message telling the InCallScreen to close
+ * the OTA failure notice after the specified delay.
+ * @see OtaUtils.otaShowProgramFailureNotice
+ */
+ /* package */ void requestCloseOtaFailureNotice(long timeout) {
+ if (DBG) log("requestCloseOtaFailureNotice() with timeout: " + timeout);
+ mHandler.sendEmptyMessageDelayed(REQUEST_CLOSE_OTA_FAILURE_NOTICE, timeout);
+
+ // TODO: we probably ought to call removeMessages() for this
+ // message code in either onPause or onResume, just to be 100%
+ // sure that the message we just posted has no way to affect a
+ // *different* call if the user quickly backs out and restarts.
+ // (This is also true for requestCloseSpcErrorNotice() below, and
+ // probably anywhere else we use mHandler.sendEmptyMessageDelayed().)
+ }
+
+ /**
+ * Posts a handler message telling the InCallScreen to close
+ * the SPC error notice after the specified delay.
+ * @see OtaUtils.otaShowSpcErrorNotice
+ */
+ /* package */ void requestCloseSpcErrorNotice(long timeout) {
+ if (DBG) log("requestCloseSpcErrorNotice() with timeout: " + timeout);
+ mHandler.sendEmptyMessageDelayed(REQUEST_CLOSE_SPC_ERROR_NOTICE, timeout);
+ }
+
+ public boolean isOtaCallInActiveState() {
+ if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL)
+ || ((mApp.cdmaOtaScreenState != null)
+ && (mApp.cdmaOtaScreenState.otaScreenState ==
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION))) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Handle OTA Call End scenario when display becomes dark during OTA Call
+ * and InCallScreen is in pause mode. CallNotifier will listen for call
+ * end indication and call this api to handle OTA Call end scenario
+ */
+ public void handleOtaCallEnd() {
+ if (DBG) log("handleOtaCallEnd entering");
+ if (((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL)
+ || ((mApp.cdmaOtaScreenState != null)
+ && (mApp.cdmaOtaScreenState.otaScreenState !=
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED)))
+ && ((mApp.cdmaOtaProvisionData != null)
+ && (!mApp.cdmaOtaProvisionData.inOtaSpcState))) {
+ if (DBG) log("handleOtaCallEnd - Set OTA Call End stater");
+ setInCallScreenMode(InCallScreenMode.OTA_ENDED);
+ updateScreen();
+ }
+ }
+
+ public boolean isOtaCallInEndState() {
+ return (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED);
+ }
+
+
+ /**
+ * Upon resuming the in-call UI, check to see if an OTASP call is in
+ * progress, and if so enable the special OTASP-specific UI.
+ *
+ * TODO: have a simple single flag in InCallUiState for this rather than
+ * needing to know about all those mApp.cdma*State objects.
+ *
+ * @return true if any OTASP-related UI is active
+ */
+ private boolean checkOtaspStateOnResume() {
+ // If there's no OtaUtils instance, that means we haven't even tried
+ // to start an OTASP call (yet), so there's definitely nothing to do here.
+ if (mApp.otaUtils == null) {
+ if (DBG) log("checkOtaspStateOnResume: no OtaUtils instance; nothing to do.");
+ return false;
+ }
+
+ if ((mApp.cdmaOtaScreenState == null) || (mApp.cdmaOtaProvisionData == null)) {
+ // Uh oh -- something wrong with our internal OTASP state.
+ // (Since this is an OTASP-capable device, these objects
+ // *should* have already been created by PhoneApp.onCreate().)
+ throw new IllegalStateException("checkOtaspStateOnResume: "
+ + "app.cdmaOta* objects(s) not initialized");
+ }
+
+ // The PhoneApp.cdmaOtaInCallScreenUiState instance is the
+ // authoritative source saying whether or not the in-call UI should
+ // show its OTASP-related UI.
+
+ OtaUtils.CdmaOtaInCallScreenUiState.State cdmaOtaInCallScreenState =
+ mApp.otaUtils.getCdmaOtaInCallScreenUiState();
+ // These states are:
+ // - UNDEFINED: no OTASP-related UI is visible
+ // - NORMAL: OTASP call in progress, so show in-progress OTASP UI
+ // - ENDED: OTASP call just ended, so show success/failure indication
+
+ boolean otaspUiActive =
+ (cdmaOtaInCallScreenState == OtaUtils.CdmaOtaInCallScreenUiState.State.NORMAL)
+ || (cdmaOtaInCallScreenState == OtaUtils.CdmaOtaInCallScreenUiState.State.ENDED);
+
+ if (otaspUiActive) {
+ // Make sure the OtaUtils instance knows about the InCallScreen's
+ // OTASP-related UI widgets.
+ //
+ // (This call has no effect if the UI widgets have already been set up.
+ // It only really matters the very first time that the InCallScreen instance
+ // is onResume()d after starting an OTASP call.)
+ mApp.otaUtils.updateUiWidgets(this, mInCallTouchUi, mCallCard);
+
+ // Also update the InCallScreenMode based on the cdmaOtaInCallScreenState.
+
+ if (cdmaOtaInCallScreenState == OtaUtils.CdmaOtaInCallScreenUiState.State.NORMAL) {
+ if (DBG) log("checkOtaspStateOnResume - in OTA Normal mode");
+ setInCallScreenMode(InCallScreenMode.OTA_NORMAL);
+ } else if (cdmaOtaInCallScreenState ==
+ OtaUtils.CdmaOtaInCallScreenUiState.State.ENDED) {
+ if (DBG) log("checkOtaspStateOnResume - in OTA END mode");
+ setInCallScreenMode(InCallScreenMode.OTA_ENDED);
+ }
+
+ // TODO(OTASP): we might also need to go into OTA_ENDED mode
+ // in one extra case:
+ //
+ // else if (mApp.cdmaOtaScreenState.otaScreenState ==
+ // CdmaOtaScreenState.OtaScreenState.OTA_STATUS_SUCCESS_FAILURE_DLG) {
+ // if (DBG) log("checkOtaspStateOnResume - set OTA END Mode");
+ // setInCallScreenMode(InCallScreenMode.OTA_ENDED);
+ // }
+
+ } else {
+ // OTASP is not active; reset to regular in-call UI.
+
+ if (DBG) log("checkOtaspStateOnResume - Set OTA NORMAL Mode");
+ setInCallScreenMode(InCallScreenMode.OTA_NORMAL);
+
+ if (mApp.otaUtils != null) {
+ mApp.otaUtils.cleanOtaScreen(false);
+ }
+ }
+
+ // TODO(OTASP):
+ // The original check from checkIsOtaCall() when handling ACTION_MAIN was this:
+ //
+ // [ . . . ]
+ // else if (action.equals(intent.ACTION_MAIN)) {
+ // if (DBG) log("checkIsOtaCall action ACTION_MAIN");
+ // boolean isRingingCall = mCM.hasActiveRingingCall();
+ // if (isRingingCall) {
+ // if (DBG) log("checkIsOtaCall isRingingCall: " + isRingingCall);
+ // return false;
+ // } else if ((mApp.cdmaOtaInCallScreenUiState.state
+ // == CdmaOtaInCallScreenUiState.State.NORMAL)
+ // || (mApp.cdmaOtaInCallScreenUiState.state
+ // == CdmaOtaInCallScreenUiState.State.ENDED)) {
+ // if (DBG) log("action ACTION_MAIN, OTA call already in progress");
+ // isOtaCall = true;
+ // } else {
+ // if (mApp.cdmaOtaScreenState.otaScreenState !=
+ // CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED) {
+ // if (DBG) log("checkIsOtaCall action ACTION_MAIN, "
+ // + "OTA call in progress with UNDEFINED");
+ // isOtaCall = true;
+ // }
+ // }
+ // }
+ //
+ // Also, in internalResolveIntent() we used to do this:
+ //
+ // if ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_NORMAL)
+ // || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.OTA_ENDED)) {
+ // // If in OTA Call, update the OTA UI
+ // updateScreen();
+ // return;
+ // }
+ //
+ // We still need more cleanup to simplify the mApp.cdma*State objects.
+
+ return otaspUiActive;
+ }
+
+ /**
+ * Updates and returns the InCallControlState instance.
+ */
+ public InCallControlState getUpdatedInCallControlState() {
+ if (VDBG) log("getUpdatedInCallControlState()...");
+ mInCallControlState.update();
+ return mInCallControlState;
+ }
+
+ public void resetInCallScreenMode() {
+ if (DBG) log("resetInCallScreenMode: setting mode to UNDEFINED...");
+ setInCallScreenMode(InCallScreenMode.UNDEFINED);
+ }
+
+ /**
+ * Updates the onscreen hint displayed while the user is dragging one
+ * of the handles of the RotarySelector widget used for incoming
+ * calls.
+ *
+ * @param hintTextResId resource ID of the hint text to display,
+ * or 0 if no hint should be visible.
+ * @param hintColorResId resource ID for the color of the hint text
+ */
+ /* package */ void updateIncomingCallWidgetHint(int hintTextResId, int hintColorResId) {
+ if (VDBG) log("updateIncomingCallWidgetHint(" + hintTextResId + ")...");
+ if (mCallCard != null) {
+ mCallCard.setIncomingCallWidgetHint(hintTextResId, hintColorResId);
+ mCallCard.updateState(mCM);
+ // TODO: if hintTextResId == 0, consider NOT clearing the onscreen
+ // hint right away, but instead post a delayed handler message to
+ // keep it onscreen for an extra second or two. (This might make
+ // the hint more helpful if the user quickly taps one of the
+ // handles without dragging at all...)
+ // (Or, maybe this should happen completely within the RotarySelector
+ // widget, since the widget itself probably wants to keep the colored
+ // arrow visible for some extra time also...)
+ }
+ }
+
+
+ /**
+ * Used when we need to update buttons outside InCallTouchUi's updateInCallControls() along
+ * with that method being called. CallCard may call this too because it doesn't have
+ * enough information to update buttons inside itself (more specifically, the class cannot
+ * obtain mInCallControllState without some side effect. See also
+ * {@link #getUpdatedInCallControlState()}. We probably don't want a method like
+ * getRawCallControlState() which returns raw intance with no side effect just for this
+ * corner case scenario)
+ *
+ * TODO: need better design for buttons outside InCallTouchUi.
+ */
+ /* package */ void updateButtonStateOutsideInCallTouchUi() {
+ if (mCallCard != null) {
+ mCallCard.setSecondaryCallClickable(mInCallControlState.canSwap);
+ }
+ }
+
+ @Override
+ public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+ super.dispatchPopulateAccessibilityEvent(event);
+ mCallCard.dispatchPopulateAccessibilityEvent(event);
+ return true;
+ }
+
+ /**
+ * Manually handle configuration changes.
+ *
+ * Originally android:configChanges was set to "orientation|keyboardHidden|uiMode"
+ * in order "to make sure the system doesn't destroy and re-create us due to the
+ * above config changes". However it is currently set to "keyboardHidden" since
+ * the system needs to handle rotation when inserted into a compatible cardock.
+ * Even without explicitly handling orientation and uiMode, the app still runs
+ * and does not drop the call when rotated.
+ *
+ */
+ public void onConfigurationChanged(Configuration newConfig) {
+ if (DBG) log("onConfigurationChanged: newConfig = " + newConfig);
+
+ // Note: At the time this function is called, our Resources object
+ // will have already been updated to return resource values matching
+ // the new configuration.
+
+ // Watch out: we *can* still get destroyed and recreated if a
+ // configuration change occurs that is *not* listed in the
+ // android:configChanges attribute. TODO: Any others we need to list?
+
+ super.onConfigurationChanged(newConfig);
+
+ // Nothing else to do here, since (currently) the InCallScreen looks
+ // exactly the same regardless of configuration.
+ // (Specifically, we'll never be in landscape mode because we set
+ // android:screenOrientation="portrait" in our manifest, and we don't
+ // change our UI at all based on newConfig.keyboardHidden or
+ // newConfig.uiMode.)
+
+ // TODO: we do eventually want to handle at least some config changes, such as:
+ boolean isKeyboardOpen = (newConfig.keyboardHidden == Configuration.KEYBOARDHIDDEN_NO);
+ if (DBG) log(" - isKeyboardOpen = " + isKeyboardOpen);
+ boolean isLandscape = (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE);
+ if (DBG) log(" - isLandscape = " + isLandscape);
+ if (DBG) log(" - uiMode = " + newConfig.uiMode);
+ // See bug 2089513.
+ }
+
+ /**
+ * Handles an incoming RING event from the telephony layer.
+ */
+ private void onIncomingRing() {
+ if (DBG) log("onIncomingRing()...");
+ // IFF we're visible, forward this event to the InCallTouchUi
+ // instance (which uses this event to drive the animation of the
+ // incoming-call UI.)
+ if (mIsForegroundActivity && (mInCallTouchUi != null)) {
+ mInCallTouchUi.onIncomingRing();
+ }
+ }
+
+ /**
+ * Handles a "new ringing connection" event from the telephony layer.
+ *
+ * This event comes in right at the start of the incoming-call sequence,
+ * exactly once per incoming call.
+ *
+ * Watch out: this won't be called if InCallScreen isn't ready yet,
+ * which typically happens for the first incoming phone call (even before
+ * the possible first outgoing call).
+ */
+ private void onNewRingingConnection() {
+ if (DBG) log("onNewRingingConnection()...");
+
+ // We use this event to reset any incoming-call-related UI elements
+ // that might have been left in an inconsistent state after a prior
+ // incoming call.
+ // (Note we do this whether or not we're the foreground activity,
+ // since this event comes in *before* we actually get launched to
+ // display the incoming-call UI.)
+
+ // If there's a "Respond via SMS" popup still around since the
+ // last time we were the foreground activity, make sure it's not
+ // still active(!) since that would interfere with *this* incoming
+ // call.
+ // (Note that we also do this same check in onResume(). But we
+ // need it here too, to make sure the popup gets reset in the case
+ // where a call-waiting call comes in while the InCallScreen is
+ // already in the foreground.)
+ mRespondViaSmsManager.dismissPopup(); // safe even if already dismissed
+ }
+
+ /**
+ * Enables or disables the status bar "window shade" based on the current situation.
+ */
+ private void updateExpandedViewState() {
+ if (mIsForegroundActivity) {
+ if (mApp.proximitySensorModeEnabled()) {
+ // We should not enable notification's expanded view on RINGING state.
+ mApp.notificationMgr.statusBarHelper.enableExpandedView(
+ mCM.getState() != PhoneConstants.State.RINGING);
+ } else {
+ // If proximity sensor is unavailable on the device, disable it to avoid false
+ // touches toward notifications.
+ mApp.notificationMgr.statusBarHelper.enableExpandedView(false);
+ }
+ } else {
+ mApp.notificationMgr.statusBarHelper.enableExpandedView(true);
+ }
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+
+ /**
+ * Requests to remove provider info frame after having
+ * {@link #PROVIDER_INFO_TIMEOUT}) msec delay.
+ */
+ /* package */ void requestRemoveProviderInfoWithDelay() {
+ // Remove any zombie messages and then send a message to
+ // self to remove the provider info after some time.
+ mHandler.removeMessages(EVENT_HIDE_PROVIDER_INFO);
+ Message msg = Message.obtain(mHandler, EVENT_HIDE_PROVIDER_INFO);
+ mHandler.sendMessageDelayed(msg, PROVIDER_INFO_TIMEOUT);
+ if (DBG) {
+ log("Requested to remove provider info after " + PROVIDER_INFO_TIMEOUT + " msec.");
+ }
+ }
+
+ /**
+ * Indicates whether or not the QuickResponseDialog is currently showing in the call screen
+ */
+ public boolean isQuickResponseDialogShowing() {
+ return mRespondViaSmsManager != null && mRespondViaSmsManager.isShowingPopup();
+ }
+}
diff --git a/src/com/android/phone/InCallScreenShowActivation.java b/src/com/android/phone/InCallScreenShowActivation.java
new file mode 100644
index 0000000..221b915
--- /dev/null
+++ b/src/com/android/phone/InCallScreenShowActivation.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.TelephonyCapabilities;
+
+/**
+ * Invisible activity that handles the com.android.phone.PERFORM_CDMA_PROVISIONING intent.
+ * This activity is protected by the android.permission.PERFORM_CDMA_PROVISIONING permission.
+ *
+ * We handle the PERFORM_CDMA_PROVISIONING action by launching an OTASP
+ * call via one of the OtaUtils helper methods: startInteractiveOtasp() on
+ * regular phones, or startNonInteractiveOtasp() on data-only devices.
+ *
+ * TODO: The class name InCallScreenShowActivation is misleading, since
+ * this activity is totally unrelated to the InCallScreen (which
+ * implements the in-call UI.) Let's eventually rename this to something
+ * like CdmaProvisioningLauncher or CdmaProvisioningHandler...
+ */
+public class InCallScreenShowActivation extends Activity {
+ private static final String LOG_TAG = "InCallScreenShowActivation";
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ Intent intent = getIntent();
+ if (DBG) Log.d(LOG_TAG, "onCreate: intent = " + intent);
+ Bundle extras = intent.getExtras();
+ if (DBG && (extras != null)) {
+ Log.d(LOG_TAG, " - has extras: size = " + extras.size()); // forces an unparcel()
+ Log.d(LOG_TAG, " - extras = " + extras);
+ }
+
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ Phone phone = app.getPhone();
+ if (!TelephonyCapabilities.supportsOtasp(phone)) {
+ Log.w(LOG_TAG, "CDMA Provisioning not supported on this device");
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+
+ if (intent.getAction().equals(OtaUtils.ACTION_PERFORM_CDMA_PROVISIONING)) {
+
+ // On voice-capable devices, we perform CDMA provisioning in
+ // "interactive" mode by directly launching the InCallScreen.
+ boolean interactiveMode = PhoneGlobals.sVoiceCapable;
+ Log.d(LOG_TAG, "ACTION_PERFORM_CDMA_PROVISIONING (interactiveMode = "
+ + interactiveMode + ")...");
+
+ // Testing: this intent extra allows test apps manually
+ // enable/disable "interactive mode", regardless of whether
+ // the current device is voice-capable. This is allowed only
+ // in userdebug or eng builds.
+ if (intent.hasExtra(OtaUtils.EXTRA_OVERRIDE_INTERACTIVE_MODE)
+ && (SystemProperties.getInt("ro.debuggable", 0) == 1)) {
+ interactiveMode =
+ intent.getBooleanExtra(OtaUtils.EXTRA_OVERRIDE_INTERACTIVE_MODE, false);
+ Log.d(LOG_TAG, "===> MANUALLY OVERRIDING interactiveMode to " + interactiveMode);
+ }
+
+ // We allow the caller to pass a PendingIntent (as the
+ // EXTRA_NONINTERACTIVE_OTASP_RESULT_PENDING_INTENT extra)
+ // which we'll later use to notify them when the OTASP call
+ // fails or succeeds.
+ //
+ // Stash that away here, and we'll fire it off later in
+ // OtaUtils.sendOtaspResult().
+ app.cdmaOtaScreenState.otaspResultCodePendingIntent =
+ (PendingIntent) intent.getParcelableExtra(
+ OtaUtils.EXTRA_OTASP_RESULT_CODE_PENDING_INTENT);
+
+ if (interactiveMode) {
+ // On voice-capable devices, launch an OTASP call and arrange
+ // for the in-call UI to come up. (The InCallScreen will
+ // notice that an OTASP call is active, and display the
+ // special OTASP UI instead of the usual in-call controls.)
+
+ if (DBG) Log.d(LOG_TAG, "==> Starting interactive CDMA provisioning...");
+ OtaUtils.startInteractiveOtasp(this);
+
+ // The result we set here is actually irrelevant, since the
+ // InCallScreen's "interactive" OTASP sequence never actually
+ // finish()es; it ends by directly launching the Home
+ // activity. So our caller won't actually ever get an
+ // onActivityResult() call in this case.
+ setResult(OtaUtils.RESULT_INTERACTIVE_OTASP_STARTED);
+ } else {
+ // On data-only devices, manually launch the OTASP call
+ // *without* displaying any UI. (Our caller, presumably
+ // SetupWizardActivity, is responsible for displaying some
+ // sort of progress UI.)
+
+ if (DBG) Log.d(LOG_TAG, "==> Starting non-interactive CDMA provisioning...");
+ int callStatus = OtaUtils.startNonInteractiveOtasp(this);
+
+ if (callStatus == PhoneUtils.CALL_STATUS_DIALED) {
+ if (DBG) Log.d(LOG_TAG, " ==> successful result from startNonInteractiveOtasp(): "
+ + callStatus);
+ setResult(OtaUtils.RESULT_NONINTERACTIVE_OTASP_STARTED);
+ } else {
+ Log.w(LOG_TAG, "Failure code from startNonInteractiveOtasp(): " + callStatus);
+ setResult(OtaUtils.RESULT_NONINTERACTIVE_OTASP_FAILED);
+ }
+ }
+ } else {
+ Log.e(LOG_TAG, "Unexpected intent action: " + intent);
+ setResult(RESULT_CANCELED);
+ }
+
+ finish();
+ }
+}
diff --git a/src/com/android/phone/InCallTouchUi.java b/src/com/android/phone/InCallTouchUi.java
new file mode 100644
index 0000000..a68d066
--- /dev/null
+++ b/src/com/android/phone/InCallTouchUi.java
@@ -0,0 +1,1382 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.content.Context;
+import android.graphics.drawable.LayerDrawable;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewPropertyAnimator;
+import android.view.ViewStub;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.Animation.AnimationListener;
+import android.widget.CompoundButton;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.PopupMenu;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.widget.multiwaveview.GlowPadView;
+import com.android.internal.widget.multiwaveview.GlowPadView.OnTriggerListener;
+import com.android.phone.InCallUiState.InCallScreenMode;
+
+/**
+ * In-call onscreen touch UI elements, used on some platforms.
+ *
+ * This widget is a fullscreen overlay, drawn on top of the
+ * non-touch-sensitive parts of the in-call UI (i.e. the call card).
+ */
+public class InCallTouchUi extends FrameLayout
+ implements View.OnClickListener, View.OnLongClickListener, OnTriggerListener,
+ PopupMenu.OnMenuItemClickListener, PopupMenu.OnDismissListener {
+ private static final String LOG_TAG = "InCallTouchUi";
+ private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ // Incoming call widget targets
+ private static final int ANSWER_CALL_ID = 0; // drag right
+ private static final int SEND_SMS_ID = 1; // drag up
+ private static final int DECLINE_CALL_ID = 2; // drag left
+
+ /**
+ * Reference to the InCallScreen activity that owns us. This may be
+ * null if we haven't been initialized yet *or* after the InCallScreen
+ * activity has been destroyed.
+ */
+ private InCallScreen mInCallScreen;
+
+ // Phone app instance
+ private PhoneGlobals mApp;
+
+ // UI containers / elements
+ private GlowPadView mIncomingCallWidget; // UI used for an incoming call
+ private boolean mIncomingCallWidgetIsFadingOut;
+ private boolean mIncomingCallWidgetShouldBeReset = true;
+
+ /** UI elements while on a regular call (bottom buttons, DTMF dialpad) */
+ private View mInCallControls;
+ private boolean mShowInCallControlsDuringHidingAnimation;
+
+ //
+ private ImageButton mAddButton;
+ private ImageButton mMergeButton;
+ private ImageButton mEndButton;
+ private CompoundButton mDialpadButton;
+ private CompoundButton mMuteButton;
+ private CompoundButton mAudioButton;
+ private CompoundButton mHoldButton;
+ private ImageButton mSwapButton;
+ private View mHoldSwapSpacer;
+ private View mVideoSpacer;
+ private ImageButton mVideoButton;
+
+ // "Extra button row"
+ private ViewStub mExtraButtonRow;
+ private ViewGroup mCdmaMergeButton;
+ private ViewGroup mManageConferenceButton;
+ private ImageButton mManageConferenceButtonImage;
+
+ // "Audio mode" PopupMenu
+ private PopupMenu mAudioModePopup;
+ private boolean mAudioModePopupVisible = false;
+
+ // Time of the most recent "answer" or "reject" action (see updateState())
+ private long mLastIncomingCallActionTime; // in SystemClock.uptimeMillis() time base
+
+ // Parameters for the GlowPadView "ping" animation; see triggerPing().
+ private static final boolean ENABLE_PING_ON_RING_EVENTS = false;
+ private static final boolean ENABLE_PING_AUTO_REPEAT = true;
+ private static final long PING_AUTO_REPEAT_DELAY_MSEC = 1200;
+
+ private static final int INCOMING_CALL_WIDGET_PING = 101;
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ // If the InCallScreen activity isn't around any more,
+ // there's no point doing anything here.
+ if (mInCallScreen == null) return;
+
+ switch (msg.what) {
+ case INCOMING_CALL_WIDGET_PING:
+ if (DBG) log("INCOMING_CALL_WIDGET_PING...");
+ triggerPing();
+ break;
+ default:
+ Log.wtf(LOG_TAG, "mHandler: unexpected message: " + msg);
+ break;
+ }
+ }
+ };
+
+ public InCallTouchUi(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ if (DBG) log("InCallTouchUi constructor...");
+ if (DBG) log("- this = " + this);
+ if (DBG) log("- context " + context + ", attrs " + attrs);
+ mApp = PhoneGlobals.getInstance();
+ }
+
+ void setInCallScreenInstance(InCallScreen inCallScreen) {
+ mInCallScreen = inCallScreen;
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ if (DBG) log("InCallTouchUi onFinishInflate(this = " + this + ")...");
+
+ // Look up the various UI elements.
+
+ // "Drag-to-answer" widget for incoming calls.
+ mIncomingCallWidget = (GlowPadView) findViewById(R.id.incomingCallWidget);
+ mIncomingCallWidget.setOnTriggerListener(this);
+
+ // Container for the UI elements shown while on a regular call.
+ mInCallControls = findViewById(R.id.inCallControls);
+
+ // Regular (single-tap) buttons, where we listen for click events:
+ // Main cluster of buttons:
+ mAddButton = (ImageButton) mInCallControls.findViewById(R.id.addButton);
+ mAddButton.setOnClickListener(this);
+ mAddButton.setOnLongClickListener(this);
+ mMergeButton = (ImageButton) mInCallControls.findViewById(R.id.mergeButton);
+ mMergeButton.setOnClickListener(this);
+ mMergeButton.setOnLongClickListener(this);
+ mEndButton = (ImageButton) mInCallControls.findViewById(R.id.endButton);
+ mEndButton.setOnClickListener(this);
+ mDialpadButton = (CompoundButton) mInCallControls.findViewById(R.id.dialpadButton);
+ mDialpadButton.setOnClickListener(this);
+ mDialpadButton.setOnLongClickListener(this);
+ mMuteButton = (CompoundButton) mInCallControls.findViewById(R.id.muteButton);
+ mMuteButton.setOnClickListener(this);
+ mMuteButton.setOnLongClickListener(this);
+ mAudioButton = (CompoundButton) mInCallControls.findViewById(R.id.audioButton);
+ mAudioButton.setOnClickListener(this);
+ mAudioButton.setOnLongClickListener(this);
+ mHoldButton = (CompoundButton) mInCallControls.findViewById(R.id.holdButton);
+ mHoldButton.setOnClickListener(this);
+ mHoldButton.setOnLongClickListener(this);
+ mSwapButton = (ImageButton) mInCallControls.findViewById(R.id.swapButton);
+ mSwapButton.setOnClickListener(this);
+ mSwapButton.setOnLongClickListener(this);
+ mHoldSwapSpacer = mInCallControls.findViewById(R.id.holdSwapSpacer);
+ mVideoButton = (ImageButton) mInCallControls.findViewById(R.id.videoCallButton);
+ mVideoButton.setOnClickListener(this);
+ mVideoButton.setOnLongClickListener(this);
+ mVideoSpacer = mInCallControls.findViewById(R.id.videoCallSpacer);
+
+ // TODO: Back when these buttons had text labels, we changed
+ // the label of mSwapButton for CDMA as follows:
+ //
+ // if (PhoneApp.getPhone().getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ // // In CDMA we use a generalized text - "Manage call", as behavior on selecting
+ // // this option depends entirely on what the current call state is.
+ // mSwapButtonLabel.setText(R.string.onscreenManageCallsText);
+ // } else {
+ // mSwapButtonLabel.setText(R.string.onscreenSwapCallsText);
+ // }
+ //
+ // If this is still needed, consider having a special icon for this
+ // button in CDMA.
+
+ // Buttons shown on the "extra button row", only visible in certain (rare) states.
+ mExtraButtonRow = (ViewStub) mInCallControls.findViewById(R.id.extraButtonRow);
+
+ // If in PORTRAIT, add a custom OnTouchListener to shrink the "hit target".
+ if (!PhoneUtils.isLandscape(this.getContext())) {
+ mEndButton.setOnTouchListener(new SmallerHitTargetTouchListener());
+ }
+
+ }
+
+ /**
+ * Updates the visibility and/or state of our UI elements, based on
+ * the current state of the phone.
+ *
+ * TODO: This function should be relying on a state defined by InCallScreen,
+ * and not generic call states. The incoming call screen handles more states
+ * than Call.State or PhoneConstant.State know about.
+ */
+ /* package */ void updateState(CallManager cm) {
+ if (mInCallScreen == null) {
+ log("- updateState: mInCallScreen has been destroyed; bailing out...");
+ return;
+ }
+
+ PhoneConstants.State state = cm.getState(); // IDLE, RINGING, or OFFHOOK
+ if (DBG) log("updateState: current state = " + state);
+
+ boolean showIncomingCallControls = false;
+ boolean showInCallControls = false;
+
+ final Call ringingCall = cm.getFirstActiveRingingCall();
+ final Call.State fgCallState = cm.getActiveFgCallState();
+
+ // If the FG call is dialing/alerting, we should display for that call
+ // and ignore the ringing call. This case happens when the telephony
+ // layer rejects the ringing call while the FG call is dialing/alerting,
+ // but the incoming call *does* briefly exist in the DISCONNECTING or
+ // DISCONNECTED state.
+ if ((ringingCall.getState() != Call.State.IDLE) && !fgCallState.isDialing()) {
+ // A phone call is ringing *or* call waiting.
+
+ // Watch out: even if the phone state is RINGING, it's
+ // possible for the ringing call to be in the DISCONNECTING
+ // state. (This typically happens immediately after the user
+ // rejects an incoming call, and in that case we *don't* show
+ // the incoming call controls.)
+ if (ringingCall.getState().isAlive()) {
+ if (DBG) log("- updateState: RINGING! Showing incoming call controls...");
+ showIncomingCallControls = true;
+ }
+
+ // Ugly hack to cover up slow response from the radio:
+ // if we get an updateState() call immediately after answering/rejecting a call
+ // (via onTrigger()), *don't* show the incoming call
+ // UI even if the phone is still in the RINGING state.
+ // This covers up a slow response from the radio for some actions.
+ // To detect that situation, we are using "500 msec" heuristics.
+ //
+ // Watch out: we should *not* rely on this behavior when "instant text response" action
+ // has been chosen. See also onTrigger() for why.
+ long now = SystemClock.uptimeMillis();
+ if (now < mLastIncomingCallActionTime + 500) {
+ log("updateState: Too soon after last action; not drawing!");
+ showIncomingCallControls = false;
+ }
+
+ // b/6765896
+ // If the glowview triggers two hits of the respond-via-sms gadget in
+ // quick succession, it can cause the incoming call widget to show and hide
+ // twice in a row. However, the second hide doesn't get triggered because
+ // we are already attemping to hide. This causes an additional glowview to
+ // stay up above all other screens.
+ // In reality, we shouldn't even be showing incoming-call UI while we are
+ // showing the respond-via-sms popup, so we check for that here.
+ //
+ // TODO: In the future, this entire state machine
+ // should be reworked. Respond-via-sms was stapled onto the current
+ // design (and so were other states) and should be made a first-class
+ // citizen in a new state machine.
+ if (mInCallScreen.isQuickResponseDialogShowing()) {
+ log("updateState: quickResponse visible. Cancel showing incoming call controls.");
+ showIncomingCallControls = false;
+ }
+ } else {
+ // Ok, show the regular in-call touch UI (with some exceptions):
+ if (okToShowInCallControls()) {
+ showInCallControls = true;
+ } else {
+ if (DBG) log("- updateState: NOT OK to show touch UI; disabling...");
+ }
+ }
+
+ // In usual cases we don't allow showing both incoming call controls and in-call controls.
+ //
+ // There's one exception: if this call is during fading-out animation for the incoming
+ // call controls, we need to show both for smoother transition.
+ if (showIncomingCallControls && showInCallControls) {
+ throw new IllegalStateException(
+ "'Incoming' and 'in-call' touch controls visible at the same time!");
+ }
+ if (mShowInCallControlsDuringHidingAnimation) {
+ if (DBG) {
+ log("- updateState: FORCE showing in-call controls during incoming call widget"
+ + " being hidden with animation");
+ }
+ showInCallControls = true;
+ }
+
+ // Update visibility and state of the incoming call controls or
+ // the normal in-call controls.
+
+ if (showInCallControls) {
+ if (DBG) log("- updateState: showing in-call controls...");
+ updateInCallControls(cm);
+ mInCallControls.setVisibility(View.VISIBLE);
+ } else {
+ if (DBG) log("- updateState: HIDING in-call controls...");
+ mInCallControls.setVisibility(View.GONE);
+ }
+
+ if (showIncomingCallControls) {
+ if (DBG) log("- updateState: showing incoming call widget...");
+ showIncomingCallWidget(ringingCall);
+
+ // On devices with a system bar (soft buttons at the bottom of
+ // the screen), disable navigation while the incoming-call UI
+ // is up.
+ // This prevents false touches (e.g. on the "Recents" button)
+ // from interfering with the incoming call UI, like if you
+ // accidentally touch the system bar while pulling the phone
+ // out of your pocket.
+ mApp.notificationMgr.statusBarHelper.enableSystemBarNavigation(false);
+ } else {
+ if (DBG) log("- updateState: HIDING incoming call widget...");
+ hideIncomingCallWidget();
+
+ // The system bar is allowed to work normally in regular
+ // in-call states.
+ mApp.notificationMgr.statusBarHelper.enableSystemBarNavigation(true);
+ }
+
+ // Dismiss the "Audio mode" PopupMenu if necessary.
+ //
+ // The "Audio mode" popup is only relevant in call states that support
+ // in-call audio, namely when the phone is OFFHOOK (not RINGING), *and*
+ // the foreground call is either ALERTING (where you can hear the other
+ // end ringing) or ACTIVE (when the call is actually connected.) In any
+ // state *other* than these, the popup should not be visible.
+
+ if ((state == PhoneConstants.State.OFFHOOK)
+ && (fgCallState == Call.State.ALERTING || fgCallState == Call.State.ACTIVE)) {
+ // The audio mode popup is allowed to be visible in this state.
+ // So if it's up, leave it alone.
+ } else {
+ // The Audio mode popup isn't relevant in this state, so make sure
+ // it's not visible.
+ dismissAudioModePopup(); // safe even if not active
+ }
+ }
+
+ private boolean okToShowInCallControls() {
+ // Note that this method is concerned only with the internal state
+ // of the InCallScreen. (The InCallTouchUi widget has separate
+ // logic to make sure it's OK to display the touch UI given the
+ // current telephony state, and that it's allowed on the current
+ // device in the first place.)
+
+ // The touch UI is available in the following InCallScreenModes:
+ // - NORMAL (obviously)
+ // - CALL_ENDED (which is intended to look mostly the same as
+ // a normal in-call state, even though the in-call
+ // buttons are mostly disabled)
+ // and is hidden in any of the other modes, like MANAGE_CONFERENCE
+ // or one of the OTA modes (which use totally different UIs.)
+
+ return ((mApp.inCallUiState.inCallScreenMode == InCallScreenMode.NORMAL)
+ || (mApp.inCallUiState.inCallScreenMode == InCallScreenMode.CALL_ENDED));
+ }
+
+ @Override
+ public void onClick(View view) {
+ int id = view.getId();
+ if (DBG) log("onClick(View " + view + ", id " + id + ")...");
+
+ switch (id) {
+ case R.id.addButton:
+ case R.id.mergeButton:
+ case R.id.endButton:
+ case R.id.dialpadButton:
+ case R.id.muteButton:
+ case R.id.holdButton:
+ case R.id.swapButton:
+ case R.id.cdmaMergeButton:
+ case R.id.manageConferenceButton:
+ case R.id.videoCallButton:
+ // Clicks on the regular onscreen buttons get forwarded
+ // straight to the InCallScreen.
+ mInCallScreen.handleOnscreenButtonClick(id);
+ break;
+
+ case R.id.audioButton:
+ handleAudioButtonClick();
+ break;
+
+ default:
+ Log.w(LOG_TAG, "onClick: unexpected click: View " + view + ", id " + id);
+ break;
+ }
+ }
+
+ @Override
+ public boolean onLongClick(View view) {
+ final int id = view.getId();
+ if (DBG) log("onLongClick(View " + view + ", id " + id + ")...");
+
+ switch (id) {
+ case R.id.addButton:
+ case R.id.mergeButton:
+ case R.id.dialpadButton:
+ case R.id.muteButton:
+ case R.id.holdButton:
+ case R.id.swapButton:
+ case R.id.audioButton:
+ case R.id.videoCallButton: {
+ final CharSequence description = view.getContentDescription();
+ if (!TextUtils.isEmpty(description)) {
+ // Show description as ActionBar's menu buttons do.
+ // See also ActionMenuItemView#onLongClick() for the original implementation.
+ final Toast cheatSheet =
+ Toast.makeText(view.getContext(), description, Toast.LENGTH_SHORT);
+ cheatSheet.setGravity(
+ Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, view.getHeight());
+ cheatSheet.show();
+ }
+ return true;
+ }
+ default:
+ Log.w(LOG_TAG, "onLongClick() with unexpected View " + view + ". Ignoring it.");
+ break;
+ }
+ return false;
+ }
+
+ /**
+ * Updates the enabledness and "checked" state of the buttons on the
+ * "inCallControls" panel, based on the current telephony state.
+ */
+ private void updateInCallControls(CallManager cm) {
+ int phoneType = cm.getActiveFgCall().getPhone().getPhoneType();
+
+ // Note we do NOT need to worry here about cases where the entire
+ // in-call touch UI is disabled, like during an OTA call or if the
+ // dtmf dialpad is up. (That's handled by updateState(), which
+ // calls okToShowInCallControls().)
+ //
+ // If we get here, it *is* OK to show the in-call touch UI, so we
+ // now need to update the enabledness and/or "checked" state of
+ // each individual button.
+ //
+
+ // The InCallControlState object tells us the enabledness and/or
+ // state of the various onscreen buttons:
+ InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
+
+ if (DBG) {
+ log("updateInCallControls()...");
+ inCallControlState.dumpState();
+ }
+
+ // "Add" / "Merge":
+ // These two buttons occupy the same space onscreen, so at any
+ // given point exactly one of them must be VISIBLE and the other
+ // must be GONE.
+ if (inCallControlState.canAddCall) {
+ mAddButton.setVisibility(View.VISIBLE);
+ mAddButton.setEnabled(true);
+ mMergeButton.setVisibility(View.GONE);
+ } else if (inCallControlState.canMerge) {
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ // In CDMA "Add" option is always given to the user and the
+ // "Merge" option is provided as a button on the top left corner of the screen,
+ // we always set the mMergeButton to GONE
+ mMergeButton.setVisibility(View.GONE);
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ mMergeButton.setVisibility(View.VISIBLE);
+ mMergeButton.setEnabled(true);
+ mAddButton.setVisibility(View.GONE);
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ } else {
+ // Neither "Add" nor "Merge" is available. (This happens in
+ // some transient states, like while dialing an outgoing call,
+ // and in other rare cases like if you have both lines in use
+ // *and* there are already 5 people on the conference call.)
+ // Since the common case here is "while dialing", we show the
+ // "Add" button in a disabled state so that there won't be any
+ // jarring change in the UI when the call finally connects.
+ mAddButton.setVisibility(View.VISIBLE);
+ mAddButton.setEnabled(false);
+ mMergeButton.setVisibility(View.GONE);
+ }
+ if (inCallControlState.canAddCall && inCallControlState.canMerge) {
+ if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ // Uh oh, the InCallControlState thinks that "Add" *and* "Merge"
+ // should both be available right now. This *should* never
+ // happen with GSM, but if it's possible on any
+ // future devices we may need to re-layout Add and Merge so
+ // they can both be visible at the same time...
+ Log.w(LOG_TAG, "updateInCallControls: Add *and* Merge enabled," +
+ " but can't show both!");
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ // In CDMA "Add" option is always given to the user and the hence
+ // in this case both "Add" and "Merge" options would be available to user
+ if (DBG) log("updateInCallControls: CDMA: Add and Merge both enabled");
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ }
+
+ // "End call"
+ mEndButton.setEnabled(inCallControlState.canEndCall);
+
+ // "Dialpad": Enabled only when it's OK to use the dialpad in the
+ // first place.
+ mDialpadButton.setEnabled(inCallControlState.dialpadEnabled);
+ mDialpadButton.setChecked(inCallControlState.dialpadVisible);
+
+ // "Mute"
+ mMuteButton.setEnabled(inCallControlState.canMute);
+ mMuteButton.setChecked(inCallControlState.muteIndicatorOn);
+
+ // "Audio"
+ updateAudioButton(inCallControlState);
+
+ // "Hold" / "Swap":
+ // These two buttons occupy the same space onscreen, so at any
+ // given point exactly one of them must be VISIBLE and the other
+ // must be GONE.
+ if (inCallControlState.canHold) {
+ mHoldButton.setVisibility(View.VISIBLE);
+ mHoldButton.setEnabled(true);
+ mHoldButton.setChecked(inCallControlState.onHold);
+ mSwapButton.setVisibility(View.GONE);
+ mHoldSwapSpacer.setVisibility(View.VISIBLE);
+ } else if (inCallControlState.canSwap) {
+ mSwapButton.setVisibility(View.VISIBLE);
+ mSwapButton.setEnabled(true);
+ mHoldButton.setVisibility(View.GONE);
+ mHoldSwapSpacer.setVisibility(View.VISIBLE);
+ } else {
+ // Neither "Hold" nor "Swap" is available. This can happen for two
+ // reasons:
+ // (1) this is a transient state on a device that *can*
+ // normally hold or swap, or
+ // (2) this device just doesn't have the concept of hold/swap.
+ //
+ // In case (1), show the "Hold" button in a disabled state. In case
+ // (2), remove the button entirely. (This means that the button row
+ // will only have 4 buttons on some devices.)
+
+ if (inCallControlState.supportsHold) {
+ mHoldButton.setVisibility(View.VISIBLE);
+ mHoldButton.setEnabled(false);
+ mHoldButton.setChecked(false);
+ mSwapButton.setVisibility(View.GONE);
+ mHoldSwapSpacer.setVisibility(View.VISIBLE);
+ } else {
+ mHoldButton.setVisibility(View.GONE);
+ mSwapButton.setVisibility(View.GONE);
+ mHoldSwapSpacer.setVisibility(View.GONE);
+ }
+ }
+ mInCallScreen.updateButtonStateOutsideInCallTouchUi();
+ if (inCallControlState.canSwap && inCallControlState.canHold) {
+ // Uh oh, the InCallControlState thinks that Swap *and* Hold
+ // should both be available. This *should* never happen with
+ // either GSM or CDMA, but if it's possible on any future
+ // devices we may need to re-layout Hold and Swap so they can
+ // both be visible at the same time...
+ Log.w(LOG_TAG, "updateInCallControls: Hold *and* Swap enabled, but can't show both!");
+ }
+
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ if (inCallControlState.canSwap && inCallControlState.canMerge) {
+ // Uh oh, the InCallControlState thinks that Swap *and* Merge
+ // should both be available. This *should* never happen with
+ // CDMA, but if it's possible on any future
+ // devices we may need to re-layout Merge and Swap so they can
+ // both be visible at the same time...
+ Log.w(LOG_TAG, "updateInCallControls: Merge *and* Swap" +
+ "enabled, but can't show both!");
+ }
+ }
+
+ // Finally, update the "extra button row": It's displayed above the
+ // "End" button, but only if necessary. Also, it's never displayed
+ // while the dialpad is visible (since it would overlap.)
+ //
+ // The row contains two buttons:
+ //
+ // - "Manage conference" (used only on GSM devices)
+ // - "Merge" button (used only on CDMA devices)
+ //
+ // Note that mExtraButtonRow is ViewStub, which will be inflated for the first time when
+ // any of its buttons becomes visible.
+ final boolean showCdmaMerge =
+ (phoneType == PhoneConstants.PHONE_TYPE_CDMA) && inCallControlState.canMerge;
+ final boolean showExtraButtonRow =
+ showCdmaMerge || inCallControlState.manageConferenceVisible;
+ if (showExtraButtonRow && !inCallControlState.dialpadVisible) {
+ // This will require the ViewStub inflate itself.
+ mExtraButtonRow.setVisibility(View.VISIBLE);
+
+ // Need to set up mCdmaMergeButton and mManageConferenceButton if this is the first
+ // time they're visible.
+ if (mCdmaMergeButton == null) {
+ setupExtraButtons();
+ }
+ mCdmaMergeButton.setVisibility(showCdmaMerge ? View.VISIBLE : View.GONE);
+ if (inCallControlState.manageConferenceVisible) {
+ mManageConferenceButton.setVisibility(View.VISIBLE);
+ mManageConferenceButtonImage.setEnabled(inCallControlState.manageConferenceEnabled);
+ } else {
+ mManageConferenceButton.setVisibility(View.GONE);
+ }
+ } else {
+ mExtraButtonRow.setVisibility(View.GONE);
+ }
+
+ setupVideoCallButton();
+
+ if (DBG) {
+ log("At the end of updateInCallControls().");
+ dumpBottomButtonState();
+ }
+ }
+
+ /**
+ * Set up the video call button. Checks the system for any video call providers before
+ * displaying the video chat button.
+ */
+ private void setupVideoCallButton() {
+ // TODO: Check system to see if there are video chat providers and if not, disable the
+ // button.
+ }
+
+
+ /**
+ * Set up the buttons that are part of the "extra button row"
+ */
+ private void setupExtraButtons() {
+ // The two "buttons" here (mCdmaMergeButton and mManageConferenceButton)
+ // are actually layouts containing an icon and a text label side-by-side.
+ mCdmaMergeButton = (ViewGroup) mInCallControls.findViewById(R.id.cdmaMergeButton);
+ if (mCdmaMergeButton == null) {
+ Log.wtf(LOG_TAG, "CDMA Merge button is null even after ViewStub being inflated.");
+ return;
+ }
+ mCdmaMergeButton.setOnClickListener(this);
+
+ mManageConferenceButton =
+ (ViewGroup) mInCallControls.findViewById(R.id.manageConferenceButton);
+ mManageConferenceButton.setOnClickListener(this);
+ mManageConferenceButtonImage =
+ (ImageButton) mInCallControls.findViewById(R.id.manageConferenceButtonImage);
+ }
+
+ private void dumpBottomButtonState() {
+ log(" - dialpad: " + getButtonState(mDialpadButton));
+ log(" - speaker: " + getButtonState(mAudioButton));
+ log(" - mute: " + getButtonState(mMuteButton));
+ log(" - hold: " + getButtonState(mHoldButton));
+ log(" - swap: " + getButtonState(mSwapButton));
+ log(" - add: " + getButtonState(mAddButton));
+ log(" - merge: " + getButtonState(mMergeButton));
+ log(" - cdmaMerge: " + getButtonState(mCdmaMergeButton));
+ log(" - swap: " + getButtonState(mSwapButton));
+ log(" - manageConferenceButton: " + getButtonState(mManageConferenceButton));
+ }
+
+ private static String getButtonState(View view) {
+ if (view == null) {
+ return "(null)";
+ }
+ StringBuilder builder = new StringBuilder();
+ builder.append("visibility: " + (view.getVisibility() == View.VISIBLE ? "VISIBLE"
+ : view.getVisibility() == View.INVISIBLE ? "INVISIBLE" : "GONE"));
+ if (view instanceof ImageButton) {
+ builder.append(", enabled: " + ((ImageButton) view).isEnabled());
+ } else if (view instanceof CompoundButton) {
+ builder.append(", enabled: " + ((CompoundButton) view).isEnabled());
+ builder.append(", checked: " + ((CompoundButton) view).isChecked());
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Updates the onscreen "Audio mode" button based on the current state.
+ *
+ * - If bluetooth is available, this button's function is to bring up the
+ * "Audio mode" popup (which provides a 3-way choice between earpiece /
+ * speaker / bluetooth). So it should look like a regular action button,
+ * but should also have the small "more_indicator" triangle that indicates
+ * that a menu will pop up.
+ *
+ * - If speaker (but not bluetooth) is available, this button should look like
+ * a regular toggle button (and indicate the current speaker state.)
+ *
+ * - If even speaker isn't available, disable the button entirely.
+ */
+ private void updateAudioButton(InCallControlState inCallControlState) {
+ if (DBG) log("updateAudioButton()...");
+
+ // The various layers of artwork for this button come from
+ // btn_compound_audio.xml. Keep track of which layers we want to be
+ // visible:
+ //
+ // - This selector shows the blue bar below the button icon when
+ // this button is a toggle *and* it's currently "checked".
+ boolean showToggleStateIndication = false;
+ //
+ // - This is visible if the popup menu is enabled:
+ boolean showMoreIndicator = false;
+ //
+ // - Foreground icons for the button. Exactly one of these is enabled:
+ boolean showSpeakerOnIcon = false;
+ boolean showSpeakerOffIcon = false;
+ boolean showHandsetIcon = false;
+ boolean showBluetoothIcon = false;
+
+ if (inCallControlState.bluetoothEnabled) {
+ if (DBG) log("- updateAudioButton: 'popup menu action button' mode...");
+
+ mAudioButton.setEnabled(true);
+
+ // The audio button is NOT a toggle in this state. (And its
+ // setChecked() state is irrelevant since we completely hide the
+ // btn_compound_background layer anyway.)
+
+ // Update desired layers:
+ showMoreIndicator = true;
+ if (inCallControlState.bluetoothIndicatorOn) {
+ showBluetoothIcon = true;
+ } else if (inCallControlState.speakerOn) {
+ showSpeakerOnIcon = true;
+ } else {
+ showHandsetIcon = true;
+ // TODO: if a wired headset is plugged in, that takes precedence
+ // over the handset earpiece. If so, maybe we should show some
+ // sort of "wired headset" icon here instead of the "handset
+ // earpiece" icon. (Still need an asset for that, though.)
+ }
+ } else if (inCallControlState.speakerEnabled) {
+ if (DBG) log("- updateAudioButton: 'speaker toggle' mode...");
+
+ mAudioButton.setEnabled(true);
+
+ // The audio button *is* a toggle in this state, and indicates the
+ // current state of the speakerphone.
+ mAudioButton.setChecked(inCallControlState.speakerOn);
+
+ // Update desired layers:
+ showToggleStateIndication = true;
+
+ showSpeakerOnIcon = inCallControlState.speakerOn;
+ showSpeakerOffIcon = !inCallControlState.speakerOn;
+ } else {
+ if (DBG) log("- updateAudioButton: disabled...");
+
+ // The audio button is a toggle in this state, but that's mostly
+ // irrelevant since it's always disabled and unchecked.
+ mAudioButton.setEnabled(false);
+ mAudioButton.setChecked(false);
+
+ // Update desired layers:
+ showToggleStateIndication = true;
+ showSpeakerOffIcon = true;
+ }
+
+ // Finally, update the drawable layers (see btn_compound_audio.xml).
+
+ // Constants used below with Drawable.setAlpha():
+ final int HIDDEN = 0;
+ final int VISIBLE = 255;
+
+ LayerDrawable layers = (LayerDrawable) mAudioButton.getBackground();
+ if (DBG) log("- 'layers' drawable: " + layers);
+
+ layers.findDrawableByLayerId(R.id.compoundBackgroundItem)
+ .setAlpha(showToggleStateIndication ? VISIBLE : HIDDEN);
+
+ layers.findDrawableByLayerId(R.id.moreIndicatorItem)
+ .setAlpha(showMoreIndicator ? VISIBLE : HIDDEN);
+
+ layers.findDrawableByLayerId(R.id.bluetoothItem)
+ .setAlpha(showBluetoothIcon ? VISIBLE : HIDDEN);
+
+ layers.findDrawableByLayerId(R.id.handsetItem)
+ .setAlpha(showHandsetIcon ? VISIBLE : HIDDEN);
+
+ layers.findDrawableByLayerId(R.id.speakerphoneOnItem)
+ .setAlpha(showSpeakerOnIcon ? VISIBLE : HIDDEN);
+
+ layers.findDrawableByLayerId(R.id.speakerphoneOffItem)
+ .setAlpha(showSpeakerOffIcon ? VISIBLE : HIDDEN);
+ }
+
+ /**
+ * Handles a click on the "Audio mode" button.
+ * - If bluetooth is available, bring up the "Audio mode" popup
+ * (which provides a 3-way choice between earpiece / speaker / bluetooth).
+ * - If bluetooth is *not* available, just toggle between earpiece and
+ * speaker, with no popup at all.
+ */
+ private void handleAudioButtonClick() {
+ InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
+ if (inCallControlState.bluetoothEnabled) {
+ if (DBG) log("- handleAudioButtonClick: 'popup menu' mode...");
+ showAudioModePopup();
+ } else {
+ if (DBG) log("- handleAudioButtonClick: 'speaker toggle' mode...");
+ mInCallScreen.toggleSpeaker();
+ }
+ }
+
+ /**
+ * Brings up the "Audio mode" popup.
+ */
+ private void showAudioModePopup() {
+ if (DBG) log("showAudioModePopup()...");
+
+ mAudioModePopup = new PopupMenu(mInCallScreen /* context */,
+ mAudioButton /* anchorView */);
+ mAudioModePopup.getMenuInflater().inflate(R.menu.incall_audio_mode_menu,
+ mAudioModePopup.getMenu());
+ mAudioModePopup.setOnMenuItemClickListener(this);
+ mAudioModePopup.setOnDismissListener(this);
+
+ // Update the enabled/disabledness of menu items based on the
+ // current call state.
+ InCallControlState inCallControlState = mInCallScreen.getUpdatedInCallControlState();
+
+ Menu menu = mAudioModePopup.getMenu();
+
+ // TODO: Still need to have the "currently active" audio mode come
+ // up pre-selected (or focused?) with a blue highlight. Still
+ // need exact visual design, and possibly framework support for this.
+ // See comments below for the exact logic.
+
+ MenuItem speakerItem = menu.findItem(R.id.audio_mode_speaker);
+ speakerItem.setEnabled(inCallControlState.speakerEnabled);
+ // TODO: Show speakerItem as initially "selected" if
+ // inCallControlState.speakerOn is true.
+
+ // We display *either* "earpiece" or "wired headset", never both,
+ // depending on whether a wired headset is physically plugged in.
+ MenuItem earpieceItem = menu.findItem(R.id.audio_mode_earpiece);
+ MenuItem wiredHeadsetItem = menu.findItem(R.id.audio_mode_wired_headset);
+ final boolean usingHeadset = mApp.isHeadsetPlugged();
+ earpieceItem.setVisible(!usingHeadset);
+ earpieceItem.setEnabled(!usingHeadset);
+ wiredHeadsetItem.setVisible(usingHeadset);
+ wiredHeadsetItem.setEnabled(usingHeadset);
+ // TODO: Show the above item (either earpieceItem or wiredHeadsetItem)
+ // as initially "selected" if inCallControlState.speakerOn and
+ // inCallControlState.bluetoothIndicatorOn are both false.
+
+ MenuItem bluetoothItem = menu.findItem(R.id.audio_mode_bluetooth);
+ bluetoothItem.setEnabled(inCallControlState.bluetoothEnabled);
+ // TODO: Show bluetoothItem as initially "selected" if
+ // inCallControlState.bluetoothIndicatorOn is true.
+
+ mAudioModePopup.show();
+
+ // Unfortunately we need to manually keep track of the popup menu's
+ // visiblity, since PopupMenu doesn't have an isShowing() method like
+ // Dialogs do.
+ mAudioModePopupVisible = true;
+ }
+
+ /**
+ * Dismisses the "Audio mode" popup if it's visible.
+ *
+ * This is safe to call even if the popup is already dismissed, or even if
+ * you never called showAudioModePopup() in the first place.
+ */
+ public void dismissAudioModePopup() {
+ if (mAudioModePopup != null) {
+ mAudioModePopup.dismiss(); // safe even if already dismissed
+ mAudioModePopup = null;
+ mAudioModePopupVisible = false;
+ }
+ }
+
+ /**
+ * Refreshes the "Audio mode" popup if it's visible. This is useful
+ * (for example) when a wired headset is plugged or unplugged,
+ * since we need to switch back and forth between the "earpiece"
+ * and "wired headset" items.
+ *
+ * This is safe to call even if the popup is already dismissed, or even if
+ * you never called showAudioModePopup() in the first place.
+ */
+ public void refreshAudioModePopup() {
+ if (mAudioModePopup != null && mAudioModePopupVisible) {
+ // Dismiss the previous one
+ mAudioModePopup.dismiss(); // safe even if already dismissed
+ // And bring up a fresh PopupMenu
+ showAudioModePopup();
+ }
+ }
+
+ // PopupMenu.OnMenuItemClickListener implementation; see showAudioModePopup()
+ @Override
+ public boolean onMenuItemClick(MenuItem item) {
+ if (DBG) log("- onMenuItemClick: " + item);
+ if (DBG) log(" id: " + item.getItemId());
+ if (DBG) log(" title: '" + item.getTitle() + "'");
+
+ if (mInCallScreen == null) {
+ Log.w(LOG_TAG, "onMenuItemClick(" + item + "), but null mInCallScreen!");
+ return true;
+ }
+
+ switch (item.getItemId()) {
+ case R.id.audio_mode_speaker:
+ mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.SPEAKER);
+ break;
+ case R.id.audio_mode_earpiece:
+ case R.id.audio_mode_wired_headset:
+ // InCallAudioMode.EARPIECE means either the handset earpiece,
+ // or the wired headset (if connected.)
+ mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.EARPIECE);
+ break;
+ case R.id.audio_mode_bluetooth:
+ mInCallScreen.switchInCallAudio(InCallScreen.InCallAudioMode.BLUETOOTH);
+ break;
+ default:
+ Log.wtf(LOG_TAG,
+ "onMenuItemClick: unexpected View ID " + item.getItemId()
+ + " (MenuItem = '" + item + "')");
+ break;
+ }
+ return true;
+ }
+
+ // PopupMenu.OnDismissListener implementation; see showAudioModePopup().
+ // This gets called when the PopupMenu gets dismissed for *any* reason, like
+ // the user tapping outside its bounds, or pressing Back, or selecting one
+ // of the menu items.
+ @Override
+ public void onDismiss(PopupMenu menu) {
+ if (DBG) log("- onDismiss: " + menu);
+ mAudioModePopupVisible = false;
+ }
+
+ /**
+ * @return the amount of vertical space (in pixels) that needs to be
+ * reserved for the button cluster at the bottom of the screen.
+ * (The CallCard uses this measurement to determine how big
+ * the main "contact photo" area can be.)
+ *
+ * NOTE that this returns the "canonical height" of the main in-call
+ * button cluster, which may not match the amount of vertical space
+ * actually used. Specifically:
+ *
+ * - If an incoming call is ringing, the button cluster isn't
+ * visible at all. (And the GlowPadView widget is actually
+ * much taller than the button cluster.)
+ *
+ * - If the InCallTouchUi widget's "extra button row" is visible
+ * (in some rare phone states) the button cluster will actually
+ * be slightly taller than the "canonical height".
+ *
+ * In either of these cases, we allow the bottom edge of the contact
+ * photo to be covered up by whatever UI is actually onscreen.
+ */
+ public int getTouchUiHeight() {
+ // Add up the vertical space consumed by the various rows of buttons.
+ int height = 0;
+
+ // - The main row of buttons:
+ height += (int) getResources().getDimension(R.dimen.in_call_button_height);
+
+ // - The End button:
+ height += (int) getResources().getDimension(R.dimen.in_call_end_button_height);
+
+ // - Note we *don't* consider the InCallTouchUi widget's "extra
+ // button row" here.
+
+ //- And an extra bit of margin:
+ height += (int) getResources().getDimension(R.dimen.in_call_touch_ui_upper_margin);
+
+ return height;
+ }
+
+
+ //
+ // GlowPadView.OnTriggerListener implementation
+ //
+
+ @Override
+ public void onGrabbed(View v, int handle) {
+
+ }
+
+ @Override
+ public void onReleased(View v, int handle) {
+
+ }
+
+ /**
+ * Handles "Answer" and "Reject" actions for an incoming call.
+ * We get this callback from the incoming call widget
+ * when the user triggers an action.
+ */
+ @Override
+ public void onTrigger(View view, int whichHandle) {
+ if (DBG) log("onTrigger(whichHandle = " + whichHandle + ")...");
+
+ if (mInCallScreen == null) {
+ Log.wtf(LOG_TAG, "onTrigger(" + whichHandle
+ + ") from incoming-call widget, but null mInCallScreen!");
+ return;
+ }
+
+ // The InCallScreen actually implements all of these actions.
+ // Each possible action from the incoming call widget corresponds
+ // to an R.id value; we pass those to the InCallScreen's "button
+ // click" handler (even though the UI elements aren't actually
+ // buttons; see InCallScreen.handleOnscreenButtonClick().)
+
+ mShowInCallControlsDuringHidingAnimation = false;
+ switch (whichHandle) {
+ case ANSWER_CALL_ID:
+ if (DBG) log("ANSWER_CALL_ID: answer!");
+ mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallAnswer);
+ mShowInCallControlsDuringHidingAnimation = true;
+
+ // ...and also prevent it from reappearing right away.
+ // (This covers up a slow response from the radio for some
+ // actions; see updateState().)
+ mLastIncomingCallActionTime = SystemClock.uptimeMillis();
+ break;
+
+ case SEND_SMS_ID:
+ if (DBG) log("SEND_SMS_ID!");
+ mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallRespondViaSms);
+
+ // Watch out: mLastIncomingCallActionTime should not be updated for this case.
+ //
+ // The variable is originally for avoiding a problem caused by delayed phone state
+ // update; RINGING state may remain just after answering/declining an incoming
+ // call, so we need to wait a bit (500ms) until we get the effective phone state.
+ // For this case, we shouldn't rely on that hack.
+ //
+ // When the user selects this case, there are two possibilities, neither of which
+ // should rely on the hack.
+ //
+ // 1. The first possibility is that, the device eventually sends one of canned
+ // responses per the user's "send" request, and reject the call after sending it.
+ // At that moment the code introducing the canned responses should handle the
+ // case separately.
+ //
+ // 2. The second possibility is that, the device will show incoming call widget
+ // again per the user's "cancel" request, where the incoming call will still
+ // remain. At that moment the incoming call will keep its RINGING state.
+ // The remaining phone state should never be ignored by the hack for
+ // answering/declining calls because the RINGING state is legitimate. If we
+ // use the hack for answer/decline cases, the user loses the incoming call
+ // widget, until further screen update occurs afterward, which often results in
+ // missed calls.
+ break;
+
+ case DECLINE_CALL_ID:
+ if (DBG) log("DECLINE_CALL_ID: reject!");
+ mInCallScreen.handleOnscreenButtonClick(R.id.incomingCallReject);
+
+ // Same as "answer" case.
+ mLastIncomingCallActionTime = SystemClock.uptimeMillis();
+ break;
+
+ default:
+ Log.wtf(LOG_TAG, "onDialTrigger: unexpected whichHandle value: " + whichHandle);
+ break;
+ }
+
+ // On any action by the user, hide the widget.
+ //
+ // If requested above (i.e. if mShowInCallControlsDuringHidingAnimation is set to true),
+ // in-call controls will start being shown too.
+ //
+ // TODO: The decision to hide this should be made by the controller
+ // (InCallScreen), and not this view.
+ hideIncomingCallWidget();
+
+ // Regardless of what action the user did, be sure to clear out
+ // the hint text we were displaying while the user was dragging.
+ mInCallScreen.updateIncomingCallWidgetHint(0, 0);
+ }
+
+ public void onFinishFinalAnimation() {
+ // Not used
+ }
+
+ /**
+ * Apply an animation to hide the incoming call widget.
+ */
+ private void hideIncomingCallWidget() {
+ if (DBG) log("hideIncomingCallWidget()...");
+ if (mIncomingCallWidget.getVisibility() != View.VISIBLE
+ || mIncomingCallWidgetIsFadingOut) {
+ if (DBG) log("Skipping hideIncomingCallWidget action");
+ // Widget is already hidden or in the process of being hidden
+ return;
+ }
+
+ // Hide the incoming call screen with a transition
+ mIncomingCallWidgetIsFadingOut = true;
+ ViewPropertyAnimator animator = mIncomingCallWidget.animate();
+ animator.cancel();
+ animator.setDuration(AnimationUtils.ANIMATION_DURATION);
+ animator.setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ if (mShowInCallControlsDuringHidingAnimation) {
+ if (DBG) log("IncomingCallWidget's hiding animation started");
+ updateInCallControls(mApp.mCM);
+ mInCallControls.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (DBG) log("IncomingCallWidget's hiding animation ended");
+ mIncomingCallWidget.setAlpha(1);
+ mIncomingCallWidget.setVisibility(View.GONE);
+ mIncomingCallWidget.animate().setListener(null);
+ mShowInCallControlsDuringHidingAnimation = false;
+ mIncomingCallWidgetIsFadingOut = false;
+ mIncomingCallWidgetShouldBeReset = true;
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ mIncomingCallWidget.animate().setListener(null);
+ mShowInCallControlsDuringHidingAnimation = false;
+ mIncomingCallWidgetIsFadingOut = false;
+ mIncomingCallWidgetShouldBeReset = true;
+
+ // Note: the code which reset this animation should be responsible for
+ // alpha and visibility.
+ }
+ });
+ animator.alpha(0f);
+ }
+
+ /**
+ * Shows the incoming call widget and cancels any animation that may be fading it out.
+ */
+ private void showIncomingCallWidget(Call ringingCall) {
+ if (DBG) log("showIncomingCallWidget()...");
+
+ // TODO: wouldn't be ok to suppress this whole request if the widget is already VISIBLE
+ // and we don't need to reset it?
+ // log("showIncomingCallWidget(). widget visibility: " + mIncomingCallWidget.getVisibility());
+
+ ViewPropertyAnimator animator = mIncomingCallWidget.animate();
+ if (animator != null) {
+ animator.cancel();
+ // If animation is cancelled before it's running,
+ // onAnimationCancel will not be called and mIncomingCallWidgetIsFadingOut
+ // will be alway true. hideIncomingCallWidget() will not be excuted in this case.
+ mIncomingCallWidgetIsFadingOut = false;
+ }
+ mIncomingCallWidget.setAlpha(1.0f);
+
+ // Update the GlowPadView widget's targets based on the state of
+ // the ringing call. (Specifically, we need to disable the
+ // "respond via SMS" option for certain types of calls, like SIP
+ // addresses or numbers with blocked caller-id.)
+ final boolean allowRespondViaSms =
+ RespondViaSmsManager.allowRespondViaSmsForCall(mInCallScreen, ringingCall);
+ final int targetResourceId = allowRespondViaSms
+ ? R.array.incoming_call_widget_3way_targets
+ : R.array.incoming_call_widget_2way_targets;
+ // The widget should be updated only when appropriate; if the previous choice can be reused
+ // for this incoming call, we'll just keep using it. Otherwise we'll see UI glitch
+ // everytime when this method is called during a single incoming call.
+ if (targetResourceId != mIncomingCallWidget.getTargetResourceId()) {
+ if (allowRespondViaSms) {
+ // The GlowPadView widget is allowed to have all 3 choices:
+ // Answer, Decline, and Respond via SMS.
+ mIncomingCallWidget.setTargetResources(targetResourceId);
+ mIncomingCallWidget.setTargetDescriptionsResourceId(
+ R.array.incoming_call_widget_3way_target_descriptions);
+ mIncomingCallWidget.setDirectionDescriptionsResourceId(
+ R.array.incoming_call_widget_3way_direction_descriptions);
+ } else {
+ // You only get two choices: Answer or Decline.
+ mIncomingCallWidget.setTargetResources(targetResourceId);
+ mIncomingCallWidget.setTargetDescriptionsResourceId(
+ R.array.incoming_call_widget_2way_target_descriptions);
+ mIncomingCallWidget.setDirectionDescriptionsResourceId(
+ R.array.incoming_call_widget_2way_direction_descriptions);
+ }
+
+ // This will be used right after this block.
+ mIncomingCallWidgetShouldBeReset = true;
+ }
+ if (mIncomingCallWidgetShouldBeReset) {
+ // Watch out: be sure to call reset() and setVisibility() *after*
+ // updating the target resources, since otherwise the GlowPadView
+ // widget will make the targets visible initially (even before you
+ // touch the widget.)
+ mIncomingCallWidget.reset(false);
+ mIncomingCallWidgetShouldBeReset = false;
+ }
+
+ // On an incoming call, if the layout is landscape, then align the "incoming call" text
+ // to the left, because the incomingCallWidget (black background with glowing ring)
+ // is aligned to the right and would cover the "incoming call" text.
+ // Note that callStateLabel is within CallCard, outside of the context of InCallTouchUi
+ if (PhoneUtils.isLandscape(this.getContext())) {
+ TextView callStateLabel = (TextView) mIncomingCallWidget
+ .getRootView().findViewById(R.id.callStateLabel);
+ if (callStateLabel != null) callStateLabel.setGravity(Gravity.START);
+ }
+
+ mIncomingCallWidget.setVisibility(View.VISIBLE);
+
+ // Finally, manually trigger a "ping" animation.
+ //
+ // Normally, the ping animation is triggered by RING events from
+ // the telephony layer (see onIncomingRing().) But that *doesn't*
+ // happen for the very first RING event of an incoming call, since
+ // the incoming-call UI hasn't been set up yet at that point!
+ //
+ // So trigger an explicit ping() here, to force the animation to
+ // run when the widget first appears.
+ //
+ mHandler.removeMessages(INCOMING_CALL_WIDGET_PING);
+ mHandler.sendEmptyMessageDelayed(
+ INCOMING_CALL_WIDGET_PING,
+ // Visual polish: add a small delay here, to make the
+ // GlowPadView widget visible for a brief moment
+ // *before* starting the ping animation.
+ // This value doesn't need to be very precise.
+ 250 /* msec */);
+ }
+
+ /**
+ * Handles state changes of the incoming-call widget.
+ *
+ * In previous releases (where we used a SlidingTab widget) we would
+ * display an onscreen hint depending on which "handle" the user was
+ * dragging. But we now use a GlowPadView widget, which has only
+ * one handle, so for now we don't display a hint at all (see the TODO
+ * comment below.)
+ */
+ @Override
+ public void onGrabbedStateChange(View v, int grabbedState) {
+ if (mInCallScreen != null) {
+ // Look up the hint based on which handle is currently grabbed.
+ // (Note we don't simply pass grabbedState thru to the InCallScreen,
+ // since *this* class is the only place that knows that the left
+ // handle means "Answer" and the right handle means "Decline".)
+ int hintTextResId, hintColorResId;
+ switch (grabbedState) {
+ case GlowPadView.OnTriggerListener.NO_HANDLE:
+ case GlowPadView.OnTriggerListener.CENTER_HANDLE:
+ hintTextResId = 0;
+ hintColorResId = 0;
+ break;
+ default:
+ Log.e(LOG_TAG, "onGrabbedStateChange: unexpected grabbedState: "
+ + grabbedState);
+ hintTextResId = 0;
+ hintColorResId = 0;
+ break;
+ }
+
+ // Tell the InCallScreen to update the CallCard and force the
+ // screen to redraw.
+ mInCallScreen.updateIncomingCallWidgetHint(hintTextResId, hintColorResId);
+ }
+ }
+
+ /**
+ * Handles an incoming RING event from the telephony layer.
+ */
+ public void onIncomingRing() {
+ if (ENABLE_PING_ON_RING_EVENTS) {
+ // Each RING from the telephony layer triggers a "ping" animation
+ // of the GlowPadView widget. (The intent here is to make the
+ // pinging appear to be synchronized with the ringtone, although
+ // that only works for non-looping ringtones.)
+ triggerPing();
+ }
+ }
+
+ /**
+ * Runs a single "ping" animation of the GlowPadView widget,
+ * or do nothing if the GlowPadView widget is no longer visible.
+ *
+ * Also, if ENABLE_PING_AUTO_REPEAT is true, schedule the next ping as
+ * well (but again, only if the GlowPadView widget is still visible.)
+ */
+ public void triggerPing() {
+ if (DBG) log("triggerPing: mIncomingCallWidget = " + mIncomingCallWidget);
+
+ if (!mInCallScreen.isForegroundActivity()) {
+ // InCallScreen has been dismissed; no need to run a ping *or*
+ // schedule another one.
+ log("- triggerPing: InCallScreen no longer in foreground; ignoring...");
+ return;
+ }
+
+ if (mIncomingCallWidget == null) {
+ // This shouldn't happen; the GlowPadView widget should
+ // always be present in our layout file.
+ Log.w(LOG_TAG, "- triggerPing: null mIncomingCallWidget!");
+ return;
+ }
+
+ if (DBG) log("- triggerPing: mIncomingCallWidget visibility = "
+ + mIncomingCallWidget.getVisibility());
+
+ if (mIncomingCallWidget.getVisibility() != View.VISIBLE) {
+ if (DBG) log("- triggerPing: mIncomingCallWidget no longer visible; ignoring...");
+ return;
+ }
+
+ // Ok, run a ping (and schedule the next one too, if desired...)
+
+ mIncomingCallWidget.ping();
+
+ if (ENABLE_PING_AUTO_REPEAT) {
+ // Schedule the next ping. (ENABLE_PING_AUTO_REPEAT mode
+ // allows the ping animation to repeat much faster than in
+ // the ENABLE_PING_ON_RING_EVENTS case, since telephony RING
+ // events come fairly slowly (about 3 seconds apart.))
+
+ // No need to check here if the call is still ringing, by
+ // the way, since we hide mIncomingCallWidget as soon as the
+ // ringing stops, or if the user answers. (And at that
+ // point, any future triggerPing() call will be a no-op.)
+
+ // TODO: Rather than having a separate timer here, maybe try
+ // having these pings synchronized with the vibrator (see
+ // VibratorThread in Ringer.java; we'd just need to get
+ // events routed from there to here, probably via the
+ // PhoneApp instance.) (But watch out: make sure pings
+ // still work even if the Vibrate setting is turned off!)
+
+ mHandler.sendEmptyMessageDelayed(INCOMING_CALL_WIDGET_PING,
+ PING_AUTO_REPEAT_DELAY_MSEC);
+ }
+ }
+
+ // Debugging / testing code
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/InCallUiState.java b/src/com/android/phone/InCallUiState.java
new file mode 100644
index 0000000..3b700d7
--- /dev/null
+++ b/src/com/android/phone/InCallUiState.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2011 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.phone;
+
+import com.android.phone.Constants.CallStatusCode;
+
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.util.Log;
+
+
+/**
+ * Helper class to keep track of "persistent state" of the in-call UI.
+ *
+ * The onscreen appearance of the in-call UI mostly depends on the current
+ * Call/Connection state, which is owned by the telephony framework. But
+ * there's some application-level "UI state" too, which lives here in the
+ * phone app.
+ *
+ * This application-level state information is *not* maintained by the
+ * InCallScreen, since it needs to persist throughout an entire phone call,
+ * not just a single resume/pause cycle of the InCallScreen. So instead, that
+ * state is stored here, in a singleton instance of this class.
+ *
+ * The state kept here is a high-level abstraction of in-call UI state: we
+ * don't know about implementation details like specific widgets or strings or
+ * resources, but we do understand higher level concepts (for example "is the
+ * dialpad visible") and high-level modes (like InCallScreenMode) and error
+ * conditions (like CallStatusCode).
+ *
+ * @see InCallControlState for a separate collection of "UI state" that
+ * controls all the onscreen buttons of the in-call UI, based on the state of
+ * the telephony layer.
+ *
+ * The singleton instance of this class is owned by the PhoneApp instance.
+ */
+public class InCallUiState {
+ private static final String TAG = "InCallUiState";
+ private static final boolean DBG = false;
+
+ /** The singleton InCallUiState instance. */
+ private static InCallUiState sInstance;
+
+ private Context mContext;
+
+ /**
+ * Initialize the singleton InCallUiState instance.
+ *
+ * This is only done once, at startup, from PhoneApp.onCreate().
+ * From then on, the InCallUiState instance is available via the
+ * PhoneApp's public "inCallUiState" field, which is why there's no
+ * getInstance() method here.
+ */
+ /* package */ static InCallUiState init(Context context) {
+ synchronized (InCallUiState.class) {
+ if (sInstance == null) {
+ sInstance = new InCallUiState(context);
+ } else {
+ Log.wtf(TAG, "init() called multiple times! sInstance = " + sInstance);
+ }
+ return sInstance;
+ }
+ }
+
+ /**
+ * Private constructor (this is a singleton).
+ * @see init()
+ */
+ private InCallUiState(Context context) {
+ mContext = context;
+ }
+
+
+ //
+ // (1) High-level state of the whole in-call UI
+ //
+
+ /** High-level "modes" of the in-call UI. */
+ public enum InCallScreenMode {
+ /**
+ * Normal in-call UI elements visible.
+ */
+ NORMAL,
+ /**
+ * "Manage conference" UI is visible, totally replacing the
+ * normal in-call UI.
+ */
+ MANAGE_CONFERENCE,
+ /**
+ * Non-interactive UI state. Call card is visible,
+ * displaying information about the call that just ended.
+ */
+ CALL_ENDED,
+ /**
+ * Normal OTA in-call UI elements visible.
+ */
+ OTA_NORMAL,
+ /**
+ * OTA call ended UI visible, replacing normal OTA in-call UI.
+ */
+ OTA_ENDED,
+ /**
+ * Default state when not on call
+ */
+ UNDEFINED
+ }
+
+ /** Current high-level "mode" of the in-call UI. */
+ InCallScreenMode inCallScreenMode = InCallScreenMode.UNDEFINED;
+
+
+ //
+ // (2) State of specific UI elements
+ //
+
+ /**
+ * Is the onscreen twelve-key dialpad visible?
+ */
+ boolean showDialpad;
+
+ /**
+ * The contents of the twelve-key dialpad's "digits" display, which is
+ * visible only when the dialpad itself is visible.
+ *
+ * (This is basically the "history" of DTMF digits you've typed so far
+ * in the current call. It's cleared out any time a new call starts,
+ * to make sure the digits don't persist between two separate calls.)
+ */
+ String dialpadDigits;
+
+ /**
+ * The contact/dialed number information shown in the DTMF digits text
+ * when the user has not yet typed any digits.
+ *
+ * Currently only used for displaying "Voice Mail" since voicemail calls
+ * start directly in the dialpad view.
+ */
+ String dialpadContextText;
+
+ //
+ // (3) Error / diagnostic indications
+ //
+
+ // This section provides an abstract concept of an "error status
+ // indication" for some kind of exceptional condition that needs to be
+ // communicated to the user, in the context of the in-call UI.
+ //
+ // If mPendingCallStatusCode is any value other than SUCCESS, that
+ // indicates that the in-call UI needs to display a dialog to the user
+ // with the specified title and message text.
+ //
+ // When an error occurs outside of the InCallScreen itself (like
+ // during CallController.placeCall() for example), we inform the user
+ // by doing the following steps:
+ //
+ // (1) set the "pending call status code" to a value other than SUCCESS
+ // (based on the specific error that happened)
+ // (2) force the InCallScreen to be launched (or relaunched)
+ // (3) InCallScreen.onResume() will notice that pending call status code
+ // is set, and will actually bring up the desired dialog.
+ //
+ // Watch out: any time you set (or change!) the pending call status code
+ // field you must be sure to always (re)launch the InCallScreen.
+ //
+ // Finally, the InCallScreen itself is responsible for resetting the
+ // pending call status code, when the user dismisses the dialog (like by
+ // hitting the OK button or pressing Back). The pending call status code
+ // field is NOT cleared simply by the InCallScreen being paused or
+ // finished, since the resulting dialog needs to persist across
+ // orientation changes or if the screen turns off.
+
+ // TODO: other features we might eventually need here:
+ //
+ // - Some error status messages stay in force till reset,
+ // others may automatically clear themselves after
+ // a fixed delay
+ //
+ // - Some error statuses may be visible as a dialog with an OK
+ // button (like "call failed"), others may be an indefinite
+ // progress dialog (like "turning on radio for emergency call").
+ //
+ // - Eventually some error statuses may have extra actions (like a
+ // "retry call" button that we might provide at the bottom of the
+ // "call failed because you have no signal" dialog.)
+
+ /**
+ * The current pending "error status indication" that we need to
+ * display to the user.
+ *
+ * If this field is set to a value other than SUCCESS, this indicates to
+ * the InCallScreen that we need to show some kind of message to the user
+ * (usually an error dialog) based on the specified status code.
+ */
+ private CallStatusCode mPendingCallStatusCode = CallStatusCode.SUCCESS;
+
+ /**
+ * @return true if there's a pending "error status indication"
+ * that we need to display to the user.
+ */
+ public boolean hasPendingCallStatusCode() {
+ if (DBG) log("hasPendingCallStatusCode() ==> "
+ + (mPendingCallStatusCode != CallStatusCode.SUCCESS));
+ return (mPendingCallStatusCode != CallStatusCode.SUCCESS);
+ }
+
+ /**
+ * @return the pending "error status indication" code
+ * that we need to display to the user.
+ */
+ public CallStatusCode getPendingCallStatusCode() {
+ if (DBG) log("getPendingCallStatusCode() ==> " + mPendingCallStatusCode);
+ return mPendingCallStatusCode;
+ }
+
+ /**
+ * Sets the pending "error status indication" code.
+ */
+ public void setPendingCallStatusCode(CallStatusCode status) {
+ if (DBG) log("setPendingCallStatusCode( " + status + " )...");
+ if (mPendingCallStatusCode != CallStatusCode.SUCCESS) {
+ // Uh oh: mPendingCallStatusCode is already set to some value
+ // other than SUCCESS (which indicates that there was some kind of
+ // failure), and now we're trying to indicate another (potentially
+ // different) failure. But we can only indicate one failure at a
+ // time to the user, so the previous pending code is now going to
+ // be lost.
+ Log.w(TAG, "setPendingCallStatusCode: setting new code " + status
+ + ", but a previous code " + mPendingCallStatusCode
+ + " was already pending!");
+ }
+ mPendingCallStatusCode = status;
+ }
+
+ /**
+ * Clears out the pending "error status indication" code.
+ *
+ * This indicates that there's no longer any error or "exceptional
+ * condition" that needs to be displayed to the user. (Typically, this
+ * method is called when the user dismisses the error dialog that came up
+ * because of a previous call status code.)
+ */
+ public void clearPendingCallStatusCode() {
+ if (DBG) log("clearPendingCallStatusCode()...");
+ mPendingCallStatusCode = CallStatusCode.SUCCESS;
+ }
+
+ /**
+ * Flag used to control the CDMA-specific "call lost" dialog.
+ *
+ * If true, that means that if the *next* outgoing call fails with an
+ * abnormal disconnection cause, we need to display the "call lost"
+ * dialog. (Normally, in CDMA we handle some types of call failures
+ * by automatically retrying the call. This flag is set to true when
+ * we're about to auto-retry, which means that if the *retry* also
+ * fails we'll give up and display an error.)
+ * See the logic in InCallScreen.onDisconnect() for the full story.
+ *
+ * TODO: the state machine that maintains the needToShowCallLostDialog
+ * flag in InCallScreen.onDisconnect() should really be moved into the
+ * CallController. Then we can get rid of this extra flag, and
+ * instead simply use the CallStatusCode value CDMA_CALL_LOST to
+ * trigger the "call lost" dialog.
+ */
+ boolean needToShowCallLostDialog;
+
+
+ //
+ // Progress indications
+ //
+
+ /**
+ * Possible messages we might need to display along with
+ * an indefinite progress spinner.
+ */
+ public enum ProgressIndicationType {
+ /**
+ * No progress indication needs to be shown.
+ */
+ NONE,
+
+ /**
+ * Shown when making an emergency call from airplane mode;
+ * see CallController$EmergencyCallHelper.
+ */
+ TURNING_ON_RADIO,
+
+ /**
+ * Generic "retrying" state. (Specifically, this is shown while
+ * retrying after an initial failure from the "emergency call from
+ * airplane mode" sequence.)
+ */
+ RETRYING
+ }
+
+ /**
+ * The current progress indication that should be shown
+ * to the user. Any value other than NONE will cause the InCallScreen
+ * to bring up an indefinite progress spinner along with a message
+ * corresponding to the specified ProgressIndicationType.
+ */
+ private ProgressIndicationType progressIndication = ProgressIndicationType.NONE;
+
+ /** Sets the current progressIndication. */
+ public void setProgressIndication(ProgressIndicationType value) {
+ progressIndication = value;
+ }
+
+ /** Clears the current progressIndication. */
+ public void clearProgressIndication() {
+ progressIndication = ProgressIndicationType.NONE;
+ }
+
+ /**
+ * @return the current progress indication type, or ProgressIndicationType.NONE
+ * if no progress indication is currently active.
+ */
+ public ProgressIndicationType getProgressIndication() {
+ return progressIndication;
+ }
+
+ /** @return true if a progress indication is currently active. */
+ public boolean isProgressIndicationActive() {
+ return (progressIndication != ProgressIndicationType.NONE);
+ }
+
+
+ //
+ // (4) Optional info when a 3rd party "provider" is used.
+ // @see InCallScreen#requestRemoveProviderInfoWithDelay()
+ // @see CallCard#updateCallStateWidgets()
+ //
+
+ // TODO: maybe isolate all the provider-related stuff out to a
+ // separate inner class?
+ boolean providerInfoVisible;
+ CharSequence providerLabel;
+ Drawable providerIcon;
+ Uri providerGatewayUri;
+ // The formatted address extracted from mProviderGatewayUri. User visible.
+ String providerAddress;
+
+ /**
+ * Set the fields related to the provider support
+ * based on the specified intent.
+ */
+ public void setProviderInfo(Intent intent) {
+ providerLabel = PhoneUtils.getProviderLabel(mContext, intent);
+ providerIcon = PhoneUtils.getProviderIcon(mContext, intent);
+ providerGatewayUri = PhoneUtils.getProviderGatewayUri(intent);
+ providerAddress = PhoneUtils.formatProviderUri(providerGatewayUri);
+ providerInfoVisible = true;
+
+ // ...but if any of the "required" fields are missing, completely
+ // disable the overlay.
+ if (TextUtils.isEmpty(providerLabel) || providerIcon == null ||
+ providerGatewayUri == null || TextUtils.isEmpty(providerAddress)) {
+ clearProviderInfo();
+ }
+ }
+
+ /**
+ * Clear all the fields related to the provider support.
+ */
+ public void clearProviderInfo() {
+ providerInfoVisible = false;
+ providerLabel = null;
+ providerIcon = null;
+ providerGatewayUri = null;
+ providerAddress = null;
+ }
+
+ /**
+ * "Call origin" of the most recent phone call.
+ *
+ * Watch out: right now this is only used to determine where the user should go after the phone
+ * call. See also {@link InCallScreen} for more detail. There is *no* specific specification
+ * about how this variable will be used.
+ *
+ * @see PhoneGlobals#setLatestActiveCallOrigin(String)
+ * @see PhoneGlobals#createPhoneEndIntentUsingCallOrigin()
+ *
+ * TODO: we should determine some public behavior for this variable.
+ */
+ String latestActiveCallOrigin;
+
+ /**
+ * Timestamp for "Call origin". This will be used to preserve when the call origin was set.
+ * {@link android.os.SystemClock#elapsedRealtime()} will be used.
+ */
+ long latestActiveCallOriginTimeStamp;
+
+ /**
+ * Flag forcing Phone app to show in-call UI even when there's no phone call and thus Phone
+ * is in IDLE state. This will be turned on only when:
+ *
+ * - the last phone call is hung up, and
+ * - the screen is being turned off in the middle of in-call UI (and thus when the screen being
+ * turned on in-call UI is expected to be the next foreground activity)
+ *
+ * At that moment whole UI should show "previously disconnected phone call" for a moment and
+ * exit itself. {@link InCallScreen#onPause()} will turn this off and prevent possible weird
+ * cases which may happen with that exceptional case.
+ */
+ boolean showAlreadyDisconnectedState;
+
+ //
+ // Debugging
+ //
+
+ public void dumpState() {
+ log("dumpState():");
+ log(" - showDialpad: " + showDialpad);
+ log(" - dialpadContextText: " + dialpadContextText);
+ if (hasPendingCallStatusCode()) {
+ log(" - status indication is pending!");
+ log(" - pending call status code = " + mPendingCallStatusCode);
+ } else {
+ log(" - pending call status code: none");
+ }
+ log(" - progressIndication: " + progressIndication);
+ if (providerInfoVisible) {
+ log(" - provider info VISIBLE: "
+ + providerLabel + " / "
+ + providerIcon + " / "
+ + providerGatewayUri + " / "
+ + providerAddress);
+ } else {
+ log(" - provider info: none");
+ }
+ log(" - latestActiveCallOrigin: " + latestActiveCallOrigin);
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/ManageConferenceUtils.java b/src/com/android/phone/ManageConferenceUtils.java
new file mode 100644
index 0000000..5821754
--- /dev/null
+++ b/src/com/android/phone/ManageConferenceUtils.java
@@ -0,0 +1,358 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.Button;
+import android.widget.Chronometer;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.CallerInfoAsyncQuery;
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.Connection;
+
+import java.util.List;
+
+
+/**
+ * Helper class to initialize and run the InCallScreen's "Manage conference" UI.
+ */
+public class ManageConferenceUtils {
+ private static final String LOG_TAG = "ManageConferenceUtils";
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+
+ /**
+ * CallerInfoAsyncQuery.OnQueryCompleteListener implementation.
+ *
+ * This object listens for results from the caller-id info queries we
+ * fire off in updateManageConferenceRow(), and updates the
+ * corresponding conference row.
+ */
+ private final class QueryCompleteListener
+ implements CallerInfoAsyncQuery.OnQueryCompleteListener {
+ private final int mConferencCallListIndex;
+
+ public QueryCompleteListener(int index) {
+ mConferencCallListIndex = index;
+ }
+
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+ if (DBG) log("callerinfo query complete, updating UI." + ci);
+
+ Connection connection = (Connection) cookie;
+ int presentation = connection.getNumberPresentation();
+
+ // get the viewgroup (conference call list item) and make it visible
+ ViewGroup viewGroup = mConferenceCallList[mConferencCallListIndex];
+ viewGroup.setVisibility(View.VISIBLE);
+
+ // update the list item with this information.
+ displayCallerInfoForConferenceRow(ci, presentation,
+ (TextView) viewGroup.findViewById(R.id.conferenceCallerName),
+ (TextView) viewGroup.findViewById(R.id.conferenceCallerNumberType),
+ (TextView) viewGroup.findViewById(R.id.conferenceCallerNumber));
+ }
+ }
+
+ private InCallScreen mInCallScreen;
+ private CallManager mCM;
+
+ // "Manage conference" UI elements and state
+ private ViewGroup mManageConferencePanel;
+ private View mButtonManageConferenceDone;
+ private ViewGroup[] mConferenceCallList;
+ private int mNumCallersInConference;
+ private Chronometer mConferenceTime;
+
+ // See CallTracker.MAX_CONNECTIONS_PER_CALL
+ private static final int MAX_CALLERS_IN_CONFERENCE = 5;
+
+ public ManageConferenceUtils(InCallScreen inCallScreen, CallManager cm) {
+ if (DBG) log("ManageConferenceUtils constructor...");
+ mInCallScreen = inCallScreen;
+ mCM = cm;
+ }
+
+ public void initManageConferencePanel() {
+ if (DBG) log("initManageConferencePanel()...");
+ if (mManageConferencePanel == null) {
+ if (DBG) log("initManageConferencePanel: first-time initialization!");
+
+ // Inflate the ViewStub, look up and initialize the UI elements.
+ ViewStub stub = (ViewStub) mInCallScreen.findViewById(R.id.manageConferencePanelStub);
+ stub.inflate();
+
+ mManageConferencePanel =
+ (ViewGroup) mInCallScreen.findViewById(R.id.manageConferencePanel);
+ if (mManageConferencePanel == null) {
+ throw new IllegalStateException("Couldn't find manageConferencePanel!");
+ }
+
+ // set up the Conference Call chronometer
+ mConferenceTime =
+ (Chronometer) mInCallScreen.findViewById(R.id.manageConferencePanelHeader);
+ mConferenceTime.setFormat(mInCallScreen.getString(R.string.caller_manage_header));
+
+ // Create list of conference call widgets
+ mConferenceCallList = new ViewGroup[MAX_CALLERS_IN_CONFERENCE];
+
+ final int[] viewGroupIdList = { R.id.caller0, R.id.caller1, R.id.caller2,
+ R.id.caller3, R.id.caller4 };
+ for (int i = 0; i < MAX_CALLERS_IN_CONFERENCE; i++) {
+ mConferenceCallList[i] =
+ (ViewGroup) mInCallScreen.findViewById(viewGroupIdList[i]);
+ }
+
+ mButtonManageConferenceDone = mInCallScreen.findViewById(R.id.manage_done);
+ mButtonManageConferenceDone.setOnClickListener(mInCallScreen);
+ }
+ }
+
+ /**
+ * Shows or hides the manageConferencePanel.
+ */
+ public void setPanelVisible(boolean visible) {
+ if (mManageConferencePanel != null) {
+ mManageConferencePanel.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ /**
+ * Starts the "conference time" chronometer.
+ */
+ public void startConferenceTime(long base) {
+ if (mConferenceTime != null) {
+ mConferenceTime.setBase(base);
+ mConferenceTime.start();
+ }
+ }
+
+ /**
+ * Stops the "conference time" chronometer.
+ */
+ public void stopConferenceTime() {
+ if (mConferenceTime != null) {
+ mConferenceTime.stop();
+ }
+ }
+
+ public int getNumCallersInConference() {
+ return mNumCallersInConference;
+ }
+
+ /**
+ * Updates the "Manage conference" UI based on the specified List of
+ * connections.
+ *
+ * @param connections the List of connections belonging to
+ * the current foreground call; size must be greater than 1
+ * (or it wouldn't be a conference call in the first place.)
+ */
+ public void updateManageConferencePanel(List<Connection> connections) {
+ mNumCallersInConference = connections.size();
+ if (DBG) log("updateManageConferencePanel()... num connections in conference = "
+ + mNumCallersInConference);
+
+ // Can we give the user the option to separate out ("go private with") a single
+ // caller from this conference?
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ final boolean hasHoldingCall = mCM.hasActiveBgCall();
+ boolean canSeparate = !(hasActiveCall && hasHoldingCall);
+
+ for (int i = 0; i < MAX_CALLERS_IN_CONFERENCE; i++) {
+ if (i < mNumCallersInConference) {
+ // Fill in the row in the UI for this caller.
+ Connection connection = (Connection) connections.get(i);
+ updateManageConferenceRow(i, connection, canSeparate);
+ } else {
+ // Blank out this row in the UI
+ updateManageConferenceRow(i, null, false);
+ }
+ }
+ }
+
+ /**
+ * Updates a single row of the "Manage conference" UI. (One row in this
+ * UI represents a single caller in the conference.)
+ *
+ * @param i the row to update
+ * @param connection the Connection corresponding to this caller.
+ * If null, that means this is an "empty slot" in the conference,
+ * so hide this row in the UI.
+ * @param canSeparate if true, show a "Separate" (i.e. "Private") button
+ * on this row in the UI.
+ */
+ public void updateManageConferenceRow(final int i,
+ final Connection connection,
+ boolean canSeparate) {
+ if (DBG) log("updateManageConferenceRow(" + i + ")... connection = " + connection);
+
+ if (connection != null) {
+ // Activate this row of the Manage conference panel:
+ mConferenceCallList[i].setVisibility(View.VISIBLE);
+
+ // get the relevant children views
+ View endButton = mConferenceCallList[i].findViewById(R.id.conferenceCallerDisconnect);
+ View separateButton = mConferenceCallList[i].findViewById(
+ R.id.conferenceCallerSeparate);
+ TextView nameTextView = (TextView) mConferenceCallList[i].findViewById(
+ R.id.conferenceCallerName);
+ TextView numberTextView = (TextView) mConferenceCallList[i].findViewById(
+ R.id.conferenceCallerNumber);
+ TextView numberTypeTextView = (TextView) mConferenceCallList[i].findViewById(
+ R.id.conferenceCallerNumberType);
+
+ if (DBG) log("- button: " + endButton + ", nameTextView: " + nameTextView);
+
+ // Hook up this row's buttons.
+ View.OnClickListener endThisConnection = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ endConferenceConnection(i, connection);
+ PhoneGlobals.getInstance().pokeUserActivity();
+ }
+ };
+ endButton.setOnClickListener(endThisConnection);
+ //
+ if (canSeparate) {
+ View.OnClickListener separateThisConnection = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ separateConferenceConnection(i, connection);
+ PhoneGlobals.getInstance().pokeUserActivity();
+ }
+ };
+ separateButton.setOnClickListener(separateThisConnection);
+ separateButton.setVisibility(View.VISIBLE);
+ } else {
+ separateButton.setVisibility(View.INVISIBLE);
+ }
+
+ // Name/number for this caller.
+ QueryCompleteListener listener = new QueryCompleteListener(i);
+ PhoneUtils.CallerInfoToken info =
+ PhoneUtils.startGetCallerInfo(mInCallScreen,
+ connection, listener, connection);
+ if (DBG) log(" - got info from startGetCallerInfo(): " + info);
+
+ // display the CallerInfo.
+ displayCallerInfoForConferenceRow(info.currentInfo, connection.getNumberPresentation(),
+ nameTextView, numberTypeTextView, numberTextView);
+ } else {
+ // Disable this row of the Manage conference panel:
+ mConferenceCallList[i].setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * Helper function to fill out the Conference Call(er) information
+ * for each item in the "Manage Conference Call" list.
+ *
+ * @param presentation presentation specified by {@link Connection}.
+ */
+ public final void displayCallerInfoForConferenceRow(CallerInfo ci, int presentation,
+ TextView nameTextView, TextView numberTypeTextView, TextView numberTextView) {
+ // gather the correct name and number information.
+ String callerName = "";
+ String callerNumber = "";
+ String callerNumberType = "";
+ if (ci != null) {
+ callerName = ci.name;
+ if (TextUtils.isEmpty(callerName)) {
+ // Do similar fallback as CallCard does.
+ // See also CallCard#updateDisplayForPerson().
+ if (TextUtils.isEmpty(ci.phoneNumber)) {
+ callerName = PhoneUtils.getPresentationString(mInCallScreen, presentation);
+ } else if (!TextUtils.isEmpty(ci.cnapName)) {
+ // No name, but we do have a valid CNAP name, so use that.
+ callerName = ci.cnapName;
+ } else {
+ callerName = ci.phoneNumber;
+ }
+ } else {
+ callerNumber = ci.phoneNumber;
+ callerNumberType = ci.phoneLabel;
+ }
+ }
+
+ // set the caller name
+ nameTextView.setText(callerName);
+
+ // set the caller number in subscript, or make the field disappear.
+ if (TextUtils.isEmpty(callerNumber)) {
+ numberTextView.setVisibility(View.GONE);
+ numberTypeTextView.setVisibility(View.GONE);
+ } else {
+ numberTextView.setVisibility(View.VISIBLE);
+ numberTextView.setText(callerNumber);
+ numberTypeTextView.setVisibility(View.VISIBLE);
+ numberTypeTextView.setText(callerNumberType);
+ }
+ }
+
+ /**
+ * Ends the specified connection on a conference call. This method is
+ * run (via a closure containing a row index and Connection) when the
+ * user clicks the "End" button on a specific row in the Manage
+ * conference UI.
+ */
+ public void endConferenceConnection(int i, Connection connection) {
+ if (DBG) log("===> ENDING conference connection " + i
+ + ": Connection " + connection);
+ // The actual work of ending the connection:
+ PhoneUtils.hangup(connection);
+ // No need to manually update the "Manage conference" UI here;
+ // that'll happen automatically very soon (when we get the
+ // onDisconnect() callback triggered by this hangup() call.)
+ }
+
+ /**
+ * Separates out the specified connection on a conference call. This
+ * method is run (via a closure containing a row index and Connection)
+ * when the user clicks the "Separate" (i.e. "Private") button on a
+ * specific row in the Manage conference UI.
+ */
+ public void separateConferenceConnection(int i, Connection connection) {
+ if (DBG) log("===> SEPARATING conference connection " + i
+ + ": Connection " + connection);
+
+ PhoneUtils.separateCall(connection);
+
+ // Note that separateCall() automagically makes the
+ // newly-separated call into the foreground call (which is the
+ // desired UI), so there's no need to do any further
+ // call-switching here.
+ // There's also no need to manually update (or hide) the "Manage
+ // conference" UI; that'll happen on its own in a moment (when we
+ // get the phone state change event triggered by the call to
+ // separateCall().)
+ }
+
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/MobileNetworkSettings.java b/src/com/android/phone/MobileNetworkSettings.java
new file mode 100644
index 0000000..e4b4de6
--- /dev/null
+++ b/src/com/android/phone/MobileNetworkSettings.java
@@ -0,0 +1,590 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.TelephonyProperties;
+
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.preference.CheckBoxPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceScreen;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.MenuItem;
+
+/**
+ * "Mobile network settings" screen. This preference screen lets you
+ * enable/disable mobile data, and control data roaming and other
+ * network-specific mobile data features. It's used on non-voice-capable
+ * tablets as well as regular phone devices.
+ *
+ * Note that this PreferenceActivity is part of the phone app, even though
+ * you reach it from the "Wireless & Networks" section of the main
+ * Settings app. It's not part of the "Call settings" hierarchy that's
+ * available from the Phone app (see CallFeaturesSetting for that.)
+ */
+public class MobileNetworkSettings extends PreferenceActivity
+ implements DialogInterface.OnClickListener,
+ DialogInterface.OnDismissListener, Preference.OnPreferenceChangeListener{
+
+ // debug data
+ private static final String LOG_TAG = "NetworkSettings";
+ private static final boolean DBG = false;
+ public static final int REQUEST_CODE_EXIT_ECM = 17;
+
+ //String keys for preference lookup
+ private static final String BUTTON_DATA_ENABLED_KEY = "button_data_enabled_key";
+ private static final String BUTTON_PREFERED_NETWORK_MODE = "preferred_network_mode_key";
+ private static final String BUTTON_ROAMING_KEY = "button_roaming_key";
+ private static final String BUTTON_CDMA_LTE_DATA_SERVICE_KEY = "cdma_lte_data_service_key";
+
+ static final int preferredNetworkMode = Phone.PREFERRED_NT_MODE;
+
+ //Information about logical "up" Activity
+ private static final String UP_ACTIVITY_PACKAGE = "com.android.settings";
+ private static final String UP_ACTIVITY_CLASS =
+ "com.android.settings.Settings$WirelessSettingsActivity";
+
+ //UI objects
+ private ListPreference mButtonPreferredNetworkMode;
+ private CheckBoxPreference mButtonDataRoam;
+ private CheckBoxPreference mButtonDataEnabled;
+ private Preference mLteDataServicePref;
+
+ private static final String iface = "rmnet0"; //TODO: this will go away
+
+ private Phone mPhone;
+ private MyHandler mHandler;
+ private boolean mOkClicked;
+
+ //GsmUmts options and Cdma options
+ GsmUmtsOptions mGsmUmtsOptions;
+ CdmaOptions mCdmaOptions;
+
+ private Preference mClickedPreference;
+
+
+ //This is a method implemented for DialogInterface.OnClickListener.
+ // Used to dismiss the dialogs when they come up.
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ mPhone.setDataRoamingEnabled(true);
+ mOkClicked = true;
+ } else {
+ // Reset the toggle
+ mButtonDataRoam.setChecked(false);
+ }
+ }
+
+ public void onDismiss(DialogInterface dialog) {
+ // Assuming that onClick gets called first
+ if (!mOkClicked) {
+ mButtonDataRoam.setChecked(false);
+ }
+ }
+
+ /**
+ * Invoked on each preference click in this hierarchy, overrides
+ * PreferenceActivity's implementation. Used to make sure we track the
+ * preference click events.
+ */
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
+ /** TODO: Refactor and get rid of the if's using subclasses */
+ if (mGsmUmtsOptions != null &&
+ mGsmUmtsOptions.preferenceTreeClick(preference) == true) {
+ return true;
+ } else if (mCdmaOptions != null &&
+ mCdmaOptions.preferenceTreeClick(preference) == true) {
+ if (Boolean.parseBoolean(
+ SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE))) {
+
+ mClickedPreference = preference;
+
+ // In ECM mode launch ECM app dialog
+ startActivityForResult(
+ new Intent(TelephonyIntents.ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null),
+ REQUEST_CODE_EXIT_ECM);
+ }
+ return true;
+ } else if (preference == mButtonPreferredNetworkMode) {
+ //displays the value taken from the Settings.System
+ int settingsNetworkMode = android.provider.Settings.Global.getInt(mPhone.getContext().
+ getContentResolver(), android.provider.Settings.Global.PREFERRED_NETWORK_MODE,
+ preferredNetworkMode);
+ mButtonPreferredNetworkMode.setValue(Integer.toString(settingsNetworkMode));
+ return true;
+ } else if (preference == mButtonDataRoam) {
+ if (DBG) log("onPreferenceTreeClick: preference == mButtonDataRoam.");
+
+ //normally called on the toggle click
+ if (mButtonDataRoam.isChecked()) {
+ // First confirm with a warning dialog about charges
+ mOkClicked = false;
+ new AlertDialog.Builder(this).setMessage(
+ getResources().getString(R.string.roaming_warning))
+ .setTitle(android.R.string.dialog_alert_title)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(android.R.string.yes, this)
+ .setNegativeButton(android.R.string.no, this)
+ .show()
+ .setOnDismissListener(this);
+ } else {
+ mPhone.setDataRoamingEnabled(false);
+ }
+ return true;
+ } else if (preference == mButtonDataEnabled) {
+ if (DBG) log("onPreferenceTreeClick: preference == mButtonDataEnabled.");
+ ConnectivityManager cm =
+ (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ cm.setMobileDataEnabled(mButtonDataEnabled.isChecked());
+ return true;
+ } else if (preference == mLteDataServicePref) {
+ String tmpl = android.provider.Settings.Global.getString(getContentResolver(),
+ android.provider.Settings.Global.SETUP_PREPAID_DATA_SERVICE_URL);
+ if (!TextUtils.isEmpty(tmpl)) {
+ TelephonyManager tm = (TelephonyManager) getSystemService(
+ Context.TELEPHONY_SERVICE);
+ String imsi = tm.getSubscriberId();
+ if (imsi == null) {
+ imsi = "";
+ }
+ final String url = TextUtils.isEmpty(tmpl) ? null
+ : TextUtils.expandTemplate(tmpl, imsi).toString();
+ Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
+ startActivity(intent);
+ } else {
+ android.util.Log.e(LOG_TAG, "Missing SETUP_PREPAID_DATA_SERVICE_URL");
+ }
+ return true;
+ } else {
+ // if the button is anything but the simple toggle preference,
+ // we'll need to disable all preferences to reject all click
+ // events until the sub-activity's UI comes up.
+ preferenceScreen.setEnabled(false);
+ // Let the intents be launched by the Preference manager
+ return false;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ addPreferencesFromResource(R.xml.network_setting);
+
+ mPhone = PhoneGlobals.getPhone();
+ mHandler = new MyHandler();
+
+ //get UI object references
+ PreferenceScreen prefSet = getPreferenceScreen();
+
+ mButtonDataEnabled = (CheckBoxPreference) prefSet.findPreference(BUTTON_DATA_ENABLED_KEY);
+ mButtonDataRoam = (CheckBoxPreference) prefSet.findPreference(BUTTON_ROAMING_KEY);
+ mButtonPreferredNetworkMode = (ListPreference) prefSet.findPreference(
+ BUTTON_PREFERED_NETWORK_MODE);
+ mLteDataServicePref = prefSet.findPreference(BUTTON_CDMA_LTE_DATA_SERVICE_KEY);
+
+ boolean isLteOnCdma = mPhone.getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_TRUE;
+ if (getResources().getBoolean(R.bool.world_phone) == true) {
+ // set the listener for the mButtonPreferredNetworkMode list preference so we can issue
+ // change Preferred Network Mode.
+ mButtonPreferredNetworkMode.setOnPreferenceChangeListener(this);
+
+ //Get the networkMode from Settings.System and displays it
+ int settingsNetworkMode = android.provider.Settings.Global.getInt(mPhone.getContext().
+ getContentResolver(),android.provider.Settings.Global.PREFERRED_NETWORK_MODE,
+ preferredNetworkMode);
+ mButtonPreferredNetworkMode.setValue(Integer.toString(settingsNetworkMode));
+ mCdmaOptions = new CdmaOptions(this, prefSet, mPhone);
+ mGsmUmtsOptions = new GsmUmtsOptions(this, prefSet);
+ } else {
+ if (!isLteOnCdma) {
+ prefSet.removePreference(mButtonPreferredNetworkMode);
+ }
+ int phoneType = mPhone.getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ mCdmaOptions = new CdmaOptions(this, prefSet, mPhone);
+ if (isLteOnCdma) {
+ mButtonPreferredNetworkMode.setOnPreferenceChangeListener(this);
+ int settingsNetworkMode = android.provider.Settings.Global.getInt(
+ mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.PREFERRED_NETWORK_MODE,
+ preferredNetworkMode);
+ mButtonPreferredNetworkMode.setValue(
+ Integer.toString(settingsNetworkMode));
+ }
+
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+ mGsmUmtsOptions = new GsmUmtsOptions(this, prefSet);
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ }
+
+ final boolean missingDataServiceUrl = TextUtils.isEmpty(
+ android.provider.Settings.Global.getString(getContentResolver(),
+ android.provider.Settings.Global.SETUP_PREPAID_DATA_SERVICE_URL));
+ if (!isLteOnCdma || missingDataServiceUrl) {
+ prefSet.removePreference(mLteDataServicePref);
+ } else {
+ android.util.Log.d(LOG_TAG, "keep ltePref");
+ }
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ // android.R.id.home will be triggered in onOptionsItemSelected()
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ // upon resumption from the sub-activity, make sure we re-enable the
+ // preferences.
+ getPreferenceScreen().setEnabled(true);
+
+ ConnectivityManager cm =
+ (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
+ mButtonDataEnabled.setChecked(cm.getMobileDataEnabled());
+
+ // Set UI state in onResume because a user could go home, launch some
+ // app to change this setting's backend, and re-launch this settings app
+ // and the UI state would be inconsistent with actual state
+ mButtonDataRoam.setChecked(mPhone.getDataRoamingEnabled());
+
+ if (getPreferenceScreen().findPreference(BUTTON_PREFERED_NETWORK_MODE) != null) {
+ mPhone.getPreferredNetworkType(mHandler.obtainMessage(
+ MyHandler.MESSAGE_GET_PREFERRED_NETWORK_TYPE));
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ }
+
+ /**
+ * Implemented to support onPreferenceChangeListener to look for preference
+ * changes specifically on CLIR.
+ *
+ * @param preference is the preference to be changed, should be mButtonCLIR.
+ * @param objValue should be the value of the selection, NOT its localized
+ * display value.
+ */
+ public boolean onPreferenceChange(Preference preference, Object objValue) {
+ if (preference == mButtonPreferredNetworkMode) {
+ //NOTE onPreferenceChange seems to be called even if there is no change
+ //Check if the button value is changed from the System.Setting
+ mButtonPreferredNetworkMode.setValue((String) objValue);
+ int buttonNetworkMode;
+ buttonNetworkMode = Integer.valueOf((String) objValue).intValue();
+ int settingsNetworkMode = android.provider.Settings.Global.getInt(
+ mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.PREFERRED_NETWORK_MODE, preferredNetworkMode);
+ if (buttonNetworkMode != settingsNetworkMode) {
+ int modemNetworkMode;
+ // if new mode is invalid ignore it
+ switch (buttonNetworkMode) {
+ case Phone.NT_MODE_WCDMA_PREF:
+ case Phone.NT_MODE_GSM_ONLY:
+ case Phone.NT_MODE_WCDMA_ONLY:
+ case Phone.NT_MODE_GSM_UMTS:
+ case Phone.NT_MODE_CDMA:
+ case Phone.NT_MODE_CDMA_NO_EVDO:
+ case Phone.NT_MODE_EVDO_NO_CDMA:
+ case Phone.NT_MODE_GLOBAL:
+ case Phone.NT_MODE_LTE_CDMA_AND_EVDO:
+ case Phone.NT_MODE_LTE_GSM_WCDMA:
+ case Phone.NT_MODE_LTE_CMDA_EVDO_GSM_WCDMA:
+ case Phone.NT_MODE_LTE_ONLY:
+ case Phone.NT_MODE_LTE_WCDMA:
+ // This is one of the modes we recognize
+ modemNetworkMode = buttonNetworkMode;
+ break;
+ default:
+ loge("Invalid Network Mode (" + buttonNetworkMode + ") chosen. Ignore.");
+ return true;
+ }
+
+ UpdatePreferredNetworkModeSummary(buttonNetworkMode);
+
+ android.provider.Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.PREFERRED_NETWORK_MODE,
+ buttonNetworkMode );
+ //Set the modem network mode
+ mPhone.setPreferredNetworkType(modemNetworkMode, mHandler
+ .obtainMessage(MyHandler.MESSAGE_SET_PREFERRED_NETWORK_TYPE));
+ }
+ }
+
+ // always let the preference setting proceed.
+ return true;
+ }
+
+ private class MyHandler extends Handler {
+
+ static final int MESSAGE_GET_PREFERRED_NETWORK_TYPE = 0;
+ static final int MESSAGE_SET_PREFERRED_NETWORK_TYPE = 1;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_GET_PREFERRED_NETWORK_TYPE:
+ handleGetPreferredNetworkTypeResponse(msg);
+ break;
+
+ case MESSAGE_SET_PREFERRED_NETWORK_TYPE:
+ handleSetPreferredNetworkTypeResponse(msg);
+ break;
+ }
+ }
+
+ private void handleGetPreferredNetworkTypeResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (ar.exception == null) {
+ int modemNetworkMode = ((int[])ar.result)[0];
+
+ if (DBG) {
+ log ("handleGetPreferredNetworkTypeResponse: modemNetworkMode = " +
+ modemNetworkMode);
+ }
+
+ int settingsNetworkMode = android.provider.Settings.Global.getInt(
+ mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.PREFERRED_NETWORK_MODE,
+ preferredNetworkMode);
+
+ if (DBG) {
+ log("handleGetPreferredNetworkTypeReponse: settingsNetworkMode = " +
+ settingsNetworkMode);
+ }
+
+ //check that modemNetworkMode is from an accepted value
+ if (modemNetworkMode == Phone.NT_MODE_WCDMA_PREF ||
+ modemNetworkMode == Phone.NT_MODE_GSM_ONLY ||
+ modemNetworkMode == Phone.NT_MODE_WCDMA_ONLY ||
+ modemNetworkMode == Phone.NT_MODE_GSM_UMTS ||
+ modemNetworkMode == Phone.NT_MODE_CDMA ||
+ modemNetworkMode == Phone.NT_MODE_CDMA_NO_EVDO ||
+ modemNetworkMode == Phone.NT_MODE_EVDO_NO_CDMA ||
+ modemNetworkMode == Phone.NT_MODE_GLOBAL ||
+ modemNetworkMode == Phone.NT_MODE_LTE_CDMA_AND_EVDO ||
+ modemNetworkMode == Phone.NT_MODE_LTE_GSM_WCDMA ||
+ modemNetworkMode == Phone.NT_MODE_LTE_CMDA_EVDO_GSM_WCDMA ||
+ modemNetworkMode == Phone.NT_MODE_LTE_ONLY ||
+ modemNetworkMode == Phone.NT_MODE_LTE_WCDMA) {
+ if (DBG) {
+ log("handleGetPreferredNetworkTypeResponse: if 1: modemNetworkMode = " +
+ modemNetworkMode);
+ }
+
+ //check changes in modemNetworkMode and updates settingsNetworkMode
+ if (modemNetworkMode != settingsNetworkMode) {
+ if (DBG) {
+ log("handleGetPreferredNetworkTypeResponse: if 2: " +
+ "modemNetworkMode != settingsNetworkMode");
+ }
+
+ settingsNetworkMode = modemNetworkMode;
+
+ if (DBG) { log("handleGetPreferredNetworkTypeResponse: if 2: " +
+ "settingsNetworkMode = " + settingsNetworkMode);
+ }
+
+ //changes the Settings.System accordingly to modemNetworkMode
+ android.provider.Settings.Global.putInt(
+ mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.PREFERRED_NETWORK_MODE,
+ settingsNetworkMode );
+ }
+
+ UpdatePreferredNetworkModeSummary(modemNetworkMode);
+ // changes the mButtonPreferredNetworkMode accordingly to modemNetworkMode
+ mButtonPreferredNetworkMode.setValue(Integer.toString(modemNetworkMode));
+ } else {
+ if (DBG) log("handleGetPreferredNetworkTypeResponse: else: reset to default");
+ resetNetworkModeToDefault();
+ }
+ }
+ }
+
+ private void handleSetPreferredNetworkTypeResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (ar.exception == null) {
+ int networkMode = Integer.valueOf(
+ mButtonPreferredNetworkMode.getValue()).intValue();
+ android.provider.Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.PREFERRED_NETWORK_MODE,
+ networkMode );
+ } else {
+ mPhone.getPreferredNetworkType(obtainMessage(MESSAGE_GET_PREFERRED_NETWORK_TYPE));
+ }
+ }
+
+ private void resetNetworkModeToDefault() {
+ //set the mButtonPreferredNetworkMode
+ mButtonPreferredNetworkMode.setValue(Integer.toString(preferredNetworkMode));
+ //set the Settings.System
+ android.provider.Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.PREFERRED_NETWORK_MODE,
+ preferredNetworkMode );
+ //Set the Modem
+ mPhone.setPreferredNetworkType(preferredNetworkMode,
+ this.obtainMessage(MyHandler.MESSAGE_SET_PREFERRED_NETWORK_TYPE));
+ }
+ }
+
+ private void UpdatePreferredNetworkModeSummary(int NetworkMode) {
+ switch(NetworkMode) {
+ case Phone.NT_MODE_WCDMA_PREF:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_wcdma_perf_summary);
+ break;
+ case Phone.NT_MODE_GSM_ONLY:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_gsm_only_summary);
+ break;
+ case Phone.NT_MODE_WCDMA_ONLY:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_wcdma_only_summary);
+ break;
+ case Phone.NT_MODE_GSM_UMTS:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_gsm_wcdma_summary);
+ break;
+ case Phone.NT_MODE_CDMA:
+ switch (mPhone.getLteOnCdmaMode()) {
+ case PhoneConstants.LTE_ON_CDMA_TRUE:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_cdma_summary);
+ break;
+ case PhoneConstants.LTE_ON_CDMA_FALSE:
+ default:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_cdma_evdo_summary);
+ break;
+ }
+ break;
+ case Phone.NT_MODE_CDMA_NO_EVDO:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_cdma_only_summary);
+ break;
+ case Phone.NT_MODE_EVDO_NO_CDMA:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_evdo_only_summary);
+ break;
+ case Phone.NT_MODE_LTE_ONLY:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_lte_summary);
+ break;
+ case Phone.NT_MODE_LTE_GSM_WCDMA:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_lte_gsm_wcdma_summary);
+ break;
+ case Phone.NT_MODE_LTE_CDMA_AND_EVDO:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_lte_cdma_evdo_summary);
+ break;
+ case Phone.NT_MODE_LTE_CMDA_EVDO_GSM_WCDMA:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_global_summary);
+ break;
+ case Phone.NT_MODE_GLOBAL:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_cdma_evdo_gsm_wcdma_summary);
+ break;
+ case Phone.NT_MODE_LTE_WCDMA:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_lte_wcdma_summary);
+ break;
+ default:
+ mButtonPreferredNetworkMode.setSummary(
+ R.string.preferred_network_mode_global_summary);
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch(requestCode) {
+ case REQUEST_CODE_EXIT_ECM:
+ Boolean isChoiceYes =
+ data.getBooleanExtra(EmergencyCallbackModeExitDialog.EXTRA_EXIT_ECM_RESULT, false);
+ if (isChoiceYes) {
+ // If the phone exits from ECM mode, show the CDMA Options
+ mCdmaOptions.showDialog(mClickedPreference);
+ } else {
+ // do nothing
+ }
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+
+ private static void loge(String msg) {
+ Log.e(LOG_TAG, msg);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled()
+ // Commenting out "logical up" capability. This is a workaround for issue 5278083.
+ //
+ // Settings app may not launch this activity via UP_ACTIVITY_CLASS but the other
+ // Activity that looks exactly same as UP_ACTIVITY_CLASS ("SubSettings" Activity).
+ // At that moment, this Activity launches UP_ACTIVITY_CLASS on top of the Activity.
+ // which confuses users.
+ // TODO: introduce better mechanism for "up" capability here.
+ /*Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setClassName(UP_ACTIVITY_PACKAGE, UP_ACTIVITY_CLASS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);*/
+ finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/com/android/phone/MultiLineTitleEditTextPreference.java b/src/com/android/phone/MultiLineTitleEditTextPreference.java
new file mode 100644
index 0000000..58d79f8
--- /dev/null
+++ b/src/com/android/phone/MultiLineTitleEditTextPreference.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2011 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.phone;
+
+import android.content.Context;
+import android.preference.EditTextPreference;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.TextView;
+
+/**
+ * Ultra-simple subclass of EditTextPreference that allows the "title" to wrap
+ * onto multiple lines.
+ *
+ * (By default, the title of an EditTextPreference is singleLine="true"; see
+ * preference_holo.xml under frameworks/base. But in the "Respond via SMS"
+ * settings UI we want titles to be multi-line, since the customized messages
+ * might be fairly long, and should be able to wrap.)
+ *
+ * TODO: This is pretty cumbersome; it would be nicer for the framework to
+ * either allow modifying the title's attributes in XML, or at least provide
+ * some way from Java (given an EditTextPreference) to reach inside and get a
+ * handle to the "title" TextView.
+ *
+ * TODO: Also, it would reduce clutter if this could be an inner class in
+ * RespondViaSmsManager.java, but then there would be no way to reference the
+ * class from XML. That's because
+ * <com.android.phone.RespondViaSmsManager$MultiLineTitleEditTextPreference ... />
+ * isn't valid XML syntax due to the "$" character. And Preference
+ * elements don't have a "class" attribute, so you can't do something like
+ * <view class="com.android.phone.Foo$Bar"> as you can with regular views.
+ */
+public class MultiLineTitleEditTextPreference extends EditTextPreference {
+ public MultiLineTitleEditTextPreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public MultiLineTitleEditTextPreference(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public MultiLineTitleEditTextPreference(Context context) {
+ super(context);
+ }
+
+ // The "title" TextView inside an EditTextPreference defaults to
+ // singleLine="true" (see preference_holo.xml under frameworks/base.)
+ // We override onBindView() purely to look up that TextView and call
+ // setSingleLine(false) on it.
+ @Override
+ protected void onBindView(View view) {
+ super.onBindView(view);
+
+ TextView textView = (TextView) view.findViewById(com.android.internal.R.id.title);
+ if (textView != null) {
+ textView.setSingleLine(false);
+ }
+ }
+}
diff --git a/src/com/android/phone/NetworkQueryService.java b/src/com/android/phone/NetworkQueryService.java
new file mode 100644
index 0000000..be8c78e
--- /dev/null
+++ b/src/com/android/phone/NetworkQueryService.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.app.Service;
+import android.content.Intent;
+import com.android.internal.telephony.OperatorInfo;
+import android.os.AsyncResult;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteCallbackList;
+import android.os.RemoteException;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+import android.util.Log;
+
+import java.util.ArrayList;
+
+/**
+ * Service code used to assist in querying the network for service
+ * availability.
+ */
+public class NetworkQueryService extends Service {
+ // debug data
+ private static final String LOG_TAG = "NetworkQuery";
+ private static final boolean DBG = false;
+
+ // static events
+ private static final int EVENT_NETWORK_SCAN_COMPLETED = 100;
+
+ // static states indicating the query status of the service
+ private static final int QUERY_READY = -1;
+ private static final int QUERY_IS_RUNNING = -2;
+
+ // error statuses that will be retured in the callback.
+ public static final int QUERY_OK = 0;
+ public static final int QUERY_EXCEPTION = 1;
+
+ /** state of the query service */
+ private int mState;
+
+ /** local handle to the phone object */
+ private Phone mPhone;
+
+ /**
+ * Class for clients to access. Because we know this service always
+ * runs in the same process as its clients, we don't need to deal with
+ * IPC.
+ */
+ public class LocalBinder extends Binder {
+ INetworkQueryService getService() {
+ return mBinder;
+ }
+ }
+ private final IBinder mLocalBinder = new LocalBinder();
+
+ /**
+ * Local handler to receive the network query compete callback
+ * from the RIL.
+ */
+ Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ // if the scan is complete, broadcast the results.
+ // to all registerd callbacks.
+ case EVENT_NETWORK_SCAN_COMPLETED:
+ if (DBG) log("scan completed, broadcasting results");
+ broadcastQueryResults((AsyncResult) msg.obj);
+ break;
+ }
+ }
+ };
+
+ /**
+ * List of callback objects, also used to synchronize access to
+ * itself and to changes in state.
+ */
+ final RemoteCallbackList<INetworkQueryServiceCallback> mCallbacks =
+ new RemoteCallbackList<INetworkQueryServiceCallback> ();
+
+ /**
+ * Implementation of the INetworkQueryService interface.
+ */
+ private final INetworkQueryService.Stub mBinder = new INetworkQueryService.Stub() {
+
+ /**
+ * Starts a query with a INetworkQueryServiceCallback object if
+ * one has not been started yet. Ignore the new query request
+ * if the query has been started already. Either way, place the
+ * callback object in the queue to be notified upon request
+ * completion.
+ */
+ public void startNetworkQuery(INetworkQueryServiceCallback cb) {
+ if (cb != null) {
+ // register the callback to the list of callbacks.
+ synchronized (mCallbacks) {
+ mCallbacks.register(cb);
+ if (DBG) log("registering callback " + cb.getClass().toString());
+
+ switch (mState) {
+ case QUERY_READY:
+ // TODO: we may want to install a timeout here in case we
+ // do not get a timely response from the RIL.
+ mPhone.getAvailableNetworks(
+ mHandler.obtainMessage(EVENT_NETWORK_SCAN_COMPLETED));
+ mState = QUERY_IS_RUNNING;
+ if (DBG) log("starting new query");
+ break;
+
+ // do nothing if we're currently busy.
+ case QUERY_IS_RUNNING:
+ if (DBG) log("query already in progress");
+ break;
+ default:
+ }
+ }
+ }
+ }
+
+ /**
+ * Stops a query with a INetworkQueryServiceCallback object as
+ * a token.
+ */
+ public void stopNetworkQuery(INetworkQueryServiceCallback cb) {
+ // currently we just unregister the callback, since there is
+ // no way to tell the RIL to terminate the query request.
+ // This means that the RIL may still be busy after the stop
+ // request was made, but the state tracking logic ensures
+ // that the delay will only last for 1 request even with
+ // repeated button presses in the NetworkSetting activity.
+ if (cb != null) {
+ synchronized (mCallbacks) {
+ if (DBG) log("unregistering callback " + cb.getClass().toString());
+ mCallbacks.unregister(cb);
+ }
+ }
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ mState = QUERY_READY;
+ mPhone = PhoneFactory.getDefaultPhone();
+ }
+
+ /**
+ * Required for service implementation.
+ */
+ @Override
+ public void onStart(Intent intent, int startId) {
+ }
+
+ /**
+ * Handle the bind request.
+ */
+ @Override
+ public IBinder onBind(Intent intent) {
+ // TODO: Currently, return only the LocalBinder instance. If we
+ // end up requiring support for a remote binder, we will need to
+ // return mBinder as well, depending upon the intent.
+ if (DBG) log("binding service implementation");
+ return mLocalBinder;
+ }
+
+ /**
+ * Broadcast the results from the query to all registered callback
+ * objects.
+ */
+ private void broadcastQueryResults (AsyncResult ar) {
+ // reset the state.
+ synchronized (mCallbacks) {
+ mState = QUERY_READY;
+
+ // see if we need to do any work.
+ if (ar == null) {
+ if (DBG) log("AsyncResult is null.");
+ return;
+ }
+
+ // TODO: we may need greater accuracy here, but for now, just a
+ // simple status integer will suffice.
+ int exception = (ar.exception == null) ? QUERY_OK : QUERY_EXCEPTION;
+ if (DBG) log("AsyncResult has exception " + exception);
+
+ // Make the calls to all the registered callbacks.
+ for (int i = (mCallbacks.beginBroadcast() - 1); i >= 0; i--) {
+ INetworkQueryServiceCallback cb = mCallbacks.getBroadcastItem(i);
+ if (DBG) log("broadcasting results to " + cb.getClass().toString());
+ try {
+ cb.onQueryComplete((ArrayList<OperatorInfo>) ar.result, exception);
+ } catch (RemoteException e) {
+ }
+ }
+
+ // finish up.
+ mCallbacks.finishBroadcast();
+ }
+ }
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/NetworkSetting.java b/src/com/android/phone/NetworkSetting.java
new file mode 100644
index 0000000..5917795
--- /dev/null
+++ b/src/com/android/phone/NetworkSetting.java
@@ -0,0 +1,478 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.AsyncResult;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceGroup;
+import android.preference.PreferenceScreen;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.OperatorInfo;
+
+import java.util.HashMap;
+import java.util.List;
+
+/**
+ * "Networks" settings UI for the Phone app.
+ */
+public class NetworkSetting extends PreferenceActivity
+ implements DialogInterface.OnCancelListener {
+
+ private static final String LOG_TAG = "phone";
+ private static final boolean DBG = false;
+
+ private static final int EVENT_NETWORK_SCAN_COMPLETED = 100;
+ private static final int EVENT_NETWORK_SELECTION_DONE = 200;
+ private static final int EVENT_AUTO_SELECT_DONE = 300;
+
+ //dialog ids
+ private static final int DIALOG_NETWORK_SELECTION = 100;
+ private static final int DIALOG_NETWORK_LIST_LOAD = 200;
+ private static final int DIALOG_NETWORK_AUTO_SELECT = 300;
+
+ //String keys for preference lookup
+ private static final String LIST_NETWORKS_KEY = "list_networks_key";
+ private static final String BUTTON_SRCH_NETWRKS_KEY = "button_srch_netwrks_key";
+ private static final String BUTTON_AUTO_SELECT_KEY = "button_auto_select_key";
+
+ //map of network controls to the network data.
+ private HashMap<Preference, OperatorInfo> mNetworkMap;
+
+ Phone mPhone;
+ protected boolean mIsForeground = false;
+
+ /** message for network selection */
+ String mNetworkSelectMsg;
+
+ //preference objects
+ private PreferenceGroup mNetworkList;
+ private Preference mSearchButton;
+ private Preference mAutoSelect;
+
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ AsyncResult ar;
+ switch (msg.what) {
+ case EVENT_NETWORK_SCAN_COMPLETED:
+ networksListLoaded ((List<OperatorInfo>) msg.obj, msg.arg1);
+ break;
+
+ case EVENT_NETWORK_SELECTION_DONE:
+ if (DBG) log("hideProgressPanel");
+ removeDialog(DIALOG_NETWORK_SELECTION);
+ getPreferenceScreen().setEnabled(true);
+
+ ar = (AsyncResult) msg.obj;
+ if (ar.exception != null) {
+ if (DBG) log("manual network selection: failed!");
+ displayNetworkSelectionFailed(ar.exception);
+ } else {
+ if (DBG) log("manual network selection: succeeded!");
+ displayNetworkSelectionSucceeded();
+ }
+ break;
+ case EVENT_AUTO_SELECT_DONE:
+ if (DBG) log("hideProgressPanel");
+
+ // Always try to dismiss the dialog because activity may
+ // be moved to background after dialog is shown.
+ try {
+ dismissDialog(DIALOG_NETWORK_AUTO_SELECT);
+ } catch (IllegalArgumentException e) {
+ // "auto select" is always trigged in foreground, so "auto select" dialog
+ // should be shown when "auto select" is trigged. Should NOT get
+ // this exception, and Log it.
+ Log.w(LOG_TAG, "[NetworksList] Fail to dismiss auto select dialog", e);
+ }
+ getPreferenceScreen().setEnabled(true);
+
+ ar = (AsyncResult) msg.obj;
+ if (ar.exception != null) {
+ if (DBG) log("automatic network selection: failed!");
+ displayNetworkSelectionFailed(ar.exception);
+ } else {
+ if (DBG) log("automatic network selection: succeeded!");
+ displayNetworkSelectionSucceeded();
+ }
+ break;
+ }
+
+ return;
+ }
+ };
+
+ /**
+ * Service connection code for the NetworkQueryService.
+ * Handles the work of binding to a local object so that we can make
+ * the appropriate service calls.
+ */
+
+ /** Local service interface */
+ private INetworkQueryService mNetworkQueryService = null;
+
+ /** Service connection */
+ private final ServiceConnection mNetworkQueryServiceConnection = new ServiceConnection() {
+
+ /** Handle the task of binding the local object to the service */
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ if (DBG) log("connection created, binding local service.");
+ mNetworkQueryService = ((NetworkQueryService.LocalBinder) service).getService();
+ // as soon as it is bound, run a query.
+ loadNetworksList();
+ }
+
+ /** Handle the task of cleaning up the local binding */
+ public void onServiceDisconnected(ComponentName className) {
+ if (DBG) log("connection disconnected, cleaning local binding.");
+ mNetworkQueryService = null;
+ }
+ };
+
+ /**
+ * This implementation of INetworkQueryServiceCallback is used to receive
+ * callback notifications from the network query service.
+ */
+ private final INetworkQueryServiceCallback mCallback = new INetworkQueryServiceCallback.Stub() {
+
+ /** place the message on the looper queue upon query completion. */
+ public void onQueryComplete(List<OperatorInfo> networkInfoArray, int status) {
+ if (DBG) log("notifying message loop of query completion.");
+ Message msg = mHandler.obtainMessage(EVENT_NETWORK_SCAN_COMPLETED,
+ status, 0, networkInfoArray);
+ msg.sendToTarget();
+ }
+ };
+
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
+ boolean handled = false;
+
+ if (preference == mSearchButton) {
+ loadNetworksList();
+ handled = true;
+ } else if (preference == mAutoSelect) {
+ selectNetworkAutomatic();
+ handled = true;
+ } else {
+ Preference selectedCarrier = preference;
+
+ String networkStr = selectedCarrier.getTitle().toString();
+ if (DBG) log("selected network: " + networkStr);
+
+ Message msg = mHandler.obtainMessage(EVENT_NETWORK_SELECTION_DONE);
+ mPhone.selectNetworkManually(mNetworkMap.get(selectedCarrier), msg);
+
+ displayNetworkSeletionInProgress(networkStr);
+
+ handled = true;
+ }
+
+ return handled;
+ }
+
+ //implemented for DialogInterface.OnCancelListener
+ public void onCancel(DialogInterface dialog) {
+ // request that the service stop the query with this callback object.
+ try {
+ mNetworkQueryService.stopNetworkQuery(mCallback);
+ } catch (RemoteException e) {
+ throw new RuntimeException(e);
+ }
+ finish();
+ }
+
+ public String getNormalizedCarrierName(OperatorInfo ni) {
+ if (ni != null) {
+ return ni.getOperatorAlphaLong() + " (" + ni.getOperatorNumeric() + ")";
+ }
+ return null;
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ addPreferencesFromResource(R.xml.carrier_select);
+
+ mPhone = PhoneGlobals.getPhone();
+
+ mNetworkList = (PreferenceGroup) getPreferenceScreen().findPreference(LIST_NETWORKS_KEY);
+ mNetworkMap = new HashMap<Preference, OperatorInfo>();
+
+ mSearchButton = getPreferenceScreen().findPreference(BUTTON_SRCH_NETWRKS_KEY);
+ mAutoSelect = getPreferenceScreen().findPreference(BUTTON_AUTO_SELECT_KEY);
+
+ // Start the Network Query service, and bind it.
+ // The OS knows to start he service only once and keep the instance around (so
+ // long as startService is called) until a stopservice request is made. Since
+ // we want this service to just stay in the background until it is killed, we
+ // don't bother stopping it from our end.
+ startService (new Intent(this, NetworkQueryService.class));
+ bindService (new Intent(this, NetworkQueryService.class), mNetworkQueryServiceConnection,
+ Context.BIND_AUTO_CREATE);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mIsForeground = true;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mIsForeground = false;
+ }
+
+ /**
+ * Override onDestroy() to unbind the query service, avoiding service
+ * leak exceptions.
+ */
+ @Override
+ protected void onDestroy() {
+ // unbind the service.
+ unbindService(mNetworkQueryServiceConnection);
+
+ super.onDestroy();
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+
+ if ((id == DIALOG_NETWORK_SELECTION) || (id == DIALOG_NETWORK_LIST_LOAD) ||
+ (id == DIALOG_NETWORK_AUTO_SELECT)) {
+ ProgressDialog dialog = new ProgressDialog(this);
+ switch (id) {
+ case DIALOG_NETWORK_SELECTION:
+ // It would be more efficient to reuse this dialog by moving
+ // this setMessage() into onPreparedDialog() and NOT use
+ // removeDialog(). However, this is not possible since the
+ // message is rendered only 2 times in the ProgressDialog -
+ // after show() and before onCreate.
+ dialog.setMessage(mNetworkSelectMsg);
+ dialog.setCancelable(false);
+ dialog.setIndeterminate(true);
+ break;
+ case DIALOG_NETWORK_AUTO_SELECT:
+ dialog.setMessage(getResources().getString(R.string.register_automatically));
+ dialog.setCancelable(false);
+ dialog.setIndeterminate(true);
+ break;
+ case DIALOG_NETWORK_LIST_LOAD:
+ default:
+ // reinstate the cancelablity of the dialog.
+ dialog.setMessage(getResources().getString(R.string.load_networks_progress));
+ dialog.setCanceledOnTouchOutside(false);
+ dialog.setOnCancelListener(this);
+ break;
+ }
+ return dialog;
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPrepareDialog(int id, Dialog dialog) {
+ if ((id == DIALOG_NETWORK_SELECTION) || (id == DIALOG_NETWORK_LIST_LOAD) ||
+ (id == DIALOG_NETWORK_AUTO_SELECT)) {
+ // when the dialogs come up, we'll need to indicate that
+ // we're in a busy state to dissallow further input.
+ getPreferenceScreen().setEnabled(false);
+ }
+ }
+
+ private void displayEmptyNetworkList(boolean flag) {
+ mNetworkList.setTitle(flag ? R.string.empty_networks_list : R.string.label_available);
+ }
+
+ private void displayNetworkSeletionInProgress(String networkStr) {
+ // TODO: use notification manager?
+ mNetworkSelectMsg = getResources().getString(R.string.register_on_network, networkStr);
+
+ if (mIsForeground) {
+ showDialog(DIALOG_NETWORK_SELECTION);
+ }
+ }
+
+ private void displayNetworkQueryFailed(int error) {
+ String status = getResources().getString(R.string.network_query_error);
+
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+ app.notificationMgr.postTransientNotification(
+ NotificationMgr.NETWORK_SELECTION_NOTIFICATION, status);
+ }
+
+ private void displayNetworkSelectionFailed(Throwable ex) {
+ String status;
+
+ if ((ex != null && ex instanceof CommandException) &&
+ ((CommandException)ex).getCommandError()
+ == CommandException.Error.ILLEGAL_SIM_OR_ME)
+ {
+ status = getResources().getString(R.string.not_allowed);
+ } else {
+ status = getResources().getString(R.string.connect_later);
+ }
+
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+ app.notificationMgr.postTransientNotification(
+ NotificationMgr.NETWORK_SELECTION_NOTIFICATION, status);
+ }
+
+ private void displayNetworkSelectionSucceeded() {
+ String status = getResources().getString(R.string.registration_done);
+
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+ app.notificationMgr.postTransientNotification(
+ NotificationMgr.NETWORK_SELECTION_NOTIFICATION, status);
+
+ mHandler.postDelayed(new Runnable() {
+ public void run() {
+ finish();
+ }
+ }, 3000);
+ }
+
+ private void loadNetworksList() {
+ if (DBG) log("load networks list...");
+
+ if (mIsForeground) {
+ showDialog(DIALOG_NETWORK_LIST_LOAD);
+ }
+
+ // delegate query request to the service.
+ try {
+ mNetworkQueryService.startNetworkQuery(mCallback);
+ } catch (RemoteException e) {
+ }
+
+ displayEmptyNetworkList(false);
+ }
+
+ /**
+ * networksListLoaded has been rewritten to take an array of
+ * OperatorInfo objects and a status field, instead of an
+ * AsyncResult. Otherwise, the functionality which takes the
+ * OperatorInfo array and creates a list of preferences from it,
+ * remains unchanged.
+ */
+ private void networksListLoaded(List<OperatorInfo> result, int status) {
+ if (DBG) log("networks list loaded");
+
+ // update the state of the preferences.
+ if (DBG) log("hideProgressPanel");
+
+
+ // Always try to dismiss the dialog because activity may
+ // be moved to background after dialog is shown.
+ try {
+ dismissDialog(DIALOG_NETWORK_LIST_LOAD);
+ } catch (IllegalArgumentException e) {
+ // It's not a error in following scenario, we just ignore it.
+ // "Load list" dialog will not show, if NetworkQueryService is
+ // connected after this activity is moved to background.
+ if (DBG) log("Fail to dismiss network load list dialog");
+ }
+
+ getPreferenceScreen().setEnabled(true);
+ clearList();
+
+ if (status != NetworkQueryService.QUERY_OK) {
+ if (DBG) log("error while querying available networks");
+ displayNetworkQueryFailed(status);
+ displayEmptyNetworkList(true);
+ } else {
+ if (result != null){
+ displayEmptyNetworkList(false);
+
+ // create a preference for each item in the list.
+ // just use the operator name instead of the mildly
+ // confusing mcc/mnc.
+ for (OperatorInfo ni : result) {
+ Preference carrier = new Preference(this, null);
+ carrier.setTitle(getNetworkTitle(ni));
+ carrier.setPersistent(false);
+ mNetworkList.addPreference(carrier);
+ mNetworkMap.put(carrier, ni);
+
+ if (DBG) log(" " + ni);
+ }
+
+ } else {
+ displayEmptyNetworkList(true);
+ }
+ }
+ }
+
+ /**
+ * Returns the title of the network obtained in the manual search.
+ *
+ * @param OperatorInfo contains the information of the network.
+ *
+ * @return Long Name if not null/empty, otherwise Short Name if not null/empty,
+ * else MCCMNC string.
+ */
+
+ private String getNetworkTitle(OperatorInfo ni) {
+ if (!TextUtils.isEmpty(ni.getOperatorAlphaLong())) {
+ return ni.getOperatorAlphaLong();
+ } else if (!TextUtils.isEmpty(ni.getOperatorAlphaShort())) {
+ return ni.getOperatorAlphaShort();
+ } else {
+ return ni.getOperatorNumeric();
+ }
+ }
+
+ private void clearList() {
+ for (Preference p : mNetworkMap.keySet()) {
+ mNetworkList.removePreference(p);
+ }
+ mNetworkMap.clear();
+ }
+
+ private void selectNetworkAutomatic() {
+ if (DBG) log("select network automatically...");
+ if (mIsForeground) {
+ showDialog(DIALOG_NETWORK_AUTO_SELECT);
+ }
+
+ Message msg = mHandler.obtainMessage(EVENT_AUTO_SELECT_DONE);
+ mPhone.setNetworkSelectionModeAutomatic(msg);
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, "[NetworksList] " + msg);
+ }
+}
diff --git a/src/com/android/phone/NotificationMgr.java b/src/com/android/phone/NotificationMgr.java
new file mode 100644
index 0000000..ab0ba0c
--- /dev/null
+++ b/src/com/android/phone/NotificationMgr.java
@@ -0,0 +1,1471 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.StatusBarManager;
+import android.content.AsyncQueryHandler;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.PowerManager;
+import android.os.SystemProperties;
+import android.preference.PreferenceManager;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.PhoneLookup;
+import android.provider.Settings;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.ServiceState;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.CallerInfoAsyncQuery;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneBase;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyCapabilities;
+
+/**
+ * NotificationManager-related utility code for the Phone app.
+ *
+ * This is a singleton object which acts as the interface to the
+ * framework's NotificationManager, and is used to display status bar
+ * icons and control other status bar-related behavior.
+ *
+ * @see PhoneGlobals.notificationMgr
+ */
+public class NotificationMgr implements CallerInfoAsyncQuery.OnQueryCompleteListener{
+ private static final String LOG_TAG = "NotificationMgr";
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ // Do not check in with VDBG = true, since that may write PII to the system log.
+ private static final boolean VDBG = false;
+
+ private static final String[] CALL_LOG_PROJECTION = new String[] {
+ Calls._ID,
+ Calls.NUMBER,
+ Calls.NUMBER_PRESENTATION,
+ Calls.DATE,
+ Calls.DURATION,
+ Calls.TYPE,
+ };
+
+ // notification types
+ static final int MISSED_CALL_NOTIFICATION = 1;
+ static final int IN_CALL_NOTIFICATION = 2;
+ static final int MMI_NOTIFICATION = 3;
+ static final int NETWORK_SELECTION_NOTIFICATION = 4;
+ static final int VOICEMAIL_NOTIFICATION = 5;
+ static final int CALL_FORWARD_NOTIFICATION = 6;
+ static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 7;
+ static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 8;
+
+ /** The singleton NotificationMgr instance. */
+ private static NotificationMgr sInstance;
+
+ private PhoneGlobals mApp;
+ private Phone mPhone;
+ private CallManager mCM;
+
+ private Context mContext;
+ private NotificationManager mNotificationManager;
+ private StatusBarManager mStatusBarManager;
+ private PowerManager mPowerManager;
+ private Toast mToast;
+ private boolean mShowingSpeakerphoneIcon;
+ private boolean mShowingMuteIcon;
+
+ public StatusBarHelper statusBarHelper;
+
+ // used to track the missed call counter, default to 0.
+ private int mNumberMissedCalls = 0;
+
+ // Currently-displayed resource IDs for some status bar icons (or zero
+ // if no notification is active):
+ private int mInCallResId;
+
+ // used to track the notification of selected network unavailable
+ private boolean mSelectedUnavailableNotify = false;
+
+ // Retry params for the getVoiceMailNumber() call; see updateMwi().
+ private static final int MAX_VM_NUMBER_RETRIES = 5;
+ private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000;
+ private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES;
+
+ // Query used to look up caller-id info for the "call log" notification.
+ private QueryHandler mQueryHandler = null;
+ private static final int CALL_LOG_TOKEN = -1;
+ private static final int CONTACT_TOKEN = -2;
+
+ /**
+ * Private constructor (this is a singleton).
+ * @see init()
+ */
+ private NotificationMgr(PhoneGlobals app) {
+ mApp = app;
+ mContext = app;
+ mNotificationManager =
+ (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
+ mStatusBarManager =
+ (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE);
+ mPowerManager =
+ (PowerManager) app.getSystemService(Context.POWER_SERVICE);
+ mPhone = app.phone; // TODO: better style to use mCM.getDefaultPhone() everywhere instead
+ mCM = app.mCM;
+ statusBarHelper = new StatusBarHelper();
+ }
+
+ /**
+ * Initialize the singleton NotificationMgr instance.
+ *
+ * This is only done once, at startup, from PhoneApp.onCreate().
+ * From then on, the NotificationMgr instance is available via the
+ * PhoneApp's public "notificationMgr" field, which is why there's no
+ * getInstance() method here.
+ */
+ /* package */ static NotificationMgr init(PhoneGlobals app) {
+ synchronized (NotificationMgr.class) {
+ if (sInstance == null) {
+ sInstance = new NotificationMgr(app);
+ // Update the notifications that need to be touched at startup.
+ sInstance.updateNotificationsAtStartup();
+ } else {
+ Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance);
+ }
+ return sInstance;
+ }
+ }
+
+ /**
+ * Helper class that's a wrapper around the framework's
+ * StatusBarManager.disable() API.
+ *
+ * This class is used to control features like:
+ *
+ * - Disabling the status bar "notification windowshade"
+ * while the in-call UI is up
+ *
+ * - Disabling notification alerts (audible or vibrating)
+ * while a phone call is active
+ *
+ * - Disabling navigation via the system bar (the "soft buttons" at
+ * the bottom of the screen on devices with no hard buttons)
+ *
+ * We control these features through a single point of control to make
+ * sure that the various StatusBarManager.disable() calls don't
+ * interfere with each other.
+ */
+ public class StatusBarHelper {
+ // Current desired state of status bar / system bar behavior
+ private boolean mIsNotificationEnabled = true;
+ private boolean mIsExpandedViewEnabled = true;
+ private boolean mIsSystemBarNavigationEnabled = true;
+
+ private StatusBarHelper () {
+ }
+
+ /**
+ * Enables or disables auditory / vibrational alerts.
+ *
+ * (We disable these any time a voice call is active, regardless
+ * of whether or not the in-call UI is visible.)
+ */
+ public void enableNotificationAlerts(boolean enable) {
+ if (mIsNotificationEnabled != enable) {
+ mIsNotificationEnabled = enable;
+ updateStatusBar();
+ }
+ }
+
+ /**
+ * Enables or disables the expanded view of the status bar
+ * (i.e. the ability to pull down the "notification windowshade").
+ *
+ * (This feature is disabled by the InCallScreen while the in-call
+ * UI is active.)
+ */
+ public void enableExpandedView(boolean enable) {
+ if (mIsExpandedViewEnabled != enable) {
+ mIsExpandedViewEnabled = enable;
+ updateStatusBar();
+ }
+ }
+
+ /**
+ * Enables or disables the navigation via the system bar (the
+ * "soft buttons" at the bottom of the screen)
+ *
+ * (This feature is disabled while an incoming call is ringing,
+ * because it's easy to accidentally touch the system bar while
+ * pulling the phone out of your pocket.)
+ */
+ public void enableSystemBarNavigation(boolean enable) {
+ if (mIsSystemBarNavigationEnabled != enable) {
+ mIsSystemBarNavigationEnabled = enable;
+ updateStatusBar();
+ }
+ }
+
+ /**
+ * Updates the status bar to reflect the current desired state.
+ */
+ private void updateStatusBar() {
+ int state = StatusBarManager.DISABLE_NONE;
+
+ if (!mIsExpandedViewEnabled) {
+ state |= StatusBarManager.DISABLE_EXPAND;
+ }
+ if (!mIsNotificationEnabled) {
+ state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
+ }
+ if (!mIsSystemBarNavigationEnabled) {
+ // Disable *all* possible navigation via the system bar.
+ state |= StatusBarManager.DISABLE_HOME;
+ state |= StatusBarManager.DISABLE_RECENT;
+ state |= StatusBarManager.DISABLE_BACK;
+ }
+
+ if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state));
+ mStatusBarManager.disable(state);
+ }
+ }
+
+ /**
+ * Makes sure phone-related notifications are up to date on a
+ * freshly-booted device.
+ */
+ private void updateNotificationsAtStartup() {
+ if (DBG) log("updateNotificationsAtStartup()...");
+
+ // instantiate query handler
+ mQueryHandler = new QueryHandler(mContext.getContentResolver());
+
+ // setup query spec, look for all Missed calls that are new.
+ StringBuilder where = new StringBuilder("type=");
+ where.append(Calls.MISSED_TYPE);
+ where.append(" AND new=1");
+
+ // start the query
+ if (DBG) log("- start call log query...");
+ mQueryHandler.startQuery(CALL_LOG_TOKEN, null, Calls.CONTENT_URI, CALL_LOG_PROJECTION,
+ where.toString(), null, Calls.DEFAULT_SORT_ORDER);
+
+ // Update (or cancel) the in-call notification
+ if (DBG) log("- updating in-call notification at startup...");
+ updateInCallNotification();
+
+ // Depend on android.app.StatusBarManager to be set to
+ // disable(DISABLE_NONE) upon startup. This will be the
+ // case even if the phone app crashes.
+ }
+
+ /** The projection to use when querying the phones table */
+ static final String[] PHONES_PROJECTION = new String[] {
+ PhoneLookup.NUMBER,
+ PhoneLookup.DISPLAY_NAME,
+ PhoneLookup._ID
+ };
+
+ /**
+ * Class used to run asynchronous queries to re-populate the notifications we care about.
+ * There are really 3 steps to this:
+ * 1. Find the list of missed calls
+ * 2. For each call, run a query to retrieve the caller's name.
+ * 3. For each caller, try obtaining photo.
+ */
+ private class QueryHandler extends AsyncQueryHandler
+ implements ContactsAsyncHelper.OnImageLoadCompleteListener {
+
+ /**
+ * Used to store relevant fields for the Missed Call
+ * notifications.
+ */
+ private class NotificationInfo {
+ public String name;
+ public String number;
+ public int presentation;
+ /**
+ * Type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
+ * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
+ * {@link android.provider.CallLog.Calls#MISSED_TYPE}.
+ */
+ public String type;
+ public long date;
+ }
+
+ public QueryHandler(ContentResolver cr) {
+ super(cr);
+ }
+
+ /**
+ * Handles the query results.
+ */
+ @Override
+ protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ // TODO: it would be faster to use a join here, but for the purposes
+ // of this small record set, it should be ok.
+
+ // Note that CursorJoiner is not useable here because the number
+ // comparisons are not strictly equals; the comparisons happen in
+ // the SQL function PHONE_NUMBERS_EQUAL, which is not available for
+ // the CursorJoiner.
+
+ // Executing our own query is also feasible (with a join), but that
+ // will require some work (possibly destabilizing) in Contacts
+ // Provider.
+
+ // At this point, we will execute subqueries on each row just as
+ // CallLogActivity.java does.
+ switch (token) {
+ case CALL_LOG_TOKEN:
+ if (DBG) log("call log query complete.");
+
+ // initial call to retrieve the call list.
+ if (cursor != null) {
+ while (cursor.moveToNext()) {
+ // for each call in the call log list, create
+ // the notification object and query contacts
+ NotificationInfo n = getNotificationInfo (cursor);
+
+ if (DBG) log("query contacts for number: " + n.number);
+
+ mQueryHandler.startQuery(CONTACT_TOKEN, n,
+ Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, n.number),
+ PHONES_PROJECTION, null, null, PhoneLookup.NUMBER);
+ }
+
+ if (DBG) log("closing call log cursor.");
+ cursor.close();
+ }
+ break;
+ case CONTACT_TOKEN:
+ if (DBG) log("contact query complete.");
+
+ // subqueries to get the caller name.
+ if ((cursor != null) && (cookie != null)){
+ NotificationInfo n = (NotificationInfo) cookie;
+
+ Uri personUri = null;
+ if (cursor.moveToFirst()) {
+ n.name = cursor.getString(
+ cursor.getColumnIndexOrThrow(PhoneLookup.DISPLAY_NAME));
+ long person_id = cursor.getLong(
+ cursor.getColumnIndexOrThrow(PhoneLookup._ID));
+ if (DBG) {
+ log("contact :" + n.name + " found for phone: " + n.number
+ + ". id : " + person_id);
+ }
+ personUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, person_id);
+ }
+
+ if (personUri != null) {
+ if (DBG) {
+ log("Start obtaining picture for the missed call. Uri: "
+ + personUri);
+ }
+ // Now try to obtain a photo for this person.
+ // ContactsAsyncHelper will do that and call onImageLoadComplete()
+ // after that.
+ ContactsAsyncHelper.startObtainPhotoAsync(
+ 0, mContext, personUri, this, n);
+ } else {
+ if (DBG) {
+ log("Failed to find Uri for obtaining photo."
+ + " Just send notification without it.");
+ }
+ // We couldn't find person Uri, so we're sure we cannot obtain a photo.
+ // Call notifyMissedCall() right now.
+ notifyMissedCall(n.name, n.number, n.type, null, null, n.date);
+ }
+
+ if (DBG) log("closing contact cursor.");
+ cursor.close();
+ }
+ break;
+ default:
+ }
+ }
+
+ @Override
+ public void onImageLoadComplete(
+ int token, Drawable photo, Bitmap photoIcon, Object cookie) {
+ if (DBG) log("Finished loading image: " + photo);
+ NotificationInfo n = (NotificationInfo) cookie;
+ notifyMissedCall(n.name, n.number, n.type, photo, photoIcon, n.date);
+ }
+
+ /**
+ * Factory method to generate a NotificationInfo object given a
+ * cursor from the call log table.
+ */
+ private final NotificationInfo getNotificationInfo(Cursor cursor) {
+ NotificationInfo n = new NotificationInfo();
+ n.name = null;
+ n.number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER));
+ n.presentation = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NUMBER_PRESENTATION));
+ n.type = cursor.getString(cursor.getColumnIndexOrThrow(Calls.TYPE));
+ n.date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE));
+
+ // make sure we update the number depending upon saved values in
+ // CallLog.addCall(). If either special values for unknown or
+ // private number are detected, we need to hand off the message
+ // to the missed call notification.
+ if (n.presentation != Calls.PRESENTATION_ALLOWED) {
+ n.number = null;
+ }
+
+ if (DBG) log("NotificationInfo constructed for number: " + n.number);
+
+ return n;
+ }
+ }
+
+ /**
+ * Configures a Notification to emit the blinky green message-waiting/
+ * missed-call signal.
+ */
+ private static void configureLedNotification(Notification note) {
+ note.flags |= Notification.FLAG_SHOW_LIGHTS;
+ note.defaults |= Notification.DEFAULT_LIGHTS;
+ }
+
+ /**
+ * Displays a notification about a missed call.
+ *
+ * @param name the contact name.
+ * @param number the phone number. Note that this may be a non-callable String like "Unknown",
+ * or "Private Number", which possibly come from methods like
+ * {@link PhoneUtils#modifyForSpecialCnapCases(Context, CallerInfo, String, int)}.
+ * @param type the type of the call. {@link android.provider.CallLog.Calls#INCOMING_TYPE}
+ * {@link android.provider.CallLog.Calls#OUTGOING_TYPE}, or
+ * {@link android.provider.CallLog.Calls#MISSED_TYPE}
+ * @param photo picture which may be used for the notification (when photoIcon is null).
+ * This also can be null when the picture itself isn't available. If photoIcon is available
+ * it should be prioritized (because this may be too huge for notification).
+ * See also {@link ContactsAsyncHelper}.
+ * @param photoIcon picture which should be used for the notification. Can be null. This is
+ * the most suitable for {@link android.app.Notification.Builder#setLargeIcon(Bitmap)}, this
+ * should be used when non-null.
+ * @param date the time when the missed call happened
+ */
+ /* package */ void notifyMissedCall(
+ String name, String number, String type, Drawable photo, Bitmap photoIcon, long date) {
+
+ // When the user clicks this notification, we go to the call log.
+ final Intent callLogIntent = PhoneGlobals.createCallLogIntent();
+
+ // Never display the missed call notification on non-voice-capable
+ // devices, even if the device does somehow manage to get an
+ // incoming call.
+ if (!PhoneGlobals.sVoiceCapable) {
+ if (DBG) log("notifyMissedCall: non-voice-capable device, not posting notification");
+ return;
+ }
+
+ if (VDBG) {
+ log("notifyMissedCall(). name: " + name + ", number: " + number
+ + ", label: " + type + ", photo: " + photo + ", photoIcon: " + photoIcon
+ + ", date: " + date);
+ }
+
+ // title resource id
+ int titleResId;
+ // the text in the notification's line 1 and 2.
+ String expandedText, callName;
+
+ // increment number of missed calls.
+ mNumberMissedCalls++;
+
+ // get the name for the ticker text
+ // i.e. "Missed call from <caller name or number>"
+ if (name != null && TextUtils.isGraphic(name)) {
+ callName = name;
+ } else if (!TextUtils.isEmpty(number)){
+ callName = number;
+ } else {
+ // use "unknown" if the caller is unidentifiable.
+ callName = mContext.getString(R.string.unknown);
+ }
+
+ // display the first line of the notification:
+ // 1 missed call: call name
+ // more than 1 missed call: <number of calls> + "missed calls"
+ if (mNumberMissedCalls == 1) {
+ titleResId = R.string.notification_missedCallTitle;
+ expandedText = callName;
+ } else {
+ titleResId = R.string.notification_missedCallsTitle;
+ expandedText = mContext.getString(R.string.notification_missedCallsMsg,
+ mNumberMissedCalls);
+ }
+
+ Notification.Builder builder = new Notification.Builder(mContext);
+ builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
+ .setTicker(mContext.getString(R.string.notification_missedCallTicker, callName))
+ .setWhen(date)
+ .setContentTitle(mContext.getText(titleResId))
+ .setContentText(expandedText)
+ .setContentIntent(PendingIntent.getActivity(mContext, 0, callLogIntent, 0))
+ .setAutoCancel(true)
+ .setDeleteIntent(createClearMissedCallsIntent());
+
+ // Simple workaround for issue 6476275; refrain having actions when the given number seems
+ // not a real one but a non-number which was embedded by methods outside (like
+ // PhoneUtils#modifyForSpecialCnapCases()).
+ // TODO: consider removing equals() checks here, and modify callers of this method instead.
+ if (mNumberMissedCalls == 1
+ && !TextUtils.isEmpty(number)
+ && !TextUtils.equals(number, mContext.getString(R.string.private_num))
+ && !TextUtils.equals(number, mContext.getString(R.string.unknown))){
+ if (DBG) log("Add actions with the number " + number);
+
+ builder.addAction(R.drawable.stat_sys_phone_call,
+ mContext.getString(R.string.notification_missedCall_call_back),
+ PhoneGlobals.getCallBackPendingIntent(mContext, number));
+
+ builder.addAction(R.drawable.ic_text_holo_dark,
+ mContext.getString(R.string.notification_missedCall_message),
+ PhoneGlobals.getSendSmsFromNotificationPendingIntent(mContext, number));
+
+ if (photoIcon != null) {
+ builder.setLargeIcon(photoIcon);
+ } else if (photo instanceof BitmapDrawable) {
+ builder.setLargeIcon(((BitmapDrawable) photo).getBitmap());
+ }
+ } else {
+ if (DBG) {
+ log("Suppress actions. number: " + number + ", missedCalls: " + mNumberMissedCalls);
+ }
+ }
+
+ Notification notification = builder.getNotification();
+ configureLedNotification(notification);
+ mNotificationManager.notify(MISSED_CALL_NOTIFICATION, notification);
+ }
+
+ /** Returns an intent to be invoked when the missed call notification is cleared. */
+ private PendingIntent createClearMissedCallsIntent() {
+ Intent intent = new Intent(mContext, ClearMissedCallsService.class);
+ intent.setAction(ClearMissedCallsService.ACTION_CLEAR_MISSED_CALLS);
+ return PendingIntent.getService(mContext, 0, intent, 0);
+ }
+
+ /**
+ * Cancels the "missed call" notification.
+ *
+ * @see ITelephony.cancelMissedCallsNotification()
+ */
+ void cancelMissedCallNotification() {
+ // reset the number of missed calls to 0.
+ mNumberMissedCalls = 0;
+ mNotificationManager.cancel(MISSED_CALL_NOTIFICATION);
+ }
+
+ private void notifySpeakerphone() {
+ if (!mShowingSpeakerphoneIcon) {
+ mStatusBarManager.setIcon("speakerphone", android.R.drawable.stat_sys_speakerphone, 0,
+ mContext.getString(R.string.accessibility_speakerphone_enabled));
+ mShowingSpeakerphoneIcon = true;
+ }
+ }
+
+ private void cancelSpeakerphone() {
+ if (mShowingSpeakerphoneIcon) {
+ mStatusBarManager.removeIcon("speakerphone");
+ mShowingSpeakerphoneIcon = false;
+ }
+ }
+
+ /**
+ * Shows or hides the "speakerphone" notification in the status bar,
+ * based on the actual current state of the speaker.
+ *
+ * If you already know the current speaker state (e.g. if you just
+ * called AudioManager.setSpeakerphoneOn() yourself) then you should
+ * directly call {@link #updateSpeakerNotification(boolean)} instead.
+ *
+ * (But note that the status bar icon is *never* shown while the in-call UI
+ * is active; it only appears if you bail out to some other activity.)
+ */
+ private void updateSpeakerNotification() {
+ AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ boolean showNotification =
+ (mPhone.getState() == PhoneConstants.State.OFFHOOK) && audioManager.isSpeakerphoneOn();
+
+ if (DBG) log(showNotification
+ ? "updateSpeakerNotification: speaker ON"
+ : "updateSpeakerNotification: speaker OFF (or not offhook)");
+
+ updateSpeakerNotification(showNotification);
+ }
+
+ /**
+ * Shows or hides the "speakerphone" notification in the status bar.
+ *
+ * @param showNotification if true, call notifySpeakerphone();
+ * if false, call cancelSpeakerphone().
+ *
+ * Use {@link updateSpeakerNotification()} to update the status bar
+ * based on the actual current state of the speaker.
+ *
+ * (But note that the status bar icon is *never* shown while the in-call UI
+ * is active; it only appears if you bail out to some other activity.)
+ */
+ public void updateSpeakerNotification(boolean showNotification) {
+ if (DBG) log("updateSpeakerNotification(" + showNotification + ")...");
+
+ // Regardless of the value of the showNotification param, suppress
+ // the status bar icon if the the InCallScreen is the foreground
+ // activity, since the in-call UI already provides an onscreen
+ // indication of the speaker state. (This reduces clutter in the
+ // status bar.)
+ if (mApp.isShowingCallScreen()) {
+ cancelSpeakerphone();
+ return;
+ }
+
+ if (showNotification) {
+ notifySpeakerphone();
+ } else {
+ cancelSpeakerphone();
+ }
+ }
+
+ private void notifyMute() {
+ if (!mShowingMuteIcon) {
+ mStatusBarManager.setIcon("mute", android.R.drawable.stat_notify_call_mute, 0,
+ mContext.getString(R.string.accessibility_call_muted));
+ mShowingMuteIcon = true;
+ }
+ }
+
+ private void cancelMute() {
+ if (mShowingMuteIcon) {
+ mStatusBarManager.removeIcon("mute");
+ mShowingMuteIcon = false;
+ }
+ }
+
+ /**
+ * Shows or hides the "mute" notification in the status bar,
+ * based on the current mute state of the Phone.
+ *
+ * (But note that the status bar icon is *never* shown while the in-call UI
+ * is active; it only appears if you bail out to some other activity.)
+ */
+ void updateMuteNotification() {
+ // Suppress the status bar icon if the the InCallScreen is the
+ // foreground activity, since the in-call UI already provides an
+ // onscreen indication of the mute state. (This reduces clutter
+ // in the status bar.)
+ if (mApp.isShowingCallScreen()) {
+ cancelMute();
+ return;
+ }
+
+ if ((mCM.getState() == PhoneConstants.State.OFFHOOK) && PhoneUtils.getMute()) {
+ if (DBG) log("updateMuteNotification: MUTED");
+ notifyMute();
+ } else {
+ if (DBG) log("updateMuteNotification: not muted (or not offhook)");
+ cancelMute();
+ }
+ }
+
+ /**
+ * Updates the phone app's status bar notification based on the
+ * current telephony state, or cancels the notification if the phone
+ * is totally idle.
+ *
+ * This method will never actually launch the incoming-call UI.
+ * (Use updateNotificationAndLaunchIncomingCallUi() for that.)
+ */
+ public void updateInCallNotification() {
+ // allowFullScreenIntent=false means *don't* allow the incoming
+ // call UI to be launched.
+ updateInCallNotification(false);
+ }
+
+ /**
+ * Updates the phone app's status bar notification *and* launches the
+ * incoming call UI in response to a new incoming call.
+ *
+ * This is just like updateInCallNotification(), with one exception:
+ * If an incoming call is ringing (or call-waiting), the notification
+ * will also include a "fullScreenIntent" that will cause the
+ * InCallScreen to be launched immediately, unless the current
+ * foreground activity is marked as "immersive".
+ *
+ * (This is the mechanism that actually brings up the incoming call UI
+ * when we receive a "new ringing connection" event from the telephony
+ * layer.)
+ *
+ * Watch out: this method should ONLY be called directly from the code
+ * path in CallNotifier that handles the "new ringing connection"
+ * event from the telephony layer. All other places that update the
+ * in-call notification (like for phone state changes) should call
+ * updateInCallNotification() instead. (This ensures that we don't
+ * end up launching the InCallScreen multiple times for a single
+ * incoming call, which could cause slow responsiveness and/or visible
+ * glitches.)
+ *
+ * Also note that this method is safe to call even if the phone isn't
+ * actually ringing (or, more likely, if an incoming call *was*
+ * ringing briefly but then disconnected). In that case, we'll simply
+ * update or cancel the in-call notification based on the current
+ * phone state.
+ *
+ * @see #updateInCallNotification(boolean)
+ */
+ public void updateNotificationAndLaunchIncomingCallUi() {
+ // Set allowFullScreenIntent=true to indicate that we *should*
+ // launch the incoming call UI if necessary.
+ updateInCallNotification(true);
+ }
+
+ /**
+ * Helper method for updateInCallNotification() and
+ * updateNotificationAndLaunchIncomingCallUi(): Update the phone app's
+ * status bar notification based on the current telephony state, or
+ * cancels the notification if the phone is totally idle.
+ *
+ * @param allowFullScreenIntent If true, *and* an incoming call is
+ * ringing, the notification will include a "fullScreenIntent"
+ * pointing at the InCallScreen (which will cause the InCallScreen
+ * to be launched.)
+ * Watch out: This should be set to true *only* when directly
+ * handling the "new ringing connection" event from the telephony
+ * layer (see updateNotificationAndLaunchIncomingCallUi().)
+ */
+ private void updateInCallNotification(boolean allowFullScreenIntent) {
+ int resId;
+ if (DBG) log("updateInCallNotification(allowFullScreenIntent = "
+ + allowFullScreenIntent + ")...");
+
+ // Never display the "ongoing call" notification on
+ // non-voice-capable devices, even if the phone is actually
+ // offhook (like during a non-interactive OTASP call.)
+ if (!PhoneGlobals.sVoiceCapable) {
+ if (DBG) log("- non-voice-capable device; suppressing notification.");
+ return;
+ }
+
+ // If the phone is idle, completely clean up all call-related
+ // notifications.
+ if (mCM.getState() == PhoneConstants.State.IDLE) {
+ cancelInCall();
+ cancelMute();
+ cancelSpeakerphone();
+ return;
+ }
+
+ final boolean hasRingingCall = mCM.hasActiveRingingCall();
+ final boolean hasActiveCall = mCM.hasActiveFgCall();
+ final boolean hasHoldingCall = mCM.hasActiveBgCall();
+ if (DBG) {
+ log(" - hasRingingCall = " + hasRingingCall);
+ log(" - hasActiveCall = " + hasActiveCall);
+ log(" - hasHoldingCall = " + hasHoldingCall);
+ }
+
+ // Suppress the in-call notification if the InCallScreen is the
+ // foreground activity, since it's already obvious that you're on a
+ // call. (The status bar icon is needed only if you navigate *away*
+ // from the in-call UI.)
+ boolean suppressNotification = mApp.isShowingCallScreen();
+ // if (DBG) log("- suppressNotification: initial value: " + suppressNotification);
+
+ // ...except for a couple of cases where we *never* suppress the
+ // notification:
+ //
+ // - If there's an incoming ringing call: always show the
+ // notification, since the in-call notification is what actually
+ // launches the incoming call UI in the first place (see
+ // notification.fullScreenIntent below.) This makes sure that we'll
+ // correctly handle the case where a new incoming call comes in but
+ // the InCallScreen is already in the foreground.
+ if (hasRingingCall) suppressNotification = false;
+
+ // - If "voice privacy" mode is active: always show the notification,
+ // since that's the only "voice privacy" indication we have.
+ boolean enhancedVoicePrivacy = mApp.notifier.getVoicePrivacyState();
+ // if (DBG) log("updateInCallNotification: enhancedVoicePrivacy = " + enhancedVoicePrivacy);
+ if (enhancedVoicePrivacy) suppressNotification = false;
+
+ if (suppressNotification) {
+ if (DBG) log("- suppressNotification = true; reducing clutter in status bar...");
+ cancelInCall();
+ // Suppress the mute and speaker status bar icons too
+ // (also to reduce clutter in the status bar.)
+ cancelSpeakerphone();
+ cancelMute();
+ return;
+ }
+
+ // Display the appropriate icon in the status bar,
+ // based on the current phone and/or bluetooth state.
+
+ if (hasRingingCall) {
+ // There's an incoming ringing call.
+ resId = R.drawable.stat_sys_phone_call;
+ } else if (!hasActiveCall && hasHoldingCall) {
+ // There's only one call, and it's on hold.
+ if (enhancedVoicePrivacy) {
+ resId = R.drawable.stat_sys_vp_phone_call_on_hold;
+ } else {
+ resId = R.drawable.stat_sys_phone_call_on_hold;
+ }
+ } else {
+ if (enhancedVoicePrivacy) {
+ resId = R.drawable.stat_sys_vp_phone_call;
+ } else {
+ resId = R.drawable.stat_sys_phone_call;
+ }
+ }
+
+ // Note we can't just bail out now if (resId == mInCallResId),
+ // since even if the status icon hasn't changed, some *other*
+ // notification-related info may be different from the last time
+ // we were here (like the caller-id info of the foreground call,
+ // if the user swapped calls...)
+
+ if (DBG) log("- Updating status bar icon: resId = " + resId);
+ mInCallResId = resId;
+
+ // Even if both lines are in use, we only show a single item in
+ // the expanded Notifications UI. It's labeled "Ongoing call"
+ // (or "On hold" if there's only one call, and it's on hold.)
+ // Also, we don't have room to display caller-id info from two
+ // different calls. So if both lines are in use, display info
+ // from the foreground call. And if there's a ringing call,
+ // display that regardless of the state of the other calls.
+
+ Call currentCall;
+ if (hasRingingCall) {
+ currentCall = mCM.getFirstActiveRingingCall();
+ } else if (hasActiveCall) {
+ currentCall = mCM.getActiveFgCall();
+ } else {
+ currentCall = mCM.getFirstActiveBgCall();
+ }
+ Connection currentConn = currentCall.getEarliestConnection();
+
+ final Notification.Builder builder = new Notification.Builder(mContext);
+ builder.setSmallIcon(mInCallResId).setOngoing(true);
+
+ // PendingIntent that can be used to launch the InCallScreen. The
+ // system fires off this intent if the user pulls down the windowshade
+ // and clicks the notification's expanded view. It's also used to
+ // launch the InCallScreen immediately when when there's an incoming
+ // call (see the "fullScreenIntent" field below).
+ PendingIntent inCallPendingIntent =
+ PendingIntent.getActivity(mContext, 0,
+ PhoneGlobals.createInCallIntent(), 0);
+ builder.setContentIntent(inCallPendingIntent);
+
+ // Update icon on the left of the notification.
+ // - If it is directly available from CallerInfo, we'll just use that.
+ // - If it is not, use the same icon as in the status bar.
+ CallerInfo callerInfo = null;
+ if (currentConn != null) {
+ Object o = currentConn.getUserData();
+ if (o instanceof CallerInfo) {
+ callerInfo = (CallerInfo) o;
+ } else if (o instanceof PhoneUtils.CallerInfoToken) {
+ callerInfo = ((PhoneUtils.CallerInfoToken) o).currentInfo;
+ } else {
+ Log.w(LOG_TAG, "CallerInfo isn't available while Call object is available.");
+ }
+ }
+ boolean largeIconWasSet = false;
+ if (callerInfo != null) {
+ // In most cases, the user will see the notification after CallerInfo is already
+ // available, so photo will be available from this block.
+ if (callerInfo.isCachedPhotoCurrent) {
+ // .. and in that case CallerInfo's cachedPhotoIcon should also be available.
+ // If it happens not, then try using cachedPhoto, assuming Drawable coming from
+ // ContactProvider will be BitmapDrawable.
+ if (callerInfo.cachedPhotoIcon != null) {
+ builder.setLargeIcon(callerInfo.cachedPhotoIcon);
+ largeIconWasSet = true;
+ } else if (callerInfo.cachedPhoto instanceof BitmapDrawable) {
+ if (DBG) log("- BitmapDrawable found for large icon");
+ Bitmap bitmap = ((BitmapDrawable) callerInfo.cachedPhoto).getBitmap();
+ builder.setLargeIcon(bitmap);
+ largeIconWasSet = true;
+ } else {
+ if (DBG) {
+ log("- Failed to fetch icon from CallerInfo's cached photo."
+ + " (cachedPhotoIcon: " + callerInfo.cachedPhotoIcon
+ + ", cachedPhoto: " + callerInfo.cachedPhoto + ")."
+ + " Ignore it.");
+ }
+ }
+ }
+
+ if (!largeIconWasSet && callerInfo.photoResource > 0) {
+ if (DBG) {
+ log("- BitmapDrawable nor person Id not found for large icon."
+ + " Use photoResource: " + callerInfo.photoResource);
+ }
+ Drawable drawable =
+ mContext.getResources().getDrawable(callerInfo.photoResource);
+ if (drawable instanceof BitmapDrawable) {
+ Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
+ builder.setLargeIcon(bitmap);
+ largeIconWasSet = true;
+ } else {
+ if (DBG) {
+ log("- PhotoResource was found but it didn't return BitmapDrawable."
+ + " Ignore it");
+ }
+ }
+ }
+ } else {
+ if (DBG) log("- CallerInfo not found. Use the same icon as in the status bar.");
+ }
+
+ // Failed to fetch Bitmap.
+ if (!largeIconWasSet && DBG) {
+ log("- No useful Bitmap was found for the photo."
+ + " Use the same icon as in the status bar.");
+ }
+
+ // If the connection is valid, then build what we need for the
+ // content text of notification, and start the chronometer.
+ // Otherwise, don't bother and just stick with content title.
+ if (currentConn != null) {
+ if (DBG) log("- Updating context text and chronometer.");
+ if (hasRingingCall) {
+ // Incoming call is ringing.
+ builder.setContentText(mContext.getString(R.string.notification_incoming_call));
+ builder.setUsesChronometer(false);
+ } else if (hasHoldingCall && !hasActiveCall) {
+ // Only one call, and it's on hold.
+ builder.setContentText(mContext.getString(R.string.notification_on_hold));
+ builder.setUsesChronometer(false);
+ } else {
+ // We show the elapsed time of the current call using Chronometer.
+ builder.setUsesChronometer(true);
+
+ // Determine the "start time" of the current connection.
+ // We can't use currentConn.getConnectTime(), because (1) that's
+ // in the currentTimeMillis() time base, and (2) it's zero when
+ // the phone first goes off hook, since the getConnectTime counter
+ // doesn't start until the DIALING -> ACTIVE transition.
+ // Instead we start with the current connection's duration,
+ // and translate that into the elapsedRealtime() timebase.
+ long callDurationMsec = currentConn.getDurationMillis();
+ builder.setWhen(System.currentTimeMillis() - callDurationMsec);
+
+ int contextTextId = R.string.notification_ongoing_call;
+
+ Call call = mCM.getActiveFgCall();
+ if (TelephonyCapabilities.canDistinguishDialingAndConnected(
+ call.getPhone().getPhoneType()) && call.isDialingOrAlerting()) {
+ contextTextId = R.string.notification_dialing;
+ }
+
+ builder.setContentText(mContext.getString(contextTextId));
+ }
+ } else if (DBG) {
+ Log.w(LOG_TAG, "updateInCallNotification: null connection, can't set exp view line 1.");
+ }
+
+ // display conference call string if this call is a conference
+ // call, otherwise display the connection information.
+
+ // Line 2 of the expanded view (smaller text). This is usually a
+ // contact name or phone number.
+ String expandedViewLine2 = "";
+ // TODO: it may not make sense for every point to make separate
+ // checks for isConferenceCall, so we need to think about
+ // possibly including this in startGetCallerInfo or some other
+ // common point.
+ if (PhoneUtils.isConferenceCall(currentCall)) {
+ // if this is a conference call, just use that as the caller name.
+ expandedViewLine2 = mContext.getString(R.string.card_title_conf_call);
+ } else {
+ // If necessary, start asynchronous query to do the caller-id lookup.
+ PhoneUtils.CallerInfoToken cit =
+ PhoneUtils.startGetCallerInfo(mContext, currentCall, this, this);
+ expandedViewLine2 = PhoneUtils.getCompactNameFromCallerInfo(cit.currentInfo, mContext);
+ // Note: For an incoming call, the very first time we get here we
+ // won't have a contact name yet, since we only just started the
+ // caller-id query. So expandedViewLine2 will start off as a raw
+ // phone number, but we'll update it very quickly when the query
+ // completes (see onQueryComplete() below.)
+ }
+
+ if (DBG) log("- Updating expanded view: line 2 '" + /*expandedViewLine2*/ "xxxxxxx" + "'");
+ builder.setContentTitle(expandedViewLine2);
+
+ // TODO: We also need to *update* this notification in some cases,
+ // like when a call ends on one line but the other is still in use
+ // (ie. make sure the caller info here corresponds to the active
+ // line), and maybe even when the user swaps calls (ie. if we only
+ // show info here for the "current active call".)
+
+ // Activate a couple of special Notification features if an
+ // incoming call is ringing:
+ if (hasRingingCall) {
+ if (DBG) log("- Using hi-pri notification for ringing call!");
+
+ // This is a high-priority event that should be shown even if the
+ // status bar is hidden or if an immersive activity is running.
+ builder.setPriority(Notification.PRIORITY_HIGH);
+
+ // If an immersive activity is running, we have room for a single
+ // line of text in the small notification popup window.
+ // We use expandedViewLine2 for this (i.e. the name or number of
+ // the incoming caller), since that's more relevant than
+ // expandedViewLine1 (which is something generic like "Incoming
+ // call".)
+ builder.setTicker(expandedViewLine2);
+
+ if (allowFullScreenIntent) {
+ // Ok, we actually want to launch the incoming call
+ // UI at this point (in addition to simply posting a notification
+ // to the status bar). Setting fullScreenIntent will cause
+ // the InCallScreen to be launched immediately *unless* the
+ // current foreground activity is marked as "immersive".
+ if (DBG) log("- Setting fullScreenIntent: " + inCallPendingIntent);
+ builder.setFullScreenIntent(inCallPendingIntent, true);
+
+ // Ugly hack alert:
+ //
+ // The NotificationManager has the (undocumented) behavior
+ // that it will *ignore* the fullScreenIntent field if you
+ // post a new Notification that matches the ID of one that's
+ // already active. Unfortunately this is exactly what happens
+ // when you get an incoming call-waiting call: the
+ // "ongoing call" notification is already visible, so the
+ // InCallScreen won't get launched in this case!
+ // (The result: if you bail out of the in-call UI while on a
+ // call and then get a call-waiting call, the incoming call UI
+ // won't come up automatically.)
+ //
+ // The workaround is to just notice this exact case (this is a
+ // call-waiting call *and* the InCallScreen is not in the
+ // foreground) and manually cancel the in-call notification
+ // before (re)posting it.
+ //
+ // TODO: there should be a cleaner way of avoiding this
+ // problem (see discussion in bug 3184149.)
+ Call ringingCall = mCM.getFirstActiveRingingCall();
+ if ((ringingCall.getState() == Call.State.WAITING) && !mApp.isShowingCallScreen()) {
+ Log.i(LOG_TAG, "updateInCallNotification: call-waiting! force relaunch...");
+ // Cancel the IN_CALL_NOTIFICATION immediately before
+ // (re)posting it; this seems to force the
+ // NotificationManager to launch the fullScreenIntent.
+ mNotificationManager.cancel(IN_CALL_NOTIFICATION);
+ }
+ }
+ } else { // not ringing call
+ // Make the notification prioritized over the other normal notifications.
+ builder.setPriority(Notification.PRIORITY_HIGH);
+
+ // TODO: use "if (DBG)" for this comment.
+ log("Will show \"hang-up\" action in the ongoing active call Notification");
+ // TODO: use better asset.
+ builder.addAction(R.drawable.stat_sys_phone_call_end,
+ mContext.getText(R.string.notification_action_end_call),
+ PhoneGlobals.createHangUpOngoingCallPendingIntent(mContext));
+ }
+
+ Notification notification = builder.getNotification();
+ if (DBG) log("Notifying IN_CALL_NOTIFICATION: " + notification);
+ mNotificationManager.notify(IN_CALL_NOTIFICATION, notification);
+
+ // Finally, refresh the mute and speakerphone notifications (since
+ // some phone state changes can indirectly affect the mute and/or
+ // speaker state).
+ updateSpeakerNotification();
+ updateMuteNotification();
+ }
+
+ /**
+ * Implemented for CallerInfoAsyncQuery.OnQueryCompleteListener interface.
+ * refreshes the contentView when called.
+ */
+ @Override
+ public void onQueryComplete(int token, Object cookie, CallerInfo ci){
+ if (DBG) log("CallerInfo query complete (for NotificationMgr), "
+ + "updating in-call notification..");
+ if (DBG) log("- cookie: " + cookie);
+ if (DBG) log("- ci: " + ci);
+
+ if (cookie == this) {
+ // Ok, this is the caller-id query we fired off in
+ // updateInCallNotification(), presumably when an incoming call
+ // first appeared. If the caller-id info matched any contacts,
+ // compactName should now be a real person name rather than a raw
+ // phone number:
+ if (DBG) log("- compactName is now: "
+ + PhoneUtils.getCompactNameFromCallerInfo(ci, mContext));
+
+ // Now that our CallerInfo object has been fully filled-in,
+ // refresh the in-call notification.
+ if (DBG) log("- updating notification after query complete...");
+ updateInCallNotification();
+ } else {
+ Log.w(LOG_TAG, "onQueryComplete: caller-id query from unknown source! "
+ + "cookie = " + cookie);
+ }
+ }
+
+ /**
+ * Take down the in-call notification.
+ * @see updateInCallNotification()
+ */
+ private void cancelInCall() {
+ if (DBG) log("cancelInCall()...");
+ mNotificationManager.cancel(IN_CALL_NOTIFICATION);
+ mInCallResId = 0;
+ }
+
+ /**
+ * Completely take down the in-call notification *and* the mute/speaker
+ * notifications as well, to indicate that the phone is now idle.
+ */
+ /* package */ void cancelCallInProgressNotifications() {
+ if (DBG) log("cancelCallInProgressNotifications()...");
+ if (mInCallResId == 0) {
+ return;
+ }
+
+ if (DBG) log("cancelCallInProgressNotifications: " + mInCallResId);
+ cancelInCall();
+ cancelMute();
+ cancelSpeakerphone();
+ }
+
+ /**
+ * Updates the message waiting indicator (voicemail) notification.
+ *
+ * @param visible true if there are messages waiting
+ */
+ /* package */ void updateMwi(boolean visible) {
+ if (DBG) log("updateMwi(): " + visible);
+
+ if (visible) {
+ int resId = android.R.drawable.stat_notify_voicemail;
+
+ // This Notification can get a lot fancier once we have more
+ // information about the current voicemail messages.
+ // (For example, the current voicemail system can't tell
+ // us the caller-id or timestamp of a message, or tell us the
+ // message count.)
+
+ // But for now, the UI is ultra-simple: if the MWI indication
+ // is supposed to be visible, just show a single generic
+ // notification.
+
+ String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
+ String vmNumber = mPhone.getVoiceMailNumber();
+ if (DBG) log("- got vm number: '" + vmNumber + "'");
+
+ // Watch out: vmNumber may be null, for two possible reasons:
+ //
+ // (1) This phone really has no voicemail number
+ //
+ // (2) This phone *does* have a voicemail number, but
+ // the SIM isn't ready yet.
+ //
+ // Case (2) *does* happen in practice if you have voicemail
+ // messages when the device first boots: we get an MWI
+ // notification as soon as we register on the network, but the
+ // SIM hasn't finished loading yet.
+ //
+ // So handle case (2) by retrying the lookup after a short
+ // delay.
+
+ if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) {
+ if (DBG) log("- Null vm number: SIM records not loaded (yet)...");
+
+ // TODO: rather than retrying after an arbitrary delay, it
+ // would be cleaner to instead just wait for a
+ // SIM_RECORDS_LOADED notification.
+ // (Unfortunately right now there's no convenient way to
+ // get that notification in phone app code. We'd first
+ // want to add a call like registerForSimRecordsLoaded()
+ // to Phone.java and GSMPhone.java, and *then* we could
+ // listen for that in the CallNotifier class.)
+
+ // Limit the number of retries (in case the SIM is broken
+ // or missing and can *never* load successfully.)
+ if (mVmNumberRetriesRemaining-- > 0) {
+ if (DBG) log(" - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
+ mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS);
+ return;
+ } else {
+ Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
+ + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
+ // ...and continue with vmNumber==null, just as if the
+ // phone had no VM number set up in the first place.
+ }
+ }
+
+ if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) {
+ int vmCount = mPhone.getVoiceMessageCount();
+ String titleFormat = mContext.getString(R.string.notification_voicemail_title_count);
+ notificationTitle = String.format(titleFormat, vmCount);
+ }
+
+ String notificationText;
+ if (TextUtils.isEmpty(vmNumber)) {
+ notificationText = mContext.getString(
+ R.string.notification_voicemail_no_vm_number);
+ } else {
+ notificationText = String.format(
+ mContext.getString(R.string.notification_voicemail_text_format),
+ PhoneNumberUtils.formatNumber(vmNumber));
+ }
+
+ Intent intent = new Intent(Intent.ACTION_CALL,
+ Uri.fromParts(Constants.SCHEME_VOICEMAIL, "", null));
+ PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
+ Uri ringtoneUri;
+ String uriString = prefs.getString(
+ CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null);
+ if (!TextUtils.isEmpty(uriString)) {
+ ringtoneUri = Uri.parse(uriString);
+ } else {
+ ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
+ }
+
+ Notification.Builder builder = new Notification.Builder(mContext);
+ builder.setSmallIcon(resId)
+ .setWhen(System.currentTimeMillis())
+ .setContentTitle(notificationTitle)
+ .setContentText(notificationText)
+ .setContentIntent(pendingIntent)
+ .setSound(ringtoneUri);
+ Notification notification = builder.getNotification();
+
+ CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs);
+ final boolean vibrate = prefs.getBoolean(
+ CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false);
+ if (vibrate) {
+ notification.defaults |= Notification.DEFAULT_VIBRATE;
+ }
+ notification.flags |= Notification.FLAG_NO_CLEAR;
+ configureLedNotification(notification);
+ mNotificationManager.notify(VOICEMAIL_NOTIFICATION, notification);
+ } else {
+ mNotificationManager.cancel(VOICEMAIL_NOTIFICATION);
+ }
+ }
+
+ /**
+ * Updates the message call forwarding indicator notification.
+ *
+ * @param visible true if there are messages waiting
+ */
+ /* package */ void updateCfi(boolean visible) {
+ if (DBG) log("updateCfi(): " + visible);
+ if (visible) {
+ // If Unconditional Call Forwarding (forward all calls) for VOICE
+ // is enabled, just show a notification. We'll default to expanded
+ // view for now, so the there is less confusion about the icon. If
+ // it is deemed too weird to have CF indications as expanded views,
+ // then we'll flip the flag back.
+
+ // TODO: We may want to take a look to see if the notification can
+ // display the target to forward calls to. This will require some
+ // effort though, since there are multiple layers of messages that
+ // will need to propagate that information.
+
+ Notification notification;
+ final boolean showExpandedNotification = true;
+ if (showExpandedNotification) {
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.setClassName("com.android.phone",
+ "com.android.phone.CallFeaturesSetting");
+
+ notification = new Notification(
+ R.drawable.stat_sys_phone_call_forward, // icon
+ null, // tickerText
+ 0); // The "timestamp" of this notification is meaningless;
+ // we only care about whether CFI is currently on or not.
+ notification.setLatestEventInfo(
+ mContext, // context
+ mContext.getString(R.string.labelCF), // expandedTitle
+ mContext.getString(R.string.sum_cfu_enabled_indicator), // expandedText
+ PendingIntent.getActivity(mContext, 0, intent, 0)); // contentIntent
+ } else {
+ notification = new Notification(
+ R.drawable.stat_sys_phone_call_forward, // icon
+ null, // tickerText
+ System.currentTimeMillis() // when
+ );
+ }
+
+ notification.flags |= Notification.FLAG_ONGOING_EVENT; // also implies FLAG_NO_CLEAR
+
+ mNotificationManager.notify(
+ CALL_FORWARD_NOTIFICATION,
+ notification);
+ } else {
+ mNotificationManager.cancel(CALL_FORWARD_NOTIFICATION);
+ }
+ }
+
+ /**
+ * Shows the "data disconnected due to roaming" notification, which
+ * appears when you lose data connectivity because you're roaming and
+ * you have the "data roaming" feature turned off.
+ */
+ /* package */ void showDataDisconnectedRoaming() {
+ if (DBG) log("showDataDisconnectedRoaming()...");
+
+ // "Mobile network settings" screen / dialog
+ Intent intent = new Intent(mContext, com.android.phone.MobileNetworkSettings.class);
+
+ final CharSequence contentText = mContext.getText(R.string.roaming_reenable_message);
+
+ final Notification.Builder builder = new Notification.Builder(mContext);
+ builder.setSmallIcon(android.R.drawable.stat_sys_warning);
+ builder.setContentTitle(mContext.getText(R.string.roaming));
+ builder.setContentText(contentText);
+ builder.setContentIntent(PendingIntent.getActivity(mContext, 0, intent, 0));
+
+ final Notification notif = new Notification.BigTextStyle(builder).bigText(contentText)
+ .build();
+
+ mNotificationManager.notify(DATA_DISCONNECTED_ROAMING_NOTIFICATION, notif);
+ }
+
+ /**
+ * Turns off the "data disconnected due to roaming" notification.
+ */
+ /* package */ void hideDataDisconnectedRoaming() {
+ if (DBG) log("hideDataDisconnectedRoaming()...");
+ mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
+ }
+
+ /**
+ * Display the network selection "no service" notification
+ * @param operator is the numeric operator number
+ */
+ private void showNetworkSelection(String operator) {
+ if (DBG) log("showNetworkSelection(" + operator + ")...");
+
+ String titleText = mContext.getString(
+ R.string.notification_network_selection_title);
+ String expandedText = mContext.getString(
+ R.string.notification_network_selection_text, operator);
+
+ Notification notification = new Notification();
+ notification.icon = android.R.drawable.stat_sys_warning;
+ notification.when = 0;
+ notification.flags = Notification.FLAG_ONGOING_EVENT;
+ notification.tickerText = null;
+
+ // create the target network operators settings intent
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
+ Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
+ // Use NetworkSetting to handle the selection intent
+ intent.setComponent(new ComponentName("com.android.phone",
+ "com.android.phone.NetworkSetting"));
+ PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
+
+ notification.setLatestEventInfo(mContext, titleText, expandedText, pi);
+
+ mNotificationManager.notify(SELECTED_OPERATOR_FAIL_NOTIFICATION, notification);
+ }
+
+ /**
+ * Turn off the network selection "no service" notification
+ */
+ private void cancelNetworkSelection() {
+ if (DBG) log("cancelNetworkSelection()...");
+ mNotificationManager.cancel(SELECTED_OPERATOR_FAIL_NOTIFICATION);
+ }
+
+ /**
+ * Update notification about no service of user selected operator
+ *
+ * @param serviceState Phone service state
+ */
+ void updateNetworkSelection(int serviceState) {
+ if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) {
+ // get the shared preference of network_selection.
+ // empty is auto mode, otherwise it is the operator alpha name
+ // in case there is no operator name, check the operator numeric
+ SharedPreferences sp =
+ PreferenceManager.getDefaultSharedPreferences(mContext);
+ String networkSelection =
+ sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, "");
+ if (TextUtils.isEmpty(networkSelection)) {
+ networkSelection =
+ sp.getString(PhoneBase.NETWORK_SELECTION_KEY, "");
+ }
+
+ if (DBG) log("updateNetworkSelection()..." + "state = " +
+ serviceState + " new network " + networkSelection);
+
+ if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
+ && !TextUtils.isEmpty(networkSelection)) {
+ if (!mSelectedUnavailableNotify) {
+ showNetworkSelection(networkSelection);
+ mSelectedUnavailableNotify = true;
+ }
+ } else {
+ if (mSelectedUnavailableNotify) {
+ cancelNetworkSelection();
+ mSelectedUnavailableNotify = false;
+ }
+ }
+ }
+ }
+
+ /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
+ if (mToast != null) {
+ mToast.cancel();
+ }
+
+ mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
+ mToast.show();
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/OtaStartupReceiver.java b/src/com/android/phone/OtaStartupReceiver.java
new file mode 100644
index 0000000..594b63a
--- /dev/null
+++ b/src/com/android/phone/OtaStartupReceiver.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.telephony.PhoneStateListener;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.TelephonyCapabilities;
+
+import android.util.Log;
+
+/*
+ * Handles OTA Start procedure at phone power up. At phone power up, if phone is not OTA
+ * provisioned (check MIN value of the Phone) and 'device_provisioned' is not set,
+ * OTA Activation screen is shown that helps user activate the phone
+ */
+public class OtaStartupReceiver extends BroadcastReceiver {
+ private static final String TAG = "OtaStartupReceiver";
+ private static final boolean DBG = false;
+ private static final int MIN_READY = 10;
+ private static final int SERVICE_STATE_CHANGED = 11;
+ private Context mContext;
+
+ /**
+ * For debug purposes we're listening for otaspChanged events as
+ * this may be be used in the future for deciding if OTASP is
+ * necessary.
+ */
+ private int mOtaspMode = -1;
+ private boolean mPhoneStateListenerRegistered = false;
+ private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
+ @Override
+ public void onOtaspChanged(int otaspMode) {
+ mOtaspMode = otaspMode;
+ Log.v(TAG, "onOtaspChanged: mOtaspMode=" + mOtaspMode);
+ }
+ };
+
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MIN_READY:
+ Log.v(TAG, "Attempting OtaActivation from handler, mOtaspMode=" + mOtaspMode);
+ OtaUtils.maybeDoOtaCall(mContext, mHandler, MIN_READY);
+ break;
+ case SERVICE_STATE_CHANGED: {
+ ServiceState state = (ServiceState) ((AsyncResult) msg.obj).result;
+ if (DBG) Log.d(TAG, "onServiceStateChanged()... new state = " + state);
+
+ // Possible service states:
+ // - STATE_IN_SERVICE // Normal operation
+ // - STATE_OUT_OF_SERVICE // Still searching for an operator to register to,
+ // // or no radio signal
+ // - STATE_EMERGENCY_ONLY // Phone is locked; only emergency numbers are allowed
+ // - STATE_POWER_OFF // Radio is explicitly powered off (airplane mode)
+
+ // Once we reach STATE_IN_SERVICE
+ // it's finally OK to start OTA provisioning
+ if (state.getState() == ServiceState.STATE_IN_SERVICE) {
+ if (DBG) Log.d(TAG, "call OtaUtils.maybeDoOtaCall after network is available");
+ Phone phone = PhoneGlobals.getPhone();
+ phone.unregisterForServiceStateChanged(this);
+ OtaUtils.maybeDoOtaCall(mContext, mHandler, MIN_READY);
+ }
+ break;
+ }
+ }
+
+ }
+ };
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mContext = context;
+ if (DBG) {
+ Log.v(TAG, "onReceive: intent action=" + intent.getAction() +
+ " mOtaspMode=" + mOtaspMode);
+ }
+
+ PhoneGlobals globals = PhoneGlobals.getInstanceIfPrimary();
+ if (globals == null) {
+ if (DBG) Log.d(TAG, "Not primary user, nothing to do.");
+ return;
+ }
+
+ if (!TelephonyCapabilities.supportsOtasp(PhoneGlobals.getPhone())) {
+ if (DBG) Log.d(TAG, "OTASP not supported, nothing to do.");
+ return;
+ }
+
+ if (mPhoneStateListenerRegistered == false) {
+ if (DBG) Log.d(TAG, "Register our PhoneStateListener");
+ TelephonyManager telephonyManager = (TelephonyManager)context.getSystemService(
+ Context.TELEPHONY_SERVICE);
+ telephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_OTASP_CHANGED);
+ mPhoneStateListenerRegistered = true;
+ } else {
+ if (DBG) Log.d(TAG, "PhoneStateListener already registered");
+ }
+
+ if (shouldPostpone(context)) {
+ if (DBG) Log.d(TAG, "Postponing OTASP until wizard runs");
+ return;
+ }
+
+ // Delay OTA provisioning if network is not available yet
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ Phone phone = PhoneGlobals.getPhone();
+ if (app.mCM.getServiceState() != ServiceState.STATE_IN_SERVICE) {
+ if (DBG) Log.w(TAG, "Network is not ready. Registering to receive notification.");
+ phone.registerForServiceStateChanged(mHandler, SERVICE_STATE_CHANGED, null);
+ return;
+ }
+
+ // The following depends on the phone process being persistent. Normally we can't
+ // expect a BroadcastReceiver to persist after returning from this function but it does
+ // because the phone activity is persistent.
+ if (DBG) Log.d(TAG, "call OtaUtils.maybeDoOtaCall");
+ OtaUtils.maybeDoOtaCall(mContext, mHandler, MIN_READY);
+ }
+
+ /**
+ * On devices that provide a phone initialization wizard (such as Google Setup Wizard), we
+ * allow delaying CDMA OTA setup so it can be done in a single wizard. The wizard is responsible
+ * for (1) disabling itself once it has been run and/or (2) setting the 'device_provisioned'
+ * flag to something non-zero and (3) calling the OTA Setup with the action below.
+ *
+ * NB: Typical phone initialization wizards will install themselves as the homescreen
+ * (category "android.intent.category.HOME") with a priority higher than the default.
+ * The wizard should set 'device_provisioned' when it completes, disable itself with the
+ * PackageManager.setComponentEnabledSetting() and then start home screen.
+ *
+ * @return true if setup will be handled by wizard, false if it should be done now.
+ */
+ private boolean shouldPostpone(Context context) {
+ Intent intent = new Intent("android.intent.action.DEVICE_INITIALIZATION_WIZARD");
+ ResolveInfo resolveInfo = context.getPackageManager().resolveActivity(intent,
+ PackageManager.MATCH_DEFAULT_ONLY);
+ boolean provisioned = Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.DEVICE_PROVISIONED, 0) != 0;
+ String mode = SystemProperties.get("ro.setupwizard.mode", "REQUIRED");
+ boolean runningSetupWizard = "REQUIRED".equals(mode) || "OPTIONAL".equals(mode);
+ if (DBG) {
+ Log.v(TAG, "resolvInfo = " + resolveInfo + ", provisioned = " + provisioned
+ + ", runningSetupWizard = " + runningSetupWizard);
+ }
+ return resolveInfo != null && !provisioned && runningSetupWizard;
+ }
+}
diff --git a/src/com/android/phone/OtaUtils.java b/src/com/android/phone/OtaUtils.java
new file mode 100644
index 0000000..495df27
--- /dev/null
+++ b/src/com/android/phone/OtaUtils.java
@@ -0,0 +1,1645 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyCapabilities;
+import com.android.internal.telephony.TelephonyProperties;
+import com.android.phone.OtaUtils.CdmaOtaInCallScreenUiState.State;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.AlertDialog;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.telephony.TelephonyManager;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.WindowManager;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.ScrollView;
+import android.widget.TextView;
+import android.widget.ToggleButton;
+
+/**
+ * Handles all OTASP Call related logic and UI functionality.
+ * The InCallScreen interacts with this class to perform an OTASP Call.
+ *
+ * OTASP is a CDMA-specific feature:
+ * OTA or OTASP == Over The Air service provisioning
+ * SPC == Service Programming Code
+ * TODO: Include pointer to more detailed documentation.
+ *
+ * TODO: This is Over The Air Service Provisioning (OTASP)
+ * A better name would be OtaspUtils.java.
+ */
+public class OtaUtils {
+ private static final String LOG_TAG = "OtaUtils";
+ private static final boolean DBG = false;
+
+ public static final int OTA_SHOW_ACTIVATION_SCREEN_OFF = 0;
+ public static final int OTA_SHOW_ACTIVATION_SCREEN_ON = 1;
+ public static final int OTA_SHOW_LISTENING_SCREEN_OFF =0;
+ public static final int OTA_SHOW_LISTENING_SCREEN_ON =1;
+ public static final int OTA_SHOW_ACTIVATE_FAIL_COUNT_OFF = 0;
+ public static final int OTA_SHOW_ACTIVATE_FAIL_COUNT_THREE = 3;
+ public static final int OTA_PLAY_SUCCESS_FAILURE_TONE_OFF = 0;
+ public static final int OTA_PLAY_SUCCESS_FAILURE_TONE_ON = 1;
+
+ // SPC Timeout is 60 seconds
+ public final int OTA_SPC_TIMEOUT = 60;
+ public final int OTA_FAILURE_DIALOG_TIMEOUT = 2;
+
+ // Constants for OTASP-related Intents and intent extras.
+ // Watch out: these must agree with the corresponding constants in
+ // apps/SetupWizard!
+
+ // Intent action to launch an OTASP call.
+ public static final String ACTION_PERFORM_CDMA_PROVISIONING =
+ "com.android.phone.PERFORM_CDMA_PROVISIONING";
+
+ // Intent action to launch activation on a non-voice capable device
+ public static final String ACTION_PERFORM_VOICELESS_CDMA_PROVISIONING =
+ "com.android.phone.PERFORM_VOICELESS_CDMA_PROVISIONING";
+
+ // Intent action to display the InCallScreen in the OTASP "activation" state.
+ public static final String ACTION_DISPLAY_ACTIVATION_SCREEN =
+ "com.android.phone.DISPLAY_ACTIVATION_SCREEN";
+
+ // boolean voiceless provisioning extra that enables a "don't show this again" checkbox
+ // the user can check to never see the activity upon bootup again
+ public static final String EXTRA_VOICELESS_PROVISIONING_OFFER_DONTSHOW =
+ "com.android.phone.VOICELESS_PROVISIONING_OFFER_DONTSHOW";
+
+ // Activity result codes for the ACTION_PERFORM_CDMA_PROVISIONING intent
+ // (see the InCallScreenShowActivation activity.)
+ //
+ // Note: currently, our caller won't ever actually receive the
+ // RESULT_INTERACTIVE_OTASP_STARTED result code; see comments in
+ // InCallScreenShowActivation.onCreate() for details.
+
+ public static final int RESULT_INTERACTIVE_OTASP_STARTED = Activity.RESULT_FIRST_USER;
+ public static final int RESULT_NONINTERACTIVE_OTASP_STARTED = Activity.RESULT_FIRST_USER + 1;
+ public static final int RESULT_NONINTERACTIVE_OTASP_FAILED = Activity.RESULT_FIRST_USER + 2;
+
+ // Testing: Extra for the ACTION_PERFORM_CDMA_PROVISIONING intent that
+ // allows the caller to manually enable/disable "interactive mode" for
+ // the OTASP call. Only available in userdebug or eng builds.
+ public static final String EXTRA_OVERRIDE_INTERACTIVE_MODE =
+ "ota_override_interactive_mode";
+
+ // Extra for the ACTION_PERFORM_CDMA_PROVISIONING intent, holding a
+ // PendingIntent which the phone app can use to send a result code
+ // back to the caller.
+ public static final String EXTRA_OTASP_RESULT_CODE_PENDING_INTENT =
+ "otasp_result_code_pending_intent";
+
+ // Extra attached to the above PendingIntent that indicates
+ // success or failure.
+ public static final String EXTRA_OTASP_RESULT_CODE =
+ "otasp_result_code";
+ public static final int OTASP_UNKNOWN = 0;
+ public static final int OTASP_USER_SKIPPED = 1; // Only meaningful with interactive OTASP
+ public static final int OTASP_SUCCESS = 2;
+ public static final int OTASP_FAILURE = 3;
+ // failed due to CDMA_OTA_PROVISION_STATUS_SPC_RETRIES_EXCEEDED
+ public static final int OTASP_FAILURE_SPC_RETRIES = 4;
+ // TODO: Distinguish between interactive and non-interactive success
+ // and failure. Then, have the PendingIntent be sent after
+ // interactive OTASP as well (so the caller can find out definitively
+ // when interactive OTASP completes.)
+
+ private static final String OTASP_NUMBER = "*228";
+ private static final String OTASP_NUMBER_NON_INTERACTIVE = "*22899";
+
+ private InCallScreen mInCallScreen;
+ private Context mContext;
+ private PhoneGlobals mApplication;
+ private OtaWidgetData mOtaWidgetData;
+ private ViewGroup mInCallTouchUi; // UI controls for regular calls
+ private CallCard mCallCard;
+
+ // The DTMFTwelveKeyDialer instance. We create this in
+ // initOtaInCallScreen(), and attach it to the DTMFTwelveKeyDialerView
+ // ("otaDtmfDialerView") that comes from otacall_card.xml.
+ private DTMFTwelveKeyDialer mOtaCallCardDtmfDialer;
+
+ private static boolean sIsWizardMode = true;
+
+ // How many times do we retry maybeDoOtaCall() if the LTE state is not known yet,
+ // and how long do we wait between retries
+ private static final int OTA_CALL_LTE_RETRIES_MAX = 5;
+ private static final int OTA_CALL_LTE_RETRY_PERIOD = 3000;
+ private static int sOtaCallLteRetries = 0;
+
+ // In "interactive mode", the OtaUtils object is tied to an
+ // InCallScreen instance, where we display a bunch of UI specific to
+ // the OTASP call. But on devices that are not "voice capable", the
+ // OTASP call runs in a non-interactive mode, and we don't have
+ // an InCallScreen or CallCard or any OTASP UI elements at all.
+ private boolean mInteractive = true;
+
+
+ /**
+ * OtaWidgetData class represent all OTA UI elements
+ *
+ * TODO(OTASP): It's really ugly for the OtaUtils object to reach into the
+ * InCallScreen like this and directly manipulate its widgets.
+ *
+ * Instead, the model/view separation should be more clear: OtaUtils
+ * should only know about a higher-level abstraction of the
+ * OTASP-specific UI state (just like how the CallController uses the
+ * InCallUiState object), and the InCallScreen itself should translate
+ * that higher-level abstraction into actual onscreen views and widgets.
+ */
+ private class OtaWidgetData {
+ public Button otaEndButton;
+ public Button otaActivateButton;
+ public Button otaSkipButton;
+ public Button otaNextButton;
+ public ToggleButton otaSpeakerButton;
+ public ViewGroup otaUpperWidgets;
+ public View callCardOtaButtonsFailSuccess;
+ public ProgressBar otaTextProgressBar;
+ public TextView otaTextSuccessFail;
+ public View callCardOtaButtonsActivate;
+ public View callCardOtaButtonsListenProgress;
+ public TextView otaTextActivate;
+ public TextView otaTextListenProgress;
+ public AlertDialog spcErrorDialog;
+ public AlertDialog otaFailureDialog;
+ public AlertDialog otaSkipConfirmationDialog;
+ public TextView otaTitle;
+ public DTMFTwelveKeyDialerView otaDtmfDialerView;
+ public Button otaTryAgainButton;
+ }
+
+ /**
+ * OtaUtils constructor.
+ *
+ * @param context the Context of the calling Activity or Application
+ * @param interactive if true, use the InCallScreen to display the progress
+ * and result of the OTASP call. In practice this is
+ * true IFF the current device is a voice-capable phone.
+ *
+ * Note if interactive is true, you must also call updateUiWidgets() as soon
+ * as the InCallScreen instance is ready.
+ */
+ public OtaUtils(Context context, boolean interactive) {
+ if (DBG) log("OtaUtils constructor...");
+ mApplication = PhoneGlobals.getInstance();
+ mContext = context;
+ mInteractive = interactive;
+ }
+
+ /**
+ * Updates the OtaUtils object's references to some UI elements belonging to
+ * the InCallScreen. This is used only in interactive mode.
+ *
+ * Use clearUiWidgets() to clear out these references. (The InCallScreen
+ * is responsible for doing this from its onDestroy() method.)
+ *
+ * This method has no effect if the UI widgets have already been set up.
+ * (In other words, it's safe to call this every time through
+ * InCallScreen.onResume().)
+ */
+ public void updateUiWidgets(InCallScreen inCallScreen,
+ ViewGroup inCallTouchUi, CallCard callCard) {
+ if (DBG) log("updateUiWidgets()... mInCallScreen = " + mInCallScreen);
+
+ if (!mInteractive) {
+ throw new IllegalStateException("updateUiWidgets() called in non-interactive mode");
+ }
+
+ if (mInCallScreen != null) {
+ if (DBG) log("updateUiWidgets(): widgets already set up, nothing to do...");
+ return;
+ }
+
+ mInCallScreen = inCallScreen;
+ mInCallTouchUi = inCallTouchUi;
+ mCallCard = callCard;
+ mOtaWidgetData = new OtaWidgetData();
+
+ // Inflate OTASP-specific UI elements:
+ ViewStub otaCallCardStub = (ViewStub) mInCallScreen.findViewById(R.id.otaCallCardStub);
+ if (otaCallCardStub != null) {
+ // If otaCallCardStub is null here, that means it's already been
+ // inflated (which could have happened in the current InCallScreen
+ // instance for a *prior* OTASP call.)
+ otaCallCardStub.inflate();
+ }
+
+ readXmlSettings();
+ initOtaInCallScreen();
+ }
+
+ /**
+ * Clear out the OtaUtils object's references to any InCallScreen UI
+ * elements. This is the opposite of updateUiWidgets().
+ */
+ public void clearUiWidgets() {
+ mInCallScreen = null;
+ mInCallTouchUi = null;
+ mCallCard = null;
+ mOtaWidgetData = null;
+ }
+
+ /**
+ * Starts the OTA provisioning call. If the MIN isn't available yet, it returns false and adds
+ * an event to return the request to the calling app when it becomes available.
+ *
+ * @param context
+ * @param handler
+ * @param request
+ * @return true if we were able to launch Ota activity or it's not required; false otherwise
+ */
+ public static boolean maybeDoOtaCall(Context context, Handler handler, int request) {
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ Phone phone = app.phone;
+
+ if (ActivityManager.isRunningInTestHarness()) {
+ Log.i(LOG_TAG, "Don't run provisioning when in test harness");
+ return true;
+ }
+
+ if (!TelephonyCapabilities.supportsOtasp(phone)) {
+ // Presumably not a CDMA phone.
+ if (DBG) log("maybeDoOtaCall: OTASP not supported on this device");
+ return true; // Nothing to do here.
+ }
+
+ if (!phone.isMinInfoReady()) {
+ if (DBG) log("MIN is not ready. Registering to receive notification.");
+ phone.registerForSubscriptionInfoReady(handler, request, null);
+ return false;
+ }
+ phone.unregisterForSubscriptionInfoReady(handler);
+
+ if (getLteOnCdmaMode(context) == PhoneConstants.LTE_ON_CDMA_UNKNOWN) {
+ if (sOtaCallLteRetries < OTA_CALL_LTE_RETRIES_MAX) {
+ if (DBG) log("maybeDoOtaCall: LTE state still unknown: retrying");
+ handler.sendEmptyMessageDelayed(request, OTA_CALL_LTE_RETRY_PERIOD);
+ sOtaCallLteRetries++;
+ return false;
+ } else {
+ Log.w(LOG_TAG, "maybeDoOtaCall: LTE state still unknown: giving up");
+ return true;
+ }
+ }
+
+ boolean phoneNeedsActivation = phone.needsOtaServiceProvisioning();
+ if (DBG) log("phoneNeedsActivation is set to " + phoneNeedsActivation);
+
+ int otaShowActivationScreen = context.getResources().getInteger(
+ R.integer.OtaShowActivationScreen);
+ if (DBG) log("otaShowActivationScreen: " + otaShowActivationScreen);
+
+ // Run the OTASP call in "interactive" mode only if
+ // this is a non-LTE "voice capable" device.
+ if (PhoneGlobals.sVoiceCapable && getLteOnCdmaMode(context) == PhoneConstants.LTE_ON_CDMA_FALSE) {
+ if (phoneNeedsActivation
+ && (otaShowActivationScreen == OTA_SHOW_ACTIVATION_SCREEN_ON)) {
+ app.cdmaOtaProvisionData.isOtaCallIntentProcessed = false;
+ sIsWizardMode = false;
+
+ if (DBG) Log.d(LOG_TAG, "==> Starting interactive CDMA provisioning...");
+ OtaUtils.startInteractiveOtasp(context);
+
+ if (DBG) log("maybeDoOtaCall: voice capable; activation started.");
+ } else {
+ if (DBG) log("maybeDoOtaCall: voice capable; activation NOT started.");
+ }
+ } else {
+ if (phoneNeedsActivation) {
+ app.cdmaOtaProvisionData.isOtaCallIntentProcessed = false;
+ Intent newIntent = new Intent(ACTION_PERFORM_VOICELESS_CDMA_PROVISIONING);
+ newIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ newIntent.putExtra(EXTRA_VOICELESS_PROVISIONING_OFFER_DONTSHOW, true);
+ try {
+ context.startActivity(newIntent);
+ } catch (ActivityNotFoundException e) {
+ loge("No activity Handling PERFORM_VOICELESS_CDMA_PROVISIONING!");
+ return false;
+ }
+ if (DBG) log("maybeDoOtaCall: non-interactive; activation intent sent.");
+ } else {
+ if (DBG) log("maybeDoOtaCall: non-interactive, no need for OTASP.");
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Starts a normal "interactive" OTASP call (i.e. CDMA activation
+ * for regular voice-capable phone devices.)
+ *
+ * This method is called from the InCallScreenShowActivation activity when
+ * handling the ACTION_PERFORM_CDMA_PROVISIONING intent.
+ */
+ public static void startInteractiveOtasp(Context context) {
+ if (DBG) log("startInteractiveOtasp()...");
+ PhoneGlobals app = PhoneGlobals.getInstance();
+
+ // There are two ways to start OTASP on voice-capable devices:
+ //
+ // (1) via the PERFORM_CDMA_PROVISIONING intent
+ // - this is triggered by the "Activate device" button in settings,
+ // or can be launched automatically upon boot if the device
+ // thinks it needs to be provisioned.
+ // - the intent is handled by InCallScreenShowActivation.onCreate(),
+ // which calls this method
+ // - we prepare for OTASP by initializing the OtaUtils object
+ // - we bring up the InCallScreen in the ready-to-activate state
+ // - when the user presses the "Activate" button we launch the
+ // call by calling CallController.placeCall() via the
+ // otaPerformActivation() method.
+ //
+ // (2) by manually making an outgoing call to a special OTASP number
+ // like "*228" or "*22899".
+ // - That sequence does NOT involve this method (OtaUtils.startInteractiveOtasp()).
+ // Instead, the outgoing call request goes straight to CallController.placeCall().
+ // - CallController.placeCall() notices that it's an OTASP
+ // call, and initializes the OtaUtils object.
+ // - The InCallScreen is launched (as the last step of
+ // CallController.placeCall()). The InCallScreen notices that
+ // OTASP is active and shows the correct UI.
+
+ // Here, we start sequence (1):
+ // Do NOT immediately start the call. Instead, bring up the InCallScreen
+ // in the special "activate" state (see OtaUtils.otaShowActivateScreen()).
+ // We won't actually make the call until the user presses the "Activate"
+ // button.
+
+ Intent activationScreenIntent = new Intent().setClass(context, InCallScreen.class)
+ .setAction(ACTION_DISPLAY_ACTIVATION_SCREEN);
+
+ // Watch out: in the scenario where OTASP gets triggered from the
+ // BOOT_COMPLETED broadcast (see OtaStartupReceiver.java), we might be
+ // running in the PhoneApp's context right now.
+ // So the FLAG_ACTIVITY_NEW_TASK flag is required here.
+ activationScreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+ // We're about to start the OTASP sequence, so create and initialize the
+ // OtaUtils instance. (This needs to happen before bringing up the
+ // InCallScreen.)
+ OtaUtils.setupOtaspCall(activationScreenIntent);
+
+ // And bring up the InCallScreen...
+ Log.i(LOG_TAG, "startInteractiveOtasp: launching InCallScreen in 'activate' state: "
+ + activationScreenIntent);
+ context.startActivity(activationScreenIntent);
+ }
+
+ /**
+ * Starts the OTASP call *without* involving the InCallScreen or
+ * displaying any UI.
+ *
+ * This is used on data-only devices, which don't support any kind of
+ * in-call phone UI.
+ *
+ * @return PhoneUtils.CALL_STATUS_DIALED if we successfully
+ * dialed the OTASP number, or one of the other
+ * CALL_STATUS_* constants if there was a failure.
+ */
+ public static int startNonInteractiveOtasp(Context context) {
+ if (DBG) log("startNonInteractiveOtasp()...");
+ PhoneGlobals app = PhoneGlobals.getInstance();
+
+ if (app.otaUtils != null) {
+ // An OtaUtils instance already exists, presumably from a previous OTASP call.
+ Log.i(LOG_TAG, "startNonInteractiveOtasp: "
+ + "OtaUtils already exists; nuking the old one and starting again...");
+ }
+
+ // Create the OtaUtils instance.
+ app.otaUtils = new OtaUtils(context, false /* non-interactive mode */);
+ if (DBG) log("- created OtaUtils: " + app.otaUtils);
+
+ // ... and kick off the OTASP call.
+ // TODO(InCallScreen redesign): This should probably go through
+ // the CallController, rather than directly calling
+ // PhoneUtils.placeCall().
+ Phone phone = PhoneGlobals.getPhone();
+ String number = OTASP_NUMBER_NON_INTERACTIVE;
+ Log.i(LOG_TAG, "startNonInteractiveOtasp: placing call to '" + number + "'...");
+ int callStatus = PhoneUtils.placeCall(context,
+ phone,
+ number,
+ null, // contactRef
+ false, //isEmergencyCall
+ null); // gatewayUri
+
+ if (callStatus == PhoneUtils.CALL_STATUS_DIALED) {
+ if (DBG) log(" ==> successful return from placeCall(): callStatus = " + callStatus);
+ } else {
+ Log.w(LOG_TAG, "Failure from placeCall() for OTA number '"
+ + number + "': code " + callStatus);
+ return callStatus;
+ }
+
+ // TODO: Any other special work to do here?
+ // Such as:
+ //
+ // - manually kick off progress updates, either using TelephonyRegistry
+ // or else by sending PendingIntents directly to our caller?
+ //
+ // - manually silence the in-call audio? (Probably unnecessary
+ // if Stingray truly has no audio path from phone baseband
+ // to the device's speakers.)
+ //
+
+ return callStatus;
+ }
+
+ /**
+ * @return true if the specified Intent is a CALL action that's an attempt
+ * to initate an OTASP call.
+ *
+ * OTASP is a CDMA-specific concept, so this method will always return false
+ * on GSM phones.
+ *
+ * This code was originally part of the InCallScreen.checkIsOtaCall() method.
+ */
+ public static boolean isOtaspCallIntent(Intent intent) {
+ if (DBG) log("isOtaspCallIntent(" + intent + ")...");
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ Phone phone = app.mCM.getDefaultPhone();
+
+ if (intent == null) {
+ return false;
+ }
+ if (!TelephonyCapabilities.supportsOtasp(phone)) {
+ return false;
+ }
+
+ String action = intent.getAction();
+ if (action == null) {
+ return false;
+ }
+ if (!action.equals(Intent.ACTION_CALL)) {
+ if (DBG) log("isOtaspCallIntent: not a CALL action: '" + action + "' ==> not OTASP");
+ return false;
+ }
+
+ if ((app.cdmaOtaScreenState == null) || (app.cdmaOtaProvisionData == null)) {
+ // Uh oh -- something wrong with our internal OTASP state.
+ // (Since this is an OTASP-capable device, these objects
+ // *should* have already been created by PhoneApp.onCreate().)
+ throw new IllegalStateException("isOtaspCallIntent: "
+ + "app.cdmaOta* objects(s) not initialized");
+ }
+
+ // This is an OTASP call iff the number we're trying to dial is one of
+ // the magic OTASP numbers.
+ String number;
+ try {
+ number = PhoneUtils.getInitialNumber(intent);
+ } catch (PhoneUtils.VoiceMailNumberMissingException ex) {
+ // This was presumably a "voicemail:" intent, so it's
+ // obviously not an OTASP number.
+ if (DBG) log("isOtaspCallIntent: VoiceMailNumberMissingException => not OTASP");
+ return false;
+ }
+ if (phone.isOtaSpNumber(number)) {
+ if (DBG) log("isOtaSpNumber: ACTION_CALL to '" + number + "' ==> OTASP call!");
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Set up for an OTASP call.
+ *
+ * This method is called as part of the CallController placeCall() sequence
+ * before initiating an outgoing OTASP call.
+ *
+ * The purpose of this method is mainly to create and initialize the
+ * OtaUtils instance, along with some other misc pre-OTASP cleanup.
+ */
+ public static void setupOtaspCall(Intent intent) {
+ if (DBG) log("setupOtaspCall(): preparing for OTASP call to " + intent);
+ PhoneGlobals app = PhoneGlobals.getInstance();
+
+ if (app.otaUtils != null) {
+ // An OtaUtils instance already exists, presumably from a prior OTASP call.
+ // Nuke the old one and start this call with a fresh instance.
+ Log.i(LOG_TAG, "setupOtaspCall: "
+ + "OtaUtils already exists; replacing with new instance...");
+ }
+
+ // Create the OtaUtils instance.
+ app.otaUtils = new OtaUtils(app.getApplicationContext(), true /* interactive */);
+ if (DBG) log("- created OtaUtils: " + app.otaUtils);
+
+ // NOTE we still need to call OtaUtils.updateUiWidgets() once the
+ // InCallScreen instance is ready; see InCallScreen.checkOtaspStateOnResume()
+
+ // Make sure the InCallScreen knows that it needs to switch into OTASP mode.
+ //
+ // NOTE in gingerbread and earlier, we used to do
+ // setInCallScreenMode(InCallScreenMode.OTA_NORMAL);
+ // directly in the InCallScreen, back when this check happened inside the InCallScreen.
+ //
+ // But now, set the global CdmaOtaInCallScreenUiState object into
+ // NORMAL mode, which will then cause the InCallScreen (when it
+ // comes up) to realize that an OTA call is active.
+
+ app.otaUtils.setCdmaOtaInCallScreenUiState(
+ OtaUtils.CdmaOtaInCallScreenUiState.State.NORMAL);
+
+ // TODO(OTASP): note app.inCallUiState.inCallScreenMode and
+ // app.cdmaOtaInCallScreenUiState.state are mostly redundant. Combine them.
+ app.inCallUiState.inCallScreenMode = InCallUiState.InCallScreenMode.OTA_NORMAL;
+
+ // TODO(OTASP / bug 5092031): we ideally should call
+ // otaShowListeningScreen() here to make sure that the DTMF dialpad
+ // becomes visible at the start of the "*228" call:
+ //
+ // // ...and get the OTASP-specific UI into the right state.
+ // app.otaUtils.otaShowListeningScreen();
+ // if (app.otaUtils.mInCallScreen != null) {
+ // app.otaUtils.mInCallScreen.requestUpdateScreen();
+ // }
+ //
+ // But this doesn't actually work; the call to otaShowListeningScreen()
+ // *doesn't* actually bring up the listening screen, since the
+ // cdmaOtaConfigData.otaShowListeningScreen config parameter hasn't been
+ // initialized (we haven't run readXmlSettings() yet at this point!)
+
+ // Also, since the OTA call is now just starting, clear out
+ // the "committed" flag in app.cdmaOtaProvisionData.
+ if (app.cdmaOtaProvisionData != null) {
+ app.cdmaOtaProvisionData.isOtaCallCommitted = false;
+ }
+ }
+
+ private void setSpeaker(boolean state) {
+ if (DBG) log("setSpeaker : " + state );
+
+ if (!mInteractive) {
+ if (DBG) log("non-interactive mode, ignoring setSpeaker.");
+ return;
+ }
+
+ if (state == PhoneUtils.isSpeakerOn(mContext)) {
+ if (DBG) log("no change. returning");
+ return;
+ }
+
+ if (state && mInCallScreen.isBluetoothAvailable()
+ && mInCallScreen.isBluetoothAudioConnected()) {
+ mInCallScreen.disconnectBluetoothAudio();
+ }
+ PhoneUtils.turnOnSpeaker(mContext, state, true);
+ }
+
+ /**
+ * Handles OTA Provision events from the telephony layer.
+ * These events come in to this method whether or not
+ * the InCallScreen is visible.
+ *
+ * Possible events are:
+ * OTA Commit Event - OTA provisioning was successful
+ * SPC retries exceeded - SPC failure retries has exceeded, and Phone needs to
+ * power down.
+ */
+ public void onOtaProvisionStatusChanged(AsyncResult r) {
+ int OtaStatus[] = (int[]) r.result;
+ if (DBG) log("Provision status event!");
+ if (DBG) log("onOtaProvisionStatusChanged(): status = "
+ + OtaStatus[0] + " ==> " + otaProvisionStatusToString(OtaStatus[0]));
+
+ // In practice, in a normal successful OTASP call, events come in as follows:
+ // - SPL_UNLOCKED within a couple of seconds after the call starts
+ // - then a delay of around 45 seconds
+ // - then PRL_DOWNLOADED and MDN_DOWNLOADED and COMMITTED within a span of 2 seconds
+
+ switch(OtaStatus[0]) {
+ case Phone.CDMA_OTA_PROVISION_STATUS_SPC_RETRIES_EXCEEDED:
+ if (DBG) log("onOtaProvisionStatusChanged(): RETRIES EXCEEDED");
+ updateOtaspProgress();
+ mApplication.cdmaOtaProvisionData.otaSpcUptime = SystemClock.elapsedRealtime();
+ if (mInteractive) {
+ otaShowSpcErrorNotice(OTA_SPC_TIMEOUT);
+ } else {
+ sendOtaspResult(OTASP_FAILURE_SPC_RETRIES);
+ }
+ // Power.shutdown();
+ break;
+
+ case Phone.CDMA_OTA_PROVISION_STATUS_COMMITTED:
+ if (DBG) {
+ log("onOtaProvisionStatusChanged(): DONE, isOtaCallCommitted set to true");
+ }
+ mApplication.cdmaOtaProvisionData.isOtaCallCommitted = true;
+ if (mApplication.cdmaOtaScreenState.otaScreenState !=
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED) {
+ updateOtaspProgress();
+ }
+
+ break;
+
+ case Phone.CDMA_OTA_PROVISION_STATUS_SPL_UNLOCKED:
+ case Phone.CDMA_OTA_PROVISION_STATUS_A_KEY_EXCHANGED:
+ case Phone.CDMA_OTA_PROVISION_STATUS_SSD_UPDATED:
+ case Phone.CDMA_OTA_PROVISION_STATUS_NAM_DOWNLOADED:
+ case Phone.CDMA_OTA_PROVISION_STATUS_MDN_DOWNLOADED:
+ case Phone.CDMA_OTA_PROVISION_STATUS_IMSI_DOWNLOADED:
+ case Phone.CDMA_OTA_PROVISION_STATUS_PRL_DOWNLOADED:
+ case Phone.CDMA_OTA_PROVISION_STATUS_OTAPA_STARTED:
+ case Phone.CDMA_OTA_PROVISION_STATUS_OTAPA_STOPPED:
+ case Phone.CDMA_OTA_PROVISION_STATUS_OTAPA_ABORTED:
+ // Only update progress when OTA call is in normal state
+ if (getCdmaOtaInCallScreenUiState() == CdmaOtaInCallScreenUiState.State.NORMAL) {
+ if (DBG) log("onOtaProvisionStatusChanged(): change to ProgressScreen");
+ updateOtaspProgress();
+ }
+ break;
+
+ default:
+ if (DBG) log("onOtaProvisionStatusChanged(): Ignoring OtaStatus " + OtaStatus[0]);
+ break;
+ }
+ }
+
+ /**
+ * Handle a disconnect event from the OTASP call.
+ */
+ public void onOtaspDisconnect() {
+ if (DBG) log("onOtaspDisconnect()...");
+ // We only handle this event explicitly in non-interactive mode.
+ // (In interactive mode, the InCallScreen does any post-disconnect
+ // cleanup.)
+ if (!mInteractive) {
+ // Send a success or failure indication back to our caller.
+ updateNonInteractiveOtaSuccessFailure();
+ }
+ }
+
+ private void otaShowHome() {
+ if (DBG) log("otaShowHome()...");
+ mApplication.cdmaOtaScreenState.otaScreenState =
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED;
+ mInCallScreen.endInCallScreenSession();
+ Intent intent = new Intent(Intent.ACTION_MAIN);
+ intent.addCategory (Intent.CATEGORY_HOME);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivityAsUser(intent, UserHandle.CURRENT);
+ return;
+ }
+
+ private void otaSkipActivation() {
+ if (DBG) log("otaSkipActivation()...");
+
+ sendOtaspResult(OTASP_USER_SKIPPED);
+
+ if (mInteractive) mInCallScreen.finish();
+ return;
+ }
+
+ /**
+ * Actually initiate the OTASP call. This method is triggered by the
+ * onscreen "Activate" button, and is only used in interactive mode.
+ */
+ private void otaPerformActivation() {
+ if (DBG) log("otaPerformActivation()...");
+ if (!mInteractive) {
+ // We shouldn't ever get here in non-interactive mode!
+ Log.w(LOG_TAG, "otaPerformActivation: not interactive!");
+ return;
+ }
+
+ if (!mApplication.cdmaOtaProvisionData.inOtaSpcState) {
+ // Place an outgoing call to the special OTASP number:
+ Intent newIntent = new Intent(Intent.ACTION_CALL);
+ newIntent.setData(Uri.fromParts(Constants.SCHEME_TEL, OTASP_NUMBER, null));
+
+ // Initiate the outgoing call:
+ mApplication.callController.placeCall(newIntent);
+
+ // ...and get the OTASP-specific UI into the right state.
+ otaShowListeningScreen();
+ mInCallScreen.requestUpdateScreen();
+ }
+ return;
+ }
+
+ /**
+ * Show Activation Screen when phone powers up and OTA provision is
+ * required. Also shown when activation fails and user needs
+ * to re-attempt it. Contains ACTIVATE and SKIP buttons
+ * which allow user to start OTA activation or skip the activation process.
+ */
+ public void otaShowActivateScreen() {
+ if (DBG) log("otaShowActivateScreen()...");
+ if (mApplication.cdmaOtaConfigData.otaShowActivationScreen
+ == OTA_SHOW_ACTIVATION_SCREEN_ON) {
+ if (DBG) log("otaShowActivateScreen(): show activation screen");
+ if (!isDialerOpened()) {
+ otaScreenInitialize();
+ mOtaWidgetData.otaSkipButton.setVisibility(sIsWizardMode ?
+ View.VISIBLE : View.INVISIBLE);
+ mOtaWidgetData.otaTextActivate.setVisibility(View.VISIBLE);
+ mOtaWidgetData.callCardOtaButtonsActivate.setVisibility(View.VISIBLE);
+ }
+ mApplication.cdmaOtaScreenState.otaScreenState =
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION;
+ } else {
+ if (DBG) log("otaShowActivateScreen(): show home screen");
+ otaShowHome();
+ }
+ }
+
+ /**
+ * Show "Listen for Instruction" screen during OTA call. Shown when OTA Call
+ * is initiated and user needs to listen for network instructions and press
+ * appropriate DTMF digits to proceed to the "Programming in Progress" phase.
+ */
+ private void otaShowListeningScreen() {
+ if (DBG) log("otaShowListeningScreen()...");
+ if (!mInteractive) {
+ // We shouldn't ever get here in non-interactive mode!
+ Log.w(LOG_TAG, "otaShowListeningScreen: not interactive!");
+ return;
+ }
+
+ if (mApplication.cdmaOtaConfigData.otaShowListeningScreen
+ == OTA_SHOW_LISTENING_SCREEN_ON) {
+ if (DBG) log("otaShowListeningScreen(): show listening screen");
+ if (!isDialerOpened()) {
+ otaScreenInitialize();
+ mOtaWidgetData.otaTextListenProgress.setVisibility(View.VISIBLE);
+ mOtaWidgetData.otaTextListenProgress.setText(R.string.ota_listen);
+ mOtaWidgetData.otaDtmfDialerView.setVisibility(View.VISIBLE);
+ mOtaWidgetData.callCardOtaButtonsListenProgress.setVisibility(View.VISIBLE);
+ mOtaWidgetData.otaSpeakerButton.setVisibility(View.VISIBLE);
+ boolean speakerOn = PhoneUtils.isSpeakerOn(mContext);
+ mOtaWidgetData.otaSpeakerButton.setChecked(speakerOn);
+ }
+ mApplication.cdmaOtaScreenState.otaScreenState =
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_LISTENING;
+ } else {
+ if (DBG) log("otaShowListeningScreen(): show progress screen");
+ otaShowInProgressScreen();
+ }
+ }
+
+ /**
+ * Do any necessary updates (of onscreen UI, for example)
+ * based on the latest status of the OTASP call.
+ */
+ private void updateOtaspProgress() {
+ if (DBG) log("updateOtaspProgress()... mInteractive = " + mInteractive);
+ if (mInteractive) {
+ // On regular phones we just call through to
+ // otaShowInProgressScreen(), which updates the
+ // InCallScreen's onscreen UI.
+ otaShowInProgressScreen();
+ } else {
+ // We're not using the InCallScreen to show OTA progress.
+
+ // For now, at least, there's nothing to do here.
+ // The overall "success" or "failure" indication we send back
+ // (to our caller) is triggered by the DISCONNECT event;
+ // see updateNonInteractiveOtaSuccessFailure().
+
+ // But if we ever need to send *intermediate* progress updates back
+ // to our caller, we'd do that here, possbily using the same
+ // PendingIntent that we already use to indicate success or failure.
+ }
+ }
+
+ /**
+ * When a non-interactive OTASP call completes, send a success or
+ * failure indication back to our caller.
+ *
+ * This is basically the non-interactive equivalent of
+ * otaShowSuccessFailure().
+ */
+ private void updateNonInteractiveOtaSuccessFailure() {
+ // This is basically the same logic as otaShowSuccessFailure(): we
+ // check the isOtaCallCommitted bit, and if that's true it means
+ // that activation was successful.
+
+ if (DBG) log("updateNonInteractiveOtaSuccessFailure(): isOtaCallCommitted = "
+ + mApplication.cdmaOtaProvisionData.isOtaCallCommitted);
+ int resultCode =
+ mApplication.cdmaOtaProvisionData.isOtaCallCommitted
+ ? OTASP_SUCCESS : OTASP_FAILURE;
+ sendOtaspResult(resultCode);
+ }
+
+ /**
+ * Sends the specified OTASP result code back to our caller (presumably
+ * SetupWizard) via the PendingIntent that they originally sent along with
+ * the ACTION_PERFORM_CDMA_PROVISIONING intent.
+ */
+ private void sendOtaspResult(int resultCode) {
+ if (DBG) log("sendOtaspResult: resultCode = " + resultCode);
+
+ // Pass the success or failure indication back to our caller by
+ // adding an additional extra to the PendingIntent we already
+ // have.
+ // (NB: there's a PendingIntent send() method that takes a resultCode
+ // directly, but we can't use that here since that call is only
+ // meaningful for pending intents that are actually used as activity
+ // results.)
+
+ Intent extraStuff = new Intent();
+ extraStuff.putExtra(EXTRA_OTASP_RESULT_CODE, resultCode);
+ // When we call PendingIntent.send() below, the extras from this
+ // intent will get merged with any extras already present in
+ // cdmaOtaScreenState.otaspResultCodePendingIntent.
+
+ if (mApplication.cdmaOtaScreenState == null) {
+ Log.e(LOG_TAG, "updateNonInteractiveOtaSuccessFailure: no cdmaOtaScreenState object!");
+ return;
+ }
+ if (mApplication.cdmaOtaScreenState.otaspResultCodePendingIntent == null) {
+ Log.w(LOG_TAG, "updateNonInteractiveOtaSuccessFailure: "
+ + "null otaspResultCodePendingIntent!");
+ return;
+ }
+
+ try {
+ if (DBG) log("- sendOtaspResult: SENDING PENDING INTENT: " +
+ mApplication.cdmaOtaScreenState.otaspResultCodePendingIntent);
+ mApplication.cdmaOtaScreenState.otaspResultCodePendingIntent.send(
+ mContext,
+ 0, /* resultCode (unused) */
+ extraStuff);
+ } catch (CanceledException e) {
+ // should never happen because no code cancels the pending intent right now,
+ Log.e(LOG_TAG, "PendingIntent send() failed: " + e);
+ }
+ }
+
+ /**
+ * Show "Programming In Progress" screen during OTA call. Shown when OTA
+ * provisioning is in progress after user has selected an option.
+ */
+ private void otaShowInProgressScreen() {
+ if (DBG) log("otaShowInProgressScreen()...");
+ if (!mInteractive) {
+ // We shouldn't ever get here in non-interactive mode!
+ Log.w(LOG_TAG, "otaShowInProgressScreen: not interactive!");
+ return;
+ }
+
+ mApplication.cdmaOtaScreenState.otaScreenState =
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_PROGRESS;
+
+ if ((mOtaWidgetData == null) || (mInCallScreen == null)) {
+ Log.w(LOG_TAG, "otaShowInProgressScreen: UI widgets not set up yet!");
+
+ // TODO(OTASP): our CdmaOtaScreenState is now correct; we just set
+ // it to OTA_STATUS_PROGRESS. But we still need to make sure that
+ // when the InCallScreen eventually comes to the foreground, it
+ // notices that state and does all the same UI updating we do below.
+ return;
+ }
+
+ if (!isDialerOpened()) {
+ otaScreenInitialize();
+ mOtaWidgetData.otaTextListenProgress.setVisibility(View.VISIBLE);
+ mOtaWidgetData.otaTextListenProgress.setText(R.string.ota_progress);
+ mOtaWidgetData.otaTextProgressBar.setVisibility(View.VISIBLE);
+ mOtaWidgetData.callCardOtaButtonsListenProgress.setVisibility(View.VISIBLE);
+ mOtaWidgetData.otaSpeakerButton.setVisibility(View.VISIBLE);
+ boolean speakerOn = PhoneUtils.isSpeakerOn(mContext);
+ mOtaWidgetData.otaSpeakerButton.setChecked(speakerOn);
+ }
+ }
+
+ /**
+ * Show programming failure dialog when OTA provisioning fails.
+ * If OTA provisioning attempts fail more than 3 times, then unsuccessful
+ * dialog is shown. Otherwise a two-second notice is shown with unsuccessful
+ * information. When notice expires, phone returns to activation screen.
+ */
+ private void otaShowProgramFailure(int length) {
+ if (DBG) log("otaShowProgramFailure()...");
+ mApplication.cdmaOtaProvisionData.activationCount++;
+ if ((mApplication.cdmaOtaProvisionData.activationCount <
+ mApplication.cdmaOtaConfigData.otaShowActivateFailTimes)
+ && (mApplication.cdmaOtaConfigData.otaShowActivationScreen ==
+ OTA_SHOW_ACTIVATION_SCREEN_ON)) {
+ if (DBG) log("otaShowProgramFailure(): activationCount"
+ + mApplication.cdmaOtaProvisionData.activationCount);
+ if (DBG) log("otaShowProgramFailure(): show failure notice");
+ otaShowProgramFailureNotice(length);
+ } else {
+ if (DBG) log("otaShowProgramFailure(): show failure dialog");
+ otaShowProgramFailureDialog();
+ }
+ }
+
+ /**
+ * Show either programming success dialog when OTA provisioning succeeds, or
+ * programming failure dialog when it fails. See {@link #otaShowProgramFailure}
+ * for more details.
+ */
+ public void otaShowSuccessFailure() {
+ if (DBG) log("otaShowSuccessFailure()...");
+ if (!mInteractive) {
+ // We shouldn't ever get here in non-interactive mode!
+ Log.w(LOG_TAG, "otaShowSuccessFailure: not interactive!");
+ return;
+ }
+
+ otaScreenInitialize();
+ if (DBG) log("otaShowSuccessFailure(): isOtaCallCommitted"
+ + mApplication.cdmaOtaProvisionData.isOtaCallCommitted);
+ if (mApplication.cdmaOtaProvisionData.isOtaCallCommitted) {
+ if (DBG) log("otaShowSuccessFailure(), show success dialog");
+ otaShowProgramSuccessDialog();
+ } else {
+ if (DBG) log("otaShowSuccessFailure(), show failure dialog");
+ otaShowProgramFailure(OTA_FAILURE_DIALOG_TIMEOUT);
+ }
+ return;
+ }
+
+ /**
+ * Show programming failure dialog when OTA provisioning fails more than 3
+ * times.
+ */
+ private void otaShowProgramFailureDialog() {
+ if (DBG) log("otaShowProgramFailureDialog()...");
+ mApplication.cdmaOtaScreenState.otaScreenState =
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_SUCCESS_FAILURE_DLG;
+ mOtaWidgetData.otaTitle.setText(R.string.ota_title_problem_with_activation);
+ mOtaWidgetData.otaTextSuccessFail.setVisibility(View.VISIBLE);
+ mOtaWidgetData.otaTextSuccessFail.setText(R.string.ota_unsuccessful);
+ mOtaWidgetData.callCardOtaButtonsFailSuccess.setVisibility(View.VISIBLE);
+ mOtaWidgetData.otaTryAgainButton.setVisibility(View.VISIBLE);
+ //close the dialer if open
+ if (isDialerOpened()) {
+ mOtaCallCardDtmfDialer.closeDialer(false);
+ }
+ }
+
+ /**
+ * Show programming success dialog when OTA provisioning succeeds.
+ */
+ private void otaShowProgramSuccessDialog() {
+ if (DBG) log("otaShowProgramSuccessDialog()...");
+ mApplication.cdmaOtaScreenState.otaScreenState =
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_SUCCESS_FAILURE_DLG;
+ mOtaWidgetData.otaTitle.setText(R.string.ota_title_activate_success);
+ mOtaWidgetData.otaTextSuccessFail.setVisibility(View.VISIBLE);
+ mOtaWidgetData.otaTextSuccessFail.setText(R.string.ota_successful);
+ mOtaWidgetData.callCardOtaButtonsFailSuccess.setVisibility(View.VISIBLE);
+ mOtaWidgetData.otaNextButton.setVisibility(View.VISIBLE);
+ //close the dialer if open
+ if (isDialerOpened()) {
+ mOtaCallCardDtmfDialer.closeDialer(false);
+ }
+ }
+
+ /**
+ * Show SPC failure notice when SPC attempts exceed 15 times.
+ * During OTA provisioning, if SPC code is incorrect OTA provisioning will
+ * fail. When SPC attempts are over 15, it shows SPC failure notice for one minute and
+ * then phone will power down.
+ */
+ private void otaShowSpcErrorNotice(int length) {
+ if (DBG) log("otaShowSpcErrorNotice()...");
+ if (mOtaWidgetData.spcErrorDialog == null) {
+ mApplication.cdmaOtaProvisionData.inOtaSpcState = true;
+ DialogInterface.OnKeyListener keyListener;
+ keyListener = new DialogInterface.OnKeyListener() {
+ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
+ log("Ignoring key events...");
+ return true;
+ }};
+ mOtaWidgetData.spcErrorDialog = new AlertDialog.Builder(mInCallScreen)
+ .setMessage(R.string.ota_spc_failure)
+ .setOnKeyListener(keyListener)
+ .create();
+ mOtaWidgetData.spcErrorDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ mOtaWidgetData.spcErrorDialog.show();
+ //close the dialer if open
+ if (isDialerOpened()) {
+ mOtaCallCardDtmfDialer.closeDialer(false);
+ }
+ long noticeTime = length*1000;
+ if (DBG) log("otaShowSpcErrorNotice(), remaining SPC noticeTime" + noticeTime);
+ mInCallScreen.requestCloseSpcErrorNotice(noticeTime);
+ }
+ }
+
+ /**
+ * When SPC notice times out, force phone to power down.
+ */
+ public void onOtaCloseSpcNotice() {
+ if (DBG) log("onOtaCloseSpcNotice(), send shutdown intent");
+ Intent shutdown = new Intent(Intent.ACTION_REQUEST_SHUTDOWN);
+ shutdown.putExtra(Intent.EXTRA_KEY_CONFIRM, false);
+ shutdown.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mContext.startActivity(shutdown);
+ }
+
+ /**
+ * Show two-second notice when OTA provisioning fails and number of failed attempts
+ * is less then 3.
+ */
+ private void otaShowProgramFailureNotice(int length) {
+ if (DBG) log("otaShowProgramFailureNotice()...");
+ if (mOtaWidgetData.otaFailureDialog == null) {
+ mOtaWidgetData.otaFailureDialog = new AlertDialog.Builder(mInCallScreen)
+ .setMessage(R.string.ota_failure)
+ .create();
+ mOtaWidgetData.otaFailureDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ mOtaWidgetData.otaFailureDialog.show();
+
+ long noticeTime = length*1000;
+ mInCallScreen.requestCloseOtaFailureNotice(noticeTime);
+ }
+ }
+
+ /**
+ * Handle OTA unsuccessful notice expiry. Dismisses the
+ * two-second notice and shows the activation screen.
+ */
+ public void onOtaCloseFailureNotice() {
+ if (DBG) log("onOtaCloseFailureNotice()...");
+ if (mOtaWidgetData.otaFailureDialog != null) {
+ mOtaWidgetData.otaFailureDialog.dismiss();
+ mOtaWidgetData.otaFailureDialog = null;
+ }
+ otaShowActivateScreen();
+ }
+
+ /**
+ * Initialize all OTA UI elements to be gone. Also set inCallPanel,
+ * callCard and the dialpad handle to be gone. This is called before any OTA screen
+ * gets drawn.
+ */
+ private void otaScreenInitialize() {
+ if (DBG) log("otaScreenInitialize()...");
+
+ if (!mInteractive) {
+ // We should never be doing anything with UI elements in
+ // non-interactive mode.
+ Log.w(LOG_TAG, "otaScreenInitialize: not interactive!");
+ return;
+ }
+
+ if (mInCallTouchUi != null) mInCallTouchUi.setVisibility(View.GONE);
+ if (mCallCard != null) {
+ mCallCard.setVisibility(View.GONE);
+ // TODO: try removing this.
+ mCallCard.hideCallCardElements();
+ }
+
+ mOtaWidgetData.otaTitle.setText(R.string.ota_title_activate);
+ mOtaWidgetData.otaTextActivate.setVisibility(View.GONE);
+ mOtaWidgetData.otaTextListenProgress.setVisibility(View.GONE);
+ mOtaWidgetData.otaTextProgressBar.setVisibility(View.GONE);
+ mOtaWidgetData.otaTextSuccessFail.setVisibility(View.GONE);
+ mOtaWidgetData.callCardOtaButtonsActivate.setVisibility(View.GONE);
+ mOtaWidgetData.callCardOtaButtonsListenProgress.setVisibility(View.GONE);
+ mOtaWidgetData.callCardOtaButtonsFailSuccess.setVisibility(View.GONE);
+ mOtaWidgetData.otaDtmfDialerView.setVisibility(View.GONE);
+ mOtaWidgetData.otaSpeakerButton.setVisibility(View.GONE);
+ mOtaWidgetData.otaTryAgainButton.setVisibility(View.GONE);
+ mOtaWidgetData.otaNextButton.setVisibility(View.GONE);
+ mOtaWidgetData.otaUpperWidgets.setVisibility(View.VISIBLE);
+ mOtaWidgetData.otaSkipButton.setVisibility(View.VISIBLE);
+ }
+
+ public void hideOtaScreen() {
+ if (DBG) log("hideOtaScreen()...");
+
+ mOtaWidgetData.callCardOtaButtonsActivate.setVisibility(View.GONE);
+ mOtaWidgetData.callCardOtaButtonsListenProgress.setVisibility(View.GONE);
+ mOtaWidgetData.callCardOtaButtonsFailSuccess.setVisibility(View.GONE);
+ mOtaWidgetData.otaUpperWidgets.setVisibility(View.GONE);
+ }
+
+ public boolean isDialerOpened() {
+ boolean retval = (mOtaCallCardDtmfDialer != null && mOtaCallCardDtmfDialer.isOpened());
+ if (DBG) log("- isDialerOpened() ==> " + retval);
+ return retval;
+ }
+
+ /**
+ * Show the appropriate OTA screen based on the current state of OTA call.
+ *
+ * This is called from the InCallScreen when the screen needs to be
+ * refreshed (and thus is only ever used in interactive mode.)
+ *
+ * Since this is called as part of the InCallScreen.updateScreen() sequence,
+ * this method does *not* post an mInCallScreen.requestUpdateScreen()
+ * request.
+ */
+ public void otaShowProperScreen() {
+ if (DBG) log("otaShowProperScreen()...");
+ if (!mInteractive) {
+ // We shouldn't ever get here in non-interactive mode!
+ Log.w(LOG_TAG, "otaShowProperScreen: not interactive!");
+ return;
+ }
+
+ if ((mInCallScreen != null) && mInCallScreen.isForegroundActivity()) {
+ if (DBG) log("otaShowProperScreen(): InCallScreen in foreground, currentstate = "
+ + mApplication.cdmaOtaScreenState.otaScreenState);
+ if (mInCallTouchUi != null) {
+ mInCallTouchUi.setVisibility(View.GONE);
+ }
+ if (mCallCard != null) {
+ mCallCard.setVisibility(View.GONE);
+ }
+ if (mApplication.cdmaOtaScreenState.otaScreenState
+ == CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION) {
+ otaShowActivateScreen();
+ } else if (mApplication.cdmaOtaScreenState.otaScreenState
+ == CdmaOtaScreenState.OtaScreenState.OTA_STATUS_LISTENING) {
+ otaShowListeningScreen();
+ } else if (mApplication.cdmaOtaScreenState.otaScreenState
+ == CdmaOtaScreenState.OtaScreenState.OTA_STATUS_PROGRESS) {
+ otaShowInProgressScreen();
+ }
+
+ if (mApplication.cdmaOtaProvisionData.inOtaSpcState) {
+ otaShowSpcErrorNotice(getOtaSpcDisplayTime());
+ }
+ }
+ }
+
+ /**
+ * Read configuration values for each OTA screen from config.xml.
+ * These configuration values control visibility of each screen.
+ */
+ private void readXmlSettings() {
+ if (DBG) log("readXmlSettings()...");
+ if (mApplication.cdmaOtaConfigData.configComplete) {
+ return;
+ }
+
+ mApplication.cdmaOtaConfigData.configComplete = true;
+ int tmpOtaShowActivationScreen =
+ mContext.getResources().getInteger(R.integer.OtaShowActivationScreen);
+ mApplication.cdmaOtaConfigData.otaShowActivationScreen = tmpOtaShowActivationScreen;
+ if (DBG) log("readXmlSettings(), otaShowActivationScreen = "
+ + mApplication.cdmaOtaConfigData.otaShowActivationScreen);
+
+ int tmpOtaShowListeningScreen =
+ mContext.getResources().getInteger(R.integer.OtaShowListeningScreen);
+ mApplication.cdmaOtaConfigData.otaShowListeningScreen = tmpOtaShowListeningScreen;
+ if (DBG) log("readXmlSettings(), otaShowListeningScreen = "
+ + mApplication.cdmaOtaConfigData.otaShowListeningScreen);
+
+ int tmpOtaShowActivateFailTimes =
+ mContext.getResources().getInteger(R.integer.OtaShowActivateFailTimes);
+ mApplication.cdmaOtaConfigData.otaShowActivateFailTimes = tmpOtaShowActivateFailTimes;
+ if (DBG) log("readXmlSettings(), otaShowActivateFailTimes = "
+ + mApplication.cdmaOtaConfigData.otaShowActivateFailTimes);
+
+ int tmpOtaPlaySuccessFailureTone =
+ mContext.getResources().getInteger(R.integer.OtaPlaySuccessFailureTone);
+ mApplication.cdmaOtaConfigData.otaPlaySuccessFailureTone = tmpOtaPlaySuccessFailureTone;
+ if (DBG) log("readXmlSettings(), otaPlaySuccessFailureTone = "
+ + mApplication.cdmaOtaConfigData.otaPlaySuccessFailureTone);
+ }
+
+ /**
+ * Handle the click events for OTA buttons.
+ */
+ public void onClickHandler(int id) {
+ switch (id) {
+ case R.id.otaEndButton:
+ onClickOtaEndButton();
+ break;
+
+ case R.id.otaSpeakerButton:
+ onClickOtaSpeakerButton();
+ break;
+
+ case R.id.otaActivateButton:
+ onClickOtaActivateButton();
+ break;
+
+ case R.id.otaSkipButton:
+ onClickOtaActivateSkipButton();
+ break;
+
+ case R.id.otaNextButton:
+ onClickOtaActivateNextButton();
+ break;
+
+ case R.id.otaTryAgainButton:
+ onClickOtaTryAgainButton();
+ break;
+
+ default:
+ if (DBG) log ("onClickHandler: received a click event for unrecognized id");
+ break;
+ }
+ }
+
+ private void onClickOtaTryAgainButton() {
+ if (DBG) log("Activation Try Again Clicked!");
+ if (!mApplication.cdmaOtaProvisionData.inOtaSpcState) {
+ otaShowActivateScreen();
+ }
+ }
+
+ private void onClickOtaEndButton() {
+ if (DBG) log("Activation End Call Button Clicked!");
+ if (!mApplication.cdmaOtaProvisionData.inOtaSpcState) {
+ if (PhoneUtils.hangup(mApplication.mCM) == false) {
+ // If something went wrong when placing the OTA call,
+ // the screen is not updated by the call disconnect
+ // handler and we have to do it here
+ setSpeaker(false);
+ mInCallScreen.handleOtaCallEnd();
+ }
+ }
+ }
+
+ private void onClickOtaSpeakerButton() {
+ if (DBG) log("OTA Speaker button Clicked!");
+ if (!mApplication.cdmaOtaProvisionData.inOtaSpcState) {
+ boolean isChecked = !PhoneUtils.isSpeakerOn(mContext);
+ setSpeaker(isChecked);
+ }
+ }
+
+ private void onClickOtaActivateButton() {
+ if (DBG) log("Call Activation Clicked!");
+ otaPerformActivation();
+ }
+
+ private void onClickOtaActivateSkipButton() {
+ if (DBG) log("Activation Skip Clicked!");
+ DialogInterface.OnKeyListener keyListener;
+ keyListener = new DialogInterface.OnKeyListener() {
+ public boolean onKey(DialogInterface dialog, int keyCode,
+ KeyEvent event) {
+ if (DBG) log("Ignoring key events...");
+ return true;
+ }
+ };
+ mOtaWidgetData.otaSkipConfirmationDialog = new AlertDialog.Builder(mInCallScreen)
+ .setTitle(R.string.ota_skip_activation_dialog_title)
+ .setMessage(R.string.ota_skip_activation_dialog_message)
+ .setPositiveButton(
+ android.R.string.ok,
+ // "OK" means "skip activation".
+ new AlertDialog.OnClickListener() {
+ public void onClick(DialogInterface dialog, int which) {
+ otaSkipActivation();
+ }
+ })
+ .setNegativeButton(
+ android.R.string.cancel,
+ // "Cancel" means just dismiss the dialog.
+ // Don't actually start an activation call.
+ null)
+ .setOnKeyListener(keyListener)
+ .create();
+ mOtaWidgetData.otaSkipConfirmationDialog.show();
+ }
+
+ private void onClickOtaActivateNextButton() {
+ if (DBG) log("Dialog Next Clicked!");
+ if (!mApplication.cdmaOtaProvisionData.inOtaSpcState) {
+ mApplication.cdmaOtaScreenState.otaScreenState =
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED;
+ otaShowHome();
+ }
+ }
+
+ public void dismissAllOtaDialogs() {
+ if (mOtaWidgetData != null) {
+ if (mOtaWidgetData.spcErrorDialog != null) {
+ if (DBG) log("- DISMISSING mSpcErrorDialog.");
+ mOtaWidgetData.spcErrorDialog.dismiss();
+ mOtaWidgetData.spcErrorDialog = null;
+ }
+ if (mOtaWidgetData.otaFailureDialog != null) {
+ if (DBG) log("- DISMISSING mOtaFailureDialog.");
+ mOtaWidgetData.otaFailureDialog.dismiss();
+ mOtaWidgetData.otaFailureDialog = null;
+ }
+ }
+ }
+
+ private int getOtaSpcDisplayTime() {
+ if (DBG) log("getOtaSpcDisplayTime()...");
+ int tmpSpcTime = 1;
+ if (mApplication.cdmaOtaProvisionData.inOtaSpcState) {
+ long tmpOtaSpcRunningTime = 0;
+ long tmpOtaSpcLeftTime = 0;
+ tmpOtaSpcRunningTime = SystemClock.elapsedRealtime();
+ tmpOtaSpcLeftTime =
+ tmpOtaSpcRunningTime - mApplication.cdmaOtaProvisionData.otaSpcUptime;
+ if (tmpOtaSpcLeftTime >= OTA_SPC_TIMEOUT*1000) {
+ tmpSpcTime = 1;
+ } else {
+ tmpSpcTime = OTA_SPC_TIMEOUT - (int)tmpOtaSpcLeftTime/1000;
+ }
+ }
+ if (DBG) log("getOtaSpcDisplayTime(), time for SPC error notice: " + tmpSpcTime);
+ return tmpSpcTime;
+ }
+
+ /**
+ * Initialize the OTA widgets for all OTA screens.
+ */
+ private void initOtaInCallScreen() {
+ if (DBG) log("initOtaInCallScreen()...");
+ mOtaWidgetData.otaTitle = (TextView) mInCallScreen.findViewById(R.id.otaTitle);
+ mOtaWidgetData.otaTextActivate = (TextView) mInCallScreen.findViewById(R.id.otaActivate);
+ mOtaWidgetData.otaTextActivate.setVisibility(View.GONE);
+ mOtaWidgetData.otaTextListenProgress =
+ (TextView) mInCallScreen.findViewById(R.id.otaListenProgress);
+ mOtaWidgetData.otaTextProgressBar =
+ (ProgressBar) mInCallScreen.findViewById(R.id.progress_large);
+ mOtaWidgetData.otaTextProgressBar.setIndeterminate(true);
+ mOtaWidgetData.otaTextSuccessFail =
+ (TextView) mInCallScreen.findViewById(R.id.otaSuccessFailStatus);
+
+ mOtaWidgetData.otaUpperWidgets =
+ (ViewGroup) mInCallScreen.findViewById(R.id.otaUpperWidgets);
+ mOtaWidgetData.callCardOtaButtonsListenProgress =
+ (View) mInCallScreen.findViewById(R.id.callCardOtaListenProgress);
+ mOtaWidgetData.callCardOtaButtonsActivate =
+ (View) mInCallScreen.findViewById(R.id.callCardOtaActivate);
+ mOtaWidgetData.callCardOtaButtonsFailSuccess =
+ (View) mInCallScreen.findViewById(R.id.callCardOtaFailOrSuccessful);
+
+ mOtaWidgetData.otaEndButton = (Button) mInCallScreen.findViewById(R.id.otaEndButton);
+ mOtaWidgetData.otaEndButton.setOnClickListener(mInCallScreen);
+ mOtaWidgetData.otaSpeakerButton =
+ (ToggleButton) mInCallScreen.findViewById(R.id.otaSpeakerButton);
+ mOtaWidgetData.otaSpeakerButton.setOnClickListener(mInCallScreen);
+ mOtaWidgetData.otaActivateButton =
+ (Button) mInCallScreen.findViewById(R.id.otaActivateButton);
+ mOtaWidgetData.otaActivateButton.setOnClickListener(mInCallScreen);
+ mOtaWidgetData.otaSkipButton = (Button) mInCallScreen.findViewById(R.id.otaSkipButton);
+ mOtaWidgetData.otaSkipButton.setOnClickListener(mInCallScreen);
+ mOtaWidgetData.otaNextButton = (Button) mInCallScreen.findViewById(R.id.otaNextButton);
+ mOtaWidgetData.otaNextButton.setOnClickListener(mInCallScreen);
+ mOtaWidgetData.otaTryAgainButton =
+ (Button) mInCallScreen.findViewById(R.id.otaTryAgainButton);
+ mOtaWidgetData.otaTryAgainButton.setOnClickListener(mInCallScreen);
+
+ mOtaWidgetData.otaDtmfDialerView =
+ (DTMFTwelveKeyDialerView) mInCallScreen.findViewById(R.id.otaDtmfDialerView);
+ // Sanity-check: the otaDtmfDialerView widget should *always* be present.
+ if (mOtaWidgetData.otaDtmfDialerView == null) {
+ throw new IllegalStateException("initOtaInCallScreen: couldn't find otaDtmfDialerView");
+ }
+
+ // Create a new DTMFTwelveKeyDialer instance purely for use by the
+ // DTMFTwelveKeyDialerView ("otaDtmfDialerView") that comes from
+ // otacall_card.xml.
+ mOtaCallCardDtmfDialer = new DTMFTwelveKeyDialer(mInCallScreen,
+ mOtaWidgetData.otaDtmfDialerView);
+
+ // Initialize the new DTMFTwelveKeyDialer instance. This is
+ // needed to play local DTMF tones.
+ mOtaCallCardDtmfDialer.startDialerSession();
+
+ mOtaWidgetData.otaDtmfDialerView.setDialer(mOtaCallCardDtmfDialer);
+ }
+
+ /**
+ * Clear out all OTA UI widget elements. Needs to get called
+ * when OTA call ends or InCallScreen is destroyed.
+ * @param disableSpeaker parameter control whether Speaker should be turned off.
+ */
+ public void cleanOtaScreen(boolean disableSpeaker) {
+ if (DBG) log("OTA ends, cleanOtaScreen!");
+
+ mApplication.cdmaOtaScreenState.otaScreenState =
+ CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED;
+ mApplication.cdmaOtaProvisionData.isOtaCallCommitted = false;
+ mApplication.cdmaOtaProvisionData.isOtaCallIntentProcessed = false;
+ mApplication.cdmaOtaProvisionData.inOtaSpcState = false;
+ mApplication.cdmaOtaProvisionData.activationCount = 0;
+ mApplication.cdmaOtaProvisionData.otaSpcUptime = 0;
+ mApplication.cdmaOtaInCallScreenUiState.state = State.UNDEFINED;
+
+ if (mInteractive && (mOtaWidgetData != null)) {
+ if (mInCallTouchUi != null) mInCallTouchUi.setVisibility(View.VISIBLE);
+ if (mCallCard != null) {
+ mCallCard.setVisibility(View.VISIBLE);
+ mCallCard.hideCallCardElements();
+ }
+
+ // Free resources from the DTMFTwelveKeyDialer instance we created
+ // in initOtaInCallScreen().
+ if (mOtaCallCardDtmfDialer != null) {
+ mOtaCallCardDtmfDialer.stopDialerSession();
+ }
+
+ mOtaWidgetData.otaTextActivate.setVisibility(View.GONE);
+ mOtaWidgetData.otaTextListenProgress.setVisibility(View.GONE);
+ mOtaWidgetData.otaTextProgressBar.setVisibility(View.GONE);
+ mOtaWidgetData.otaTextSuccessFail.setVisibility(View.GONE);
+ mOtaWidgetData.callCardOtaButtonsActivate.setVisibility(View.GONE);
+ mOtaWidgetData.callCardOtaButtonsListenProgress.setVisibility(View.GONE);
+ mOtaWidgetData.callCardOtaButtonsFailSuccess.setVisibility(View.GONE);
+ mOtaWidgetData.otaUpperWidgets.setVisibility(View.GONE);
+ mOtaWidgetData.otaDtmfDialerView.setVisibility(View.GONE);
+ mOtaWidgetData.otaNextButton.setVisibility(View.GONE);
+ mOtaWidgetData.otaTryAgainButton.setVisibility(View.GONE);
+ }
+
+ // turn off the speaker in case it was turned on
+ // but the OTA call could not be completed
+ if (disableSpeaker) {
+ setSpeaker(false);
+ }
+ }
+
+ /**
+ * Defines OTA information that needs to be maintained during
+ * an OTA call when display orientation changes.
+ */
+ public static class CdmaOtaProvisionData {
+ public boolean isOtaCallCommitted;
+ public boolean isOtaCallIntentProcessed;
+ public boolean inOtaSpcState;
+ public int activationCount;
+ public long otaSpcUptime;
+ }
+
+ /**
+ * Defines OTA screen configuration items read from config.xml
+ * and used to control OTA display.
+ */
+ public static class CdmaOtaConfigData {
+ public int otaShowActivationScreen;
+ public int otaShowListeningScreen;
+ public int otaShowActivateFailTimes;
+ public int otaPlaySuccessFailureTone;
+ public boolean configComplete;
+ public CdmaOtaConfigData() {
+ if (DBG) log("CdmaOtaConfigData constructor!");
+ otaShowActivationScreen = OTA_SHOW_ACTIVATION_SCREEN_OFF;
+ otaShowListeningScreen = OTA_SHOW_LISTENING_SCREEN_OFF;
+ otaShowActivateFailTimes = OTA_SHOW_ACTIVATE_FAIL_COUNT_OFF;
+ otaPlaySuccessFailureTone = OTA_PLAY_SUCCESS_FAILURE_TONE_OFF;
+ }
+ }
+
+ /**
+ * The state of the OTA InCallScreen UI.
+ */
+ public static class CdmaOtaInCallScreenUiState {
+ public enum State {
+ UNDEFINED,
+ NORMAL,
+ ENDED
+ }
+
+ public State state;
+
+ public CdmaOtaInCallScreenUiState() {
+ if (DBG) log("CdmaOtaInCallScreenState: constructor init to UNDEFINED");
+ state = CdmaOtaInCallScreenUiState.State.UNDEFINED;
+ }
+ }
+
+ /**
+ * Save the Ota InCallScreen UI state
+ */
+ public void setCdmaOtaInCallScreenUiState(CdmaOtaInCallScreenUiState.State state) {
+ if (DBG) log("setCdmaOtaInCallScreenState: " + state);
+ mApplication.cdmaOtaInCallScreenUiState.state = state;
+ }
+
+ /**
+ * Get the Ota InCallScreen UI state
+ */
+ public CdmaOtaInCallScreenUiState.State getCdmaOtaInCallScreenUiState() {
+ if (DBG) log("getCdmaOtaInCallScreenState: "
+ + mApplication.cdmaOtaInCallScreenUiState.state);
+ return mApplication.cdmaOtaInCallScreenUiState.state;
+ }
+
+ /**
+ * The OTA screen state machine.
+ */
+ public static class CdmaOtaScreenState {
+ public enum OtaScreenState {
+ OTA_STATUS_UNDEFINED,
+ OTA_STATUS_ACTIVATION,
+ OTA_STATUS_LISTENING,
+ OTA_STATUS_PROGRESS,
+ OTA_STATUS_SUCCESS_FAILURE_DLG
+ }
+
+ public OtaScreenState otaScreenState;
+
+ public CdmaOtaScreenState() {
+ otaScreenState = OtaScreenState.OTA_STATUS_UNDEFINED;
+ }
+
+ /**
+ * {@link PendingIntent} used to report an OTASP result status code
+ * back to our caller. Can be null.
+ *
+ * Our caller (presumably SetupWizard) may create this PendingIntent,
+ * pointing back at itself, and passes it along as an extra with the
+ * ACTION_PERFORM_CDMA_PROVISIONING intent. Then, when there's an
+ * OTASP result to report, we send that PendingIntent back, adding an
+ * extra called EXTRA_OTASP_RESULT_CODE to indicate the result.
+ *
+ * Possible result values are the OTASP_RESULT_* constants.
+ */
+ public PendingIntent otaspResultCodePendingIntent;
+ }
+
+ /** @see com.android.internal.telephony.Phone */
+ private static String otaProvisionStatusToString(int status) {
+ switch (status) {
+ case Phone.CDMA_OTA_PROVISION_STATUS_SPL_UNLOCKED:
+ return "SPL_UNLOCKED";
+ case Phone.CDMA_OTA_PROVISION_STATUS_SPC_RETRIES_EXCEEDED:
+ return "SPC_RETRIES_EXCEEDED";
+ case Phone.CDMA_OTA_PROVISION_STATUS_A_KEY_EXCHANGED:
+ return "A_KEY_EXCHANGED";
+ case Phone.CDMA_OTA_PROVISION_STATUS_SSD_UPDATED:
+ return "SSD_UPDATED";
+ case Phone.CDMA_OTA_PROVISION_STATUS_NAM_DOWNLOADED:
+ return "NAM_DOWNLOADED";
+ case Phone.CDMA_OTA_PROVISION_STATUS_MDN_DOWNLOADED:
+ return "MDN_DOWNLOADED";
+ case Phone.CDMA_OTA_PROVISION_STATUS_IMSI_DOWNLOADED:
+ return "IMSI_DOWNLOADED";
+ case Phone.CDMA_OTA_PROVISION_STATUS_PRL_DOWNLOADED:
+ return "PRL_DOWNLOADED";
+ case Phone.CDMA_OTA_PROVISION_STATUS_COMMITTED:
+ return "COMMITTED";
+ case Phone.CDMA_OTA_PROVISION_STATUS_OTAPA_STARTED:
+ return "OTAPA_STARTED";
+ case Phone.CDMA_OTA_PROVISION_STATUS_OTAPA_STOPPED:
+ return "OTAPA_STOPPED";
+ case Phone.CDMA_OTA_PROVISION_STATUS_OTAPA_ABORTED:
+ return "OTAPA_ABORTED";
+ default:
+ return "<unknown status" + status + ">";
+ }
+ }
+
+ private static int getLteOnCdmaMode(Context context) {
+ final TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(
+ Context.TELEPHONY_SERVICE);
+ // If the telephony manager is not available yet, or if it doesn't know the answer yet,
+ // try falling back on the system property that may or may not be there
+ if (telephonyManager == null
+ || telephonyManager.getLteOnCdmaMode() == PhoneConstants.LTE_ON_CDMA_UNKNOWN) {
+ return SystemProperties.getInt(TelephonyProperties.PROPERTY_LTE_ON_CDMA_DEVICE,
+ PhoneConstants.LTE_ON_CDMA_UNKNOWN);
+ }
+ return telephonyManager.getLteOnCdmaMode();
+ }
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+
+ private static void loge(String msg) {
+ Log.e(LOG_TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/OutgoingCallBroadcaster.java b/src/com/android/phone/OutgoingCallBroadcaster.java
new file mode 100644
index 0000000..fbec3a9
--- /dev/null
+++ b/src/com/android/phone/OutgoingCallBroadcaster.java
@@ -0,0 +1,750 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.app.Activity;
+import android.app.ActivityManagerNative;
+import android.app.AlertDialog;
+import android.app.AppOpsManager;
+import android.app.Dialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.os.UserHandle;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.ProgressBar;
+
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyCapabilities;
+
+/**
+ * OutgoingCallBroadcaster receives CALL and CALL_PRIVILEGED Intents, and
+ * broadcasts the ACTION_NEW_OUTGOING_CALL intent which allows other
+ * applications to monitor, redirect, or prevent the outgoing call.
+
+ * After the other applications have had a chance to see the
+ * ACTION_NEW_OUTGOING_CALL intent, it finally reaches the
+ * {@link OutgoingCallReceiver}, which passes the (possibly modified)
+ * intent on to the {@link SipCallOptionHandler}, which will
+ * ultimately start the call using the CallController.placeCall() API.
+ *
+ * Emergency calls and calls where no number is present (like for a CDMA
+ * "empty flash" or a nonexistent voicemail number) are exempt from being
+ * broadcast.
+ */
+public class OutgoingCallBroadcaster extends Activity
+ implements DialogInterface.OnClickListener, DialogInterface.OnCancelListener {
+
+ private static final String PERMISSION = android.Manifest.permission.PROCESS_OUTGOING_CALLS;
+ private static final String TAG = "OutgoingCallBroadcaster";
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ // Do not check in with VDBG = true, since that may write PII to the system log.
+ private static final boolean VDBG = false;
+
+ public static final String ACTION_SIP_SELECT_PHONE = "com.android.phone.SIP_SELECT_PHONE";
+ public static final String EXTRA_ALREADY_CALLED = "android.phone.extra.ALREADY_CALLED";
+ public static final String EXTRA_ORIGINAL_URI = "android.phone.extra.ORIGINAL_URI";
+ public static final String EXTRA_NEW_CALL_INTENT = "android.phone.extra.NEW_CALL_INTENT";
+ public static final String EXTRA_SIP_PHONE_URI = "android.phone.extra.SIP_PHONE_URI";
+ public static final String EXTRA_ACTUAL_NUMBER_TO_DIAL =
+ "android.phone.extra.ACTUAL_NUMBER_TO_DIAL";
+
+ /**
+ * Identifier for intent extra for sending an empty Flash message for
+ * CDMA networks. This message is used by the network to simulate a
+ * press/depress of the "hookswitch" of a landline phone. Aka "empty flash".
+ *
+ * TODO: Receiving an intent extra to tell the phone to send this flash is a
+ * temporary measure. To be replaced with an external ITelephony call in the future.
+ * TODO: Keep in sync with the string defined in TwelveKeyDialer.java in Contacts app
+ * until this is replaced with the ITelephony API.
+ */
+ public static final String EXTRA_SEND_EMPTY_FLASH =
+ "com.android.phone.extra.SEND_EMPTY_FLASH";
+
+ // Dialog IDs
+ private static final int DIALOG_NOT_VOICE_CAPABLE = 1;
+
+ /** Note message codes < 100 are reserved for the PhoneApp. */
+ private static final int EVENT_OUTGOING_CALL_TIMEOUT = 101;
+ private static final int OUTGOING_CALL_TIMEOUT_THRESHOLD = 2000; // msec
+ /**
+ * ProgressBar object with "spinner" style, which will be shown if we take more than
+ * {@link #EVENT_OUTGOING_CALL_TIMEOUT} msec to handle the incoming Intent.
+ */
+ private ProgressBar mWaitingSpinner;
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == EVENT_OUTGOING_CALL_TIMEOUT) {
+ Log.i(TAG, "Outgoing call takes too long. Showing the spinner.");
+ mWaitingSpinner.setVisibility(View.VISIBLE);
+ } else {
+ Log.wtf(TAG, "Unknown message id: " + msg.what);
+ }
+ }
+ };
+
+ /**
+ * OutgoingCallReceiver finishes NEW_OUTGOING_CALL broadcasts, starting
+ * the InCallScreen if the broadcast has not been canceled, possibly with
+ * a modified phone number and optional provider info (uri + package name + remote views.)
+ */
+ public class OutgoingCallReceiver extends BroadcastReceiver {
+ private static final String TAG = "OutgoingCallReceiver";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ mHandler.removeMessages(EVENT_OUTGOING_CALL_TIMEOUT);
+ doReceive(context, intent);
+ if (DBG) Log.v(TAG, "OutgoingCallReceiver is going to finish the Activity itself.");
+ finish();
+ }
+
+ public void doReceive(Context context, Intent intent) {
+ if (DBG) Log.v(TAG, "doReceive: " + intent);
+
+ boolean alreadyCalled;
+ String number;
+ String originalUri;
+
+ alreadyCalled = intent.getBooleanExtra(
+ OutgoingCallBroadcaster.EXTRA_ALREADY_CALLED, false);
+ if (alreadyCalled) {
+ if (DBG) Log.v(TAG, "CALL already placed -- returning.");
+ return;
+ }
+
+ // Once the NEW_OUTGOING_CALL broadcast is finished, the resultData
+ // is used as the actual number to call. (If null, no call will be
+ // placed.)
+
+ number = getResultData();
+ if (VDBG) Log.v(TAG, "- got number from resultData: '" + number + "'");
+
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+
+ // OTASP-specific checks.
+ // TODO: This should probably all happen in
+ // OutgoingCallBroadcaster.onCreate(), since there's no reason to
+ // even bother with the NEW_OUTGOING_CALL broadcast if we're going
+ // to disallow the outgoing call anyway...
+ if (TelephonyCapabilities.supportsOtasp(app.phone)) {
+ boolean activateState = (app.cdmaOtaScreenState.otaScreenState
+ == OtaUtils.CdmaOtaScreenState.OtaScreenState.OTA_STATUS_ACTIVATION);
+ boolean dialogState = (app.cdmaOtaScreenState.otaScreenState
+ == OtaUtils.CdmaOtaScreenState.OtaScreenState
+ .OTA_STATUS_SUCCESS_FAILURE_DLG);
+ boolean isOtaCallActive = false;
+
+ // TODO: Need cleaner way to check if OTA is active.
+ // Also, this check seems to be broken in one obscure case: if
+ // you interrupt an OTASP call by pressing Back then Skip,
+ // otaScreenState somehow gets left in either PROGRESS or
+ // LISTENING.
+ if ((app.cdmaOtaScreenState.otaScreenState
+ == OtaUtils.CdmaOtaScreenState.OtaScreenState.OTA_STATUS_PROGRESS)
+ || (app.cdmaOtaScreenState.otaScreenState
+ == OtaUtils.CdmaOtaScreenState.OtaScreenState.OTA_STATUS_LISTENING)) {
+ isOtaCallActive = true;
+ }
+
+ if (activateState || dialogState) {
+ // The OTASP sequence is active, but either (1) the call
+ // hasn't started yet, or (2) the call has ended and we're
+ // showing the success/failure screen. In either of these
+ // cases it's OK to make a new outgoing call, but we need
+ // to take down any OTASP-related UI first.
+ if (dialogState) app.dismissOtaDialogs();
+ app.clearOtaState();
+ app.clearInCallScreenMode();
+ } else if (isOtaCallActive) {
+ // The actual OTASP call is active. Don't allow new
+ // outgoing calls at all from this state.
+ Log.w(TAG, "OTASP call is active: disallowing a new outgoing call.");
+ return;
+ }
+ }
+
+ if (number == null) {
+ if (DBG) Log.v(TAG, "CALL cancelled (null number), returning...");
+ return;
+ } else if (TelephonyCapabilities.supportsOtasp(app.phone)
+ && (app.phone.getState() != PhoneConstants.State.IDLE)
+ && (app.phone.isOtaSpNumber(number))) {
+ if (DBG) Log.v(TAG, "Call is active, a 2nd OTA call cancelled -- returning.");
+ return;
+ } else if (PhoneNumberUtils.isPotentialLocalEmergencyNumber(number, context)) {
+ // Just like 3rd-party apps aren't allowed to place emergency
+ // calls via the ACTION_CALL intent, we also don't allow 3rd
+ // party apps to use the NEW_OUTGOING_CALL broadcast to rewrite
+ // an outgoing call into an emergency number.
+ Log.w(TAG, "Cannot modify outgoing call to emergency number " + number + ".");
+ return;
+ }
+
+ originalUri = intent.getStringExtra(
+ OutgoingCallBroadcaster.EXTRA_ORIGINAL_URI);
+ if (originalUri == null) {
+ Log.e(TAG, "Intent is missing EXTRA_ORIGINAL_URI -- returning.");
+ return;
+ }
+
+ Uri uri = Uri.parse(originalUri);
+
+ // We already called convertKeypadLettersToDigits() and
+ // stripSeparators() way back in onCreate(), before we sent out the
+ // NEW_OUTGOING_CALL broadcast. But we need to do it again here
+ // too, since the number might have been modified/rewritten during
+ // the broadcast (and may now contain letters or separators again.)
+ number = PhoneNumberUtils.convertKeypadLettersToDigits(number);
+ number = PhoneNumberUtils.stripSeparators(number);
+
+ if (DBG) Log.v(TAG, "doReceive: proceeding with call...");
+ if (VDBG) Log.v(TAG, "- uri: " + uri);
+ if (VDBG) Log.v(TAG, "- actual number to dial: '" + number + "'");
+
+ startSipCallOptionHandler(context, intent, uri, number);
+ }
+ }
+
+ /**
+ * Launch the SipCallOptionHandler, which is the next step(*) in the
+ * outgoing-call sequence after the outgoing call broadcast is
+ * complete.
+ *
+ * (*) We now know exactly what phone number we need to dial, so the next
+ * step is for the SipCallOptionHandler to decide which Phone type (SIP
+ * or PSTN) should be used. (Depending on the user's preferences, this
+ * decision may also involve popping up a dialog to ask the user to
+ * choose what type of call this should be.)
+ *
+ * @param context used for the startActivity() call
+ *
+ * @param intent the intent from the previous step of the outgoing-call
+ * sequence. Normally this will be the NEW_OUTGOING_CALL broadcast intent
+ * that came in to the OutgoingCallReceiver, although it can also be the
+ * original ACTION_CALL intent that started the whole sequence (in cases
+ * where we don't do the NEW_OUTGOING_CALL broadcast at all, like for
+ * emergency numbers or SIP addresses).
+ *
+ * @param uri the data URI from the original CALL intent, presumably either
+ * a tel: or sip: URI. For tel: URIs, note that the scheme-specific part
+ * does *not* necessarily have separators and keypad letters stripped (so
+ * we might see URIs like "tel:(650)%20555-1234" or "tel:1-800-GOOG-411"
+ * here.)
+ *
+ * @param number the actual number (or SIP address) to dial. This is
+ * guaranteed to be either a PSTN phone number with separators stripped
+ * out and keypad letters converted to digits (like "16505551234"), or a
+ * raw SIP address (like "user@example.com").
+ */
+ private void startSipCallOptionHandler(Context context, Intent intent,
+ Uri uri, String number) {
+ if (VDBG) {
+ Log.i(TAG, "startSipCallOptionHandler...");
+ Log.i(TAG, "- intent: " + intent);
+ Log.i(TAG, "- uri: " + uri);
+ Log.i(TAG, "- number: " + number);
+ }
+
+ // Create a copy of the original CALL intent that started the whole
+ // outgoing-call sequence. This intent will ultimately be passed to
+ // CallController.placeCall() after the SipCallOptionHandler step.
+
+ Intent newIntent = new Intent(Intent.ACTION_CALL, uri);
+ newIntent.putExtra(EXTRA_ACTUAL_NUMBER_TO_DIAL, number);
+ PhoneUtils.checkAndCopyPhoneProviderExtras(intent, newIntent);
+
+ // Finally, launch the SipCallOptionHandler, with the copy of the
+ // original CALL intent stashed away in the EXTRA_NEW_CALL_INTENT
+ // extra.
+
+ Intent selectPhoneIntent = new Intent(ACTION_SIP_SELECT_PHONE, uri);
+ selectPhoneIntent.setClass(context, SipCallOptionHandler.class);
+ selectPhoneIntent.putExtra(EXTRA_NEW_CALL_INTENT, newIntent);
+ selectPhoneIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ if (DBG) {
+ Log.v(TAG, "startSipCallOptionHandler(): " +
+ "calling startActivity: " + selectPhoneIntent);
+ }
+ context.startActivity(selectPhoneIntent);
+ // ...and see SipCallOptionHandler.onCreate() for the next step of the sequence.
+ }
+
+ /**
+ * This method is the single point of entry for the CALL intent, which is used (by built-in
+ * apps like Contacts / Dialer, as well as 3rd-party apps) to initiate an outgoing voice call.
+ *
+ *
+ */
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.outgoing_call_broadcaster);
+ mWaitingSpinner = (ProgressBar) findViewById(R.id.spinner);
+
+ Intent intent = getIntent();
+ if (DBG) {
+ final Configuration configuration = getResources().getConfiguration();
+ Log.v(TAG, "onCreate: this = " + this + ", icicle = " + icicle);
+ Log.v(TAG, " - getIntent() = " + intent);
+ Log.v(TAG, " - configuration = " + configuration);
+ }
+
+ if (icicle != null) {
+ // A non-null icicle means that this activity is being
+ // re-initialized after previously being shut down.
+ //
+ // In practice this happens very rarely (because the lifetime
+ // of this activity is so short!), but it *can* happen if the
+ // framework detects a configuration change at exactly the
+ // right moment; see bug 2202413.
+ //
+ // In this case, do nothing. Our onCreate() method has already
+ // run once (with icicle==null the first time), which means
+ // that the NEW_OUTGOING_CALL broadcast for this new call has
+ // already been sent.
+ Log.i(TAG, "onCreate: non-null icicle! "
+ + "Bailing out, not sending NEW_OUTGOING_CALL broadcast...");
+
+ // No need to finish() here, since the OutgoingCallReceiver from
+ // our original instance will do that. (It'll actually call
+ // finish() on our original instance, which apparently works fine
+ // even though the ActivityManager has already shut that instance
+ // down. And note that if we *do* call finish() here, that just
+ // results in an "ActivityManager: Duplicate finish request"
+ // warning when the OutgoingCallReceiver runs.)
+
+ return;
+ }
+
+ processIntent(intent);
+
+ // isFinishing() return false when 1. broadcast is still ongoing, or 2. dialog is being
+ // shown. Otherwise finish() is called inside processIntent(), is isFinishing() here will
+ // return true.
+ if (DBG) Log.v(TAG, "At the end of onCreate(). isFinishing(): " + isFinishing());
+ }
+
+ /**
+ * Interprets a given Intent and starts something relevant to the Intent.
+ *
+ * This method will handle three kinds of actions:
+ *
+ * - CALL (action for usual outgoing voice calls)
+ * - CALL_PRIVILEGED (can come from built-in apps like contacts / voice dialer / bluetooth)
+ * - CALL_EMERGENCY (from the EmergencyDialer that's reachable from the lockscreen.)
+ *
+ * The exact behavior depends on the intent's data:
+ *
+ * - The most typical is a tel: URI, which we handle by starting the
+ * NEW_OUTGOING_CALL broadcast. That broadcast eventually triggers
+ * the sequence OutgoingCallReceiver -> SipCallOptionHandler ->
+ * InCallScreen.
+ *
+ * - Or, with a sip: URI we skip the NEW_OUTGOING_CALL broadcast and
+ * go directly to SipCallOptionHandler, which then leads to the
+ * InCallScreen.
+ *
+ * - voicemail: URIs take the same path as regular tel: URIs.
+ *
+ * Other special cases:
+ *
+ * - Outgoing calls are totally disallowed on non-voice-capable
+ * devices (see handleNonVoiceCapable()).
+ *
+ * - A CALL intent with the EXTRA_SEND_EMPTY_FLASH extra (and
+ * presumably no data at all) means "send an empty flash" (which
+ * is only meaningful on CDMA devices while a call is already
+ * active.)
+ *
+ */
+ private void processIntent(Intent intent) {
+ if (DBG) {
+ Log.v(TAG, "processIntent() = " + intent + ", thread: " + Thread.currentThread());
+ }
+ final Configuration configuration = getResources().getConfiguration();
+
+ // Outgoing phone calls are only allowed on "voice-capable" devices.
+ if (!PhoneGlobals.sVoiceCapable) {
+ Log.i(TAG, "This device is detected as non-voice-capable device.");
+ handleNonVoiceCapable(intent);
+ return;
+ }
+
+ String action = intent.getAction();
+ String number = PhoneNumberUtils.getNumberFromIntent(intent, this);
+ // Check the number, don't convert for sip uri
+ // TODO put uriNumber under PhoneNumberUtils
+ if (number != null) {
+ if (!PhoneNumberUtils.isUriNumber(number)) {
+ number = PhoneNumberUtils.convertKeypadLettersToDigits(number);
+ number = PhoneNumberUtils.stripSeparators(number);
+ }
+ } else {
+ Log.w(TAG, "The number obtained from Intent is null.");
+ }
+
+ AppOpsManager appOps = (AppOpsManager)getSystemService(Context.APP_OPS_SERVICE);
+ int launchedFromUid;
+ String launchedFromPackage;
+ try {
+ launchedFromUid = ActivityManagerNative.getDefault().getLaunchedFromUid(
+ getActivityToken());
+ launchedFromPackage = ActivityManagerNative.getDefault().getLaunchedFromPackage(
+ getActivityToken());
+ } catch (RemoteException e) {
+ launchedFromUid = -1;
+ launchedFromPackage = null;
+ }
+ if (appOps.noteOp(AppOpsManager.OP_CALL_PHONE, launchedFromUid, launchedFromPackage)
+ != AppOpsManager.MODE_ALLOWED) {
+ Log.w(TAG, "Rejecting call from uid " + launchedFromUid + " package "
+ + launchedFromPackage);
+ finish();
+ return;
+ }
+
+ // If true, this flag will indicate that the current call is a special kind
+ // of call (most likely an emergency number) that 3rd parties aren't allowed
+ // to intercept or affect in any way. (In that case, we start the call
+ // immediately rather than going through the NEW_OUTGOING_CALL sequence.)
+ boolean callNow;
+
+ if (getClass().getName().equals(intent.getComponent().getClassName())) {
+ // If we were launched directly from the OutgoingCallBroadcaster,
+ // not one of its more privileged aliases, then make sure that
+ // only the non-privileged actions are allowed.
+ if (!Intent.ACTION_CALL.equals(intent.getAction())) {
+ Log.w(TAG, "Attempt to deliver non-CALL action; forcing to CALL");
+ intent.setAction(Intent.ACTION_CALL);
+ }
+ }
+
+ // Check whether or not this is an emergency number, in order to
+ // enforce the restriction that only the CALL_PRIVILEGED and
+ // CALL_EMERGENCY intents are allowed to make emergency calls.
+ //
+ // (Note that the ACTION_CALL check below depends on the result of
+ // isPotentialLocalEmergencyNumber() rather than just plain
+ // isLocalEmergencyNumber(), to be 100% certain that we *don't*
+ // allow 3rd party apps to make emergency calls by passing in an
+ // "invalid" number like "9111234" that isn't technically an
+ // emergency number but might still result in an emergency call
+ // with some networks.)
+ final boolean isExactEmergencyNumber =
+ (number != null) && PhoneNumberUtils.isLocalEmergencyNumber(number, this);
+ final boolean isPotentialEmergencyNumber =
+ (number != null) && PhoneNumberUtils.isPotentialLocalEmergencyNumber(number, this);
+ if (VDBG) {
+ Log.v(TAG, " - Checking restrictions for number '" + number + "':");
+ Log.v(TAG, " isExactEmergencyNumber = " + isExactEmergencyNumber);
+ Log.v(TAG, " isPotentialEmergencyNumber = " + isPotentialEmergencyNumber);
+ }
+
+ /* Change CALL_PRIVILEGED into CALL or CALL_EMERGENCY as needed. */
+ // TODO: This code is redundant with some code in InCallScreen: refactor.
+ if (Intent.ACTION_CALL_PRIVILEGED.equals(action)) {
+ // We're handling a CALL_PRIVILEGED intent, so we know this request came
+ // from a trusted source (like the built-in dialer.) So even a number
+ // that's *potentially* an emergency number can safely be promoted to
+ // CALL_EMERGENCY (since we *should* allow you to dial "91112345" from
+ // the dialer if you really want to.)
+ if (isPotentialEmergencyNumber) {
+ Log.i(TAG, "ACTION_CALL_PRIVILEGED is used while the number is a potential"
+ + " emergency number. Use ACTION_CALL_EMERGENCY as an action instead.");
+ action = Intent.ACTION_CALL_EMERGENCY;
+ } else {
+ action = Intent.ACTION_CALL;
+ }
+ if (DBG) Log.v(TAG, " - updating action from CALL_PRIVILEGED to " + action);
+ intent.setAction(action);
+ }
+
+ if (Intent.ACTION_CALL.equals(action)) {
+ if (isPotentialEmergencyNumber) {
+ Log.w(TAG, "Cannot call potential emergency number '" + number
+ + "' with CALL Intent " + intent + ".");
+ Log.i(TAG, "Launching default dialer instead...");
+
+ Intent invokeFrameworkDialer = new Intent();
+
+ // TwelveKeyDialer is in a tab so we really want
+ // DialtactsActivity. Build the intent 'manually' to
+ // use the java resolver to find the dialer class (as
+ // opposed to a Context which look up known android
+ // packages only)
+ invokeFrameworkDialer.setClassName("com.android.dialer",
+ "com.android.dialer.DialtactsActivity");
+ invokeFrameworkDialer.setAction(Intent.ACTION_DIAL);
+ invokeFrameworkDialer.setData(intent.getData());
+
+ if (DBG) Log.v(TAG, "onCreate(): calling startActivity for Dialer: "
+ + invokeFrameworkDialer);
+ startActivity(invokeFrameworkDialer);
+ finish();
+ return;
+ }
+ callNow = false;
+ } else if (Intent.ACTION_CALL_EMERGENCY.equals(action)) {
+ // ACTION_CALL_EMERGENCY case: this is either a CALL_PRIVILEGED
+ // intent that we just turned into a CALL_EMERGENCY intent (see
+ // above), or else it really is an CALL_EMERGENCY intent that
+ // came directly from some other app (e.g. the EmergencyDialer
+ // activity built in to the Phone app.)
+ // Make sure it's at least *possible* that this is really an
+ // emergency number.
+ if (!isPotentialEmergencyNumber) {
+ Log.w(TAG, "Cannot call non-potential-emergency number " + number
+ + " with EMERGENCY_CALL Intent " + intent + "."
+ + " Finish the Activity immediately.");
+ finish();
+ return;
+ }
+ callNow = true;
+ } else {
+ Log.e(TAG, "Unhandled Intent " + intent + ". Finish the Activity immediately.");
+ finish();
+ return;
+ }
+
+ // Make sure the screen is turned on. This is probably the right
+ // thing to do, and more importantly it works around an issue in the
+ // activity manager where we will not launch activities consistently
+ // when the screen is off (since it is trying to keep them paused
+ // and has... issues).
+ //
+ // Also, this ensures the device stays awake while doing the following
+ // broadcast; technically we should be holding a wake lock here
+ // as well.
+ PhoneGlobals.getInstance().wakeUpScreen();
+
+ // If number is null, we're probably trying to call a non-existent voicemail number,
+ // send an empty flash or something else is fishy. Whatever the problem, there's no
+ // number, so there's no point in allowing apps to modify the number.
+ if (TextUtils.isEmpty(number)) {
+ if (intent.getBooleanExtra(EXTRA_SEND_EMPTY_FLASH, false)) {
+ Log.i(TAG, "onCreate: SEND_EMPTY_FLASH...");
+ PhoneUtils.sendEmptyFlash(PhoneGlobals.getPhone());
+ finish();
+ return;
+ } else {
+ Log.i(TAG, "onCreate: null or empty number, setting callNow=true...");
+ callNow = true;
+ }
+ }
+
+ if (callNow) {
+ // This is a special kind of call (most likely an emergency number)
+ // that 3rd parties aren't allowed to intercept or affect in any way.
+ // So initiate the outgoing call immediately.
+
+ Log.i(TAG, "onCreate(): callNow case! Calling placeCall(): " + intent);
+
+ // Initiate the outgoing call, and simultaneously launch the
+ // InCallScreen to display the in-call UI:
+ PhoneGlobals.getInstance().callController.placeCall(intent);
+
+ // Note we do *not* "return" here, but instead continue and
+ // send the ACTION_NEW_OUTGOING_CALL broadcast like for any
+ // other outgoing call. (But when the broadcast finally
+ // reaches the OutgoingCallReceiver, we'll know not to
+ // initiate the call again because of the presence of the
+ // EXTRA_ALREADY_CALLED extra.)
+ }
+
+ // Remember the call origin so that users will be able to see an appropriate screen
+ // after the phone call. This should affect both phone calls and SIP calls.
+ final String callOrigin = intent.getStringExtra(PhoneGlobals.EXTRA_CALL_ORIGIN);
+ if (callOrigin != null) {
+ if (DBG) Log.v(TAG, " - Call origin is passed (" + callOrigin + ")");
+ PhoneGlobals.getInstance().setLatestActiveCallOrigin(callOrigin);
+ } else {
+ if (DBG) Log.v(TAG, " - Call origin is not passed. Reset current one.");
+ PhoneGlobals.getInstance().resetLatestActiveCallOrigin();
+ }
+
+ // For now, SIP calls will be processed directly without a
+ // NEW_OUTGOING_CALL broadcast.
+ //
+ // TODO: In the future, though, 3rd party apps *should* be allowed to
+ // intercept outgoing calls to SIP addresses as well. To do this, we should
+ // (1) update the NEW_OUTGOING_CALL intent documentation to explain this
+ // case, and (2) pass the outgoing SIP address by *not* overloading the
+ // EXTRA_PHONE_NUMBER extra, but instead using a new separate extra to hold
+ // the outgoing SIP address. (Be sure to document whether it's a URI or just
+ // a plain address, whether it could be a tel: URI, etc.)
+ Uri uri = intent.getData();
+ String scheme = uri.getScheme();
+ if (Constants.SCHEME_SIP.equals(scheme) || PhoneNumberUtils.isUriNumber(number)) {
+ Log.i(TAG, "The requested number was detected as SIP call.");
+ startSipCallOptionHandler(this, intent, uri, number);
+ finish();
+ return;
+
+ // TODO: if there's ever a way for SIP calls to trigger a
+ // "callNow=true" case (see above), we'll need to handle that
+ // case here too (most likely by just doing nothing at all.)
+ }
+
+ Intent broadcastIntent = new Intent(Intent.ACTION_NEW_OUTGOING_CALL);
+ if (number != null) {
+ broadcastIntent.putExtra(Intent.EXTRA_PHONE_NUMBER, number);
+ }
+ PhoneUtils.checkAndCopyPhoneProviderExtras(intent, broadcastIntent);
+ broadcastIntent.putExtra(EXTRA_ALREADY_CALLED, callNow);
+ broadcastIntent.putExtra(EXTRA_ORIGINAL_URI, uri.toString());
+ // Need to raise foreground in-call UI as soon as possible while allowing 3rd party app
+ // to intercept the outgoing call.
+ broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ if (DBG) Log.v(TAG, " - Broadcasting intent: " + broadcastIntent + ".");
+
+ // Set a timer so that we can prepare for unexpected delay introduced by the broadcast.
+ // If it takes too much time, the timer will show "waiting" spinner.
+ // This message will be removed when OutgoingCallReceiver#onReceive() is called before the
+ // timeout.
+ mHandler.sendEmptyMessageDelayed(EVENT_OUTGOING_CALL_TIMEOUT,
+ OUTGOING_CALL_TIMEOUT_THRESHOLD);
+ sendOrderedBroadcastAsUser(broadcastIntent, UserHandle.OWNER,
+ PERMISSION, new OutgoingCallReceiver(),
+ null, // scheduler
+ Activity.RESULT_OK, // initialCode
+ number, // initialData: initial value for the result data
+ null); // initialExtras
+ }
+
+ @Override
+ protected void onStop() {
+ // Clean up (and dismiss if necessary) any managed dialogs.
+ //
+ // We don't do this in onPause() since we can be paused/resumed
+ // due to orientation changes (in which case we don't want to
+ // disturb the dialog), but we *do* need it here in onStop() to be
+ // sure we clean up if the user hits HOME while the dialog is up.
+ //
+ // Note it's safe to call removeDialog() even if there's no dialog
+ // associated with that ID.
+ removeDialog(DIALOG_NOT_VOICE_CAPABLE);
+
+ super.onStop();
+ }
+
+ /**
+ * Handle the specified CALL or CALL_* intent on a non-voice-capable
+ * device.
+ *
+ * This method may launch a different intent (if there's some useful
+ * alternative action to take), or otherwise display an error dialog,
+ * and in either case will finish() the current activity when done.
+ */
+ private void handleNonVoiceCapable(Intent intent) {
+ if (DBG) Log.v(TAG, "handleNonVoiceCapable: handling " + intent
+ + " on non-voice-capable device...");
+ String action = intent.getAction();
+ Uri uri = intent.getData();
+ String scheme = uri.getScheme();
+
+ // Handle one special case: If this is a regular CALL to a tel: URI,
+ // bring up a UI letting you do something useful with the phone number
+ // (like "Add to contacts" if it isn't a contact yet.)
+ //
+ // This UI is provided by the contacts app in response to a DIAL
+ // intent, so we bring it up here by demoting this CALL to a DIAL and
+ // relaunching.
+ //
+ // TODO: it's strange and unintuitive to manually launch a DIAL intent
+ // to do this; it would be cleaner to have some shared UI component
+ // that we could bring up directly. (But for now at least, since both
+ // Contacts and Phone are built-in apps, this implementation is fine.)
+
+ if (Intent.ACTION_CALL.equals(action) && (Constants.SCHEME_TEL.equals(scheme))) {
+ Intent newIntent = new Intent(Intent.ACTION_DIAL, uri);
+ if (DBG) Log.v(TAG, "- relaunching as a DIAL intent: " + newIntent);
+ startActivity(newIntent);
+ finish();
+ return;
+ }
+
+ // In all other cases, just show a generic "voice calling not
+ // supported" dialog.
+ showDialog(DIALOG_NOT_VOICE_CAPABLE);
+ // ...and we'll eventually finish() when the user dismisses
+ // or cancels the dialog.
+ }
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ Dialog dialog;
+ switch(id) {
+ case DIALOG_NOT_VOICE_CAPABLE:
+ dialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.not_voice_capable)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(android.R.string.ok, this)
+ .setOnCancelListener(this)
+ .create();
+ break;
+ default:
+ Log.w(TAG, "onCreateDialog: unexpected ID " + id);
+ dialog = null;
+ break;
+ }
+ return dialog;
+ }
+
+ /** DialogInterface.OnClickListener implementation */
+ @Override
+ public void onClick(DialogInterface dialog, int id) {
+ // DIALOG_NOT_VOICE_CAPABLE is the only dialog we ever use (so far
+ // at least), and its only button is "OK".
+ finish();
+ }
+
+ /** DialogInterface.OnCancelListener implementation */
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ // DIALOG_NOT_VOICE_CAPABLE is the only dialog we ever use (so far
+ // at least), and canceling it is just like hitting "OK".
+ finish();
+ }
+
+ /**
+ * Implement onConfigurationChanged() purely for debugging purposes,
+ * to make sure that the android:configChanges element in our manifest
+ * is working properly.
+ */
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (DBG) Log.v(TAG, "onConfigurationChanged: newConfig = " + newConfig);
+ }
+}
diff --git a/src/com/android/phone/PhoneApp.java b/src/com/android/phone/PhoneApp.java
new file mode 100644
index 0000000..e3d3fa9
--- /dev/null
+++ b/src/com/android/phone/PhoneApp.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import android.app.Application;
+import android.content.res.Configuration;
+import android.os.UserHandle;
+
+/**
+ * Top-level Application class for the Phone app.
+ */
+public class PhoneApp extends Application {
+ PhoneGlobals mPhoneGlobals;
+
+ public PhoneApp() {
+ }
+
+ @Override
+ public void onCreate() {
+ if (UserHandle.myUserId() == 0) {
+ // We are running as the primary user, so should bring up the
+ // global phone state.
+ mPhoneGlobals = new PhoneGlobals(this);
+ mPhoneGlobals.onCreate();
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ if (mPhoneGlobals != null) {
+ mPhoneGlobals.onConfigurationChanged(newConfig);
+ }
+ super.onConfigurationChanged(newConfig);
+ }
+}
diff --git a/src/com/android/phone/PhoneGlobals.java b/src/com/android/phone/PhoneGlobals.java
new file mode 100644
index 0000000..019c74a
--- /dev/null
+++ b/src/com/android/phone/PhoneGlobals.java
@@ -0,0 +1,1859 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.Activity;
+import android.app.KeyguardManager;
+import android.app.PendingIntent;
+import android.app.ProgressDialog;
+import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
+import android.bluetooth.IBluetoothHeadsetPhone;
+import android.content.ActivityNotFoundException;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.res.Configuration;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IPowerManager;
+import android.os.Message;
+import android.os.PowerManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.UpdateLock;
+import android.os.UserHandle;
+import android.preference.PreferenceManager;
+import android.provider.Settings.System;
+import android.telephony.ServiceState;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.view.KeyEvent;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.IccCard;
+import com.android.internal.telephony.IccCardConstants;
+import com.android.internal.telephony.MmiCode;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.TelephonyCapabilities;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.cdma.TtyIntent;
+import com.android.phone.common.CallLogAsync;
+import com.android.phone.OtaUtils.CdmaOtaScreenState;
+import com.android.server.sip.SipService;
+
+/**
+ * Global state for the telephony subsystem when running in the primary
+ * phone process.
+ */
+public class PhoneGlobals extends ContextWrapper
+ implements AccelerometerListener.OrientationListener {
+ /* package */ static final String LOG_TAG = "PhoneApp";
+
+ /**
+ * Phone app-wide debug level:
+ * 0 - no debug logging
+ * 1 - normal debug logging if ro.debuggable is set (which is true in
+ * "eng" and "userdebug" builds but not "user" builds)
+ * 2 - ultra-verbose debug logging
+ *
+ * Most individual classes in the phone app have a local DBG constant,
+ * typically set to
+ * (PhoneApp.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1)
+ * or else
+ * (PhoneApp.DBG_LEVEL >= 2)
+ * depending on the desired verbosity.
+ *
+ * ***** DO NOT SUBMIT WITH DBG_LEVEL > 0 *************
+ */
+ /* package */ static final int DBG_LEVEL = 0;
+
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ private static final boolean VDBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ // Message codes; see mHandler below.
+ private static final int EVENT_SIM_NETWORK_LOCKED = 3;
+ private static final int EVENT_WIRED_HEADSET_PLUG = 7;
+ private static final int EVENT_SIM_STATE_CHANGED = 8;
+ private static final int EVENT_UPDATE_INCALL_NOTIFICATION = 9;
+ private static final int EVENT_DATA_ROAMING_DISCONNECTED = 10;
+ private static final int EVENT_DATA_ROAMING_OK = 11;
+ private static final int EVENT_UNSOL_CDMA_INFO_RECORD = 12;
+ private static final int EVENT_DOCK_STATE_CHANGED = 13;
+ private static final int EVENT_TTY_PREFERRED_MODE_CHANGED = 14;
+ private static final int EVENT_TTY_MODE_GET = 15;
+ private static final int EVENT_TTY_MODE_SET = 16;
+ private static final int EVENT_START_SIP_SERVICE = 17;
+
+ // The MMI codes are also used by the InCallScreen.
+ public static final int MMI_INITIATE = 51;
+ public static final int MMI_COMPLETE = 52;
+ public static final int MMI_CANCEL = 53;
+ // Don't use message codes larger than 99 here; those are reserved for
+ // the individual Activities of the Phone UI.
+
+ /**
+ * Allowable values for the wake lock code.
+ * SLEEP means the device can be put to sleep.
+ * PARTIAL means wake the processor, but we display can be kept off.
+ * FULL means wake both the processor and the display.
+ */
+ public enum WakeState {
+ SLEEP,
+ PARTIAL,
+ FULL
+ }
+
+ /**
+ * Intent Action used for hanging up the current call from Notification bar. This will
+ * choose first ringing call, first active call, or first background call (typically in
+ * HOLDING state).
+ */
+ public static final String ACTION_HANG_UP_ONGOING_CALL =
+ "com.android.phone.ACTION_HANG_UP_ONGOING_CALL";
+
+ /**
+ * Intent Action used for making a phone call from Notification bar.
+ * This is for missed call notifications.
+ */
+ public static final String ACTION_CALL_BACK_FROM_NOTIFICATION =
+ "com.android.phone.ACTION_CALL_BACK_FROM_NOTIFICATION";
+
+ /**
+ * Intent Action used for sending a SMS from notification bar.
+ * This is for missed call notifications.
+ */
+ public static final String ACTION_SEND_SMS_FROM_NOTIFICATION =
+ "com.android.phone.ACTION_SEND_SMS_FROM_NOTIFICATION";
+
+ private static PhoneGlobals sMe;
+
+ // A few important fields we expose to the rest of the package
+ // directly (rather than thru set/get methods) for efficiency.
+ Phone phone;
+ CallController callController;
+ InCallUiState inCallUiState;
+ CallerInfoCache callerInfoCache;
+ CallNotifier notifier;
+ NotificationMgr notificationMgr;
+ Ringer ringer;
+ IBluetoothHeadsetPhone mBluetoothPhone;
+ PhoneInterfaceManager phoneMgr;
+ CallManager mCM;
+ CallStateMonitor callStateMonitor;
+ int mBluetoothHeadsetState = BluetoothProfile.STATE_DISCONNECTED;
+ int mBluetoothHeadsetAudioState = BluetoothHeadset.STATE_AUDIO_DISCONNECTED;
+ boolean mShowBluetoothIndication = false;
+ static int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
+ static boolean sVoiceCapable = true;
+
+ // Internal PhoneApp Call state tracker
+ CdmaPhoneCallState cdmaPhoneCallState;
+
+ // The InCallScreen instance (or null if the InCallScreen hasn't been
+ // created yet.)
+ private InCallScreen mInCallScreen;
+
+ // The currently-active PUK entry activity and progress dialog.
+ // Normally, these are the Emergency Dialer and the subsequent
+ // progress dialog. null if there is are no such objects in
+ // the foreground.
+ private Activity mPUKEntryActivity;
+ private ProgressDialog mPUKEntryProgressDialog;
+
+ private boolean mIsSimPinEnabled;
+ private String mCachedSimPin;
+
+ // True if a wired headset is currently plugged in, based on the state
+ // from the latest Intent.ACTION_HEADSET_PLUG broadcast we received in
+ // mReceiver.onReceive().
+ private boolean mIsHeadsetPlugged;
+
+ // True if the keyboard is currently *not* hidden
+ // Gets updated whenever there is a Configuration change
+ private boolean mIsHardKeyboardOpen;
+
+ // True if we are beginning a call, but the phone state has not changed yet
+ private boolean mBeginningCall;
+
+ // Last phone state seen by updatePhoneState()
+ private PhoneConstants.State mLastPhoneState = PhoneConstants.State.IDLE;
+
+ private WakeState mWakeState = WakeState.SLEEP;
+
+ private PowerManager mPowerManager;
+ private IPowerManager mPowerManagerService;
+ private PowerManager.WakeLock mWakeLock;
+ private PowerManager.WakeLock mPartialWakeLock;
+ private PowerManager.WakeLock mProximityWakeLock;
+ private KeyguardManager mKeyguardManager;
+ private AccelerometerListener mAccelerometerListener;
+ private int mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN;
+
+ private UpdateLock mUpdateLock;
+
+ // Broadcast receiver for various intent broadcasts (see onCreate())
+ private final BroadcastReceiver mReceiver = new PhoneAppBroadcastReceiver();
+
+ // Broadcast receiver purely for ACTION_MEDIA_BUTTON broadcasts
+ private final BroadcastReceiver mMediaButtonReceiver = new MediaButtonBroadcastReceiver();
+
+ /** boolean indicating restoring mute state on InCallScreen.onResume() */
+ private boolean mShouldRestoreMuteOnInCallResume;
+
+ /**
+ * The singleton OtaUtils instance used for OTASP calls.
+ *
+ * The OtaUtils instance is created lazily the first time we need to
+ * make an OTASP call, regardless of whether it's an interactive or
+ * non-interactive OTASP call.
+ */
+ public OtaUtils otaUtils;
+
+ // Following are the CDMA OTA information Objects used during OTA Call.
+ // cdmaOtaProvisionData object store static OTA information that needs
+ // to be maintained even during Slider open/close scenarios.
+ // cdmaOtaConfigData object stores configuration info to control visiblity
+ // of each OTA Screens.
+ // cdmaOtaScreenState object store OTA Screen State information.
+ public OtaUtils.CdmaOtaProvisionData cdmaOtaProvisionData;
+ public OtaUtils.CdmaOtaConfigData cdmaOtaConfigData;
+ public OtaUtils.CdmaOtaScreenState cdmaOtaScreenState;
+ public OtaUtils.CdmaOtaInCallScreenUiState cdmaOtaInCallScreenUiState;
+
+ // TTY feature enabled on this platform
+ private boolean mTtyEnabled;
+ // Current TTY operating mode selected by user
+ private int mPreferredTtyMode = Phone.TTY_MODE_OFF;
+
+ /**
+ * Set the restore mute state flag. Used when we are setting the mute state
+ * OUTSIDE of user interaction {@link PhoneUtils#startNewCall(Phone)}
+ */
+ /*package*/void setRestoreMuteOnInCallResume (boolean mode) {
+ mShouldRestoreMuteOnInCallResume = mode;
+ }
+
+ /**
+ * Get the restore mute state flag.
+ * This is used by the InCallScreen {@link InCallScreen#onResume()} to figure
+ * out if we need to restore the mute state for the current active call.
+ */
+ /*package*/boolean getRestoreMuteOnInCallResume () {
+ return mShouldRestoreMuteOnInCallResume;
+ }
+
+ Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ PhoneConstants.State phoneState;
+ switch (msg.what) {
+ // Starts the SIP service. It's a no-op if SIP API is not supported
+ // on the deivce.
+ // TODO: Having the phone process host the SIP service is only
+ // temporary. Will move it to a persistent communication process
+ // later.
+ case EVENT_START_SIP_SERVICE:
+ SipService.start(getApplicationContext());
+ break;
+
+ // TODO: This event should be handled by the lock screen, just
+ // like the "SIM missing" and "Sim locked" cases (bug 1804111).
+ case EVENT_SIM_NETWORK_LOCKED:
+ if (getResources().getBoolean(R.bool.ignore_sim_network_locked_events)) {
+ // Some products don't have the concept of a "SIM network lock"
+ Log.i(LOG_TAG, "Ignoring EVENT_SIM_NETWORK_LOCKED event; "
+ + "not showing 'SIM network unlock' PIN entry screen");
+ } else {
+ // Normal case: show the "SIM network unlock" PIN entry screen.
+ // The user won't be able to do anything else until
+ // they enter a valid SIM network PIN.
+ Log.i(LOG_TAG, "show sim depersonal panel");
+ IccNetworkDepersonalizationPanel ndpPanel =
+ new IccNetworkDepersonalizationPanel(PhoneGlobals.getInstance());
+ ndpPanel.show();
+ }
+ break;
+
+ case EVENT_UPDATE_INCALL_NOTIFICATION:
+ // Tell the NotificationMgr to update the "ongoing
+ // call" icon in the status bar, if necessary.
+ // Currently, this is triggered by a bluetooth headset
+ // state change (since the status bar icon needs to
+ // turn blue when bluetooth is active.)
+ if (DBG) Log.d (LOG_TAG, "- updating in-call notification from handler...");
+ notificationMgr.updateInCallNotification();
+ break;
+
+ case EVENT_DATA_ROAMING_DISCONNECTED:
+ notificationMgr.showDataDisconnectedRoaming();
+ break;
+
+ case EVENT_DATA_ROAMING_OK:
+ notificationMgr.hideDataDisconnectedRoaming();
+ break;
+
+ case MMI_COMPLETE:
+ onMMIComplete((AsyncResult) msg.obj);
+ break;
+
+ case MMI_CANCEL:
+ PhoneUtils.cancelMmiCode(phone);
+ break;
+
+ case EVENT_WIRED_HEADSET_PLUG:
+ // Since the presence of a wired headset or bluetooth affects the
+ // speakerphone, update the "speaker" state. We ONLY want to do
+ // this on the wired headset connect / disconnect events for now
+ // though, so we're only triggering on EVENT_WIRED_HEADSET_PLUG.
+
+ phoneState = mCM.getState();
+ // Do not change speaker state if phone is not off hook
+ if (phoneState == PhoneConstants.State.OFFHOOK && !isBluetoothHeadsetAudioOn()) {
+ if (!isHeadsetPlugged()) {
+ // if the state is "not connected", restore the speaker state.
+ PhoneUtils.restoreSpeakerMode(getApplicationContext());
+ } else {
+ // if the state is "connected", force the speaker off without
+ // storing the state.
+ PhoneUtils.turnOnSpeaker(getApplicationContext(), false, false);
+ }
+ }
+ // Update the Proximity sensor based on headset state
+ updateProximitySensorMode(phoneState);
+
+ // Force TTY state update according to new headset state
+ if (mTtyEnabled) {
+ sendMessage(obtainMessage(EVENT_TTY_PREFERRED_MODE_CHANGED, 0));
+ }
+ break;
+
+ case EVENT_SIM_STATE_CHANGED:
+ // Marks the event where the SIM goes into ready state.
+ // Right now, this is only used for the PUK-unlocking
+ // process.
+ if (msg.obj.equals(IccCardConstants.INTENT_VALUE_ICC_READY)) {
+ // when the right event is triggered and there
+ // are UI objects in the foreground, we close
+ // them to display the lock panel.
+ if (mPUKEntryActivity != null) {
+ mPUKEntryActivity.finish();
+ mPUKEntryActivity = null;
+ }
+ if (mPUKEntryProgressDialog != null) {
+ mPUKEntryProgressDialog.dismiss();
+ mPUKEntryProgressDialog = null;
+ }
+ }
+ break;
+
+ case EVENT_UNSOL_CDMA_INFO_RECORD:
+ //TODO: handle message here;
+ break;
+
+ case EVENT_DOCK_STATE_CHANGED:
+ // If the phone is docked/undocked during a call, and no wired or BT headset
+ // is connected: turn on/off the speaker accordingly.
+ boolean inDockMode = false;
+ if (mDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) {
+ inDockMode = true;
+ }
+ if (VDBG) Log.d(LOG_TAG, "received EVENT_DOCK_STATE_CHANGED. Phone inDock = "
+ + inDockMode);
+
+ phoneState = mCM.getState();
+ if (phoneState == PhoneConstants.State.OFFHOOK &&
+ !isHeadsetPlugged() && !isBluetoothHeadsetAudioOn()) {
+ PhoneUtils.turnOnSpeaker(getApplicationContext(), inDockMode, true);
+ updateInCallScreen(); // Has no effect if the InCallScreen isn't visible
+ }
+ break;
+
+ case EVENT_TTY_PREFERRED_MODE_CHANGED:
+ // TTY mode is only applied if a headset is connected
+ int ttyMode;
+ if (isHeadsetPlugged()) {
+ ttyMode = mPreferredTtyMode;
+ } else {
+ ttyMode = Phone.TTY_MODE_OFF;
+ }
+ phone.setTTYMode(ttyMode, mHandler.obtainMessage(EVENT_TTY_MODE_SET));
+ break;
+
+ case EVENT_TTY_MODE_GET:
+ handleQueryTTYModeResponse(msg);
+ break;
+
+ case EVENT_TTY_MODE_SET:
+ handleSetTTYModeResponse(msg);
+ break;
+ }
+ }
+ };
+
+ public PhoneGlobals(Context context) {
+ super(context);
+ sMe = this;
+ }
+
+ public void onCreate() {
+ if (VDBG) Log.v(LOG_TAG, "onCreate()...");
+
+ ContentResolver resolver = getContentResolver();
+
+ // Cache the "voice capable" flag.
+ // This flag currently comes from a resource (which is
+ // overrideable on a per-product basis):
+ sVoiceCapable =
+ getResources().getBoolean(com.android.internal.R.bool.config_voice_capable);
+ // ...but this might eventually become a PackageManager "system
+ // feature" instead, in which case we'd do something like:
+ // sVoiceCapable =
+ // getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_VOICE_CALLS);
+
+ if (phone == null) {
+ // Initialize the telephony framework
+ PhoneFactory.makeDefaultPhones(this);
+
+ // Get the default phone
+ phone = PhoneFactory.getDefaultPhone();
+
+ // Start TelephonyDebugService After the default phone is created.
+ Intent intent = new Intent(this, TelephonyDebugService.class);
+ startService(intent);
+
+ mCM = CallManager.getInstance();
+ mCM.registerPhone(phone);
+
+ // Create the NotificationMgr singleton, which is used to display
+ // status bar icons and control other status bar behavior.
+ notificationMgr = NotificationMgr.init(this);
+
+ phoneMgr = PhoneInterfaceManager.init(this, phone);
+
+ mHandler.sendEmptyMessage(EVENT_START_SIP_SERVICE);
+
+ int phoneType = phone.getPhoneType();
+
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ // Create an instance of CdmaPhoneCallState and initialize it to IDLE
+ cdmaPhoneCallState = new CdmaPhoneCallState();
+ cdmaPhoneCallState.CdmaPhoneCallStateInit();
+ }
+
+ if (BluetoothAdapter.getDefaultAdapter() != null) {
+ // Start BluetoothPhoneService even if device is not voice capable.
+ // The device can still support VOIP.
+ startService(new Intent(this, BluetoothPhoneService.class));
+ bindService(new Intent(this, BluetoothPhoneService.class),
+ mBluetoothPhoneConnection, 0);
+ } else {
+ // Device is not bluetooth capable
+ mBluetoothPhone = null;
+ }
+
+ ringer = Ringer.init(this);
+
+ // before registering for phone state changes
+ mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ mWakeLock = mPowerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, LOG_TAG);
+ // lock used to keep the processor awake, when we don't care for the display.
+ mPartialWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK
+ | PowerManager.ON_AFTER_RELEASE, LOG_TAG);
+ // Wake lock used to control proximity sensor behavior.
+ if (mPowerManager.isWakeLockLevelSupported(
+ PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
+ mProximityWakeLock = mPowerManager.newWakeLock(
+ PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, LOG_TAG);
+ }
+ if (DBG) Log.d(LOG_TAG, "onCreate: mProximityWakeLock: " + mProximityWakeLock);
+
+ // create mAccelerometerListener only if we are using the proximity sensor
+ if (proximitySensorModeEnabled()) {
+ mAccelerometerListener = new AccelerometerListener(this, this);
+ }
+
+ mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
+
+ // get a handle to the service so that we can use it later when we
+ // want to set the poke lock.
+ mPowerManagerService = IPowerManager.Stub.asInterface(
+ ServiceManager.getService("power"));
+
+ // Get UpdateLock to suppress system-update related events (e.g. dialog show-up)
+ // during phone calls.
+ mUpdateLock = new UpdateLock("phone");
+
+ if (DBG) Log.d(LOG_TAG, "onCreate: mUpdateLock: " + mUpdateLock);
+
+ CallLogger callLogger = new CallLogger(this, new CallLogAsync());
+
+ // Create the CallController singleton, which is the interface
+ // to the telephony layer for user-initiated telephony functionality
+ // (like making outgoing calls.)
+ callController = CallController.init(this, callLogger);
+ // ...and also the InCallUiState instance, used by the CallController to
+ // keep track of some "persistent state" of the in-call UI.
+ inCallUiState = InCallUiState.init(this);
+
+ // Create the CallerInfoCache singleton, which remembers custom ring tone and
+ // send-to-voicemail settings.
+ //
+ // The asynchronous caching will start just after this call.
+ callerInfoCache = CallerInfoCache.init(this);
+
+ // Monitors call activity from the telephony layer
+ callStateMonitor = new CallStateMonitor(mCM);
+
+ // Create the CallNotifer singleton, which handles
+ // asynchronous events from the telephony layer (like
+ // launching the incoming-call UI when an incoming call comes
+ // in.)
+ notifier = CallNotifier.init(this, phone, ringer, callLogger, callStateMonitor);
+
+ // register for ICC status
+ IccCard sim = phone.getIccCard();
+ if (sim != null) {
+ if (VDBG) Log.v(LOG_TAG, "register for ICC status");
+ sim.registerForNetworkLocked(mHandler, EVENT_SIM_NETWORK_LOCKED, null);
+ }
+
+ // register for MMI/USSD
+ mCM.registerForMmiComplete(mHandler, MMI_COMPLETE, null);
+
+ // register connection tracking to PhoneUtils
+ PhoneUtils.initializeConnectionHandler(mCM);
+
+ // Read platform settings for TTY feature
+ mTtyEnabled = getResources().getBoolean(R.bool.tty_enabled);
+
+ // Register for misc other intent broadcasts.
+ IntentFilter intentFilter =
+ new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ intentFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+ intentFilter.addAction(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
+ intentFilter.addAction(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED);
+ intentFilter.addAction(Intent.ACTION_HEADSET_PLUG);
+ intentFilter.addAction(Intent.ACTION_DOCK_EVENT);
+ intentFilter.addAction(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
+ intentFilter.addAction(TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED);
+ intentFilter.addAction(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED);
+ intentFilter.addAction(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED);
+ if (mTtyEnabled) {
+ intentFilter.addAction(TtyIntent.TTY_PREFERRED_MODE_CHANGE_ACTION);
+ }
+ intentFilter.addAction(AudioManager.RINGER_MODE_CHANGED_ACTION);
+ registerReceiver(mReceiver, intentFilter);
+
+ // Use a separate receiver for ACTION_MEDIA_BUTTON broadcasts,
+ // since we need to manually adjust its priority (to make sure
+ // we get these intents *before* the media player.)
+ IntentFilter mediaButtonIntentFilter =
+ new IntentFilter(Intent.ACTION_MEDIA_BUTTON);
+ // TODO verify the independent priority doesn't need to be handled thanks to the
+ // private intent handler registration
+ // Make sure we're higher priority than the media player's
+ // MediaButtonIntentReceiver (which currently has the default
+ // priority of zero; see apps/Music/AndroidManifest.xml.)
+ mediaButtonIntentFilter.setPriority(1);
+ //
+ registerReceiver(mMediaButtonReceiver, mediaButtonIntentFilter);
+ // register the component so it gets priority for calls
+ AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ am.registerMediaButtonEventReceiverForCalls(new ComponentName(this.getPackageName(),
+ MediaButtonBroadcastReceiver.class.getName()));
+
+ //set the default values for the preferences in the phone.
+ PreferenceManager.setDefaultValues(this, R.xml.network_setting, false);
+
+ PreferenceManager.setDefaultValues(this, R.xml.call_feature_setting, false);
+
+ // Make sure the audio mode (along with some
+ // audio-mode-related state of our own) is initialized
+ // correctly, given the current state of the phone.
+ PhoneUtils.setAudioMode(mCM);
+ }
+
+ if (TelephonyCapabilities.supportsOtasp(phone)) {
+ cdmaOtaProvisionData = new OtaUtils.CdmaOtaProvisionData();
+ cdmaOtaConfigData = new OtaUtils.CdmaOtaConfigData();
+ cdmaOtaScreenState = new OtaUtils.CdmaOtaScreenState();
+ cdmaOtaInCallScreenUiState = new OtaUtils.CdmaOtaInCallScreenUiState();
+ }
+
+ // XXX pre-load the SimProvider so that it's ready
+ resolver.getType(Uri.parse("content://icc/adn"));
+
+ // start with the default value to set the mute state.
+ mShouldRestoreMuteOnInCallResume = false;
+
+ // TODO: Register for Cdma Information Records
+ // phone.registerCdmaInformationRecord(mHandler, EVENT_UNSOL_CDMA_INFO_RECORD, null);
+
+ // Read TTY settings and store it into BP NV.
+ // AP owns (i.e. stores) the TTY setting in AP settings database and pushes the setting
+ // to BP at power up (BP does not need to make the TTY setting persistent storage).
+ // This way, there is a single owner (i.e AP) for the TTY setting in the phone.
+ if (mTtyEnabled) {
+ mPreferredTtyMode = android.provider.Settings.Secure.getInt(
+ phone.getContext().getContentResolver(),
+ android.provider.Settings.Secure.PREFERRED_TTY_MODE,
+ Phone.TTY_MODE_OFF);
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_TTY_PREFERRED_MODE_CHANGED, 0));
+ }
+ // Read HAC settings and configure audio hardware
+ if (getResources().getBoolean(R.bool.hac_enabled)) {
+ int hac = android.provider.Settings.System.getInt(phone.getContext().getContentResolver(),
+ android.provider.Settings.System.HEARING_AID,
+ 0);
+ AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ audioManager.setParameter(CallFeaturesSetting.HAC_KEY, hac != 0 ?
+ CallFeaturesSetting.HAC_VAL_ON :
+ CallFeaturesSetting.HAC_VAL_OFF);
+ }
+ }
+
+ public void onConfigurationChanged(Configuration newConfig) {
+ if (newConfig.hardKeyboardHidden == Configuration.HARDKEYBOARDHIDDEN_NO) {
+ mIsHardKeyboardOpen = true;
+ } else {
+ mIsHardKeyboardOpen = false;
+ }
+
+ // Update the Proximity sensor based on keyboard state
+ updateProximitySensorMode(mCM.getState());
+ }
+
+ /**
+ * Returns the singleton instance of the PhoneApp.
+ */
+ static PhoneGlobals getInstance() {
+ if (sMe == null) {
+ throw new IllegalStateException("No PhoneGlobals here!");
+ }
+ return sMe;
+ }
+
+ /**
+ * Returns the singleton instance of the PhoneApp if running as the
+ * primary user, otherwise null.
+ */
+ static PhoneGlobals getInstanceIfPrimary() {
+ return sMe;
+ }
+
+ /**
+ * Returns the Phone associated with this instance
+ */
+ static Phone getPhone() {
+ return getInstance().phone;
+ }
+
+ Ringer getRinger() {
+ return ringer;
+ }
+
+ IBluetoothHeadsetPhone getBluetoothPhoneService() {
+ return mBluetoothPhone;
+ }
+
+ boolean isBluetoothHeadsetAudioOn() {
+ return (mBluetoothHeadsetAudioState != BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+ }
+
+ /**
+ * Returns an Intent that can be used to go to the "Call log"
+ * UI (aka CallLogActivity) in the Contacts app.
+ *
+ * Watch out: there's no guarantee that the system has any activity to
+ * handle this intent. (In particular there may be no "Call log" at
+ * all on on non-voice-capable devices.)
+ */
+ /* package */ static Intent createCallLogIntent() {
+ Intent intent = new Intent(Intent.ACTION_VIEW, null);
+ intent.setType("vnd.android.cursor.dir/calls");
+ return intent;
+ }
+
+ /**
+ * Return an Intent that can be used to bring up the in-call screen.
+ *
+ * This intent can only be used from within the Phone app, since the
+ * InCallScreen is not exported from our AndroidManifest.
+ */
+ /* package */ static Intent createInCallIntent() {
+ Intent intent = new Intent(Intent.ACTION_MAIN, null);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
+ | Intent.FLAG_ACTIVITY_NO_USER_ACTION);
+ intent.setClassName("com.android.phone", getCallScreenClassName());
+ return intent;
+ }
+
+ /**
+ * Variation of createInCallIntent() that also specifies whether the
+ * DTMF dialpad should be initially visible when the InCallScreen
+ * comes up.
+ */
+ /* package */ static Intent createInCallIntent(boolean showDialpad) {
+ Intent intent = createInCallIntent();
+ intent.putExtra(InCallScreen.SHOW_DIALPAD_EXTRA, showDialpad);
+ return intent;
+ }
+
+ /**
+ * Returns PendingIntent for hanging up ongoing phone call. This will typically be used from
+ * Notification context.
+ */
+ /* package */ static PendingIntent createHangUpOngoingCallPendingIntent(Context context) {
+ Intent intent = new Intent(PhoneGlobals.ACTION_HANG_UP_ONGOING_CALL, null,
+ context, NotificationBroadcastReceiver.class);
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ /* package */ static PendingIntent getCallBackPendingIntent(Context context, String number) {
+ Intent intent = new Intent(ACTION_CALL_BACK_FROM_NOTIFICATION,
+ Uri.fromParts(Constants.SCHEME_TEL, number, null),
+ context, NotificationBroadcastReceiver.class);
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ /* package */ static PendingIntent getSendSmsFromNotificationPendingIntent(
+ Context context, String number) {
+ Intent intent = new Intent(ACTION_SEND_SMS_FROM_NOTIFICATION,
+ Uri.fromParts(Constants.SCHEME_SMSTO, number, null),
+ context, NotificationBroadcastReceiver.class);
+ return PendingIntent.getBroadcast(context, 0, intent, 0);
+ }
+
+ private static String getCallScreenClassName() {
+ return InCallScreen.class.getName();
+ }
+
+ /**
+ * Starts the InCallScreen Activity.
+ */
+ /* package */ void displayCallScreen() {
+ if (VDBG) Log.d(LOG_TAG, "displayCallScreen()...");
+
+ // On non-voice-capable devices we shouldn't ever be trying to
+ // bring up the InCallScreen in the first place.
+ if (!sVoiceCapable) {
+ Log.w(LOG_TAG, "displayCallScreen() not allowed: non-voice-capable device",
+ new Throwable("stack dump")); // Include a stack trace since this warning
+ // indicates a bug in our caller
+ return;
+ }
+
+ try {
+ startActivity(createInCallIntent());
+ } catch (ActivityNotFoundException e) {
+ // It's possible that the in-call UI might not exist (like on
+ // non-voice-capable devices), so don't crash if someone
+ // accidentally tries to bring it up...
+ Log.w(LOG_TAG, "displayCallScreen: transition to InCallScreen failed: " + e);
+ }
+ Profiler.callScreenRequested();
+ }
+
+ boolean isSimPinEnabled() {
+ return mIsSimPinEnabled;
+ }
+
+ boolean authenticateAgainstCachedSimPin(String pin) {
+ return (mCachedSimPin != null && mCachedSimPin.equals(pin));
+ }
+
+ void setCachedSimPin(String pin) {
+ mCachedSimPin = pin;
+ }
+
+ void setInCallScreenInstance(InCallScreen inCallScreen) {
+ mInCallScreen = inCallScreen;
+ }
+
+ /**
+ * @return true if the in-call UI is running as the foreground
+ * activity. (In other words, from the perspective of the
+ * InCallScreen activity, return true between onResume() and
+ * onPause().)
+ *
+ * Note this method will return false if the screen is currently off,
+ * even if the InCallScreen *was* in the foreground just before the
+ * screen turned off. (This is because the foreground activity is
+ * always "paused" while the screen is off.)
+ */
+ boolean isShowingCallScreen() {
+ if (mInCallScreen == null) return false;
+ return mInCallScreen.isForegroundActivity();
+ }
+
+ /**
+ * @return true if the in-call UI is running as the foreground activity, or,
+ * it went to background due to screen being turned off. This might be useful
+ * to determine if the in-call screen went to background because of other
+ * activities, or its proximity sensor state or manual power-button press.
+ *
+ * Here are some examples.
+ *
+ * - If you want to know if the activity is in foreground or screen is turned off
+ * from the in-call UI (i.e. though it is not "foreground" anymore it will become
+ * so after screen being turned on), check
+ * {@link #isShowingCallScreenForProximity()} is true or not.
+ * {@link #updateProximitySensorMode(com.android.internal.telephony.PhoneConstants.State)} is
+ * doing this.
+ *
+ * - If you want to know if the activity is not in foreground just because screen
+ * is turned off (not due to other activity's interference), check
+ * {@link #isShowingCallScreen()} is false *and* {@link #isShowingCallScreenForProximity()}
+ * is true. InCallScreen#onDisconnect() is doing this check.
+ *
+ * @see #isShowingCallScreen()
+ *
+ * TODO: come up with better naming..
+ */
+ boolean isShowingCallScreenForProximity() {
+ if (mInCallScreen == null) return false;
+ return mInCallScreen.isForegroundActivityForProximity();
+ }
+
+ /**
+ * Dismisses the in-call UI.
+ *
+ * This also ensures that you won't be able to get back to the in-call
+ * UI via the BACK button (since this call removes the InCallScreen
+ * from the activity history.)
+ * For OTA Call, it call InCallScreen api to handle OTA Call End scenario
+ * to display OTA Call End screen.
+ */
+ /* package */ void dismissCallScreen() {
+ if (mInCallScreen != null) {
+ if ((TelephonyCapabilities.supportsOtasp(phone)) &&
+ (mInCallScreen.isOtaCallInActiveState()
+ || mInCallScreen.isOtaCallInEndState()
+ || ((cdmaOtaScreenState != null)
+ && (cdmaOtaScreenState.otaScreenState
+ != CdmaOtaScreenState.OtaScreenState.OTA_STATUS_UNDEFINED)))) {
+ // TODO: During OTA Call, display should not become dark to
+ // allow user to see OTA UI update. Phone app needs to hold
+ // a SCREEN_DIM_WAKE_LOCK wake lock during the entire OTA call.
+ wakeUpScreen();
+ // If InCallScreen is not in foreground we resume it to show the OTA call end screen
+ // Fire off the InCallScreen intent
+ displayCallScreen();
+
+ mInCallScreen.handleOtaCallEnd();
+ return;
+ } else {
+ mInCallScreen.finish();
+ }
+ }
+ }
+
+ /**
+ * Handles OTASP-related events from the telephony layer.
+ *
+ * While an OTASP call is active, the CallNotifier forwards
+ * OTASP-related telephony events to this method.
+ */
+ void handleOtaspEvent(Message msg) {
+ if (DBG) Log.d(LOG_TAG, "handleOtaspEvent(message " + msg + ")...");
+
+ if (otaUtils == null) {
+ // We shouldn't be getting OTASP events without ever
+ // having started the OTASP call in the first place!
+ Log.w(LOG_TAG, "handleOtaEvents: got an event but otaUtils is null! "
+ + "message = " + msg);
+ return;
+ }
+
+ otaUtils.onOtaProvisionStatusChanged((AsyncResult) msg.obj);
+ }
+
+ /**
+ * Similarly, handle the disconnect event of an OTASP call
+ * by forwarding it to the OtaUtils instance.
+ */
+ /* package */ void handleOtaspDisconnect() {
+ if (DBG) Log.d(LOG_TAG, "handleOtaspDisconnect()...");
+
+ if (otaUtils == null) {
+ // We shouldn't be getting OTASP events without ever
+ // having started the OTASP call in the first place!
+ Log.w(LOG_TAG, "handleOtaspDisconnect: otaUtils is null!");
+ return;
+ }
+
+ otaUtils.onOtaspDisconnect();
+ }
+
+ /**
+ * Sets the activity responsible for un-PUK-blocking the device
+ * so that we may close it when we receive a positive result.
+ * mPUKEntryActivity is also used to indicate to the device that
+ * we are trying to un-PUK-lock the phone. In other words, iff
+ * it is NOT null, then we are trying to unlock and waiting for
+ * the SIM to move to READY state.
+ *
+ * @param activity is the activity to close when PUK has
+ * finished unlocking. Can be set to null to indicate the unlock
+ * or SIM READYing process is over.
+ */
+ void setPukEntryActivity(Activity activity) {
+ mPUKEntryActivity = activity;
+ }
+
+ Activity getPUKEntryActivity() {
+ return mPUKEntryActivity;
+ }
+
+ /**
+ * Sets the dialog responsible for notifying the user of un-PUK-
+ * blocking - SIM READYing progress, so that we may dismiss it
+ * when we receive a positive result.
+ *
+ * @param dialog indicates the progress dialog informing the user
+ * of the state of the device. Dismissed upon completion of
+ * READYing process
+ */
+ void setPukEntryProgressDialog(ProgressDialog dialog) {
+ mPUKEntryProgressDialog = dialog;
+ }
+
+ ProgressDialog getPUKEntryProgressDialog() {
+ return mPUKEntryProgressDialog;
+ }
+
+ /**
+ * Controls whether or not the screen is allowed to sleep.
+ *
+ * Once sleep is allowed (WakeState is SLEEP), it will rely on the
+ * settings for the poke lock to determine when to timeout and let
+ * the device sleep {@link PhoneGlobals#setScreenTimeout}.
+ *
+ * @param ws tells the device to how to wake.
+ */
+ /* package */ void requestWakeState(WakeState ws) {
+ if (VDBG) Log.d(LOG_TAG, "requestWakeState(" + ws + ")...");
+ synchronized (this) {
+ if (mWakeState != ws) {
+ switch (ws) {
+ case PARTIAL:
+ // acquire the processor wake lock, and release the FULL
+ // lock if it is being held.
+ mPartialWakeLock.acquire();
+ if (mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ break;
+ case FULL:
+ // acquire the full wake lock, and release the PARTIAL
+ // lock if it is being held.
+ mWakeLock.acquire();
+ if (mPartialWakeLock.isHeld()) {
+ mPartialWakeLock.release();
+ }
+ break;
+ case SLEEP:
+ default:
+ // release both the PARTIAL and FULL locks.
+ if (mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ if (mPartialWakeLock.isHeld()) {
+ mPartialWakeLock.release();
+ }
+ break;
+ }
+ mWakeState = ws;
+ }
+ }
+ }
+
+ /**
+ * If we are not currently keeping the screen on, then poke the power
+ * manager to wake up the screen for the user activity timeout duration.
+ */
+ /* package */ void wakeUpScreen() {
+ synchronized (this) {
+ if (mWakeState == WakeState.SLEEP) {
+ if (DBG) Log.d(LOG_TAG, "pulse screen lock");
+ mPowerManager.wakeUp(SystemClock.uptimeMillis());
+ }
+ }
+ }
+
+ /**
+ * Sets the wake state and screen timeout based on the current state
+ * of the phone, and the current state of the in-call UI.
+ *
+ * This method is a "UI Policy" wrapper around
+ * {@link PhoneGlobals#requestWakeState} and {@link PhoneGlobals#setScreenTimeout}.
+ *
+ * It's safe to call this method regardless of the state of the Phone
+ * (e.g. whether or not it's idle), and regardless of the state of the
+ * Phone UI (e.g. whether or not the InCallScreen is active.)
+ */
+ /* package */ void updateWakeState() {
+ PhoneConstants.State state = mCM.getState();
+
+ // True if the in-call UI is the foreground activity.
+ // (Note this will be false if the screen is currently off,
+ // since in that case *no* activity is in the foreground.)
+ boolean isShowingCallScreen = isShowingCallScreen();
+
+ // True if the InCallScreen's DTMF dialer is currently opened.
+ // (Note this does NOT imply whether or not the InCallScreen
+ // itself is visible.)
+ boolean isDialerOpened = (mInCallScreen != null) && mInCallScreen.isDialerOpened();
+
+ // True if the speakerphone is in use. (If so, we *always* use
+ // the default timeout. Since the user is obviously not holding
+ // the phone up to his/her face, we don't need to worry about
+ // false touches, and thus don't need to turn the screen off so
+ // aggressively.)
+ // Note that we need to make a fresh call to this method any
+ // time the speaker state changes. (That happens in
+ // PhoneUtils.turnOnSpeaker().)
+ boolean isSpeakerInUse = (state == PhoneConstants.State.OFFHOOK) && PhoneUtils.isSpeakerOn(this);
+
+ // TODO (bug 1440854): The screen timeout *might* also need to
+ // depend on the bluetooth state, but this isn't as clear-cut as
+ // the speaker state (since while using BT it's common for the
+ // user to put the phone straight into a pocket, in which case the
+ // timeout should probably still be short.)
+
+ if (DBG) Log.d(LOG_TAG, "updateWakeState: callscreen " + isShowingCallScreen
+ + ", dialer " + isDialerOpened
+ + ", speaker " + isSpeakerInUse + "...");
+
+ //
+ // Decide whether to force the screen on or not.
+ //
+ // Force the screen to be on if the phone is ringing or dialing,
+ // or if we're displaying the "Call ended" UI for a connection in
+ // the "disconnected" state.
+ // However, if the phone is disconnected while the user is in the
+ // middle of selecting a quick response message, we should not force
+ // the screen to be on.
+ //
+ boolean isRinging = (state == PhoneConstants.State.RINGING);
+ boolean isDialing = (phone.getForegroundCall().getState() == Call.State.DIALING);
+ boolean showingQuickResponseDialog = (mInCallScreen != null) &&
+ mInCallScreen.isQuickResponseDialogShowing();
+ boolean showingDisconnectedConnection =
+ PhoneUtils.hasDisconnectedConnections(phone) && isShowingCallScreen;
+ boolean keepScreenOn = isRinging || isDialing ||
+ (showingDisconnectedConnection && !showingQuickResponseDialog);
+ if (DBG) Log.d(LOG_TAG, "updateWakeState: keepScreenOn = " + keepScreenOn
+ + " (isRinging " + isRinging
+ + ", isDialing " + isDialing
+ + ", showingQuickResponse " + showingQuickResponseDialog
+ + ", showingDisc " + showingDisconnectedConnection + ")");
+ // keepScreenOn == true means we'll hold a full wake lock:
+ requestWakeState(keepScreenOn ? WakeState.FULL : WakeState.SLEEP);
+ }
+
+ /**
+ * Manually pokes the PowerManager's userActivity method. Since we
+ * set the {@link WindowManager.LayoutParams#INPUT_FEATURE_DISABLE_USER_ACTIVITY}
+ * flag while the InCallScreen is active when there is no proximity sensor,
+ * we need to do this for touch events that really do count as user activity
+ * (like pressing any onscreen UI elements.)
+ */
+ /* package */ void pokeUserActivity() {
+ if (VDBG) Log.d(LOG_TAG, "pokeUserActivity()...");
+ mPowerManager.userActivity(SystemClock.uptimeMillis(), false);
+ }
+
+ /**
+ * Set when a new outgoing call is beginning, so we can update
+ * the proximity sensor state.
+ * Cleared when the InCallScreen is no longer in the foreground,
+ * in case the call fails without changing the telephony state.
+ */
+ /* package */ void setBeginningCall(boolean beginning) {
+ // Note that we are beginning a new call, for proximity sensor support
+ mBeginningCall = beginning;
+ // Update the Proximity sensor based on mBeginningCall state
+ updateProximitySensorMode(mCM.getState());
+ }
+
+ /**
+ * Updates the wake lock used to control proximity sensor behavior,
+ * based on the current state of the phone. This method is called
+ * from the CallNotifier on any phone state change.
+ *
+ * On devices that have a proximity sensor, to avoid false touches
+ * during a call, we hold a PROXIMITY_SCREEN_OFF_WAKE_LOCK wake lock
+ * whenever the phone is off hook. (When held, that wake lock causes
+ * the screen to turn off automatically when the sensor detects an
+ * object close to the screen.)
+ *
+ * This method is a no-op for devices that don't have a proximity
+ * sensor.
+ *
+ * Note this method doesn't care if the InCallScreen is the foreground
+ * activity or not. That's because we want the proximity sensor to be
+ * enabled any time the phone is in use, to avoid false cheek events
+ * for whatever app you happen to be running.
+ *
+ * Proximity wake lock will *not* be held if any one of the
+ * conditions is true while on a call:
+ * 1) If the audio is routed via Bluetooth
+ * 2) If a wired headset is connected
+ * 3) if the speaker is ON
+ * 4) If the slider is open(i.e. the hardkeyboard is *not* hidden)
+ *
+ * @param state current state of the phone (see {@link Phone#State})
+ */
+ /* package */ void updateProximitySensorMode(PhoneConstants.State state) {
+ if (VDBG) Log.d(LOG_TAG, "updateProximitySensorMode: state = " + state);
+
+ if (proximitySensorModeEnabled()) {
+ synchronized (mProximityWakeLock) {
+ // turn proximity sensor off and turn screen on immediately if
+ // we are using a headset, the keyboard is open, or the device
+ // is being held in a horizontal position.
+ boolean screenOnImmediately = (isHeadsetPlugged()
+ || PhoneUtils.isSpeakerOn(this)
+ || isBluetoothHeadsetAudioOn()
+ || mIsHardKeyboardOpen);
+
+ // We do not keep the screen off when the user is outside in-call screen and we are
+ // horizontal, but we do not force it on when we become horizontal until the
+ // proximity sensor goes negative.
+ boolean horizontal =
+ (mOrientation == AccelerometerListener.ORIENTATION_HORIZONTAL);
+ screenOnImmediately |= !isShowingCallScreenForProximity() && horizontal;
+
+ // We do not keep the screen off when dialpad is visible, we are horizontal, and
+ // the in-call screen is being shown.
+ // At that moment we're pretty sure users want to use it, instead of letting the
+ // proximity sensor turn off the screen by their hands.
+ boolean dialpadVisible = false;
+ if (mInCallScreen != null) {
+ dialpadVisible =
+ mInCallScreen.getUpdatedInCallControlState().dialpadEnabled
+ && mInCallScreen.getUpdatedInCallControlState().dialpadVisible
+ && isShowingCallScreen();
+ }
+ screenOnImmediately |= dialpadVisible && horizontal;
+
+ if (((state == PhoneConstants.State.OFFHOOK) || mBeginningCall) && !screenOnImmediately) {
+ // Phone is in use! Arrange for the screen to turn off
+ // automatically when the sensor detects a close object.
+ if (!mProximityWakeLock.isHeld()) {
+ if (DBG) Log.d(LOG_TAG, "updateProximitySensorMode: acquiring...");
+ mProximityWakeLock.acquire();
+ } else {
+ if (VDBG) Log.d(LOG_TAG, "updateProximitySensorMode: lock already held.");
+ }
+ } else {
+ // Phone is either idle, or ringing. We don't want any
+ // special proximity sensor behavior in either case.
+ if (mProximityWakeLock.isHeld()) {
+ if (DBG) Log.d(LOG_TAG, "updateProximitySensorMode: releasing...");
+ // Wait until user has moved the phone away from his head if we are
+ // releasing due to the phone call ending.
+ // Qtherwise, turn screen on immediately
+ int flags =
+ (screenOnImmediately ? 0 : PowerManager.WAIT_FOR_PROXIMITY_NEGATIVE);
+ mProximityWakeLock.release(flags);
+ } else {
+ if (VDBG) {
+ Log.d(LOG_TAG, "updateProximitySensorMode: lock already released.");
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void orientationChanged(int orientation) {
+ mOrientation = orientation;
+ updateProximitySensorMode(mCM.getState());
+ }
+
+ /**
+ * Notifies the phone app when the phone state changes.
+ *
+ * This method will updates various states inside Phone app (e.g. proximity sensor mode,
+ * accelerometer listener state, update-lock state, etc.)
+ */
+ /* package */ void updatePhoneState(PhoneConstants.State state) {
+ if (state != mLastPhoneState) {
+ mLastPhoneState = state;
+ updateProximitySensorMode(state);
+
+ // Try to acquire or release UpdateLock.
+ //
+ // Watch out: we don't release the lock here when the screen is still in foreground.
+ // At that time InCallScreen will release it on onPause().
+ if (state != PhoneConstants.State.IDLE) {
+ // UpdateLock is a recursive lock, while we may get "acquire" request twice and
+ // "release" request once for a single call (RINGING + OFFHOOK and IDLE).
+ // We need to manually ensure the lock is just acquired once for each (and this
+ // will prevent other possible buggy situations too).
+ if (!mUpdateLock.isHeld()) {
+ mUpdateLock.acquire();
+ }
+ } else {
+ if (!isShowingCallScreen()) {
+ if (!mUpdateLock.isHeld()) {
+ mUpdateLock.release();
+ }
+ } else {
+ // For this case InCallScreen will take care of the release() call.
+ }
+ }
+
+ if (mAccelerometerListener != null) {
+ // use accelerometer to augment proximity sensor when in call
+ mOrientation = AccelerometerListener.ORIENTATION_UNKNOWN;
+ mAccelerometerListener.enable(state == PhoneConstants.State.OFFHOOK);
+ }
+ // clear our beginning call flag
+ mBeginningCall = false;
+ // While we are in call, the in-call screen should dismiss the keyguard.
+ // This allows the user to press Home to go directly home without going through
+ // an insecure lock screen.
+ // But we do not want to do this if there is no active call so we do not
+ // bypass the keyguard if the call is not answered or declined.
+ if (mInCallScreen != null) {
+ mInCallScreen.updateKeyguardPolicy(state == PhoneConstants.State.OFFHOOK);
+ }
+ }
+ }
+
+ /* package */ PhoneConstants.State getPhoneState() {
+ return mLastPhoneState;
+ }
+
+ /**
+ * Returns UpdateLock object.
+ */
+ /* package */ UpdateLock getUpdateLock() {
+ return mUpdateLock;
+ }
+
+ /**
+ * @return true if this device supports the "proximity sensor
+ * auto-lock" feature while in-call (see updateProximitySensorMode()).
+ */
+ /* package */ boolean proximitySensorModeEnabled() {
+ return (mProximityWakeLock != null);
+ }
+
+ KeyguardManager getKeyguardManager() {
+ return mKeyguardManager;
+ }
+
+ private void onMMIComplete(AsyncResult r) {
+ if (VDBG) Log.d(LOG_TAG, "onMMIComplete()...");
+ MmiCode mmiCode = (MmiCode) r.result;
+ PhoneUtils.displayMMIComplete(phone, getInstance(), mmiCode, null, null);
+ }
+
+ private void initForNewRadioTechnology() {
+ if (DBG) Log.d(LOG_TAG, "initForNewRadioTechnology...");
+
+ if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ // Create an instance of CdmaPhoneCallState and initialize it to IDLE
+ cdmaPhoneCallState = new CdmaPhoneCallState();
+ cdmaPhoneCallState.CdmaPhoneCallStateInit();
+ }
+ if (TelephonyCapabilities.supportsOtasp(phone)) {
+ //create instances of CDMA OTA data classes
+ if (cdmaOtaProvisionData == null) {
+ cdmaOtaProvisionData = new OtaUtils.CdmaOtaProvisionData();
+ }
+ if (cdmaOtaConfigData == null) {
+ cdmaOtaConfigData = new OtaUtils.CdmaOtaConfigData();
+ }
+ if (cdmaOtaScreenState == null) {
+ cdmaOtaScreenState = new OtaUtils.CdmaOtaScreenState();
+ }
+ if (cdmaOtaInCallScreenUiState == null) {
+ cdmaOtaInCallScreenUiState = new OtaUtils.CdmaOtaInCallScreenUiState();
+ }
+ } else {
+ //Clean up OTA data in GSM/UMTS. It is valid only for CDMA
+ clearOtaState();
+ }
+
+ ringer.updateRingerContextAfterRadioTechnologyChange(this.phone);
+ notifier.updateCallNotifierRegistrationsAfterRadioTechnologyChange();
+ callStateMonitor.updateAfterRadioTechnologyChange();
+
+ if (mBluetoothPhone != null) {
+ try {
+ mBluetoothPhone.updateBtHandsfreeAfterRadioTechnologyChange();
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, Log.getStackTraceString(new Throwable()));
+ }
+ }
+ if (mInCallScreen != null) {
+ mInCallScreen.updateAfterRadioTechnologyChange();
+ }
+
+ // Update registration for ICC status after radio technology change
+ IccCard sim = phone.getIccCard();
+ if (sim != null) {
+ if (DBG) Log.d(LOG_TAG, "Update registration for ICC status...");
+
+ //Register all events new to the new active phone
+ sim.registerForNetworkLocked(mHandler, EVENT_SIM_NETWORK_LOCKED, null);
+ }
+ }
+
+
+ /**
+ * @return true if a wired headset is currently plugged in.
+ *
+ * @see Intent.ACTION_HEADSET_PLUG (which we listen for in mReceiver.onReceive())
+ */
+ boolean isHeadsetPlugged() {
+ return mIsHeadsetPlugged;
+ }
+
+ /**
+ * @return true if the onscreen UI should currently be showing the
+ * special "bluetooth is active" indication in a couple of places (in
+ * which UI elements turn blue and/or show the bluetooth logo.)
+ *
+ * This depends on the BluetoothHeadset state *and* the current
+ * telephony state; see shouldShowBluetoothIndication().
+ *
+ * @see CallCard
+ * @see NotificationMgr.updateInCallNotification
+ */
+ /* package */ boolean showBluetoothIndication() {
+ return mShowBluetoothIndication;
+ }
+
+ /**
+ * Recomputes the mShowBluetoothIndication flag based on the current
+ * bluetooth state and current telephony state.
+ *
+ * This needs to be called any time the bluetooth headset state or the
+ * telephony state changes.
+ *
+ * @param forceUiUpdate if true, force the UI elements that care
+ * about this flag to update themselves.
+ */
+ /* package */ void updateBluetoothIndication(boolean forceUiUpdate) {
+ mShowBluetoothIndication = shouldShowBluetoothIndication(mBluetoothHeadsetState,
+ mBluetoothHeadsetAudioState,
+ mCM);
+ if (forceUiUpdate) {
+ // Post Handler messages to the various components that might
+ // need to be refreshed based on the new state.
+ if (isShowingCallScreen()) mInCallScreen.requestUpdateBluetoothIndication();
+ if (DBG) Log.d (LOG_TAG, "- updating in-call notification for BT state change...");
+ mHandler.sendEmptyMessage(EVENT_UPDATE_INCALL_NOTIFICATION);
+ }
+
+ // Update the Proximity sensor based on Bluetooth audio state
+ updateProximitySensorMode(mCM.getState());
+ }
+
+ /**
+ * UI policy helper function for the couple of places in the UI that
+ * have some way of indicating that "bluetooth is in use."
+ *
+ * @return true if the onscreen UI should indicate that "bluetooth is in use",
+ * based on the specified bluetooth headset state, and the
+ * current state of the phone.
+ * @see showBluetoothIndication()
+ */
+ private static boolean shouldShowBluetoothIndication(int bluetoothState,
+ int bluetoothAudioState,
+ CallManager cm) {
+ // We want the UI to indicate that "bluetooth is in use" in two
+ // slightly different cases:
+ //
+ // (a) The obvious case: if a bluetooth headset is currently in
+ // use for an ongoing call.
+ //
+ // (b) The not-so-obvious case: if an incoming call is ringing,
+ // and we expect that audio *will* be routed to a bluetooth
+ // headset once the call is answered.
+
+ switch (cm.getState()) {
+ case OFFHOOK:
+ // This covers normal active calls, and also the case if
+ // the foreground call is DIALING or ALERTING. In this
+ // case, bluetooth is considered "active" if a headset
+ // is connected *and* audio is being routed to it.
+ return ((bluetoothState == BluetoothHeadset.STATE_CONNECTED)
+ && (bluetoothAudioState == BluetoothHeadset.STATE_AUDIO_CONNECTED));
+
+ case RINGING:
+ // If an incoming call is ringing, we're *not* yet routing
+ // audio to the headset (since there's no in-call audio
+ // yet!) In this case, if a bluetooth headset is
+ // connected at all, we assume that it'll become active
+ // once the user answers the phone.
+ return (bluetoothState == BluetoothHeadset.STATE_CONNECTED);
+
+ default: // Presumably IDLE
+ return false;
+ }
+ }
+
+
+ /**
+ * Receiver for misc intent broadcasts the Phone app cares about.
+ */
+ private class PhoneAppBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
+ boolean enabled = System.getInt(getContentResolver(),
+ System.AIRPLANE_MODE_ON, 0) == 0;
+ phone.setRadioPower(enabled);
+ } else if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
+ mBluetoothHeadsetState = intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
+ BluetoothHeadset.STATE_DISCONNECTED);
+ if (VDBG) Log.d(LOG_TAG, "mReceiver: HEADSET_STATE_CHANGED_ACTION");
+ if (VDBG) Log.d(LOG_TAG, "==> new state: " + mBluetoothHeadsetState);
+ updateBluetoothIndication(true); // Also update any visible UI if necessary
+ } else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
+ mBluetoothHeadsetAudioState =
+ intent.getIntExtra(BluetoothHeadset.EXTRA_STATE,
+ BluetoothHeadset.STATE_AUDIO_DISCONNECTED);
+ if (VDBG) Log.d(LOG_TAG, "mReceiver: HEADSET_AUDIO_STATE_CHANGED_ACTION");
+ if (VDBG) Log.d(LOG_TAG, "==> new state: " + mBluetoothHeadsetAudioState);
+ updateBluetoothIndication(true); // Also update any visible UI if necessary
+ } else if (action.equals(TelephonyIntents.ACTION_ANY_DATA_CONNECTION_STATE_CHANGED)) {
+ if (VDBG) Log.d(LOG_TAG, "mReceiver: ACTION_ANY_DATA_CONNECTION_STATE_CHANGED");
+ if (VDBG) Log.d(LOG_TAG, "- state: " + intent.getStringExtra(PhoneConstants.STATE_KEY));
+ if (VDBG) Log.d(LOG_TAG, "- reason: "
+ + intent.getStringExtra(PhoneConstants.STATE_CHANGE_REASON_KEY));
+
+ // The "data disconnected due to roaming" notification is shown
+ // if (a) you have the "data roaming" feature turned off, and
+ // (b) you just lost data connectivity because you're roaming.
+ boolean disconnectedDueToRoaming =
+ !phone.getDataRoamingEnabled()
+ && "DISCONNECTED".equals(intent.getStringExtra(PhoneConstants.STATE_KEY))
+ && Phone.REASON_ROAMING_ON.equals(
+ intent.getStringExtra(PhoneConstants.STATE_CHANGE_REASON_KEY));
+ mHandler.sendEmptyMessage(disconnectedDueToRoaming
+ ? EVENT_DATA_ROAMING_DISCONNECTED
+ : EVENT_DATA_ROAMING_OK);
+ } else if (action.equals(Intent.ACTION_HEADSET_PLUG)) {
+ if (VDBG) Log.d(LOG_TAG, "mReceiver: ACTION_HEADSET_PLUG");
+ if (VDBG) Log.d(LOG_TAG, " state: " + intent.getIntExtra("state", 0));
+ if (VDBG) Log.d(LOG_TAG, " name: " + intent.getStringExtra("name"));
+ mIsHeadsetPlugged = (intent.getIntExtra("state", 0) == 1);
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_WIRED_HEADSET_PLUG, 0));
+ } else if ((action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) &&
+ (mPUKEntryActivity != null)) {
+ // if an attempt to un-PUK-lock the device was made, while we're
+ // receiving this state change notification, notify the handler.
+ // NOTE: This is ONLY triggered if an attempt to un-PUK-lock has
+ // been attempted.
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_SIM_STATE_CHANGED,
+ intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE)));
+ } else if (action.equals(TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED)) {
+ String newPhone = intent.getStringExtra(PhoneConstants.PHONE_NAME_KEY);
+ Log.d(LOG_TAG, "Radio technology switched. Now " + newPhone + " is active.");
+ initForNewRadioTechnology();
+ } else if (action.equals(TelephonyIntents.ACTION_SERVICE_STATE_CHANGED)) {
+ handleServiceStateChanged(intent);
+ } else if (action.equals(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED)) {
+ if (TelephonyCapabilities.supportsEcm(phone)) {
+ Log.d(LOG_TAG, "Emergency Callback Mode arrived in PhoneApp.");
+ // Start Emergency Callback Mode service
+ if (intent.getBooleanExtra("phoneinECMState", false)) {
+ context.startService(new Intent(context,
+ EmergencyCallbackModeService.class));
+ }
+ } else {
+ // It doesn't make sense to get ACTION_EMERGENCY_CALLBACK_MODE_CHANGED
+ // on a device that doesn't support ECM in the first place.
+ Log.e(LOG_TAG, "Got ACTION_EMERGENCY_CALLBACK_MODE_CHANGED, "
+ + "but ECM isn't supported for phone: " + phone.getPhoneName());
+ }
+ } else if (action.equals(Intent.ACTION_DOCK_EVENT)) {
+ mDockState = intent.getIntExtra(Intent.EXTRA_DOCK_STATE,
+ Intent.EXTRA_DOCK_STATE_UNDOCKED);
+ if (VDBG) Log.d(LOG_TAG, "ACTION_DOCK_EVENT -> mDockState = " + mDockState);
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_DOCK_STATE_CHANGED, 0));
+ } else if (action.equals(TtyIntent.TTY_PREFERRED_MODE_CHANGE_ACTION)) {
+ mPreferredTtyMode = intent.getIntExtra(TtyIntent.TTY_PREFFERED_MODE,
+ Phone.TTY_MODE_OFF);
+ if (VDBG) Log.d(LOG_TAG, "mReceiver: TTY_PREFERRED_MODE_CHANGE_ACTION");
+ if (VDBG) Log.d(LOG_TAG, " mode: " + mPreferredTtyMode);
+ mHandler.sendMessage(mHandler.obtainMessage(EVENT_TTY_PREFERRED_MODE_CHANGED, 0));
+ } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
+ int ringerMode = intent.getIntExtra(AudioManager.EXTRA_RINGER_MODE,
+ AudioManager.RINGER_MODE_NORMAL);
+ if (ringerMode == AudioManager.RINGER_MODE_SILENT) {
+ notifier.silenceRinger();
+ }
+ }
+ }
+ }
+
+ /**
+ * Broadcast receiver for the ACTION_MEDIA_BUTTON broadcast intent.
+ *
+ * This functionality isn't lumped in with the other intents in
+ * PhoneAppBroadcastReceiver because we instantiate this as a totally
+ * separate BroadcastReceiver instance, since we need to manually
+ * adjust its IntentFilter's priority (to make sure we get these
+ * intents *before* the media player.)
+ */
+ private class MediaButtonBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ KeyEvent event = (KeyEvent) intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
+ if (VDBG) Log.d(LOG_TAG,
+ "MediaButtonBroadcastReceiver.onReceive()... event = " + event);
+ if ((event != null)
+ && (event.getKeyCode() == KeyEvent.KEYCODE_HEADSETHOOK)) {
+ if (VDBG) Log.d(LOG_TAG, "MediaButtonBroadcastReceiver: HEADSETHOOK");
+ boolean consumed = PhoneUtils.handleHeadsetHook(phone, event);
+ if (VDBG) Log.d(LOG_TAG, "==> handleHeadsetHook(): consumed = " + consumed);
+ if (consumed) {
+ // If a headset is attached and the press is consumed, also update
+ // any UI items (such as an InCallScreen mute button) that may need to
+ // be updated if their state changed.
+ updateInCallScreen(); // Has no effect if the InCallScreen isn't visible
+ abortBroadcast();
+ }
+ } else {
+ if (mCM.getState() != PhoneConstants.State.IDLE) {
+ // If the phone is anything other than completely idle,
+ // then we consume and ignore any media key events,
+ // Otherwise it is too easy to accidentally start
+ // playing music while a phone call is in progress.
+ if (VDBG) Log.d(LOG_TAG, "MediaButtonBroadcastReceiver: consumed");
+ abortBroadcast();
+ }
+ }
+ }
+ }
+
+ /**
+ * Accepts broadcast Intents which will be prepared by {@link NotificationMgr} and thus
+ * sent from framework's notification mechanism (which is outside Phone context).
+ * This should be visible from outside, but shouldn't be in "exported" state.
+ *
+ * TODO: If possible merge this into PhoneAppBroadcastReceiver.
+ */
+ public static class NotificationBroadcastReceiver extends BroadcastReceiver {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ // TODO: use "if (VDBG)" here.
+ Log.d(LOG_TAG, "Broadcast from Notification: " + action);
+
+ if (action.equals(ACTION_HANG_UP_ONGOING_CALL)) {
+ PhoneUtils.hangup(PhoneGlobals.getInstance().mCM);
+ } else if (action.equals(ACTION_CALL_BACK_FROM_NOTIFICATION)) {
+ // Collapse the expanded notification and the notification item itself.
+ closeSystemDialogs(context);
+ clearMissedCallNotification(context);
+
+ Intent callIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
+ callIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ context.startActivity(callIntent);
+ } else if (action.equals(ACTION_SEND_SMS_FROM_NOTIFICATION)) {
+ // Collapse the expanded notification and the notification item itself.
+ closeSystemDialogs(context);
+ clearMissedCallNotification(context);
+
+ Intent smsIntent = new Intent(Intent.ACTION_SENDTO, intent.getData());
+ smsIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(smsIntent);
+ } else {
+ Log.w(LOG_TAG, "Received hang-up request from notification,"
+ + " but there's no call the system can hang up.");
+ }
+ }
+
+ private void closeSystemDialogs(Context context) {
+ Intent intent = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
+ context.sendBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
+ private void clearMissedCallNotification(Context context) {
+ Intent clearIntent = new Intent(context, ClearMissedCallsService.class);
+ clearIntent.setAction(ClearMissedCallsService.ACTION_CLEAR_MISSED_CALLS);
+ context.startService(clearIntent);
+ }
+ }
+
+ private void handleServiceStateChanged(Intent intent) {
+ /**
+ * This used to handle updating EriTextWidgetProvider this routine
+ * and and listening for ACTION_SERVICE_STATE_CHANGED intents could
+ * be removed. But leaving just in case it might be needed in the near
+ * future.
+ */
+
+ // If service just returned, start sending out the queued messages
+ ServiceState ss = ServiceState.newFromBundle(intent.getExtras());
+
+ if (ss != null) {
+ int state = ss.getState();
+ notificationMgr.updateNetworkSelection(state);
+ }
+ }
+
+ public boolean isOtaCallInActiveState() {
+ boolean otaCallActive = false;
+ if (mInCallScreen != null) {
+ otaCallActive = mInCallScreen.isOtaCallInActiveState();
+ }
+ if (VDBG) Log.d(LOG_TAG, "- isOtaCallInActiveState " + otaCallActive);
+ return otaCallActive;
+ }
+
+ public boolean isOtaCallInEndState() {
+ boolean otaCallEnded = false;
+ if (mInCallScreen != null) {
+ otaCallEnded = mInCallScreen.isOtaCallInEndState();
+ }
+ if (VDBG) Log.d(LOG_TAG, "- isOtaCallInEndState " + otaCallEnded);
+ return otaCallEnded;
+ }
+
+ // it is safe to call clearOtaState() even if the InCallScreen isn't active
+ public void clearOtaState() {
+ if (DBG) Log.d(LOG_TAG, "- clearOtaState ...");
+ if ((mInCallScreen != null)
+ && (otaUtils != null)) {
+ otaUtils.cleanOtaScreen(true);
+ if (DBG) Log.d(LOG_TAG, " - clearOtaState clears OTA screen");
+ }
+ }
+
+ // it is safe to call dismissOtaDialogs() even if the InCallScreen isn't active
+ public void dismissOtaDialogs() {
+ if (DBG) Log.d(LOG_TAG, "- dismissOtaDialogs ...");
+ if ((mInCallScreen != null)
+ && (otaUtils != null)) {
+ otaUtils.dismissAllOtaDialogs();
+ if (DBG) Log.d(LOG_TAG, " - dismissOtaDialogs clears OTA dialogs");
+ }
+ }
+
+ // it is safe to call clearInCallScreenMode() even if the InCallScreen isn't active
+ public void clearInCallScreenMode() {
+ if (DBG) Log.d(LOG_TAG, "- clearInCallScreenMode ...");
+ if (mInCallScreen != null) {
+ mInCallScreen.resetInCallScreenMode();
+ }
+ }
+
+ /**
+ * Force the in-call UI to refresh itself, if it's currently visible.
+ *
+ * This method can be used any time there's a state change anywhere in
+ * the phone app that needs to be reflected in the onscreen UI.
+ *
+ * Note that it's *not* necessary to manually refresh the in-call UI
+ * (via this method) for regular telephony state changes like
+ * DIALING -> ALERTING -> ACTIVE, since the InCallScreen already
+ * listens for those state changes itself.
+ *
+ * This method does *not* force the in-call UI to come up if it's not
+ * already visible. To do that, use displayCallScreen().
+ */
+ /* package */ void updateInCallScreen() {
+ if (DBG) Log.d(LOG_TAG, "- updateInCallScreen()...");
+ if (mInCallScreen != null) {
+ // Post an updateScreen() request. Note that the
+ // updateScreen() call will end up being a no-op if the
+ // InCallScreen isn't the foreground activity.
+ mInCallScreen.requestUpdateScreen();
+ }
+ }
+
+ private void handleQueryTTYModeResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+ if (ar.exception != null) {
+ if (DBG) Log.d(LOG_TAG, "handleQueryTTYModeResponse: Error getting TTY state.");
+ } else {
+ if (DBG) Log.d(LOG_TAG,
+ "handleQueryTTYModeResponse: TTY enable state successfully queried.");
+
+ int ttymode = ((int[]) ar.result)[0];
+ if (DBG) Log.d(LOG_TAG, "handleQueryTTYModeResponse:ttymode=" + ttymode);
+
+ Intent ttyModeChanged = new Intent(TtyIntent.TTY_ENABLED_CHANGE_ACTION);
+ ttyModeChanged.putExtra("ttyEnabled", ttymode != Phone.TTY_MODE_OFF);
+ sendBroadcastAsUser(ttyModeChanged, UserHandle.ALL);
+
+ String audioTtyMode;
+ switch (ttymode) {
+ case Phone.TTY_MODE_FULL:
+ audioTtyMode = "tty_full";
+ break;
+ case Phone.TTY_MODE_VCO:
+ audioTtyMode = "tty_vco";
+ break;
+ case Phone.TTY_MODE_HCO:
+ audioTtyMode = "tty_hco";
+ break;
+ case Phone.TTY_MODE_OFF:
+ default:
+ audioTtyMode = "tty_off";
+ break;
+ }
+ AudioManager audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ audioManager.setParameters("tty_mode="+audioTtyMode);
+ }
+ }
+
+ private void handleSetTTYModeResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (ar.exception != null) {
+ if (DBG) Log.d (LOG_TAG,
+ "handleSetTTYModeResponse: Error setting TTY mode, ar.exception"
+ + ar.exception);
+ }
+ phone.queryTTYMode(mHandler.obtainMessage(EVENT_TTY_MODE_GET));
+ }
+
+ /**
+ * "Call origin" may be used by Contacts app to specify where the phone call comes from.
+ * Currently, the only permitted value for this extra is {@link #ALLOWED_EXTRA_CALL_ORIGIN}.
+ * Any other value will be ignored, to make sure that malicious apps can't trick the in-call
+ * UI into launching some random other app after a call ends.
+ *
+ * TODO: make this more generic. Note that we should let the "origin" specify its package
+ * while we are now assuming it is "com.android.contacts"
+ */
+ public static final String EXTRA_CALL_ORIGIN = "com.android.phone.CALL_ORIGIN";
+ private static final String DEFAULT_CALL_ORIGIN_PACKAGE = "com.android.dialer";
+ private static final String ALLOWED_EXTRA_CALL_ORIGIN =
+ "com.android.dialer.DialtactsActivity";
+ /**
+ * Used to determine if the preserved call origin is fresh enough.
+ */
+ private static final long CALL_ORIGIN_EXPIRATION_MILLIS = 30 * 1000;
+
+ public void setLatestActiveCallOrigin(String callOrigin) {
+ inCallUiState.latestActiveCallOrigin = callOrigin;
+ if (callOrigin != null) {
+ inCallUiState.latestActiveCallOriginTimeStamp = SystemClock.elapsedRealtime();
+ } else {
+ inCallUiState.latestActiveCallOriginTimeStamp = 0;
+ }
+ }
+
+ /**
+ * Reset call origin depending on its timestamp.
+ *
+ * See if the current call origin preserved by the app is fresh enough or not. If it is,
+ * previous call origin will be used as is. If not, call origin will be reset.
+ *
+ * This will be effective especially for 3rd party apps which want to bypass phone calls with
+ * their own telephone lines. In that case Phone app may finish the phone call once and make
+ * another for the external apps, which will drop call origin information in Intent.
+ * Even in that case we are sure the second phone call should be initiated just after the first
+ * phone call, so here we restore it from the previous information iff the second call is done
+ * fairly soon.
+ */
+ public void resetLatestActiveCallOrigin() {
+ final long callOriginTimestamp = inCallUiState.latestActiveCallOriginTimeStamp;
+ final long currentTimestamp = SystemClock.elapsedRealtime();
+ if (VDBG) {
+ Log.d(LOG_TAG, "currentTimeMillis: " + currentTimestamp
+ + ", saved timestamp for call origin: " + callOriginTimestamp);
+ }
+ if (inCallUiState.latestActiveCallOriginTimeStamp > 0
+ && (currentTimestamp - callOriginTimestamp < CALL_ORIGIN_EXPIRATION_MILLIS)) {
+ if (VDBG) {
+ Log.d(LOG_TAG, "Resume previous call origin (" +
+ inCallUiState.latestActiveCallOrigin + ")");
+ }
+ // Do nothing toward call origin itself but update the timestamp just in case.
+ inCallUiState.latestActiveCallOriginTimeStamp = currentTimestamp;
+ } else {
+ if (VDBG) Log.d(LOG_TAG, "Drop previous call origin and set the current one to null");
+ setLatestActiveCallOrigin(null);
+ }
+ }
+
+ /**
+ * @return Intent which will be used when in-call UI is shown and the phone call is hang up.
+ * By default CallLog screen will be introduced, but the destination may change depending on
+ * its latest call origin state.
+ */
+ public Intent createPhoneEndIntentUsingCallOrigin() {
+ if (TextUtils.equals(inCallUiState.latestActiveCallOrigin, ALLOWED_EXTRA_CALL_ORIGIN)) {
+ if (VDBG) Log.d(LOG_TAG, "Valid latestActiveCallOrigin("
+ + inCallUiState.latestActiveCallOrigin + ") was found. "
+ + "Go back to the previous screen.");
+ // Right now we just launch the Activity which launched in-call UI. Note that we're
+ // assuming the origin is from "com.android.dialer", which may be incorrect in the
+ // future.
+ final Intent intent = new Intent();
+ intent.setClassName(DEFAULT_CALL_ORIGIN_PACKAGE, inCallUiState.latestActiveCallOrigin);
+ return intent;
+ } else {
+ if (VDBG) Log.d(LOG_TAG, "Current latestActiveCallOrigin ("
+ + inCallUiState.latestActiveCallOrigin + ") is not valid. "
+ + "Just use CallLog as a default destination.");
+ return PhoneGlobals.createCallLogIntent();
+ }
+ }
+
+ /** Service connection */
+ private final ServiceConnection mBluetoothPhoneConnection = new ServiceConnection() {
+
+ /** Handle the task of binding the local object to the service */
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ Log.i(LOG_TAG, "Headset phone created, binding local service.");
+ mBluetoothPhone = IBluetoothHeadsetPhone.Stub.asInterface(service);
+ }
+
+ /** Handle the task of cleaning up the local binding */
+ public void onServiceDisconnected(ComponentName className) {
+ Log.i(LOG_TAG, "Headset phone disconnected, cleaning local binding.");
+ mBluetoothPhone = null;
+ }
+ };
+}
diff --git a/src/com/android/phone/PhoneInterfaceManager.java b/src/com/android/phone/PhoneInterfaceManager.java
new file mode 100644
index 0000000..6600ae5
--- /dev/null
+++ b/src/com/android/phone/PhoneInterfaceManager.java
@@ -0,0 +1,884 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.ActivityManager;
+import android.app.AppOpsManager;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+import android.telephony.NeighboringCellInfo;
+import android.telephony.CellInfo;
+import android.telephony.ServiceState;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.telephony.DefaultPhoneNotifier;
+import com.android.internal.telephony.IccCard;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.PhoneConstants;
+
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * Implementation of the ITelephony interface.
+ */
+public class PhoneInterfaceManager extends ITelephony.Stub {
+ private static final String LOG_TAG = "PhoneInterfaceManager";
+ private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+ private static final boolean DBG_LOC = false;
+
+ // Message codes used with mMainThreadHandler
+ private static final int CMD_HANDLE_PIN_MMI = 1;
+ private static final int CMD_HANDLE_NEIGHBORING_CELL = 2;
+ private static final int EVENT_NEIGHBORING_CELL_DONE = 3;
+ private static final int CMD_ANSWER_RINGING_CALL = 4;
+ private static final int CMD_END_CALL = 5; // not used yet
+ private static final int CMD_SILENCE_RINGER = 6;
+
+ /** The singleton instance. */
+ private static PhoneInterfaceManager sInstance;
+
+ PhoneGlobals mApp;
+ Phone mPhone;
+ CallManager mCM;
+ AppOpsManager mAppOps;
+ MainThreadHandler mMainThreadHandler;
+
+ /**
+ * A request object for use with {@link MainThreadHandler}. Requesters should wait() on the
+ * request after sending. The main thread will notify the request when it is complete.
+ */
+ private static final class MainThreadRequest {
+ /** The argument to use for the request */
+ public Object argument;
+ /** The result of the request that is run on the main thread */
+ public Object result;
+
+ public MainThreadRequest(Object argument) {
+ this.argument = argument;
+ }
+ }
+
+ /**
+ * A handler that processes messages on the main thread in the phone process. Since many
+ * of the Phone calls are not thread safe this is needed to shuttle the requests from the
+ * inbound binder threads to the main thread in the phone process. The Binder thread
+ * may provide a {@link MainThreadRequest} object in the msg.obj field that they are waiting
+ * on, which will be notified when the operation completes and will contain the result of the
+ * request.
+ *
+ * <p>If a MainThreadRequest object is provided in the msg.obj field,
+ * note that request.result must be set to something non-null for the calling thread to
+ * unblock.
+ */
+ private final class MainThreadHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ MainThreadRequest request;
+ Message onCompleted;
+ AsyncResult ar;
+
+ switch (msg.what) {
+ case CMD_HANDLE_PIN_MMI:
+ request = (MainThreadRequest) msg.obj;
+ request.result = Boolean.valueOf(
+ mPhone.handlePinMmi((String) request.argument));
+ // Wake up the requesting thread
+ synchronized (request) {
+ request.notifyAll();
+ }
+ break;
+
+ case CMD_HANDLE_NEIGHBORING_CELL:
+ request = (MainThreadRequest) msg.obj;
+ onCompleted = obtainMessage(EVENT_NEIGHBORING_CELL_DONE,
+ request);
+ mPhone.getNeighboringCids(onCompleted);
+ break;
+
+ case EVENT_NEIGHBORING_CELL_DONE:
+ ar = (AsyncResult) msg.obj;
+ request = (MainThreadRequest) ar.userObj;
+ if (ar.exception == null && ar.result != null) {
+ request.result = ar.result;
+ } else {
+ // create an empty list to notify the waiting thread
+ request.result = new ArrayList<NeighboringCellInfo>();
+ }
+ // Wake up the requesting thread
+ synchronized (request) {
+ request.notifyAll();
+ }
+ break;
+
+ case CMD_ANSWER_RINGING_CALL:
+ answerRingingCallInternal();
+ break;
+
+ case CMD_SILENCE_RINGER:
+ silenceRingerInternal();
+ break;
+
+ case CMD_END_CALL:
+ request = (MainThreadRequest) msg.obj;
+ boolean hungUp = false;
+ int phoneType = mPhone.getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ // CDMA: If the user presses the Power button we treat it as
+ // ending the complete call session
+ hungUp = PhoneUtils.hangupRingingAndActive(mPhone);
+ } else if (phoneType == PhoneConstants.PHONE_TYPE_GSM) {
+ // GSM: End the call as per the Phone state
+ hungUp = PhoneUtils.hangup(mCM);
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ if (DBG) log("CMD_END_CALL: " + (hungUp ? "hung up!" : "no call to hang up"));
+ request.result = hungUp;
+ // Wake up the requesting thread
+ synchronized (request) {
+ request.notifyAll();
+ }
+ break;
+
+ default:
+ Log.w(LOG_TAG, "MainThreadHandler: unexpected message code: " + msg.what);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Posts the specified command to be executed on the main thread,
+ * waits for the request to complete, and returns the result.
+ * @see #sendRequestAsync
+ */
+ private Object sendRequest(int command, Object argument) {
+ if (Looper.myLooper() == mMainThreadHandler.getLooper()) {
+ throw new RuntimeException("This method will deadlock if called from the main thread.");
+ }
+
+ MainThreadRequest request = new MainThreadRequest(argument);
+ Message msg = mMainThreadHandler.obtainMessage(command, request);
+ msg.sendToTarget();
+
+ // Wait for the request to complete
+ synchronized (request) {
+ while (request.result == null) {
+ try {
+ request.wait();
+ } catch (InterruptedException e) {
+ // Do nothing, go back and wait until the request is complete
+ }
+ }
+ }
+ return request.result;
+ }
+
+ /**
+ * Asynchronous ("fire and forget") version of sendRequest():
+ * Posts the specified command to be executed on the main thread, and
+ * returns immediately.
+ * @see #sendRequest
+ */
+ private void sendRequestAsync(int command) {
+ mMainThreadHandler.sendEmptyMessage(command);
+ }
+
+ /**
+ * Initialize the singleton PhoneInterfaceManager instance.
+ * This is only done once, at startup, from PhoneApp.onCreate().
+ */
+ /* package */ static PhoneInterfaceManager init(PhoneGlobals app, Phone phone) {
+ synchronized (PhoneInterfaceManager.class) {
+ if (sInstance == null) {
+ sInstance = new PhoneInterfaceManager(app, phone);
+ } else {
+ Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance);
+ }
+ return sInstance;
+ }
+ }
+
+ /** Private constructor; @see init() */
+ private PhoneInterfaceManager(PhoneGlobals app, Phone phone) {
+ mApp = app;
+ mPhone = phone;
+ mCM = PhoneGlobals.getInstance().mCM;
+ mAppOps = (AppOpsManager)app.getSystemService(Context.APP_OPS_SERVICE);
+ mMainThreadHandler = new MainThreadHandler();
+ publish();
+ }
+
+ private void publish() {
+ if (DBG) log("publish: " + this);
+
+ ServiceManager.addService("phone", this);
+ }
+
+ //
+ // Implementation of the ITelephony interface.
+ //
+
+ public void dial(String number) {
+ if (DBG) log("dial: " + number);
+ // No permission check needed here: This is just a wrapper around the
+ // ACTION_DIAL intent, which is available to any app since it puts up
+ // the UI before it does anything.
+
+ String url = createTelUrl(number);
+ if (url == null) {
+ return;
+ }
+
+ // PENDING: should we just silently fail if phone is offhook or ringing?
+ PhoneConstants.State state = mCM.getState();
+ if (state != PhoneConstants.State.OFFHOOK && state != PhoneConstants.State.RINGING) {
+ Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(url));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mApp.startActivity(intent);
+ }
+ }
+
+ public void call(String callingPackage, String number) {
+ if (DBG) log("call: " + number);
+
+ // This is just a wrapper around the ACTION_CALL intent, but we still
+ // need to do a permission check since we're calling startActivity()
+ // from the context of the phone app.
+ enforceCallPermission();
+
+ if (mAppOps.noteOp(AppOpsManager.OP_CALL_PHONE, Binder.getCallingUid(), callingPackage)
+ != AppOpsManager.MODE_ALLOWED) {
+ return;
+ }
+
+ String url = createTelUrl(number);
+ if (url == null) {
+ return;
+ }
+
+ Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse(url));
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ mApp.startActivity(intent);
+ }
+
+ private boolean showCallScreenInternal(boolean specifyInitialDialpadState,
+ boolean initialDialpadState) {
+ if (!PhoneGlobals.sVoiceCapable) {
+ // Never allow the InCallScreen to appear on data-only devices.
+ return false;
+ }
+ if (isIdle()) {
+ return false;
+ }
+ // If the phone isn't idle then go to the in-call screen
+ long callingId = Binder.clearCallingIdentity();
+ try {
+ Intent intent;
+ if (specifyInitialDialpadState) {
+ intent = PhoneGlobals.createInCallIntent(initialDialpadState);
+ } else {
+ intent = PhoneGlobals.createInCallIntent();
+ }
+ try {
+ mApp.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ // It's possible that the in-call UI might not exist
+ // (like on non-voice-capable devices), although we
+ // shouldn't be trying to bring up the InCallScreen on
+ // devices like that in the first place!
+ Log.w(LOG_TAG, "showCallScreenInternal: "
+ + "transition to InCallScreen failed; intent = " + intent);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(callingId);
+ }
+ return true;
+ }
+
+ // Show the in-call screen without specifying the initial dialpad state.
+ public boolean showCallScreen() {
+ return showCallScreenInternal(false, false);
+ }
+
+ // The variation of showCallScreen() that specifies the initial dialpad state.
+ // (Ideally this would be called showCallScreen() too, just with a different
+ // signature, but AIDL doesn't allow that.)
+ public boolean showCallScreenWithDialpad(boolean showDialpad) {
+ return showCallScreenInternal(true, showDialpad);
+ }
+
+ /**
+ * End a call based on call state
+ * @return true is a call was ended
+ */
+ public boolean endCall() {
+ enforceCallPermission();
+ return (Boolean) sendRequest(CMD_END_CALL, null);
+ }
+
+ public void answerRingingCall() {
+ if (DBG) log("answerRingingCall...");
+ // TODO: there should eventually be a separate "ANSWER_PHONE" permission,
+ // but that can probably wait till the big TelephonyManager API overhaul.
+ // For now, protect this call with the MODIFY_PHONE_STATE permission.
+ enforceModifyPermission();
+ sendRequestAsync(CMD_ANSWER_RINGING_CALL);
+ }
+
+ /**
+ * Make the actual telephony calls to implement answerRingingCall().
+ * This should only be called from the main thread of the Phone app.
+ * @see #answerRingingCall
+ *
+ * TODO: it would be nice to return true if we answered the call, or
+ * false if there wasn't actually a ringing incoming call, or some
+ * other error occurred. (In other words, pass back the return value
+ * from PhoneUtils.answerCall() or PhoneUtils.answerAndEndActive().)
+ * But that would require calling this method via sendRequest() rather
+ * than sendRequestAsync(), and right now we don't actually *need* that
+ * return value, so let's just return void for now.
+ */
+ private void answerRingingCallInternal() {
+ final boolean hasRingingCall = !mPhone.getRingingCall().isIdle();
+ if (hasRingingCall) {
+ final boolean hasActiveCall = !mPhone.getForegroundCall().isIdle();
+ final boolean hasHoldingCall = !mPhone.getBackgroundCall().isIdle();
+ if (hasActiveCall && hasHoldingCall) {
+ // Both lines are in use!
+ // TODO: provide a flag to let the caller specify what
+ // policy to use if both lines are in use. (The current
+ // behavior is hardwired to "answer incoming, end ongoing",
+ // which is how the CALL button is specced to behave.)
+ PhoneUtils.answerAndEndActive(mCM, mCM.getFirstActiveRingingCall());
+ return;
+ } else {
+ // answerCall() will automatically hold the current active
+ // call, if there is one.
+ PhoneUtils.answerCall(mCM.getFirstActiveRingingCall());
+ return;
+ }
+ } else {
+ // No call was ringing.
+ return;
+ }
+ }
+
+ public void silenceRinger() {
+ if (DBG) log("silenceRinger...");
+ // TODO: find a more appropriate permission to check here.
+ // (That can probably wait till the big TelephonyManager API overhaul.
+ // For now, protect this call with the MODIFY_PHONE_STATE permission.)
+ enforceModifyPermission();
+ sendRequestAsync(CMD_SILENCE_RINGER);
+ }
+
+ /**
+ * Internal implemenation of silenceRinger().
+ * This should only be called from the main thread of the Phone app.
+ * @see #silenceRinger
+ */
+ private void silenceRingerInternal() {
+ if ((mCM.getState() == PhoneConstants.State.RINGING)
+ && mApp.notifier.isRinging()) {
+ // Ringer is actually playing, so silence it.
+ if (DBG) log("silenceRingerInternal: silencing...");
+ mApp.notifier.silenceRinger();
+ }
+ }
+
+ public boolean isOffhook() {
+ return (mCM.getState() == PhoneConstants.State.OFFHOOK);
+ }
+
+ public boolean isRinging() {
+ return (mCM.getState() == PhoneConstants.State.RINGING);
+ }
+
+ public boolean isIdle() {
+ return (mCM.getState() == PhoneConstants.State.IDLE);
+ }
+
+ public boolean isSimPinEnabled() {
+ enforceReadPermission();
+ return (PhoneGlobals.getInstance().isSimPinEnabled());
+ }
+
+ public boolean supplyPin(String pin) {
+ enforceModifyPermission();
+ final UnlockSim checkSimPin = new UnlockSim(mPhone.getIccCard());
+ checkSimPin.start();
+ return checkSimPin.unlockSim(null, pin);
+ }
+
+ public boolean supplyPuk(String puk, String pin) {
+ enforceModifyPermission();
+ final UnlockSim checkSimPuk = new UnlockSim(mPhone.getIccCard());
+ checkSimPuk.start();
+ return checkSimPuk.unlockSim(puk, pin);
+ }
+
+ /**
+ * Helper thread to turn async call to {@link SimCard#supplyPin} into
+ * a synchronous one.
+ */
+ private static class UnlockSim extends Thread {
+
+ private final IccCard mSimCard;
+
+ private boolean mDone = false;
+ private boolean mResult = false;
+
+ // For replies from SimCard interface
+ private Handler mHandler;
+
+ // For async handler to identify request type
+ private static final int SUPPLY_PIN_COMPLETE = 100;
+
+ public UnlockSim(IccCard simCard) {
+ mSimCard = simCard;
+ }
+
+ @Override
+ public void run() {
+ Looper.prepare();
+ synchronized (UnlockSim.this) {
+ mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+ switch (msg.what) {
+ case SUPPLY_PIN_COMPLETE:
+ Log.d(LOG_TAG, "SUPPLY_PIN_COMPLETE");
+ synchronized (UnlockSim.this) {
+ mResult = (ar.exception == null);
+ mDone = true;
+ UnlockSim.this.notifyAll();
+ }
+ break;
+ }
+ }
+ };
+ UnlockSim.this.notifyAll();
+ }
+ Looper.loop();
+ }
+
+ /*
+ * Use PIN or PUK to unlock SIM card
+ *
+ * If PUK is null, unlock SIM card with PIN
+ *
+ * If PUK is not null, unlock SIM card with PUK and set PIN code
+ */
+ synchronized boolean unlockSim(String puk, String pin) {
+
+ while (mHandler == null) {
+ try {
+ wait();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ Message callback = Message.obtain(mHandler, SUPPLY_PIN_COMPLETE);
+
+ if (puk == null) {
+ mSimCard.supplyPin(pin, callback);
+ } else {
+ mSimCard.supplyPuk(puk, pin, callback);
+ }
+
+ while (!mDone) {
+ try {
+ Log.d(LOG_TAG, "wait for done");
+ wait();
+ } catch (InterruptedException e) {
+ // Restore the interrupted status
+ Thread.currentThread().interrupt();
+ }
+ }
+ Log.d(LOG_TAG, "done");
+ return mResult;
+ }
+ }
+
+ public void updateServiceLocation() {
+ // No permission check needed here: this call is harmless, and it's
+ // needed for the ServiceState.requestStateUpdate() call (which is
+ // already intentionally exposed to 3rd parties.)
+ mPhone.updateServiceLocation();
+ }
+
+ public boolean isRadioOn() {
+ return mPhone.getServiceState().getVoiceRegState() != ServiceState.STATE_POWER_OFF;
+ }
+
+ public void toggleRadioOnOff() {
+ enforceModifyPermission();
+ mPhone.setRadioPower(!isRadioOn());
+ }
+ public boolean setRadio(boolean turnOn) {
+ enforceModifyPermission();
+ if ((mPhone.getServiceState().getVoiceRegState() != ServiceState.STATE_POWER_OFF) != turnOn) {
+ toggleRadioOnOff();
+ }
+ return true;
+ }
+ public boolean setRadioPower(boolean turnOn) {
+ enforceModifyPermission();
+ mPhone.setRadioPower(turnOn);
+ return true;
+ }
+
+ public boolean enableDataConnectivity() {
+ enforceModifyPermission();
+ ConnectivityManager cm =
+ (ConnectivityManager)mApp.getSystemService(Context.CONNECTIVITY_SERVICE);
+ cm.setMobileDataEnabled(true);
+ return true;
+ }
+
+ public int enableApnType(String type) {
+ enforceModifyPermission();
+ return mPhone.enableApnType(type);
+ }
+
+ public int disableApnType(String type) {
+ enforceModifyPermission();
+ return mPhone.disableApnType(type);
+ }
+
+ public boolean disableDataConnectivity() {
+ enforceModifyPermission();
+ ConnectivityManager cm =
+ (ConnectivityManager)mApp.getSystemService(Context.CONNECTIVITY_SERVICE);
+ cm.setMobileDataEnabled(false);
+ return true;
+ }
+
+ public boolean isDataConnectivityPossible() {
+ return mPhone.isDataConnectivityPossible();
+ }
+
+ public boolean handlePinMmi(String dialString) {
+ enforceModifyPermission();
+ return (Boolean) sendRequest(CMD_HANDLE_PIN_MMI, dialString);
+ }
+
+ public void cancelMissedCallsNotification() {
+ enforceModifyPermission();
+ mApp.notificationMgr.cancelMissedCallNotification();
+ }
+
+ public int getCallState() {
+ return DefaultPhoneNotifier.convertCallState(mCM.getState());
+ }
+
+ public int getDataState() {
+ return DefaultPhoneNotifier.convertDataState(mPhone.getDataConnectionState());
+ }
+
+ public int getDataActivity() {
+ return DefaultPhoneNotifier.convertDataActivityState(mPhone.getDataActivityState());
+ }
+
+ @Override
+ public Bundle getCellLocation() {
+ try {
+ mApp.enforceCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_FINE_LOCATION, null);
+ } catch (SecurityException e) {
+ // If we have ACCESS_FINE_LOCATION permission, skip the check for ACCESS_COARSE_LOCATION
+ // A failure should throw the SecurityException from ACCESS_COARSE_LOCATION since this
+ // is the weaker precondition
+ mApp.enforceCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_COARSE_LOCATION, null);
+ }
+
+ if (checkIfCallerIsSelfOrForegoundUser()) {
+ if (DBG_LOC) log("getCellLocation: is active user");
+ Bundle data = new Bundle();
+ mPhone.getCellLocation().fillInNotifierBundle(data);
+ return data;
+ } else {
+ if (DBG_LOC) log("getCellLocation: suppress non-active user");
+ return null;
+ }
+ }
+
+ @Override
+ public void enableLocationUpdates() {
+ mApp.enforceCallingOrSelfPermission(
+ android.Manifest.permission.CONTROL_LOCATION_UPDATES, null);
+ mPhone.enableLocationUpdates();
+ }
+
+ @Override
+ public void disableLocationUpdates() {
+ mApp.enforceCallingOrSelfPermission(
+ android.Manifest.permission.CONTROL_LOCATION_UPDATES, null);
+ mPhone.disableLocationUpdates();
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public List<NeighboringCellInfo> getNeighboringCellInfo(String callingPackage) {
+ try {
+ mApp.enforceCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_FINE_LOCATION, null);
+ } catch (SecurityException e) {
+ // If we have ACCESS_FINE_LOCATION permission, skip the check
+ // for ACCESS_COARSE_LOCATION
+ // A failure should throw the SecurityException from
+ // ACCESS_COARSE_LOCATION since this is the weaker precondition
+ mApp.enforceCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_COARSE_LOCATION, null);
+ }
+
+ if (mAppOps.noteOp(AppOpsManager.OP_NEIGHBORING_CELLS, Binder.getCallingUid(),
+ callingPackage) != AppOpsManager.MODE_ALLOWED) {
+ return null;
+ }
+ if (checkIfCallerIsSelfOrForegoundUser()) {
+ if (DBG_LOC) log("getNeighboringCellInfo: is active user");
+
+ ArrayList<NeighboringCellInfo> cells = null;
+
+ try {
+ cells = (ArrayList<NeighboringCellInfo>) sendRequest(
+ CMD_HANDLE_NEIGHBORING_CELL, null);
+ } catch (RuntimeException e) {
+ Log.e(LOG_TAG, "getNeighboringCellInfo " + e);
+ }
+ return cells;
+ } else {
+ if (DBG_LOC) log("getNeighboringCellInfo: suppress non-active user");
+ return null;
+ }
+ }
+
+
+ @Override
+ public List<CellInfo> getAllCellInfo() {
+ try {
+ mApp.enforceCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_FINE_LOCATION, null);
+ } catch (SecurityException e) {
+ // If we have ACCESS_FINE_LOCATION permission, skip the check for ACCESS_COARSE_LOCATION
+ // A failure should throw the SecurityException from ACCESS_COARSE_LOCATION since this
+ // is the weaker precondition
+ mApp.enforceCallingOrSelfPermission(
+ android.Manifest.permission.ACCESS_COARSE_LOCATION, null);
+ }
+
+ if (checkIfCallerIsSelfOrForegoundUser()) {
+ if (DBG_LOC) log("getAllCellInfo: is active user");
+ return mPhone.getAllCellInfo();
+ } else {
+ if (DBG_LOC) log("getAllCellInfo: suppress non-active user");
+ return null;
+ }
+ }
+
+ public void setCellInfoListRate(int rateInMillis) {
+ mPhone.setCellInfoListRate(rateInMillis);
+ }
+
+ //
+ // Internal helper methods.
+ //
+
+ private boolean checkIfCallerIsSelfOrForegoundUser() {
+ boolean ok;
+
+ boolean self = Binder.getCallingUid() == Process.myUid();
+ if (!self) {
+ // Get the caller's user id then clear the calling identity
+ // which will be restored in the finally clause.
+ int callingUser = UserHandle.getCallingUserId();
+ long ident = Binder.clearCallingIdentity();
+
+ try {
+ // With calling identity cleared the current user is the foreground user.
+ int foregroundUser = ActivityManager.getCurrentUser();
+ ok = (foregroundUser == callingUser);
+ if (DBG_LOC) {
+ log("checkIfCallerIsSelfOrForegoundUser: foregroundUser=" + foregroundUser
+ + " callingUser=" + callingUser + " ok=" + ok);
+ }
+ } catch (Exception ex) {
+ if (DBG_LOC) loge("checkIfCallerIsSelfOrForegoundUser: Exception ex=" + ex);
+ ok = false;
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ } else {
+ if (DBG_LOC) log("checkIfCallerIsSelfOrForegoundUser: is self");
+ ok = true;
+ }
+ if (DBG_LOC) log("checkIfCallerIsSelfOrForegoundUser: ret=" + ok);
+ return ok;
+ }
+
+ /**
+ * Make sure the caller has the READ_PHONE_STATE permission.
+ *
+ * @throws SecurityException if the caller does not have the required permission
+ */
+ private void enforceReadPermission() {
+ mApp.enforceCallingOrSelfPermission(android.Manifest.permission.READ_PHONE_STATE, null);
+ }
+
+ /**
+ * Make sure the caller has the MODIFY_PHONE_STATE permission.
+ *
+ * @throws SecurityException if the caller does not have the required permission
+ */
+ private void enforceModifyPermission() {
+ mApp.enforceCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE, null);
+ }
+
+ /**
+ * Make sure the caller has the CALL_PHONE permission.
+ *
+ * @throws SecurityException if the caller does not have the required permission
+ */
+ private void enforceCallPermission() {
+ mApp.enforceCallingOrSelfPermission(android.Manifest.permission.CALL_PHONE, null);
+ }
+
+
+ private String createTelUrl(String number) {
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ StringBuilder buf = new StringBuilder("tel:");
+ buf.append(number);
+ return buf.toString();
+ }
+
+ private void log(String msg) {
+ Log.d(LOG_TAG, "[PhoneIntfMgr] " + msg);
+ }
+
+ private void loge(String msg) {
+ Log.e(LOG_TAG, "[PhoneIntfMgr] " + msg);
+ }
+
+ public int getActivePhoneType() {
+ return mPhone.getPhoneType();
+ }
+
+ /**
+ * Returns the CDMA ERI icon index to display
+ */
+ public int getCdmaEriIconIndex() {
+ return mPhone.getCdmaEriIconIndex();
+ }
+
+ /**
+ * Returns the CDMA ERI icon mode,
+ * 0 - ON
+ * 1 - FLASHING
+ */
+ public int getCdmaEriIconMode() {
+ return mPhone.getCdmaEriIconMode();
+ }
+
+ /**
+ * Returns the CDMA ERI text,
+ */
+ public String getCdmaEriText() {
+ return mPhone.getCdmaEriText();
+ }
+
+ /**
+ * Returns true if CDMA provisioning needs to run.
+ */
+ public boolean needsOtaServiceProvisioning() {
+ return mPhone.needsOtaServiceProvisioning();
+ }
+
+ /**
+ * Returns the unread count of voicemails
+ */
+ public int getVoiceMessageCount() {
+ return mPhone.getVoiceMessageCount();
+ }
+
+ /**
+ * Returns the data network type
+ *
+ * @Deprecated to be removed Q3 2013 use {@link #getDataNetworkType}.
+ */
+ @Override
+ public int getNetworkType() {
+ return mPhone.getServiceState().getDataNetworkType();
+ }
+
+ /**
+ * Returns the data network type
+ */
+ @Override
+ public int getDataNetworkType() {
+ return mPhone.getServiceState().getDataNetworkType();
+ }
+
+ /**
+ * Returns the data network type
+ */
+ @Override
+ public int getVoiceNetworkType() {
+ return mPhone.getServiceState().getVoiceNetworkType();
+ }
+
+ /**
+ * @return true if a ICC card is present
+ */
+ public boolean hasIccCard() {
+ return mPhone.getIccCard().hasIccCard();
+ }
+
+ /**
+ * Return if the current radio is LTE on CDMA. This
+ * is a tri-state return value as for a period of time
+ * the mode may be unknown.
+ *
+ * @return {@link Phone#LTE_ON_CDMA_UNKNOWN}, {@link Phone#LTE_ON_CDMA_FALSE}
+ * or {@link PHone#LTE_ON_CDMA_TRUE}
+ */
+ public int getLteOnCdmaMode() {
+ return mPhone.getLteOnCdmaMode();
+ }
+}
diff --git a/src/com/android/phone/PhoneUtils.java b/src/com/android/phone/PhoneUtils.java
new file mode 100644
index 0000000..4334962
--- /dev/null
+++ b/src/com/android/phone/PhoneUtils.java
@@ -0,0 +1,2717 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.bluetooth.IBluetoothHeadsetPhone;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.net.Uri;
+import android.net.sip.SipManager;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.EditText;
+import android.widget.Toast;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.CallStateException;
+import com.android.internal.telephony.CallerInfo;
+import com.android.internal.telephony.CallerInfoAsyncQuery;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.MmiCode;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.TelephonyCapabilities;
+import com.android.internal.telephony.TelephonyProperties;
+import com.android.internal.telephony.cdma.CdmaConnection;
+import com.android.internal.telephony.sip.SipPhone;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Hashtable;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * Misc utilities for the Phone app.
+ */
+public class PhoneUtils {
+ private static final String LOG_TAG = "PhoneUtils";
+ private static final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ // Do not check in with VDBG = true, since that may write PII to the system log.
+ private static final boolean VDBG = false;
+
+ /** Control stack trace for Audio Mode settings */
+ private static final boolean DBG_SETAUDIOMODE_STACK = false;
+
+ /** Identifier for the "Add Call" intent extra. */
+ static final String ADD_CALL_MODE_KEY = "add_call_mode";
+
+ // Return codes from placeCall()
+ static final int CALL_STATUS_DIALED = 0; // The number was successfully dialed
+ static final int CALL_STATUS_DIALED_MMI = 1; // The specified number was an MMI code
+ static final int CALL_STATUS_FAILED = 2; // The call failed
+
+ // State of the Phone's audio modes
+ // Each state can move to the other states, but within the state only certain
+ // transitions for AudioManager.setMode() are allowed.
+ static final int AUDIO_IDLE = 0; /** audio behaviour at phone idle */
+ static final int AUDIO_RINGING = 1; /** audio behaviour while ringing */
+ static final int AUDIO_OFFHOOK = 2; /** audio behaviour while in call. */
+
+ // USSD string length for MMI operations
+ static final int MIN_USSD_LEN = 1;
+ static final int MAX_USSD_LEN = 160;
+
+ /** Speaker state, persisting between wired headset connection events */
+ private static boolean sIsSpeakerEnabled = false;
+
+ /** Hash table to store mute (Boolean) values based upon the connection.*/
+ private static Hashtable<Connection, Boolean> sConnectionMuteTable =
+ new Hashtable<Connection, Boolean>();
+
+ /** Static handler for the connection/mute tracking */
+ private static ConnectionHandler mConnectionHandler;
+
+ /** Phone state changed event*/
+ private static final int PHONE_STATE_CHANGED = -1;
+
+ /** Define for not a special CNAP string */
+ private static final int CNAP_SPECIAL_CASE_NO = -1;
+
+ /** Noise suppression status as selected by user */
+ private static boolean sIsNoiseSuppressionEnabled = true;
+
+ /**
+ * Handler that tracks the connections and updates the value of the
+ * Mute settings for each connection as needed.
+ */
+ private static class ConnectionHandler extends Handler {
+ @Override
+ public void handleMessage(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+ switch (msg.what) {
+ case PHONE_STATE_CHANGED:
+ if (DBG) log("ConnectionHandler: updating mute state for each connection");
+
+ CallManager cm = (CallManager) ar.userObj;
+
+ // update the foreground connections, if there are new connections.
+ // Have to get all foreground calls instead of the active one
+ // because there may two foreground calls co-exist in shore period
+ // (a racing condition based on which phone changes firstly)
+ // Otherwise the connection may get deleted.
+ List<Connection> fgConnections = new ArrayList<Connection>();
+ for (Call fgCall : cm.getForegroundCalls()) {
+ if (!fgCall.isIdle()) {
+ fgConnections.addAll(fgCall.getConnections());
+ }
+ }
+ for (Connection cn : fgConnections) {
+ if (sConnectionMuteTable.get(cn) == null) {
+ sConnectionMuteTable.put(cn, Boolean.FALSE);
+ }
+ }
+
+ // mute is connection based operation, we need loop over
+ // all background calls instead of the first one to update
+ // the background connections, if there are new connections.
+ List<Connection> bgConnections = new ArrayList<Connection>();
+ for (Call bgCall : cm.getBackgroundCalls()) {
+ if (!bgCall.isIdle()) {
+ bgConnections.addAll(bgCall.getConnections());
+ }
+ }
+ for (Connection cn : bgConnections) {
+ if (sConnectionMuteTable.get(cn) == null) {
+ sConnectionMuteTable.put(cn, Boolean.FALSE);
+ }
+ }
+
+ // Check to see if there are any lingering connections here
+ // (disconnected connections), use old-school iterators to avoid
+ // concurrent modification exceptions.
+ Connection cn;
+ for (Iterator<Connection> cnlist = sConnectionMuteTable.keySet().iterator();
+ cnlist.hasNext();) {
+ cn = cnlist.next();
+ if (!fgConnections.contains(cn) && !bgConnections.contains(cn)) {
+ if (DBG) log("connection '" + cn + "' not accounted for, removing.");
+ cnlist.remove();
+ }
+ }
+
+ // Restore the mute state of the foreground call if we're not IDLE,
+ // otherwise just clear the mute state. This is really saying that
+ // as long as there is one or more connections, we should update
+ // the mute state with the earliest connection on the foreground
+ // call, and that with no connections, we should be back to a
+ // non-mute state.
+ if (cm.getState() != PhoneConstants.State.IDLE) {
+ restoreMuteState();
+ } else {
+ setMuteInternal(cm.getFgPhone(), false);
+ }
+
+ break;
+ }
+ }
+ }
+
+ /**
+ * Register the ConnectionHandler with the phone, to receive connection events
+ */
+ public static void initializeConnectionHandler(CallManager cm) {
+ if (mConnectionHandler == null) {
+ mConnectionHandler = new ConnectionHandler();
+ }
+
+ // pass over cm as user.obj
+ cm.registerForPreciseCallStateChanged(mConnectionHandler, PHONE_STATE_CHANGED, cm);
+
+ }
+
+ /** This class is never instantiated. */
+ private PhoneUtils() {
+ }
+
+ /**
+ * Answer the currently-ringing call.
+ *
+ * @return true if we answered the call, or false if there wasn't
+ * actually a ringing incoming call, or some other error occurred.
+ *
+ * @see #answerAndEndHolding(CallManager, Call)
+ * @see #answerAndEndActive(CallManager, Call)
+ */
+ /* package */ static boolean answerCall(Call ringingCall) {
+ log("answerCall(" + ringingCall + ")...");
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+ final CallNotifier notifier = app.notifier;
+
+ // If the ringer is currently ringing and/or vibrating, stop it
+ // right now (before actually answering the call.)
+ notifier.silenceRinger();
+
+ final Phone phone = ringingCall.getPhone();
+ final boolean phoneIsCdma = (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA);
+ boolean answered = false;
+ IBluetoothHeadsetPhone btPhone = null;
+
+ if (phoneIsCdma) {
+ // Stop any signalInfo tone being played when a Call waiting gets answered
+ if (ringingCall.getState() == Call.State.WAITING) {
+ notifier.stopSignalInfoTone();
+ }
+ }
+
+ if (ringingCall != null && ringingCall.isRinging()) {
+ if (DBG) log("answerCall: call state = " + ringingCall.getState());
+ try {
+ if (phoneIsCdma) {
+ if (app.cdmaPhoneCallState.getCurrentCallState()
+ == CdmaPhoneCallState.PhoneCallState.IDLE) {
+ // This is the FIRST incoming call being answered.
+ // Set the Phone Call State to SINGLE_ACTIVE
+ app.cdmaPhoneCallState.setCurrentCallState(
+ CdmaPhoneCallState.PhoneCallState.SINGLE_ACTIVE);
+ } else {
+ // This is the CALL WAITING call being answered.
+ // Set the Phone Call State to CONF_CALL
+ app.cdmaPhoneCallState.setCurrentCallState(
+ CdmaPhoneCallState.PhoneCallState.CONF_CALL);
+ // Enable "Add Call" option after answering a Call Waiting as the user
+ // should be allowed to add another call in case one of the parties
+ // drops off
+ app.cdmaPhoneCallState.setAddCallMenuStateAfterCallWaiting(true);
+
+ // If a BluetoothPhoneService is valid we need to set the second call state
+ // so that the Bluetooth client can update the Call state correctly when
+ // a call waiting is answered from the Phone.
+ btPhone = app.getBluetoothPhoneService();
+ if (btPhone != null) {
+ try {
+ btPhone.cdmaSetSecondCallState(true);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+ }
+
+ final boolean isRealIncomingCall = isRealIncomingCall(ringingCall.getState());
+
+ //if (DBG) log("sPhone.acceptCall");
+ app.mCM.acceptCall(ringingCall);
+ answered = true;
+
+ // Always reset to "unmuted" for a freshly-answered call
+ setMute(false);
+
+ setAudioMode();
+
+ // Check is phone in any dock, and turn on speaker accordingly
+ final boolean speakerActivated = activateSpeakerIfDocked(phone);
+
+ // When answering a phone call, the user will move the phone near to her/his ear
+ // and start conversation, without checking its speaker status. If some other
+ // application turned on the speaker mode before the call and didn't turn it off,
+ // Phone app would need to be responsible for the speaker phone.
+ // Here, we turn off the speaker if
+ // - the phone call is the first in-coming call,
+ // - we did not activate speaker by ourselves during the process above, and
+ // - Bluetooth headset is not in use.
+ if (isRealIncomingCall && !speakerActivated && isSpeakerOn(app)
+ && !app.isBluetoothHeadsetAudioOn()) {
+ // This is not an error but might cause users' confusion. Add log just in case.
+ Log.i(LOG_TAG, "Forcing speaker off due to new incoming call...");
+ turnOnSpeaker(app, false, true);
+ }
+ } catch (CallStateException ex) {
+ Log.w(LOG_TAG, "answerCall: caught " + ex, ex);
+
+ if (phoneIsCdma) {
+ // restore the cdmaPhoneCallState and btPhone.cdmaSetSecondCallState:
+ app.cdmaPhoneCallState.setCurrentCallState(
+ app.cdmaPhoneCallState.getPreviousCallState());
+ if (btPhone != null) {
+ try {
+ btPhone.cdmaSetSecondCallState(false);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, Log.getStackTraceString(new Throwable()));
+ }
+ }
+ }
+ }
+ }
+ return answered;
+ }
+
+ /**
+ * Smart "hang up" helper method which hangs up exactly one connection,
+ * based on the current Phone state, as follows:
+ * <ul>
+ * <li>If there's a ringing call, hang that up.
+ * <li>Else if there's a foreground call, hang that up.
+ * <li>Else if there's a background call, hang that up.
+ * <li>Otherwise do nothing.
+ * </ul>
+ * @return true if we successfully hung up, or false
+ * if there were no active calls at all.
+ */
+ static boolean hangup(CallManager cm) {
+ boolean hungup = false;
+ Call ringing = cm.getFirstActiveRingingCall();
+ Call fg = cm.getActiveFgCall();
+ Call bg = cm.getFirstActiveBgCall();
+
+ if (!ringing.isIdle()) {
+ log("hangup(): hanging up ringing call");
+ hungup = hangupRingingCall(ringing);
+ } else if (!fg.isIdle()) {
+ log("hangup(): hanging up foreground call");
+ hungup = hangup(fg);
+ } else if (!bg.isIdle()) {
+ log("hangup(): hanging up background call");
+ hungup = hangup(bg);
+ } else {
+ // No call to hang up! This is unlikely in normal usage,
+ // since the UI shouldn't be providing an "End call" button in
+ // the first place. (But it *can* happen, rarely, if an
+ // active call happens to disconnect on its own right when the
+ // user is trying to hang up..)
+ log("hangup(): no active call to hang up");
+ }
+ if (DBG) log("==> hungup = " + hungup);
+
+ return hungup;
+ }
+
+ static boolean hangupRingingCall(Call ringing) {
+ if (DBG) log("hangup ringing call");
+ int phoneType = ringing.getPhone().getPhoneType();
+ Call.State state = ringing.getState();
+
+ if (state == Call.State.INCOMING) {
+ // Regular incoming call (with no other active calls)
+ log("hangupRingingCall(): regular incoming call: hangup()");
+ return hangup(ringing);
+ } else if (state == Call.State.WAITING) {
+ // Call-waiting: there's an incoming call, but another call is
+ // already active.
+ // TODO: It would be better for the telephony layer to provide
+ // a "hangupWaitingCall()" API that works on all devices,
+ // rather than us having to check the phone type here and do
+ // the notifier.sendCdmaCallWaitingReject() hack for CDMA phones.
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ // CDMA: Ringing call and Call waiting hangup is handled differently.
+ // For Call waiting we DO NOT call the conventional hangup(call) function
+ // as in CDMA we just want to hangup the Call waiting connection.
+ log("hangupRingingCall(): CDMA-specific call-waiting hangup");
+ final CallNotifier notifier = PhoneGlobals.getInstance().notifier;
+ notifier.sendCdmaCallWaitingReject();
+ return true;
+ } else {
+ // Otherwise, the regular hangup() API works for
+ // call-waiting calls too.
+ log("hangupRingingCall(): call-waiting call: hangup()");
+ return hangup(ringing);
+ }
+ } else {
+ // Unexpected state: the ringing call isn't INCOMING or
+ // WAITING, so there's no reason to have called
+ // hangupRingingCall() in the first place.
+ // (Presumably the incoming call went away at the exact moment
+ // we got here, so just do nothing.)
+ Log.w(LOG_TAG, "hangupRingingCall: no INCOMING or WAITING call");
+ return false;
+ }
+ }
+
+ static boolean hangupActiveCall(Call foreground) {
+ if (DBG) log("hangup active call");
+ return hangup(foreground);
+ }
+
+ static boolean hangupHoldingCall(Call background) {
+ if (DBG) log("hangup holding call");
+ return hangup(background);
+ }
+
+ /**
+ * Used in CDMA phones to end the complete Call session
+ * @param phone the Phone object.
+ * @return true if *any* call was successfully hung up
+ */
+ static boolean hangupRingingAndActive(Phone phone) {
+ boolean hungUpRingingCall = false;
+ boolean hungUpFgCall = false;
+ Call ringingCall = phone.getRingingCall();
+ Call fgCall = phone.getForegroundCall();
+
+ // Hang up any Ringing Call
+ if (!ringingCall.isIdle()) {
+ log("hangupRingingAndActive: Hang up Ringing Call");
+ hungUpRingingCall = hangupRingingCall(ringingCall);
+ }
+
+ // Hang up any Active Call
+ if (!fgCall.isIdle()) {
+ log("hangupRingingAndActive: Hang up Foreground Call");
+ hungUpFgCall = hangupActiveCall(fgCall);
+ }
+
+ return hungUpRingingCall || hungUpFgCall;
+ }
+
+ /**
+ * Trivial wrapper around Call.hangup(), except that we return a
+ * boolean success code rather than throwing CallStateException on
+ * failure.
+ *
+ * @return true if the call was successfully hung up, or false
+ * if the call wasn't actually active.
+ */
+ static boolean hangup(Call call) {
+ try {
+ CallManager cm = PhoneGlobals.getInstance().mCM;
+
+ if (call.getState() == Call.State.ACTIVE && cm.hasActiveBgCall()) {
+ // handle foreground call hangup while there is background call
+ log("- hangup(Call): hangupForegroundResumeBackground...");
+ cm.hangupForegroundResumeBackground(cm.getFirstActiveBgCall());
+ } else {
+ log("- hangup(Call): regular hangup()...");
+ call.hangup();
+ }
+ return true;
+ } catch (CallStateException ex) {
+ Log.e(LOG_TAG, "Call hangup: caught " + ex, ex);
+ }
+
+ return false;
+ }
+
+ /**
+ * Trivial wrapper around Connection.hangup(), except that we silently
+ * do nothing (rather than throwing CallStateException) if the
+ * connection wasn't actually active.
+ */
+ static void hangup(Connection c) {
+ try {
+ if (c != null) {
+ c.hangup();
+ }
+ } catch (CallStateException ex) {
+ Log.w(LOG_TAG, "Connection hangup: caught " + ex, ex);
+ }
+ }
+
+ static boolean answerAndEndHolding(CallManager cm, Call ringing) {
+ if (DBG) log("end holding & answer waiting: 1");
+ if (!hangupHoldingCall(cm.getFirstActiveBgCall())) {
+ Log.e(LOG_TAG, "end holding failed!");
+ return false;
+ }
+
+ if (DBG) log("end holding & answer waiting: 2");
+ return answerCall(ringing);
+
+ }
+
+ /**
+ * Answers the incoming call specified by "ringing", and ends the currently active phone call.
+ *
+ * This method is useful when's there's an incoming call which we cannot manage with the
+ * current call. e.g. when you are having a phone call with CDMA network and has received
+ * a SIP call, then we won't expect our telephony can manage those phone calls simultaneously.
+ * Note that some types of network may allow multiple phone calls at once; GSM allows to hold
+ * an ongoing phone call, so we don't need to end the active call. The caller of this method
+ * needs to check if the network allows multiple phone calls or not.
+ *
+ * @see #answerCall(Call)
+ * @see InCallScreen#internalAnswerCall()
+ */
+ /* package */ static boolean answerAndEndActive(CallManager cm, Call ringing) {
+ if (DBG) log("answerAndEndActive()...");
+
+ // Unlike the answerCall() method, we *don't* need to stop the
+ // ringer or change audio modes here since the user is already
+ // in-call, which means that the audio mode is already set
+ // correctly, and that we wouldn't have started the ringer in the
+ // first place.
+
+ // hanging up the active call also accepts the waiting call
+ // while active call and waiting call are from the same phone
+ // i.e. both from GSM phone
+ if (!hangupActiveCall(cm.getActiveFgCall())) {
+ Log.w(LOG_TAG, "end active call failed!");
+ return false;
+ }
+
+ // since hangupActiveCall() also accepts the ringing call
+ // check if the ringing call was already answered or not
+ // only answer it when the call still is ringing
+ if (ringing.isRinging()) {
+ return answerCall(ringing);
+ }
+
+ return true;
+ }
+
+ /**
+ * For a CDMA phone, advance the call state upon making a new
+ * outgoing call.
+ *
+ * <pre>
+ * IDLE -> SINGLE_ACTIVE
+ * or
+ * SINGLE_ACTIVE -> THRWAY_ACTIVE
+ * </pre>
+ * @param app The phone instance.
+ */
+ private static void updateCdmaCallStateOnNewOutgoingCall(PhoneGlobals app) {
+ if (app.cdmaPhoneCallState.getCurrentCallState() ==
+ CdmaPhoneCallState.PhoneCallState.IDLE) {
+ // This is the first outgoing call. Set the Phone Call State to ACTIVE
+ app.cdmaPhoneCallState.setCurrentCallState(
+ CdmaPhoneCallState.PhoneCallState.SINGLE_ACTIVE);
+ } else {
+ // This is the second outgoing call. Set the Phone Call State to 3WAY
+ app.cdmaPhoneCallState.setCurrentCallState(
+ CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE);
+ }
+ }
+
+ /**
+ * Dial the number using the phone passed in.
+ *
+ * If the connection is establised, this method issues a sync call
+ * that may block to query the caller info.
+ * TODO: Change the logic to use the async query.
+ *
+ * @param context To perform the CallerInfo query.
+ * @param phone the Phone object.
+ * @param number to be dialed as requested by the user. This is
+ * NOT the phone number to connect to. It is used only to build the
+ * call card and to update the call log. See above for restrictions.
+ * @param contactRef that triggered the call. Typically a 'tel:'
+ * uri but can also be a 'content://contacts' one.
+ * @param isEmergencyCall indicates that whether or not this is an
+ * emergency call
+ * @param gatewayUri Is the address used to setup the connection, null
+ * if not using a gateway
+ *
+ * @return either CALL_STATUS_DIALED or CALL_STATUS_FAILED
+ */
+ public static int placeCall(Context context, Phone phone,
+ String number, Uri contactRef, boolean isEmergencyCall,
+ Uri gatewayUri) {
+ if (VDBG) {
+ log("placeCall()... number: '" + number + "'"
+ + ", GW:'" + gatewayUri + "'"
+ + ", contactRef:" + contactRef
+ + ", isEmergencyCall: " + isEmergencyCall);
+ } else {
+ log("placeCall()... number: " + toLogSafePhoneNumber(number)
+ + ", GW: " + (gatewayUri != null ? "non-null" : "null")
+ + ", emergency? " + isEmergencyCall);
+ }
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+
+ boolean useGateway = false;
+ if (null != gatewayUri &&
+ !isEmergencyCall &&
+ PhoneUtils.isRoutableViaGateway(number)) { // Filter out MMI, OTA and other codes.
+ useGateway = true;
+ }
+
+ int status = CALL_STATUS_DIALED;
+ Connection connection;
+ String numberToDial;
+ if (useGateway) {
+ // TODO: 'tel' should be a constant defined in framework base
+ // somewhere (it is in webkit.)
+ if (null == gatewayUri || !Constants.SCHEME_TEL.equals(gatewayUri.getScheme())) {
+ Log.e(LOG_TAG, "Unsupported URL:" + gatewayUri);
+ return CALL_STATUS_FAILED;
+ }
+
+ // We can use getSchemeSpecificPart because we don't allow #
+ // in the gateway numbers (treated a fragment delim.) However
+ // if we allow more complex gateway numbers sequence (with
+ // passwords or whatnot) that use #, this may break.
+ // TODO: Need to support MMI codes.
+ numberToDial = gatewayUri.getSchemeSpecificPart();
+ } else {
+ numberToDial = number;
+ }
+
+ // Remember if the phone state was in IDLE state before this call.
+ // After calling CallManager#dial(), getState() will return different state.
+ final boolean initiallyIdle = app.mCM.getState() == PhoneConstants.State.IDLE;
+
+ try {
+ connection = app.mCM.dial(phone, numberToDial);
+ } catch (CallStateException ex) {
+ // CallStateException means a new outgoing call is not currently
+ // possible: either no more call slots exist, or there's another
+ // call already in the process of dialing or ringing.
+ Log.w(LOG_TAG, "Exception from app.mCM.dial()", ex);
+ return CALL_STATUS_FAILED;
+
+ // Note that it's possible for CallManager.dial() to return
+ // null *without* throwing an exception; that indicates that
+ // we dialed an MMI (see below).
+ }
+
+ int phoneType = phone.getPhoneType();
+
+ // On GSM phones, null is returned for MMI codes
+ if (null == connection) {
+ if (phoneType == PhoneConstants.PHONE_TYPE_GSM && gatewayUri == null) {
+ if (DBG) log("dialed MMI code: " + number);
+ status = CALL_STATUS_DIALED_MMI;
+ } else {
+ status = CALL_STATUS_FAILED;
+ }
+ } else {
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ updateCdmaCallStateOnNewOutgoingCall(app);
+ }
+
+ // Clean up the number to be displayed.
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ number = CdmaConnection.formatDialString(number);
+ }
+ number = PhoneNumberUtils.extractNetworkPortion(number);
+ number = PhoneNumberUtils.convertKeypadLettersToDigits(number);
+ number = PhoneNumberUtils.formatNumber(number);
+
+ if (gatewayUri == null) {
+ // phone.dial() succeeded: we're now in a normal phone call.
+ // attach the URI to the CallerInfo Object if it is there,
+ // otherwise just attach the Uri Reference.
+ // if the uri does not have a "content" scheme, then we treat
+ // it as if it does NOT have a unique reference.
+ String content = context.getContentResolver().SCHEME_CONTENT;
+ if ((contactRef != null) && (contactRef.getScheme().equals(content))) {
+ Object userDataObject = connection.getUserData();
+ if (userDataObject == null) {
+ connection.setUserData(contactRef);
+ } else {
+ // TODO: This branch is dead code, we have
+ // just created the connection which has
+ // no user data (null) by default.
+ if (userDataObject instanceof CallerInfo) {
+ ((CallerInfo) userDataObject).contactRefUri = contactRef;
+ } else {
+ ((CallerInfoToken) userDataObject).currentInfo.contactRefUri =
+ contactRef;
+ }
+ }
+ }
+ } else {
+ // Get the caller info synchronously because we need the final
+ // CallerInfo object to update the dialed number with the one
+ // requested by the user (and not the provider's gateway number).
+ CallerInfo info = null;
+ String content = phone.getContext().getContentResolver().SCHEME_CONTENT;
+ if ((contactRef != null) && (contactRef.getScheme().equals(content))) {
+ info = CallerInfo.getCallerInfo(context, contactRef);
+ }
+
+ // Fallback, lookup contact using the phone number if the
+ // contact's URI scheme was not content:// or if is was but
+ // the lookup failed.
+ if (null == info) {
+ info = CallerInfo.getCallerInfo(context, number);
+ }
+ info.phoneNumber = number;
+ connection.setUserData(info);
+ }
+ setAudioMode();
+
+ if (DBG) log("about to activate speaker");
+ // Check is phone in any dock, and turn on speaker accordingly
+ final boolean speakerActivated = activateSpeakerIfDocked(phone);
+
+ // See also similar logic in answerCall().
+ if (initiallyIdle && !speakerActivated && isSpeakerOn(app)
+ && !app.isBluetoothHeadsetAudioOn()) {
+ // This is not an error but might cause users' confusion. Add log just in case.
+ Log.i(LOG_TAG, "Forcing speaker off when initiating a new outgoing call...");
+ PhoneUtils.turnOnSpeaker(app, false, true);
+ }
+ }
+
+ return status;
+ }
+
+ /* package */ static String toLogSafePhoneNumber(String number) {
+ // For unknown number, log empty string.
+ if (number == null) {
+ return "";
+ }
+
+ if (VDBG) {
+ // When VDBG is true we emit PII.
+ return number;
+ }
+
+ // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare
+ // sanitized phone numbers.
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < number.length(); i++) {
+ char c = number.charAt(i);
+ if (c == '-' || c == '@' || c == '.') {
+ builder.append(c);
+ } else {
+ builder.append('x');
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Wrapper function to control when to send an empty Flash command to the network.
+ * Mainly needed for CDMA networks, such as scenarios when we need to send a blank flash
+ * to the network prior to placing a 3-way call for it to be successful.
+ */
+ static void sendEmptyFlash(Phone phone) {
+ if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ Call fgCall = phone.getForegroundCall();
+ if (fgCall.getState() == Call.State.ACTIVE) {
+ // Send the empty flash
+ if (DBG) Log.d(LOG_TAG, "onReceive: (CDMA) sending empty flash to network");
+ switchHoldingAndActive(phone.getBackgroundCall());
+ }
+ }
+ }
+
+ /**
+ * @param heldCall is the background call want to be swapped
+ */
+ static void switchHoldingAndActive(Call heldCall) {
+ log("switchHoldingAndActive()...");
+ try {
+ CallManager cm = PhoneGlobals.getInstance().mCM;
+ if (heldCall.isIdle()) {
+ // no heldCall, so it is to hold active call
+ cm.switchHoldingAndActive(cm.getFgPhone().getBackgroundCall());
+ } else {
+ // has particular heldCall, so to switch
+ cm.switchHoldingAndActive(heldCall);
+ }
+ setAudioMode(cm);
+ } catch (CallStateException ex) {
+ Log.w(LOG_TAG, "switchHoldingAndActive: caught " + ex, ex);
+ }
+ }
+
+ /**
+ * Restore the mute setting from the earliest connection of the
+ * foreground call.
+ */
+ static Boolean restoreMuteState() {
+ Phone phone = PhoneGlobals.getInstance().mCM.getFgPhone();
+
+ //get the earliest connection
+ Connection c = phone.getForegroundCall().getEarliestConnection();
+
+ // only do this if connection is not null.
+ if (c != null) {
+
+ int phoneType = phone.getPhoneType();
+
+ // retrieve the mute value.
+ Boolean shouldMute = null;
+
+ // In CDMA, mute is not maintained per Connection. Single mute apply for
+ // a call where call can have multiple connections such as
+ // Three way and Call Waiting. Therefore retrieving Mute state for
+ // latest connection can apply for all connection in that call
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ shouldMute = sConnectionMuteTable.get(
+ phone.getForegroundCall().getLatestConnection());
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ shouldMute = sConnectionMuteTable.get(c);
+ }
+ if (shouldMute == null) {
+ if (DBG) log("problem retrieving mute value for this connection.");
+ shouldMute = Boolean.FALSE;
+ }
+
+ // set the mute value and return the result.
+ setMute (shouldMute.booleanValue());
+ return shouldMute;
+ }
+ return Boolean.valueOf(getMute());
+ }
+
+ static void mergeCalls() {
+ mergeCalls(PhoneGlobals.getInstance().mCM);
+ }
+
+ static void mergeCalls(CallManager cm) {
+ int phoneType = cm.getFgPhone().getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ log("mergeCalls(): CDMA...");
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ if (app.cdmaPhoneCallState.getCurrentCallState()
+ == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE) {
+ // Set the Phone Call State to conference
+ app.cdmaPhoneCallState.setCurrentCallState(
+ CdmaPhoneCallState.PhoneCallState.CONF_CALL);
+
+ // Send flash cmd
+ // TODO: Need to change the call from switchHoldingAndActive to
+ // something meaningful as we are not actually trying to swap calls but
+ // instead are merging two calls by sending a Flash command.
+ log("- sending flash...");
+ switchHoldingAndActive(cm.getFirstActiveBgCall());
+ }
+ } else {
+ try {
+ log("mergeCalls(): calling cm.conference()...");
+ cm.conference(cm.getFirstActiveBgCall());
+ } catch (CallStateException ex) {
+ Log.w(LOG_TAG, "mergeCalls: caught " + ex, ex);
+ }
+ }
+ }
+
+ static void separateCall(Connection c) {
+ try {
+ if (DBG) log("separateCall: " + toLogSafePhoneNumber(c.getAddress()));
+ c.separate();
+ } catch (CallStateException ex) {
+ Log.w(LOG_TAG, "separateCall: caught " + ex, ex);
+ }
+ }
+
+ /**
+ * Handle the MMIInitiate message and put up an alert that lets
+ * the user cancel the operation, if applicable.
+ *
+ * @param context context to get strings.
+ * @param mmiCode the MmiCode object being started.
+ * @param buttonCallbackMessage message to post when button is clicked.
+ * @param previousAlert a previous alert used in this activity.
+ * @return the dialog handle
+ */
+ static Dialog displayMMIInitiate(Context context,
+ MmiCode mmiCode,
+ Message buttonCallbackMessage,
+ Dialog previousAlert) {
+ if (DBG) log("displayMMIInitiate: " + mmiCode);
+ if (previousAlert != null) {
+ previousAlert.dismiss();
+ }
+
+ // The UI paradigm we are using now requests that all dialogs have
+ // user interaction, and that any other messages to the user should
+ // be by way of Toasts.
+ //
+ // In adhering to this request, all MMI initiating "OK" dialogs
+ // (non-cancelable MMIs) that end up being closed when the MMI
+ // completes (thereby showing a completion dialog) are being
+ // replaced with Toasts.
+ //
+ // As a side effect, moving to Toasts for the non-cancelable MMIs
+ // also means that buttonCallbackMessage (which was tied into "OK")
+ // is no longer invokable for these dialogs. This is not a problem
+ // since the only callback messages we supported were for cancelable
+ // MMIs anyway.
+ //
+ // A cancelable MMI is really just a USSD request. The term
+ // "cancelable" here means that we can cancel the request when the
+ // system prompts us for a response, NOT while the network is
+ // processing the MMI request. Any request to cancel a USSD while
+ // the network is NOT ready for a response may be ignored.
+ //
+ // With this in mind, we replace the cancelable alert dialog with
+ // a progress dialog, displayed until we receive a request from
+ // the the network. For more information, please see the comments
+ // in the displayMMIComplete() method below.
+ //
+ // Anything that is NOT a USSD request is a normal MMI request,
+ // which will bring up a toast (desribed above).
+
+ boolean isCancelable = (mmiCode != null) && mmiCode.isCancelable();
+
+ if (!isCancelable) {
+ if (DBG) log("not a USSD code, displaying status toast.");
+ CharSequence text = context.getText(R.string.mmiStarted);
+ Toast.makeText(context, text, Toast.LENGTH_SHORT)
+ .show();
+ return null;
+ } else {
+ if (DBG) log("running USSD code, displaying indeterminate progress.");
+
+ // create the indeterminate progress dialog and display it.
+ ProgressDialog pd = new ProgressDialog(context);
+ pd.setMessage(context.getText(R.string.ussdRunning));
+ pd.setCancelable(false);
+ pd.setIndeterminate(true);
+ pd.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+
+ pd.show();
+
+ return pd;
+ }
+
+ }
+
+ /**
+ * Handle the MMIComplete message and fire off an intent to display
+ * the message.
+ *
+ * @param context context to get strings.
+ * @param mmiCode MMI result.
+ * @param previousAlert a previous alert used in this activity.
+ */
+ static void displayMMIComplete(final Phone phone, Context context, final MmiCode mmiCode,
+ Message dismissCallbackMessage,
+ AlertDialog previousAlert) {
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+ CharSequence text;
+ int title = 0; // title for the progress dialog, if needed.
+ MmiCode.State state = mmiCode.getState();
+
+ if (DBG) log("displayMMIComplete: state=" + state);
+
+ switch (state) {
+ case PENDING:
+ // USSD code asking for feedback from user.
+ text = mmiCode.getMessage();
+ if (DBG) log("- using text from PENDING MMI message: '" + text + "'");
+ break;
+ case CANCELLED:
+ text = null;
+ break;
+ case COMPLETE:
+ if (app.getPUKEntryActivity() != null) {
+ // if an attempt to unPUK the device was made, we specify
+ // the title and the message here.
+ title = com.android.internal.R.string.PinMmi;
+ text = context.getText(R.string.puk_unlocked);
+ break;
+ }
+ // All other conditions for the COMPLETE mmi state will cause
+ // the case to fall through to message logic in common with
+ // the FAILED case.
+
+ case FAILED:
+ text = mmiCode.getMessage();
+ if (DBG) log("- using text from MMI message: '" + text + "'");
+ break;
+ default:
+ throw new IllegalStateException("Unexpected MmiCode state: " + state);
+ }
+
+ if (previousAlert != null) {
+ previousAlert.dismiss();
+ }
+
+ // Check to see if a UI exists for the PUK activation. If it does
+ // exist, then it indicates that we're trying to unblock the PUK.
+ if ((app.getPUKEntryActivity() != null) && (state == MmiCode.State.COMPLETE)) {
+ if (DBG) log("displaying PUK unblocking progress dialog.");
+
+ // create the progress dialog, make sure the flags and type are
+ // set correctly.
+ ProgressDialog pd = new ProgressDialog(app);
+ pd.setTitle(title);
+ pd.setMessage(text);
+ pd.setCancelable(false);
+ pd.setIndeterminate(true);
+ pd.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
+ pd.getWindow().addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+
+ // display the dialog
+ pd.show();
+
+ // indicate to the Phone app that the progress dialog has
+ // been assigned for the PUK unlock / SIM READY process.
+ app.setPukEntryProgressDialog(pd);
+
+ } else {
+ // In case of failure to unlock, we'll need to reset the
+ // PUK unlock activity, so that the user may try again.
+ if (app.getPUKEntryActivity() != null) {
+ app.setPukEntryActivity(null);
+ }
+
+ // A USSD in a pending state means that it is still
+ // interacting with the user.
+ if (state != MmiCode.State.PENDING) {
+ if (DBG) log("MMI code has finished running.");
+
+ if (DBG) log("Extended NW displayMMIInitiate (" + text + ")");
+ if (text == null || text.length() == 0)
+ return;
+
+ // displaying system alert dialog on the screen instead of
+ // using another activity to display the message. This
+ // places the message at the forefront of the UI.
+ AlertDialog newDialog = new AlertDialog.Builder(context)
+ .setMessage(text)
+ .setPositiveButton(R.string.ok, null)
+ .setCancelable(true)
+ .create();
+
+ newDialog.getWindow().setType(
+ WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
+ newDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+
+ newDialog.show();
+ } else {
+ if (DBG) log("USSD code has requested user input. Constructing input dialog.");
+
+ // USSD MMI code that is interacting with the user. The
+ // basic set of steps is this:
+ // 1. User enters a USSD request
+ // 2. We recognize the request and displayMMIInitiate
+ // (above) creates a progress dialog.
+ // 3. Request returns and we get a PENDING or COMPLETE
+ // message.
+ // 4. These MMI messages are caught in the PhoneApp
+ // (onMMIComplete) and the InCallScreen
+ // (mHandler.handleMessage) which bring up this dialog
+ // and closes the original progress dialog,
+ // respectively.
+ // 5. If the message is anything other than PENDING,
+ // we are done, and the alert dialog (directly above)
+ // displays the outcome.
+ // 6. If the network is requesting more information from
+ // the user, the MMI will be in a PENDING state, and
+ // we display this dialog with the message.
+ // 7. User input, or cancel requests result in a return
+ // to step 1. Keep in mind that this is the only
+ // time that a USSD should be canceled.
+
+ // inflate the layout with the scrolling text area for the dialog.
+ LayoutInflater inflater = (LayoutInflater) context.getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ View dialogView = inflater.inflate(R.layout.dialog_ussd_response, null);
+
+ // get the input field.
+ final EditText inputText = (EditText) dialogView.findViewById(R.id.input_field);
+
+ // specify the dialog's click listener, with SEND and CANCEL logic.
+ final DialogInterface.OnClickListener mUSSDDialogListener =
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ switch (whichButton) {
+ case DialogInterface.BUTTON_POSITIVE:
+ // As per spec 24.080, valid length of ussd string
+ // is 1 - 160. If length is out of the range then
+ // display toast message & Cancel MMI operation.
+ if (inputText.length() < MIN_USSD_LEN
+ || inputText.length() > MAX_USSD_LEN) {
+ Toast.makeText(app,
+ app.getResources().getString(R.string.enter_input,
+ MIN_USSD_LEN, MAX_USSD_LEN),
+ Toast.LENGTH_LONG).show();
+ if (mmiCode.isCancelable()) {
+ mmiCode.cancel();
+ }
+ } else {
+ phone.sendUssdResponse(inputText.getText().toString());
+ }
+ break;
+ case DialogInterface.BUTTON_NEGATIVE:
+ if (mmiCode.isCancelable()) {
+ mmiCode.cancel();
+ }
+ break;
+ }
+ }
+ };
+
+ // build the dialog
+ final AlertDialog newDialog = new AlertDialog.Builder(context)
+ .setMessage(text)
+ .setView(dialogView)
+ .setPositiveButton(R.string.send_button, mUSSDDialogListener)
+ .setNegativeButton(R.string.cancel, mUSSDDialogListener)
+ .setCancelable(false)
+ .create();
+
+ // attach the key listener to the dialog's input field and make
+ // sure focus is set.
+ final View.OnKeyListener mUSSDDialogInputListener =
+ new View.OnKeyListener() {
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CALL:
+ case KeyEvent.KEYCODE_ENTER:
+ if(event.getAction() == KeyEvent.ACTION_DOWN) {
+ phone.sendUssdResponse(inputText.getText().toString());
+ newDialog.dismiss();
+ }
+ return true;
+ }
+ return false;
+ }
+ };
+ inputText.setOnKeyListener(mUSSDDialogInputListener);
+ inputText.requestFocus();
+
+ // set the window properties of the dialog
+ newDialog.getWindow().setType(
+ WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);
+ newDialog.getWindow().addFlags(
+ WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+
+ // now show the dialog!
+ newDialog.show();
+ }
+ }
+ }
+
+ /**
+ * Cancels the current pending MMI operation, if applicable.
+ * @return true if we canceled an MMI operation, or false
+ * if the current pending MMI wasn't cancelable
+ * or if there was no current pending MMI at all.
+ *
+ * @see displayMMIInitiate
+ */
+ static boolean cancelMmiCode(Phone phone) {
+ List<? extends MmiCode> pendingMmis = phone.getPendingMmiCodes();
+ int count = pendingMmis.size();
+ if (DBG) log("cancelMmiCode: num pending MMIs = " + count);
+
+ boolean canceled = false;
+ if (count > 0) {
+ // assume that we only have one pending MMI operation active at a time.
+ // I don't think it's possible to enter multiple MMI codes concurrently
+ // in the phone UI, because during the MMI operation, an Alert panel
+ // is displayed, which prevents more MMI code from being entered.
+ MmiCode mmiCode = pendingMmis.get(0);
+ if (mmiCode.isCancelable()) {
+ mmiCode.cancel();
+ canceled = true;
+ }
+ }
+ return canceled;
+ }
+
+ public static class VoiceMailNumberMissingException extends Exception {
+ VoiceMailNumberMissingException() {
+ super();
+ }
+
+ VoiceMailNumberMissingException(String msg) {
+ super(msg);
+ }
+ }
+
+ /**
+ * Given an Intent (which is presumably the ACTION_CALL intent that
+ * initiated this outgoing call), figure out the actual phone number we
+ * should dial.
+ *
+ * Note that the returned "number" may actually be a SIP address,
+ * if the specified intent contains a sip: URI.
+ *
+ * This method is basically a wrapper around PhoneUtils.getNumberFromIntent(),
+ * except it's also aware of the EXTRA_ACTUAL_NUMBER_TO_DIAL extra.
+ * (That extra, if present, tells us the exact string to pass down to the
+ * telephony layer. It's guaranteed to be safe to dial: it's either a PSTN
+ * phone number with separators and keypad letters stripped out, or a raw
+ * unencoded SIP address.)
+ *
+ * @return the phone number corresponding to the specified Intent, or null
+ * if the Intent has no action or if the intent's data is malformed or
+ * missing.
+ *
+ * @throws VoiceMailNumberMissingException if the intent
+ * contains a "voicemail" URI, but there's no voicemail
+ * number configured on the device.
+ */
+ public static String getInitialNumber(Intent intent)
+ throws PhoneUtils.VoiceMailNumberMissingException {
+ if (DBG) log("getInitialNumber(): " + intent);
+
+ String action = intent.getAction();
+ if (TextUtils.isEmpty(action)) {
+ return null;
+ }
+
+ // If the EXTRA_ACTUAL_NUMBER_TO_DIAL extra is present, get the phone
+ // number from there. (That extra takes precedence over the actual data
+ // included in the intent.)
+ if (intent.hasExtra(OutgoingCallBroadcaster.EXTRA_ACTUAL_NUMBER_TO_DIAL)) {
+ String actualNumberToDial =
+ intent.getStringExtra(OutgoingCallBroadcaster.EXTRA_ACTUAL_NUMBER_TO_DIAL);
+ if (DBG) {
+ log("==> got EXTRA_ACTUAL_NUMBER_TO_DIAL; returning '"
+ + toLogSafePhoneNumber(actualNumberToDial) + "'");
+ }
+ return actualNumberToDial;
+ }
+
+ return getNumberFromIntent(PhoneGlobals.getInstance(), intent);
+ }
+
+ /**
+ * Gets the phone number to be called from an intent. Requires a Context
+ * to access the contacts database, and a Phone to access the voicemail
+ * number.
+ *
+ * <p>If <code>phone</code> is <code>null</code>, the function will return
+ * <code>null</code> for <code>voicemail:</code> URIs;
+ * if <code>context</code> is <code>null</code>, the function will return
+ * <code>null</code> for person/phone URIs.</p>
+ *
+ * <p>If the intent contains a <code>sip:</code> URI, the returned
+ * "number" is actually the SIP address.
+ *
+ * @param context a context to use (or
+ * @param intent the intent
+ *
+ * @throws VoiceMailNumberMissingException if <code>intent</code> contains
+ * a <code>voicemail:</code> URI, but <code>phone</code> does not
+ * have a voicemail number set.
+ *
+ * @return the phone number (or SIP address) that would be called by the intent,
+ * or <code>null</code> if the number cannot be found.
+ */
+ private static String getNumberFromIntent(Context context, Intent intent)
+ throws VoiceMailNumberMissingException {
+ Uri uri = intent.getData();
+ String scheme = uri.getScheme();
+
+ // The sip: scheme is simple: just treat the rest of the URI as a
+ // SIP address.
+ if (Constants.SCHEME_SIP.equals(scheme)) {
+ return uri.getSchemeSpecificPart();
+ }
+
+ // Otherwise, let PhoneNumberUtils.getNumberFromIntent() handle
+ // the other cases (i.e. tel: and voicemail: and contact: URIs.)
+
+ final String number = PhoneNumberUtils.getNumberFromIntent(intent, context);
+
+ // Check for a voicemail-dialing request. If the voicemail number is
+ // empty, throw a VoiceMailNumberMissingException.
+ if (Constants.SCHEME_VOICEMAIL.equals(scheme) &&
+ (number == null || TextUtils.isEmpty(number)))
+ throw new VoiceMailNumberMissingException();
+
+ return number;
+ }
+
+ /**
+ * Returns the caller-id info corresponding to the specified Connection.
+ * (This is just a simple wrapper around CallerInfo.getCallerInfo(): we
+ * extract a phone number from the specified Connection, and feed that
+ * number into CallerInfo.getCallerInfo().)
+ *
+ * The returned CallerInfo may be null in certain error cases, like if the
+ * specified Connection was null, or if we weren't able to get a valid
+ * phone number from the Connection.
+ *
+ * Finally, if the getCallerInfo() call did succeed, we save the resulting
+ * CallerInfo object in the "userData" field of the Connection.
+ *
+ * NOTE: This API should be avoided, with preference given to the
+ * asynchronous startGetCallerInfo API.
+ */
+ static CallerInfo getCallerInfo(Context context, Connection c) {
+ CallerInfo info = null;
+
+ if (c != null) {
+ //See if there is a URI attached. If there is, this means
+ //that there is no CallerInfo queried yet, so we'll need to
+ //replace the URI with a full CallerInfo object.
+ Object userDataObject = c.getUserData();
+ if (userDataObject instanceof Uri) {
+ info = CallerInfo.getCallerInfo(context, (Uri) userDataObject);
+ if (info != null) {
+ c.setUserData(info);
+ }
+ } else {
+ if (userDataObject instanceof CallerInfoToken) {
+ //temporary result, while query is running
+ info = ((CallerInfoToken) userDataObject).currentInfo;
+ } else {
+ //final query result
+ info = (CallerInfo) userDataObject;
+ }
+ if (info == null) {
+ // No URI, or Existing CallerInfo, so we'll have to make do with
+ // querying a new CallerInfo using the connection's phone number.
+ String number = c.getAddress();
+
+ if (DBG) log("getCallerInfo: number = " + toLogSafePhoneNumber(number));
+
+ if (!TextUtils.isEmpty(number)) {
+ info = CallerInfo.getCallerInfo(context, number);
+ if (info != null) {
+ c.setUserData(info);
+ }
+ }
+ }
+ }
+ }
+ return info;
+ }
+
+ /**
+ * Class returned by the startGetCallerInfo call to package a temporary
+ * CallerInfo Object, to be superceded by the CallerInfo Object passed
+ * into the listener when the query with token mAsyncQueryToken is complete.
+ */
+ public static class CallerInfoToken {
+ /**indicates that there will no longer be updates to this request.*/
+ public boolean isFinal;
+
+ public CallerInfo currentInfo;
+ public CallerInfoAsyncQuery asyncQuery;
+ }
+
+ /**
+ * Start a CallerInfo Query based on the earliest connection in the call.
+ */
+ static CallerInfoToken startGetCallerInfo(Context context, Call call,
+ CallerInfoAsyncQuery.OnQueryCompleteListener listener, Object cookie) {
+ Connection conn = null;
+ int phoneType = call.getPhone().getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ conn = call.getLatestConnection();
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ conn = call.getEarliestConnection();
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+
+ return startGetCallerInfo(context, conn, listener, cookie);
+ }
+
+ /**
+ * place a temporary callerinfo object in the hands of the caller and notify
+ * caller when the actual query is done.
+ */
+ static CallerInfoToken startGetCallerInfo(Context context, Connection c,
+ CallerInfoAsyncQuery.OnQueryCompleteListener listener, Object cookie) {
+ CallerInfoToken cit;
+
+ if (c == null) {
+ //TODO: perhaps throw an exception here.
+ cit = new CallerInfoToken();
+ cit.asyncQuery = null;
+ return cit;
+ }
+
+ Object userDataObject = c.getUserData();
+
+ // There are now 3 states for the Connection's userData object:
+ //
+ // (1) Uri - query has not been executed yet
+ //
+ // (2) CallerInfoToken - query is executing, but has not completed.
+ //
+ // (3) CallerInfo - query has executed.
+ //
+ // In each case we have slightly different behaviour:
+ // 1. If the query has not been executed yet (Uri or null), we start
+ // query execution asynchronously, and note it by attaching a
+ // CallerInfoToken as the userData.
+ // 2. If the query is executing (CallerInfoToken), we've essentially
+ // reached a state where we've received multiple requests for the
+ // same callerInfo. That means that once the query is complete,
+ // we'll need to execute the additional listener requested.
+ // 3. If the query has already been executed (CallerInfo), we just
+ // return the CallerInfo object as expected.
+ // 4. Regarding isFinal - there are cases where the CallerInfo object
+ // will not be attached, like when the number is empty (caller id
+ // blocking). This flag is used to indicate that the
+ // CallerInfoToken object is going to be permanent since no
+ // query results will be returned. In the case where a query
+ // has been completed, this flag is used to indicate to the caller
+ // that the data will not be updated since it is valid.
+ //
+ // Note: For the case where a number is NOT retrievable, we leave
+ // the CallerInfo as null in the CallerInfoToken. This is
+ // something of a departure from the original code, since the old
+ // code manufactured a CallerInfo object regardless of the query
+ // outcome. From now on, we will append an empty CallerInfo
+ // object, to mirror previous behaviour, and to avoid Null Pointer
+ // Exceptions.
+
+ if (userDataObject instanceof Uri) {
+ // State (1): query has not been executed yet
+
+ //create a dummy callerinfo, populate with what we know from URI.
+ cit = new CallerInfoToken();
+ cit.currentInfo = new CallerInfo();
+ cit.asyncQuery = CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context,
+ (Uri) userDataObject, sCallerInfoQueryListener, c);
+ cit.asyncQuery.addQueryListener(QUERY_TOKEN, listener, cookie);
+ cit.isFinal = false;
+
+ c.setUserData(cit);
+
+ if (DBG) log("startGetCallerInfo: query based on Uri: " + userDataObject);
+
+ } else if (userDataObject == null) {
+ // No URI, or Existing CallerInfo, so we'll have to make do with
+ // querying a new CallerInfo using the connection's phone number.
+ String number = c.getAddress();
+
+ if (DBG) {
+ log("PhoneUtils.startGetCallerInfo: new query for phone number...");
+ log("- number (address): " + toLogSafePhoneNumber(number));
+ log("- c: " + c);
+ log("- phone: " + c.getCall().getPhone());
+ int phoneType = c.getCall().getPhone().getPhoneType();
+ log("- phoneType: " + phoneType);
+ switch (phoneType) {
+ case PhoneConstants.PHONE_TYPE_NONE: log(" ==> PHONE_TYPE_NONE"); break;
+ case PhoneConstants.PHONE_TYPE_GSM: log(" ==> PHONE_TYPE_GSM"); break;
+ case PhoneConstants.PHONE_TYPE_CDMA: log(" ==> PHONE_TYPE_CDMA"); break;
+ case PhoneConstants.PHONE_TYPE_SIP: log(" ==> PHONE_TYPE_SIP"); break;
+ default: log(" ==> Unknown phone type"); break;
+ }
+ }
+
+ cit = new CallerInfoToken();
+ cit.currentInfo = new CallerInfo();
+
+ // Store CNAP information retrieved from the Connection (we want to do this
+ // here regardless of whether the number is empty or not).
+ cit.currentInfo.cnapName = c.getCnapName();
+ cit.currentInfo.name = cit.currentInfo.cnapName; // This can still get overwritten
+ // by ContactInfo later
+ cit.currentInfo.numberPresentation = c.getNumberPresentation();
+ cit.currentInfo.namePresentation = c.getCnapNamePresentation();
+
+ if (VDBG) {
+ log("startGetCallerInfo: number = " + number);
+ log("startGetCallerInfo: CNAP Info from FW(1): name="
+ + cit.currentInfo.cnapName
+ + ", Name/Number Pres=" + cit.currentInfo.numberPresentation);
+ }
+
+ // handling case where number is null (caller id hidden) as well.
+ if (!TextUtils.isEmpty(number)) {
+ // Check for special CNAP cases and modify the CallerInfo accordingly
+ // to be sure we keep the right information to display/log later
+ number = modifyForSpecialCnapCases(context, cit.currentInfo, number,
+ cit.currentInfo.numberPresentation);
+
+ cit.currentInfo.phoneNumber = number;
+ // For scenarios where we may receive a valid number from the network but a
+ // restricted/unavailable presentation, we do not want to perform a contact query
+ // (see note on isFinal above). So we set isFinal to true here as well.
+ if (cit.currentInfo.numberPresentation != PhoneConstants.PRESENTATION_ALLOWED) {
+ cit.isFinal = true;
+ } else {
+ if (DBG) log("==> Actually starting CallerInfoAsyncQuery.startQuery()...");
+ cit.asyncQuery = CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context,
+ number, sCallerInfoQueryListener, c);
+ cit.asyncQuery.addQueryListener(QUERY_TOKEN, listener, cookie);
+ cit.isFinal = false;
+ }
+ } else {
+ // This is the case where we are querying on a number that
+ // is null or empty, like a caller whose caller id is
+ // blocked or empty (CLIR). The previous behaviour was to
+ // throw a null CallerInfo object back to the user, but
+ // this departure is somewhat cleaner.
+ if (DBG) log("startGetCallerInfo: No query to start, send trivial reply.");
+ cit.isFinal = true; // please see note on isFinal, above.
+ }
+
+ c.setUserData(cit);
+
+ if (DBG) {
+ log("startGetCallerInfo: query based on number: " + toLogSafePhoneNumber(number));
+ }
+
+ } else if (userDataObject instanceof CallerInfoToken) {
+ // State (2): query is executing, but has not completed.
+
+ // just tack on this listener to the queue.
+ cit = (CallerInfoToken) userDataObject;
+
+ // handling case where number is null (caller id hidden) as well.
+ if (cit.asyncQuery != null) {
+ cit.asyncQuery.addQueryListener(QUERY_TOKEN, listener, cookie);
+
+ if (DBG) log("startGetCallerInfo: query already running, adding listener: " +
+ listener.getClass().toString());
+ } else {
+ // handling case where number/name gets updated later on by the network
+ String updatedNumber = c.getAddress();
+ if (DBG) {
+ log("startGetCallerInfo: updatedNumber initially = "
+ + toLogSafePhoneNumber(updatedNumber));
+ }
+ if (!TextUtils.isEmpty(updatedNumber)) {
+ // Store CNAP information retrieved from the Connection
+ cit.currentInfo.cnapName = c.getCnapName();
+ // This can still get overwritten by ContactInfo
+ cit.currentInfo.name = cit.currentInfo.cnapName;
+ cit.currentInfo.numberPresentation = c.getNumberPresentation();
+ cit.currentInfo.namePresentation = c.getCnapNamePresentation();
+
+ updatedNumber = modifyForSpecialCnapCases(context, cit.currentInfo,
+ updatedNumber, cit.currentInfo.numberPresentation);
+
+ cit.currentInfo.phoneNumber = updatedNumber;
+ if (DBG) {
+ log("startGetCallerInfo: updatedNumber="
+ + toLogSafePhoneNumber(updatedNumber));
+ }
+ if (VDBG) {
+ log("startGetCallerInfo: CNAP Info from FW(2): name="
+ + cit.currentInfo.cnapName
+ + ", Name/Number Pres=" + cit.currentInfo.numberPresentation);
+ } else if (DBG) {
+ log("startGetCallerInfo: CNAP Info from FW(2)");
+ }
+ // For scenarios where we may receive a valid number from the network but a
+ // restricted/unavailable presentation, we do not want to perform a contact query
+ // (see note on isFinal above). So we set isFinal to true here as well.
+ if (cit.currentInfo.numberPresentation != PhoneConstants.PRESENTATION_ALLOWED) {
+ cit.isFinal = true;
+ } else {
+ cit.asyncQuery = CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context,
+ updatedNumber, sCallerInfoQueryListener, c);
+ cit.asyncQuery.addQueryListener(QUERY_TOKEN, listener, cookie);
+ cit.isFinal = false;
+ }
+ } else {
+ if (DBG) log("startGetCallerInfo: No query to attach to, send trivial reply.");
+ if (cit.currentInfo == null) {
+ cit.currentInfo = new CallerInfo();
+ }
+ // Store CNAP information retrieved from the Connection
+ cit.currentInfo.cnapName = c.getCnapName(); // This can still get
+ // overwritten by ContactInfo
+ cit.currentInfo.name = cit.currentInfo.cnapName;
+ cit.currentInfo.numberPresentation = c.getNumberPresentation();
+ cit.currentInfo.namePresentation = c.getCnapNamePresentation();
+
+ if (VDBG) {
+ log("startGetCallerInfo: CNAP Info from FW(3): name="
+ + cit.currentInfo.cnapName
+ + ", Name/Number Pres=" + cit.currentInfo.numberPresentation);
+ } else if (DBG) {
+ log("startGetCallerInfo: CNAP Info from FW(3)");
+ }
+ cit.isFinal = true; // please see note on isFinal, above.
+ }
+ }
+ } else {
+ // State (3): query is complete.
+
+ // The connection's userDataObject is a full-fledged
+ // CallerInfo instance. Wrap it in a CallerInfoToken and
+ // return it to the user.
+
+ cit = new CallerInfoToken();
+ cit.currentInfo = (CallerInfo) userDataObject;
+ cit.asyncQuery = null;
+ cit.isFinal = true;
+ // since the query is already done, call the listener.
+ if (DBG) log("startGetCallerInfo: query already done, returning CallerInfo");
+ if (DBG) log("==> cit.currentInfo = " + cit.currentInfo);
+ }
+ return cit;
+ }
+
+ /**
+ * Static CallerInfoAsyncQuery.OnQueryCompleteListener instance that
+ * we use with all our CallerInfoAsyncQuery.startQuery() requests.
+ */
+ private static final int QUERY_TOKEN = -1;
+ static CallerInfoAsyncQuery.OnQueryCompleteListener sCallerInfoQueryListener =
+ new CallerInfoAsyncQuery.OnQueryCompleteListener () {
+ /**
+ * When the query completes, we stash the resulting CallerInfo
+ * object away in the Connection's "userData" (where it will
+ * later be retrieved by the in-call UI.)
+ */
+ public void onQueryComplete(int token, Object cookie, CallerInfo ci) {
+ if (DBG) log("query complete, updating connection.userdata");
+ Connection conn = (Connection) cookie;
+
+ // Added a check if CallerInfo is coming from ContactInfo or from Connection.
+ // If no ContactInfo, then we want to use CNAP information coming from network
+ if (DBG) log("- onQueryComplete: CallerInfo:" + ci);
+ if (ci.contactExists || ci.isEmergencyNumber() || ci.isVoiceMailNumber()) {
+ // If the number presentation has not been set by
+ // the ContactInfo, use the one from the
+ // connection.
+
+ // TODO: Need a new util method to merge the info
+ // from the Connection in a CallerInfo object.
+ // Here 'ci' is a new CallerInfo instance read
+ // from the DB. It has lost all the connection
+ // info preset before the query (see PhoneUtils
+ // line 1334). We should have a method to merge
+ // back into this new instance the info from the
+ // connection object not set by the DB. If the
+ // Connection already has a CallerInfo instance in
+ // userData, then we could use this instance to
+ // fill 'ci' in. The same routine could be used in
+ // PhoneUtils.
+ if (0 == ci.numberPresentation) {
+ ci.numberPresentation = conn.getNumberPresentation();
+ }
+ } else {
+ // No matching contact was found for this number.
+ // Return a new CallerInfo based solely on the CNAP
+ // information from the network.
+
+ CallerInfo newCi = getCallerInfo(null, conn);
+
+ // ...but copy over the (few) things we care about
+ // from the original CallerInfo object:
+ if (newCi != null) {
+ newCi.phoneNumber = ci.phoneNumber; // To get formatted phone number
+ newCi.geoDescription = ci.geoDescription; // To get geo description string
+ ci = newCi;
+ }
+ }
+
+ if (DBG) log("==> Stashing CallerInfo " + ci + " into the connection...");
+ conn.setUserData(ci);
+ }
+ };
+
+
+ /**
+ * Returns a single "name" for the specified given a CallerInfo object.
+ * If the name is null, return defaultString as the default value, usually
+ * context.getString(R.string.unknown).
+ */
+ static String getCompactNameFromCallerInfo(CallerInfo ci, Context context) {
+ if (DBG) log("getCompactNameFromCallerInfo: info = " + ci);
+
+ String compactName = null;
+ if (ci != null) {
+ if (TextUtils.isEmpty(ci.name)) {
+ // Perform any modifications for special CNAP cases to
+ // the phone number being displayed, if applicable.
+ compactName = modifyForSpecialCnapCases(context, ci, ci.phoneNumber,
+ ci.numberPresentation);
+ } else {
+ // Don't call modifyForSpecialCnapCases on regular name. See b/2160795.
+ compactName = ci.name;
+ }
+ }
+
+ if ((compactName == null) || (TextUtils.isEmpty(compactName))) {
+ // If we're still null/empty here, then check if we have a presentation
+ // string that takes precedence that we could return, otherwise display
+ // "unknown" string.
+ if (ci != null && ci.numberPresentation == PhoneConstants.PRESENTATION_RESTRICTED) {
+ compactName = context.getString(R.string.private_num);
+ } else if (ci != null && ci.numberPresentation == PhoneConstants.PRESENTATION_PAYPHONE) {
+ compactName = context.getString(R.string.payphone);
+ } else {
+ compactName = context.getString(R.string.unknown);
+ }
+ }
+ if (VDBG) log("getCompactNameFromCallerInfo: compactName=" + compactName);
+ return compactName;
+ }
+
+ /**
+ * Returns true if the specified Call is a "conference call", meaning
+ * that it owns more than one Connection object. This information is
+ * used to trigger certain UI changes that appear when a conference
+ * call is active (like displaying the label "Conference call", and
+ * enabling the "Manage conference" UI.)
+ *
+ * Watch out: This method simply checks the number of Connections,
+ * *not* their states. So if a Call has (for example) one ACTIVE
+ * connection and one DISCONNECTED connection, this method will return
+ * true (which is unintuitive, since the Call isn't *really* a
+ * conference call any more.)
+ *
+ * @return true if the specified call has more than one connection (in any state.)
+ */
+ static boolean isConferenceCall(Call call) {
+ // CDMA phones don't have the same concept of "conference call" as
+ // GSM phones do; there's no special "conference call" state of
+ // the UI or a "manage conference" function. (Instead, when
+ // you're in a 3-way call, all we can do is display the "generic"
+ // state of the UI.) So as far as the in-call UI is concerned,
+ // Conference corresponds to generic display.
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+ int phoneType = call.getPhone().getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ CdmaPhoneCallState.PhoneCallState state = app.cdmaPhoneCallState.getCurrentCallState();
+ if ((state == CdmaPhoneCallState.PhoneCallState.CONF_CALL)
+ || ((state == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE)
+ && !app.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing())) {
+ return true;
+ }
+ } else {
+ List<Connection> connections = call.getConnections();
+ if (connections != null && connections.size() > 1) {
+ return true;
+ }
+ }
+ return false;
+
+ // TODO: We may still want to change the semantics of this method
+ // to say that a given call is only really a conference call if
+ // the number of ACTIVE connections, not the total number of
+ // connections, is greater than one. (See warning comment in the
+ // javadoc above.)
+ // Here's an implementation of that:
+ // if (connections == null) {
+ // return false;
+ // }
+ // int numActiveConnections = 0;
+ // for (Connection conn : connections) {
+ // if (DBG) log(" - CONN: " + conn + ", state = " + conn.getState());
+ // if (conn.getState() == Call.State.ACTIVE) numActiveConnections++;
+ // if (numActiveConnections > 1) {
+ // return true;
+ // }
+ // }
+ // return false;
+ }
+
+ /**
+ * Launch the Dialer to start a new call.
+ * This is just a wrapper around the ACTION_DIAL intent.
+ */
+ /* package */ static boolean startNewCall(final CallManager cm) {
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+
+ // Sanity-check that this is OK given the current state of the phone.
+ if (!okToAddCall(cm)) {
+ Log.w(LOG_TAG, "startNewCall: can't add a new call in the current state");
+ dumpCallManager();
+ return false;
+ }
+
+ // if applicable, mute the call while we're showing the add call UI.
+ if (cm.hasActiveFgCall()) {
+ setMuteInternal(cm.getActiveFgCall().getPhone(), true);
+ // Inform the phone app that this mute state was NOT done
+ // voluntarily by the User.
+ app.setRestoreMuteOnInCallResume(true);
+ }
+
+ 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 {
+ app.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.
+ Log.e(LOG_TAG, "Activity for adding calls isn't found.");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Turns on/off speaker.
+ *
+ * @param context Context
+ * @param flag True when speaker should be on. False otherwise.
+ * @param store True when the settings should be stored in the device.
+ */
+ /* package */ static void turnOnSpeaker(Context context, boolean flag, boolean store) {
+ if (DBG) log("turnOnSpeaker(flag=" + flag + ", store=" + store + ")...");
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ audioManager.setSpeakerphoneOn(flag);
+
+ // record the speaker-enable value
+ if (store) {
+ sIsSpeakerEnabled = flag;
+ }
+
+ // Update the status bar icon
+ app.notificationMgr.updateSpeakerNotification(flag);
+
+ // We also need to make a fresh call to PhoneApp.updateWakeState()
+ // any time the speaker state changes, since the screen timeout is
+ // sometimes different depending on whether or not the speaker is
+ // in use.
+ app.updateWakeState();
+
+ // Update the Proximity sensor based on speaker state
+ app.updateProximitySensorMode(app.mCM.getState());
+
+ app.mCM.setEchoSuppressionEnabled(flag);
+ }
+
+ /**
+ * Restore the speaker mode, called after a wired headset disconnect
+ * event.
+ */
+ static void restoreSpeakerMode(Context context) {
+ if (DBG) log("restoreSpeakerMode, restoring to: " + sIsSpeakerEnabled);
+
+ // change the mode if needed.
+ if (isSpeakerOn(context) != sIsSpeakerEnabled) {
+ turnOnSpeaker(context, sIsSpeakerEnabled, false);
+ }
+ }
+
+ static boolean isSpeakerOn(Context context) {
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ return audioManager.isSpeakerphoneOn();
+ }
+
+
+ static void turnOnNoiseSuppression(Context context, boolean flag, boolean store) {
+ if (DBG) log("turnOnNoiseSuppression: " + flag);
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+
+ if (!context.getResources().getBoolean(R.bool.has_in_call_noise_suppression)) {
+ return;
+ }
+
+ if (flag) {
+ audioManager.setParameters("noise_suppression=auto");
+ } else {
+ audioManager.setParameters("noise_suppression=off");
+ }
+
+ // record the speaker-enable value
+ if (store) {
+ sIsNoiseSuppressionEnabled = flag;
+ }
+
+ // TODO: implement and manage ICON
+
+ }
+
+ static void restoreNoiseSuppression(Context context) {
+ if (DBG) log("restoreNoiseSuppression, restoring to: " + sIsNoiseSuppressionEnabled);
+
+ if (!context.getResources().getBoolean(R.bool.has_in_call_noise_suppression)) {
+ return;
+ }
+
+ // change the mode if needed.
+ if (isNoiseSuppressionOn(context) != sIsNoiseSuppressionEnabled) {
+ turnOnNoiseSuppression(context, sIsNoiseSuppressionEnabled, false);
+ }
+ }
+
+ static boolean isNoiseSuppressionOn(Context context) {
+
+ if (!context.getResources().getBoolean(R.bool.has_in_call_noise_suppression)) {
+ return false;
+ }
+
+ AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
+ String noiseSuppression = audioManager.getParameters("noise_suppression");
+ if (DBG) log("isNoiseSuppressionOn: " + noiseSuppression);
+ if (noiseSuppression.contains("off")) {
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ /**
+ *
+ * Mute / umute the foreground phone, which has the current foreground call
+ *
+ * All muting / unmuting from the in-call UI should go through this
+ * wrapper.
+ *
+ * Wrapper around Phone.setMute() and setMicrophoneMute().
+ * It also updates the connectionMuteTable and mute icon in the status bar.
+ *
+ */
+ static void setMute(boolean muted) {
+ CallManager cm = PhoneGlobals.getInstance().mCM;
+
+ // make the call to mute the audio
+ setMuteInternal(cm.getFgPhone(), muted);
+
+ // update the foreground connections to match. This includes
+ // all the connections on conference calls.
+ for (Connection cn : cm.getActiveFgCall().getConnections()) {
+ if (sConnectionMuteTable.get(cn) == null) {
+ if (DBG) log("problem retrieving mute value for this connection.");
+ }
+ sConnectionMuteTable.put(cn, Boolean.valueOf(muted));
+ }
+ }
+
+ /**
+ * Internally used muting function.
+ */
+ private static void setMuteInternal(Phone phone, boolean muted) {
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+ Context context = phone.getContext();
+ boolean routeToAudioManager =
+ context.getResources().getBoolean(R.bool.send_mic_mute_to_AudioManager);
+ if (routeToAudioManager) {
+ AudioManager audioManager =
+ (AudioManager) phone.getContext().getSystemService(Context.AUDIO_SERVICE);
+ if (DBG) log("setMuteInternal: using setMicrophoneMute(" + muted + ")...");
+ audioManager.setMicrophoneMute(muted);
+ } else {
+ if (DBG) log("setMuteInternal: using phone.setMute(" + muted + ")...");
+ phone.setMute(muted);
+ }
+ app.notificationMgr.updateMuteNotification();
+ }
+
+ /**
+ * Get the mute state of foreground phone, which has the current
+ * foreground call
+ */
+ static boolean getMute() {
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+
+ boolean routeToAudioManager =
+ app.getResources().getBoolean(R.bool.send_mic_mute_to_AudioManager);
+ if (routeToAudioManager) {
+ AudioManager audioManager =
+ (AudioManager) app.getSystemService(Context.AUDIO_SERVICE);
+ return audioManager.isMicrophoneMute();
+ } else {
+ return app.mCM.getMute();
+ }
+ }
+
+ /* package */ static void setAudioMode() {
+ setAudioMode(PhoneGlobals.getInstance().mCM);
+ }
+
+ /**
+ * Sets the audio mode per current phone state.
+ */
+ /* package */ static void setAudioMode(CallManager cm) {
+ if (DBG) Log.d(LOG_TAG, "setAudioMode()..." + cm.getState());
+
+ Context context = PhoneGlobals.getInstance();
+ AudioManager audioManager = (AudioManager)
+ context.getSystemService(Context.AUDIO_SERVICE);
+ int modeBefore = audioManager.getMode();
+ cm.setAudioMode();
+ int modeAfter = audioManager.getMode();
+
+ if (modeBefore != modeAfter) {
+ // Enable stack dump only when actively debugging ("new Throwable()" is expensive!)
+ if (DBG_SETAUDIOMODE_STACK) Log.d(LOG_TAG, "Stack:", new Throwable("stack dump"));
+ } else {
+ if (DBG) Log.d(LOG_TAG, "setAudioMode() no change: "
+ + audioModeToString(modeBefore));
+ }
+ }
+ private static String audioModeToString(int mode) {
+ switch (mode) {
+ case AudioManager.MODE_INVALID: return "MODE_INVALID";
+ case AudioManager.MODE_CURRENT: return "MODE_CURRENT";
+ case AudioManager.MODE_NORMAL: return "MODE_NORMAL";
+ case AudioManager.MODE_RINGTONE: return "MODE_RINGTONE";
+ case AudioManager.MODE_IN_CALL: return "MODE_IN_CALL";
+ default: return String.valueOf(mode);
+ }
+ }
+
+ /**
+ * Handles the wired headset button while in-call.
+ *
+ * This is called from the PhoneApp, not from the InCallScreen,
+ * since the HEADSETHOOK button means "mute or unmute the current
+ * call" *any* time a call is active, even if the user isn't actually
+ * on the in-call screen.
+ *
+ * @return true if we consumed the event.
+ */
+ /* package */ static boolean handleHeadsetHook(Phone phone, KeyEvent event) {
+ if (DBG) log("handleHeadsetHook()..." + event.getAction() + " " + event.getRepeatCount());
+ final PhoneGlobals app = PhoneGlobals.getInstance();
+
+ // If the phone is totally idle, we ignore HEADSETHOOK events
+ // (and instead let them fall through to the media player.)
+ if (phone.getState() == PhoneConstants.State.IDLE) {
+ return false;
+ }
+
+ // Ok, the phone is in use.
+ // The headset button button means "Answer" if an incoming call is
+ // ringing. If not, it toggles the mute / unmute state.
+ //
+ // And in any case we *always* consume this event; this means
+ // that the usual mediaplayer-related behavior of the headset
+ // button will NEVER happen while the user is on a call.
+
+ final boolean hasRingingCall = !phone.getRingingCall().isIdle();
+ final boolean hasActiveCall = !phone.getForegroundCall().isIdle();
+ final boolean hasHoldingCall = !phone.getBackgroundCall().isIdle();
+
+ if (hasRingingCall &&
+ event.getRepeatCount() == 0 &&
+ event.getAction() == KeyEvent.ACTION_UP) {
+ // If an incoming call is ringing, answer it (just like with the
+ // CALL button):
+ int phoneType = phone.getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ answerCall(phone.getRingingCall());
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ if (hasActiveCall && hasHoldingCall) {
+ if (DBG) log("handleHeadsetHook: ringing (both lines in use) ==> answer!");
+ answerAndEndActive(app.mCM, phone.getRingingCall());
+ } else {
+ if (DBG) log("handleHeadsetHook: ringing ==> answer!");
+ // answerCall() will automatically hold the current
+ // active call, if there is one.
+ answerCall(phone.getRingingCall());
+ }
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ } else {
+ // No incoming ringing call.
+ if (event.isLongPress()) {
+ if (DBG) log("handleHeadsetHook: longpress -> hangup");
+ hangup(app.mCM);
+ }
+ else if (event.getAction() == KeyEvent.ACTION_UP &&
+ event.getRepeatCount() == 0) {
+ Connection c = phone.getForegroundCall().getLatestConnection();
+ // If it is NOT an emg #, toggle the mute state. Otherwise, ignore the hook.
+ if (c != null && !PhoneNumberUtils.isLocalEmergencyNumber(c.getAddress(),
+ PhoneGlobals.getInstance())) {
+ if (getMute()) {
+ if (DBG) log("handleHeadsetHook: UNmuting...");
+ setMute(false);
+ } else {
+ if (DBG) log("handleHeadsetHook: muting...");
+ setMute(true);
+ }
+ }
+ }
+ }
+
+ // Even if the InCallScreen is the current activity, there's no
+ // need to force it to update, because (1) if we answered a
+ // ringing call, the InCallScreen will imminently get a phone
+ // state change event (causing an update), and (2) if we muted or
+ // unmuted, the setMute() call automagically updates the status
+ // bar, and there's no "mute" indication in the InCallScreen
+ // itself (other than the menu item, which only ever stays
+ // onscreen for a second anyway.)
+ // TODO: (2) isn't entirely true anymore. Once we return our result
+ // to the PhoneApp, we ask InCallScreen to update its control widgets
+ // in case we changed mute or speaker state and phones with touch-
+ // screen [toggle] buttons need to update themselves.
+
+ return true;
+ }
+
+ /**
+ * Look for ANY connections on the phone that qualify as being
+ * disconnected.
+ *
+ * @return true if we find a connection that is disconnected over
+ * all the phone's call objects.
+ */
+ /* package */ static boolean hasDisconnectedConnections(Phone phone) {
+ return hasDisconnectedConnections(phone.getForegroundCall()) ||
+ hasDisconnectedConnections(phone.getBackgroundCall()) ||
+ hasDisconnectedConnections(phone.getRingingCall());
+ }
+
+ /**
+ * Iterate over all connections in a call to see if there are any
+ * that are not alive (disconnected or idle).
+ *
+ * @return true if we find a connection that is disconnected, and
+ * pending removal via
+ * {@link com.android.internal.telephony.gsm.GsmCall#clearDisconnected()}.
+ */
+ private static final boolean hasDisconnectedConnections(Call call) {
+ // look through all connections for non-active ones.
+ for (Connection c : call.getConnections()) {
+ if (!c.isAlive()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ //
+ // Misc UI policy helper functions
+ //
+
+ /**
+ * @return true if we're allowed to swap calls, given the current
+ * state of the Phone.
+ */
+ /* package */ static boolean okToSwapCalls(CallManager cm) {
+ int phoneType = cm.getDefaultPhone().getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ // CDMA: "Swap" is enabled only when the phone reaches a *generic*.
+ // state by either accepting a Call Waiting or by merging two calls
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ return (app.cdmaPhoneCallState.getCurrentCallState()
+ == CdmaPhoneCallState.PhoneCallState.CONF_CALL);
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ // GSM: "Swap" is available if both lines are in use and there's no
+ // incoming call. (Actually we need to verify that the active
+ // call really is in the ACTIVE state and the holding call really
+ // is in the HOLDING state, since you *can't* actually swap calls
+ // when the foreground call is DIALING or ALERTING.)
+ return !cm.hasActiveRingingCall()
+ && (cm.getActiveFgCall().getState() == Call.State.ACTIVE)
+ && (cm.getFirstActiveBgCall().getState() == Call.State.HOLDING);
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ }
+
+ /**
+ * @return true if we're allowed to merge calls, given the current
+ * state of the Phone.
+ */
+ /* package */ static boolean okToMergeCalls(CallManager cm) {
+ int phoneType = cm.getFgPhone().getPhoneType();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ // CDMA: "Merge" is enabled only when the user is in a 3Way call.
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ return ((app.cdmaPhoneCallState.getCurrentCallState()
+ == CdmaPhoneCallState.PhoneCallState.THRWAY_ACTIVE)
+ && !app.cdmaPhoneCallState.IsThreeWayCallOrigStateDialing());
+ } else {
+ // GSM: "Merge" is available if both lines are in use and there's no
+ // incoming call, *and* the current conference isn't already
+ // "full".
+ // TODO: shall move all okToMerge logic to CallManager
+ return !cm.hasActiveRingingCall() && cm.hasActiveFgCall()
+ && cm.hasActiveBgCall()
+ && cm.canConference(cm.getFirstActiveBgCall());
+ }
+ }
+
+ /**
+ * @return true if the UI should let you add a new call, given the current
+ * state of the Phone.
+ */
+ /* package */ static boolean okToAddCall(CallManager cm) {
+ Phone phone = cm.getActiveFgCall().getPhone();
+
+ // "Add call" is never allowed in emergency callback mode (ECM).
+ if (isPhoneInEcm(phone)) {
+ return false;
+ }
+
+ int phoneType = phone.getPhoneType();
+ final Call.State fgCallState = cm.getActiveFgCall().getState();
+ if (phoneType == PhoneConstants.PHONE_TYPE_CDMA) {
+ // CDMA: "Add call" button is only enabled when:
+ // - ForegroundCall is in ACTIVE state
+ // - After 30 seconds of user Ignoring/Missing a Call Waiting call.
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ return ((fgCallState == Call.State.ACTIVE)
+ && (app.cdmaPhoneCallState.getAddCallMenuStateAfterCallWaiting()));
+ } else if ((phoneType == PhoneConstants.PHONE_TYPE_GSM)
+ || (phoneType == PhoneConstants.PHONE_TYPE_SIP)) {
+ // GSM: "Add call" is available only if ALL of the following are true:
+ // - There's no incoming ringing call
+ // - There's < 2 lines in use
+ // - The foreground call is ACTIVE or IDLE or DISCONNECTED.
+ // (We mainly need to make sure it *isn't* DIALING or ALERTING.)
+ final boolean hasRingingCall = cm.hasActiveRingingCall();
+ final boolean hasActiveCall = cm.hasActiveFgCall();
+ final boolean hasHoldingCall = cm.hasActiveBgCall();
+ final boolean allLinesTaken = hasActiveCall && hasHoldingCall;
+
+ return !hasRingingCall
+ && !allLinesTaken
+ && ((fgCallState == Call.State.ACTIVE)
+ || (fgCallState == Call.State.IDLE)
+ || (fgCallState == Call.State.DISCONNECTED));
+ } else {
+ throw new IllegalStateException("Unexpected phone type: " + phoneType);
+ }
+ }
+
+ /**
+ * Based on the input CNAP number string,
+ * @return _RESTRICTED or _UNKNOWN for all the special CNAP strings.
+ * Otherwise, return CNAP_SPECIAL_CASE_NO.
+ */
+ private static int checkCnapSpecialCases(String n) {
+ if (n.equals("PRIVATE") ||
+ n.equals("P") ||
+ n.equals("RES")) {
+ if (DBG) log("checkCnapSpecialCases, PRIVATE string: " + n);
+ return PhoneConstants.PRESENTATION_RESTRICTED;
+ } else if (n.equals("UNAVAILABLE") ||
+ n.equals("UNKNOWN") ||
+ n.equals("UNA") ||
+ n.equals("U")) {
+ if (DBG) log("checkCnapSpecialCases, UNKNOWN string: " + n);
+ return PhoneConstants.PRESENTATION_UNKNOWN;
+ } else {
+ if (DBG) log("checkCnapSpecialCases, normal str. number: " + n);
+ return CNAP_SPECIAL_CASE_NO;
+ }
+ }
+
+ /**
+ * Handles certain "corner cases" for CNAP. When we receive weird phone numbers
+ * from the network to indicate different number presentations, convert them to
+ * expected number and presentation values within the CallerInfo object.
+ * @param number number we use to verify if we are in a corner case
+ * @param presentation presentation value used to verify if we are in a corner case
+ * @return the new String that should be used for the phone number
+ */
+ /* package */ static String modifyForSpecialCnapCases(Context context, CallerInfo ci,
+ String number, int presentation) {
+ // Obviously we return number if ci == null, but still return number if
+ // number == null, because in these cases the correct string will still be
+ // displayed/logged after this function returns based on the presentation value.
+ if (ci == null || number == null) return number;
+
+ if (DBG) {
+ log("modifyForSpecialCnapCases: initially, number="
+ + toLogSafePhoneNumber(number)
+ + ", presentation=" + presentation + " ci " + ci);
+ }
+
+ // "ABSENT NUMBER" is a possible value we could get from the network as the
+ // phone number, so if this happens, change it to "Unknown" in the CallerInfo
+ // and fix the presentation to be the same.
+ final String[] absentNumberValues =
+ context.getResources().getStringArray(R.array.absent_num);
+ if (Arrays.asList(absentNumberValues).contains(number)
+ && presentation == PhoneConstants.PRESENTATION_ALLOWED) {
+ number = context.getString(R.string.unknown);
+ ci.numberPresentation = PhoneConstants.PRESENTATION_UNKNOWN;
+ }
+
+ // Check for other special "corner cases" for CNAP and fix them similarly. Corner
+ // cases only apply if we received an allowed presentation from the network, so check
+ // if we think we have an allowed presentation, or if the CallerInfo presentation doesn't
+ // match the presentation passed in for verification (meaning we changed it previously
+ // because it's a corner case and we're being called from a different entry point).
+ if (ci.numberPresentation == PhoneConstants.PRESENTATION_ALLOWED
+ || (ci.numberPresentation != presentation
+ && presentation == PhoneConstants.PRESENTATION_ALLOWED)) {
+ int cnapSpecialCase = checkCnapSpecialCases(number);
+ if (cnapSpecialCase != CNAP_SPECIAL_CASE_NO) {
+ // For all special strings, change number & numberPresentation.
+ if (cnapSpecialCase == PhoneConstants.PRESENTATION_RESTRICTED) {
+ number = context.getString(R.string.private_num);
+ } else if (cnapSpecialCase == PhoneConstants.PRESENTATION_UNKNOWN) {
+ number = context.getString(R.string.unknown);
+ }
+ if (DBG) {
+ log("SpecialCnap: number=" + toLogSafePhoneNumber(number)
+ + "; presentation now=" + cnapSpecialCase);
+ }
+ ci.numberPresentation = cnapSpecialCase;
+ }
+ }
+ if (DBG) {
+ log("modifyForSpecialCnapCases: returning number string="
+ + toLogSafePhoneNumber(number));
+ }
+ return number;
+ }
+
+ //
+ // Support for 3rd party phone service providers.
+ //
+
+ /**
+ * Check if all the provider's info is present in the intent.
+ * @param intent Expected to have the provider's extra.
+ * @return true if the intent has all the extras to build the
+ * in-call screen's provider info overlay.
+ */
+ /* package */ static boolean hasPhoneProviderExtras(Intent intent) {
+ if (null == intent) {
+ return false;
+ }
+ final String name = intent.getStringExtra(InCallScreen.EXTRA_GATEWAY_PROVIDER_PACKAGE);
+ final String gatewayUri = intent.getStringExtra(InCallScreen.EXTRA_GATEWAY_URI);
+
+ return !TextUtils.isEmpty(name) && !TextUtils.isEmpty(gatewayUri);
+ }
+
+ /**
+ * Copy all the expected extras set when a 3rd party provider is
+ * used from the source intent to the destination one. Checks all
+ * the required extras are present, if any is missing, none will
+ * be copied.
+ * @param src Intent which may contain the provider's extras.
+ * @param dst Intent where a copy of the extras will be added if applicable.
+ */
+ /* package */ static void checkAndCopyPhoneProviderExtras(Intent src, Intent dst) {
+ if (!hasPhoneProviderExtras(src)) {
+ Log.d(LOG_TAG, "checkAndCopyPhoneProviderExtras: some or all extras are missing.");
+ return;
+ }
+
+ dst.putExtra(InCallScreen.EXTRA_GATEWAY_PROVIDER_PACKAGE,
+ src.getStringExtra(InCallScreen.EXTRA_GATEWAY_PROVIDER_PACKAGE));
+ dst.putExtra(InCallScreen.EXTRA_GATEWAY_URI,
+ src.getStringExtra(InCallScreen.EXTRA_GATEWAY_URI));
+ }
+
+ /**
+ * Get the provider's label from the intent.
+ * @param context to lookup the provider's package name.
+ * @param intent with an extra set to the provider's package name.
+ * @return The provider's application label. null if an error
+ * occurred during the lookup of the package name or the label.
+ */
+ /* package */ static CharSequence getProviderLabel(Context context, Intent intent) {
+ String packageName = intent.getStringExtra(InCallScreen.EXTRA_GATEWAY_PROVIDER_PACKAGE);
+ PackageManager pm = context.getPackageManager();
+
+ try {
+ ApplicationInfo info = pm.getApplicationInfo(packageName, 0);
+
+ return pm.getApplicationLabel(info);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Get the provider's icon.
+ * @param context to lookup the provider's icon.
+ * @param intent with an extra set to the provider's package name.
+ * @return The provider's application icon. null if an error occured during the icon lookup.
+ */
+ /* package */ static Drawable getProviderIcon(Context context, Intent intent) {
+ String packageName = intent.getStringExtra(InCallScreen.EXTRA_GATEWAY_PROVIDER_PACKAGE);
+ PackageManager pm = context.getPackageManager();
+
+ try {
+ return pm.getApplicationIcon(packageName);
+ } catch (PackageManager.NameNotFoundException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Return the gateway uri from the intent.
+ * @param intent With the gateway uri extra.
+ * @return The gateway URI or null if not found.
+ */
+ /* package */ static Uri getProviderGatewayUri(Intent intent) {
+ String uri = intent.getStringExtra(InCallScreen.EXTRA_GATEWAY_URI);
+ return TextUtils.isEmpty(uri) ? null : Uri.parse(uri);
+ }
+
+ /**
+ * Return a formatted version of the uri's scheme specific
+ * part. E.g for 'tel:12345678', return '1-234-5678'.
+ * @param uri A 'tel:' URI with the gateway phone number.
+ * @return the provider's address (from the gateway uri) formatted
+ * for user display. null if uri was null or its scheme was not 'tel:'.
+ */
+ /* package */ static String formatProviderUri(Uri uri) {
+ if (null != uri) {
+ if (Constants.SCHEME_TEL.equals(uri.getScheme())) {
+ return PhoneNumberUtils.formatNumber(uri.getSchemeSpecificPart());
+ } else {
+ return uri.toString();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Check if a phone number can be route through a 3rd party
+ * gateway. The number must be a global phone number in numerical
+ * form (1-800-666-SEXY won't work).
+ *
+ * MMI codes and the like cannot be used as a dial number for the
+ * gateway either.
+ *
+ * @param number To be dialed via a 3rd party gateway.
+ * @return true If the number can be routed through the 3rd party network.
+ */
+ /* package */ static boolean isRoutableViaGateway(String number) {
+ if (TextUtils.isEmpty(number)) {
+ return false;
+ }
+ number = PhoneNumberUtils.stripSeparators(number);
+ if (!number.equals(PhoneNumberUtils.convertKeypadLettersToDigits(number))) {
+ return false;
+ }
+ number = PhoneNumberUtils.extractNetworkPortion(number);
+ return PhoneNumberUtils.isGlobalPhoneNumber(number);
+ }
+
+ /**
+ * This function is called when phone answers or places a call.
+ * Check if the phone is in a car dock or desk dock.
+ * If yes, turn on the speaker, when no wired or BT headsets are connected.
+ * Otherwise do nothing.
+ * @return true if activated
+ */
+ private static boolean activateSpeakerIfDocked(Phone phone) {
+ if (DBG) log("activateSpeakerIfDocked()...");
+
+ boolean activated = false;
+ if (PhoneGlobals.mDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) {
+ if (DBG) log("activateSpeakerIfDocked(): In a dock -> may need to turn on speaker.");
+ PhoneGlobals app = PhoneGlobals.getInstance();
+
+ if (!app.isHeadsetPlugged() && !app.isBluetoothHeadsetAudioOn()) {
+ turnOnSpeaker(phone.getContext(), true, true);
+ activated = true;
+ }
+ }
+ return activated;
+ }
+
+
+ /**
+ * Returns whether the phone is in ECM ("Emergency Callback Mode") or not.
+ */
+ /* package */ static boolean isPhoneInEcm(Phone phone) {
+ if ((phone != null) && TelephonyCapabilities.supportsEcm(phone)) {
+ // For phones that support ECM, return true iff PROPERTY_INECM_MODE == "true".
+ // TODO: There ought to be a better API for this than just
+ // exposing a system property all the way up to the app layer,
+ // probably a method like "inEcm()" provided by the telephony
+ // layer.
+ String ecmMode =
+ SystemProperties.get(TelephonyProperties.PROPERTY_INECM_MODE);
+ if (ecmMode != null) {
+ return ecmMode.equals("true");
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the most appropriate Phone object to handle a call
+ * to the specified number.
+ *
+ * @param cm the CallManager.
+ * @param scheme the scheme from the data URI that the number originally came from.
+ * @param number the phone number, or SIP address.
+ */
+ public static Phone pickPhoneBasedOnNumber(CallManager cm,
+ String scheme, String number, String primarySipUri) {
+ if (DBG) {
+ log("pickPhoneBasedOnNumber: scheme " + scheme
+ + ", number " + toLogSafePhoneNumber(number)
+ + ", sipUri "
+ + (primarySipUri != null ? Uri.parse(primarySipUri).toSafeString() : "null"));
+ }
+
+ if (primarySipUri != null) {
+ Phone phone = getSipPhoneFromUri(cm, primarySipUri);
+ if (phone != null) return phone;
+ }
+ return cm.getDefaultPhone();
+ }
+
+ public static Phone getSipPhoneFromUri(CallManager cm, String target) {
+ for (Phone phone : cm.getAllPhones()) {
+ if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_SIP) {
+ String sipUri = ((SipPhone) phone).getSipUri();
+ if (target.equals(sipUri)) {
+ if (DBG) log("- pickPhoneBasedOnNumber:" +
+ "found SipPhone! obj = " + phone + ", "
+ + phone.getClass());
+ return phone;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Returns true when the given call is in INCOMING state and there's no foreground phone call,
+ * meaning the call is the first real incoming call the phone is having.
+ */
+ public static boolean isRealIncomingCall(Call.State state) {
+ return (state == Call.State.INCOMING && !PhoneGlobals.getInstance().mCM.hasActiveFgCall());
+ }
+
+ private static boolean sVoipSupported = false;
+ static {
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ sVoipSupported = SipManager.isVoipSupported(app)
+ && app.getResources().getBoolean(com.android.internal.R.bool.config_built_in_sip_phone)
+ && app.getResources().getBoolean(com.android.internal.R.bool.config_voice_capable);
+ }
+
+ /**
+ * @return true if this device supports voice calls using the built-in SIP stack.
+ */
+ static boolean isVoipSupported() {
+ return sVoipSupported;
+ }
+
+ public static String getPresentationString(Context context, int presentation) {
+ String name = context.getString(R.string.unknown);
+ if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) {
+ name = context.getString(R.string.private_num);
+ } else if (presentation == PhoneConstants.PRESENTATION_PAYPHONE) {
+ name = context.getString(R.string.payphone);
+ }
+ return name;
+ }
+
+ public static void sendViewNotificationAsync(Context context, Uri contactUri) {
+ if (DBG) Log.d(LOG_TAG, "Send view notification to Contacts (uri: " + contactUri + ")");
+ Intent intent = new Intent("com.android.contacts.VIEW_NOTIFICATION", contactUri);
+ intent.setClassName("com.android.contacts",
+ "com.android.contacts.ViewNotificationService");
+ context.startService(intent);
+ }
+
+ //
+ // General phone and call state debugging/testing code
+ //
+
+ /* package */ static void dumpCallState(Phone phone) {
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ Log.d(LOG_TAG, "dumpCallState():");
+ Log.d(LOG_TAG, "- Phone: " + phone + ", name = " + phone.getPhoneName()
+ + ", state = " + phone.getState());
+
+ StringBuilder b = new StringBuilder(128);
+
+ Call call = phone.getForegroundCall();
+ b.setLength(0);
+ b.append(" - FG call: ").append(call.getState());
+ b.append(" isAlive ").append(call.getState().isAlive());
+ b.append(" isRinging ").append(call.getState().isRinging());
+ b.append(" isDialing ").append(call.getState().isDialing());
+ b.append(" isIdle ").append(call.isIdle());
+ b.append(" hasConnections ").append(call.hasConnections());
+ Log.d(LOG_TAG, b.toString());
+
+ call = phone.getBackgroundCall();
+ b.setLength(0);
+ b.append(" - BG call: ").append(call.getState());
+ b.append(" isAlive ").append(call.getState().isAlive());
+ b.append(" isRinging ").append(call.getState().isRinging());
+ b.append(" isDialing ").append(call.getState().isDialing());
+ b.append(" isIdle ").append(call.isIdle());
+ b.append(" hasConnections ").append(call.hasConnections());
+ Log.d(LOG_TAG, b.toString());
+
+ call = phone.getRingingCall();
+ b.setLength(0);
+ b.append(" - RINGING call: ").append(call.getState());
+ b.append(" isAlive ").append(call.getState().isAlive());
+ b.append(" isRinging ").append(call.getState().isRinging());
+ b.append(" isDialing ").append(call.getState().isDialing());
+ b.append(" isIdle ").append(call.isIdle());
+ b.append(" hasConnections ").append(call.hasConnections());
+ Log.d(LOG_TAG, b.toString());
+
+
+ final boolean hasRingingCall = !phone.getRingingCall().isIdle();
+ final boolean hasActiveCall = !phone.getForegroundCall().isIdle();
+ final boolean hasHoldingCall = !phone.getBackgroundCall().isIdle();
+ final boolean allLinesTaken = hasActiveCall && hasHoldingCall;
+ b.setLength(0);
+ b.append(" - hasRingingCall ").append(hasRingingCall);
+ b.append(" hasActiveCall ").append(hasActiveCall);
+ b.append(" hasHoldingCall ").append(hasHoldingCall);
+ b.append(" allLinesTaken ").append(allLinesTaken);
+ Log.d(LOG_TAG, b.toString());
+
+ // On CDMA phones, dump out the CdmaPhoneCallState too:
+ if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_CDMA) {
+ if (app.cdmaPhoneCallState != null) {
+ Log.d(LOG_TAG, " - CDMA call state: "
+ + app.cdmaPhoneCallState.getCurrentCallState());
+ } else {
+ Log.d(LOG_TAG, " - CDMA device, but null cdmaPhoneCallState!");
+ }
+ }
+
+ // Watch out: the isRinging() call below does NOT tell us anything
+ // about the state of the telephony layer; it merely tells us whether
+ // the Ringer manager is currently playing the ringtone.
+ boolean ringing = app.getRinger().isRinging();
+ Log.d(LOG_TAG, " - Ringer state: " + ringing);
+ }
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+
+ static void dumpCallManager() {
+ Call call;
+ CallManager cm = PhoneGlobals.getInstance().mCM;
+ StringBuilder b = new StringBuilder(128);
+
+
+
+ Log.d(LOG_TAG, "############### dumpCallManager() ##############");
+ // TODO: Don't log "cm" itself, since CallManager.toString()
+ // already spews out almost all this same information.
+ // We should fix CallManager.toString() to be more minimal, and
+ // use an explicit dumpState() method for the verbose dump.
+ // Log.d(LOG_TAG, "CallManager: " + cm
+ // + ", state = " + cm.getState());
+ Log.d(LOG_TAG, "CallManager: state = " + cm.getState());
+ b.setLength(0);
+ call = cm.getActiveFgCall();
+ b.append(" - FG call: ").append(cm.hasActiveFgCall()? "YES ": "NO ");
+ b.append(call);
+ b.append( " State: ").append(cm.getActiveFgCallState());
+ b.append( " Conn: ").append(cm.getFgCallConnections());
+ Log.d(LOG_TAG, b.toString());
+ b.setLength(0);
+ call = cm.getFirstActiveBgCall();
+ b.append(" - BG call: ").append(cm.hasActiveBgCall()? "YES ": "NO ");
+ b.append(call);
+ b.append( " State: ").append(cm.getFirstActiveBgCall().getState());
+ b.append( " Conn: ").append(cm.getBgCallConnections());
+ Log.d(LOG_TAG, b.toString());
+ b.setLength(0);
+ call = cm.getFirstActiveRingingCall();
+ b.append(" - RINGING call: ").append(cm.hasActiveRingingCall()? "YES ": "NO ");
+ b.append(call);
+ b.append( " State: ").append(cm.getFirstActiveRingingCall().getState());
+ Log.d(LOG_TAG, b.toString());
+
+
+
+ for (Phone phone : CallManager.getInstance().getAllPhones()) {
+ if (phone != null) {
+ Log.d(LOG_TAG, "Phone: " + phone + ", name = " + phone.getPhoneName()
+ + ", state = " + phone.getState());
+ b.setLength(0);
+ call = phone.getForegroundCall();
+ b.append(" - FG call: ").append(call);
+ b.append( " State: ").append(call.getState());
+ b.append( " Conn: ").append(call.hasConnections());
+ Log.d(LOG_TAG, b.toString());
+ b.setLength(0);
+ call = phone.getBackgroundCall();
+ b.append(" - BG call: ").append(call);
+ b.append( " State: ").append(call.getState());
+ b.append( " Conn: ").append(call.hasConnections());
+ Log.d(LOG_TAG, b.toString());b.setLength(0);
+ call = phone.getRingingCall();
+ b.append(" - RINGING call: ").append(call);
+ b.append( " State: ").append(call.getState());
+ b.append( " Conn: ").append(call.hasConnections());
+ Log.d(LOG_TAG, b.toString());
+ }
+ }
+
+ Log.d(LOG_TAG, "############## END dumpCallManager() ###############");
+ }
+
+ /**
+ * @return if the context is in landscape orientation.
+ */
+ public static boolean isLandscape(Context context) {
+ return context.getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_LANDSCAPE;
+ }
+}
diff --git a/src/com/android/phone/ProcessOutgoingCallTest.java b/src/com/android/phone/ProcessOutgoingCallTest.java
new file mode 100644
index 0000000..c76fb43
--- /dev/null
+++ b/src/com/android/phone/ProcessOutgoingCallTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2008 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.phone;
+
+import android.app.SearchManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * ProcessOutgoingCallTest tests {@link OutgoingCallBroadcaster} by performing
+ * a couple of simple modifications to outgoing calls, and by printing log
+ * messages for each call.
+ */
+public class ProcessOutgoingCallTest extends BroadcastReceiver {
+ private static final String TAG = "ProcessOutgoingCallTest";
+ private static final String AREACODE = "617";
+
+ private static final boolean LOGV = false;
+
+ private static final boolean REDIRECT_411_TO_GOOG411 = true;
+ private static final boolean SEVEN_DIGIT_DIALING = true;
+ private static final boolean POUND_POUND_SEARCH = true;
+ private static final boolean BLOCK_555 = true;
+
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_NEW_OUTGOING_CALL)) {
+ String number = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
+ if (LOGV) Log.v(TAG, "Received intent " + intent + " (number = " + number + ".");
+ /* Example of how to redirect calls from one number to another. */
+ if (REDIRECT_411_TO_GOOG411 && number.equals("411")) {
+ setResultData("18004664411");
+ }
+
+ /* Example of how to modify the phone number in flight. */
+ if (SEVEN_DIGIT_DIALING && number.length() == 7) {
+ setResultData(AREACODE + number);
+ }
+
+ /* Example of how to route a call to another Application. */
+ if (POUND_POUND_SEARCH && number.startsWith("##")) {
+ Intent newIntent = new Intent(Intent.ACTION_SEARCH);
+ newIntent.putExtra(SearchManager.QUERY, number.substring(2));
+ newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ context.startActivity(newIntent);
+ setResultData(null);
+ }
+
+ /* Example of how to deny calls to a particular number.
+ * Note that no UI is displayed to the user -- the call simply
+ * does not happen. It is the application's responaibility to
+ * explain this to the user. */
+ int length = number.length();
+ if (BLOCK_555 && length >= 7) {
+ String exchange = number.substring(length - 7, length - 4);
+ Log.v(TAG, "exchange = " + exchange);
+ if (exchange.equals("555")) {
+ setResultData(null);
+ }
+ }
+ }
+ }
+}
diff --git a/src/com/android/phone/Profiler.java b/src/com/android/phone/Profiler.java
new file mode 100644
index 0000000..234073c
--- /dev/null
+++ b/src/com/android/phone/Profiler.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewParent;
+import android.view.Window;
+
+/**
+ * Profiling utilities for the Phone app.
+ */
+public class Profiler {
+ private static final String LOG_TAG = PhoneGlobals.LOG_TAG;
+
+ // Let the compiler optimize all this code out unless we're actively
+ // doing profiling runs.
+ // TODO: Instead of doing all these "if (PROFILE)" checks here, every
+ // place that *calls* any of these methods should check the value of
+ // Profiler.PROFILE first, so the method calls will get optimized out
+ // too.
+ private static final boolean PROFILE = false;
+
+ static long sTimeCallScreenRequested;
+ static long sTimeCallScreenOnCreate;
+ static long sTimeCallScreenCreated;
+
+ // TODO: Clean up any usage of these times. (There's no "incoming call
+ // panel" in the Phone UI any more; incoming calls just go straight to the
+ // regular in-call UI.)
+ static long sTimeIncomingCallPanelRequested;
+ static long sTimeIncomingCallPanelOnCreate;
+ static long sTimeIncomingCallPanelCreated;
+
+ /** This class is never instantiated. */
+ private Profiler() {
+ }
+
+ static void profileViewCreate(Window win, String tag) {
+ if (false) {
+ ViewParent p = (ViewParent) win.getDecorView();
+ while (p instanceof View) {
+ p = ((View) p).getParent();
+ }
+ //((ViewRoot)p).profile();
+ //((ViewRoot)p).setProfileTag(tag);
+ }
+ }
+
+ static void callScreenRequested() {
+ if (PROFILE) {
+ sTimeCallScreenRequested = SystemClock.uptimeMillis();
+ }
+ }
+
+ static void callScreenOnCreate() {
+ if (PROFILE) {
+ sTimeCallScreenOnCreate = SystemClock.uptimeMillis();
+ }
+ }
+
+ static void callScreenCreated() {
+ if (PROFILE) {
+ sTimeCallScreenCreated = SystemClock.uptimeMillis();
+ dumpCallScreenStat();
+ }
+ }
+
+ private static void dumpCallScreenStat() {
+ if (PROFILE) {
+ log(">>> call screen perf stats <<<");
+ log(">>> request -> onCreate = " +
+ (sTimeCallScreenOnCreate - sTimeCallScreenRequested));
+ log(">>> onCreate -> created = " +
+ (sTimeCallScreenCreated - sTimeCallScreenOnCreate));
+ }
+ }
+
+ static void incomingCallPanelRequested() {
+ if (PROFILE) {
+ sTimeIncomingCallPanelRequested = SystemClock.uptimeMillis();
+ }
+ }
+
+ static void incomingCallPanelOnCreate() {
+ if (PROFILE) {
+ sTimeIncomingCallPanelOnCreate = SystemClock.uptimeMillis();
+ }
+ }
+
+ static void incomingCallPanelCreated() {
+ if (PROFILE) {
+ sTimeIncomingCallPanelCreated = SystemClock.uptimeMillis();
+ dumpIncomingCallPanelStat();
+ }
+ }
+
+ private static void dumpIncomingCallPanelStat() {
+ if (PROFILE) {
+ log(">>> incoming call panel perf stats <<<");
+ log(">>> request -> onCreate = " +
+ (sTimeIncomingCallPanelOnCreate - sTimeIncomingCallPanelRequested));
+ log(">>> onCreate -> created = " +
+ (sTimeIncomingCallPanelCreated - sTimeIncomingCallPanelOnCreate));
+ }
+ }
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, "[Profiler] " + msg);
+ }
+}
diff --git a/src/com/android/phone/RespondViaSmsManager.java b/src/com/android/phone/RespondViaSmsManager.java
new file mode 100644
index 0000000..c851471
--- /dev/null
+++ b/src/com/android/phone/RespondViaSmsManager.java
@@ -0,0 +1,874 @@
+/*
+ * Copyright (C) 2011 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.phone;
+
+import android.app.ActivityManager;
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.preference.EditTextPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.BaseAdapter;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.internal.telephony.Call;
+import com.android.internal.telephony.Connection;
+import com.android.internal.telephony.PhoneConstants;
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Helper class to manage the "Respond via Message" feature for incoming calls.
+ *
+ * @see InCallScreen.internalRespondViaSms()
+ */
+public class RespondViaSmsManager {
+ private static final String TAG = "RespondViaSmsManager";
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+ // Do not check in with VDBG = true, since that may write PII to the system log.
+ private static final boolean VDBG = false;
+
+ private static final String PERMISSION_SEND_RESPOND_VIA_MESSAGE =
+ "android.permission.SEND_RESPOND_VIA_MESSAGE";
+
+ private int mIconSize = -1;
+
+ /**
+ * Reference to the InCallScreen activity that owns us. This may be
+ * null if we haven't been initialized yet *or* after the InCallScreen
+ * activity has been destroyed.
+ */
+ private InCallScreen mInCallScreen;
+
+ /**
+ * The popup showing the list of canned responses.
+ *
+ * This is an AlertDialog containing a ListView showing the possible
+ * choices. This may be null if the InCallScreen hasn't ever called
+ * showRespondViaSmsPopup() yet, or if the popup was visible once but
+ * then got dismissed.
+ */
+ private Dialog mCannedResponsePopup;
+
+ /**
+ * The popup dialog allowing the user to chose which app handles respond-via-sms.
+ *
+ * An AlertDialog showing the Resolve-App UI resource from the framework wchih we then fill in
+ * with the appropriate data set. Can be null when not visible.
+ */
+ private Dialog mPackageSelectionPopup;
+
+ /** The array of "canned responses"; see loadCannedResponses(). */
+ private String[] mCannedResponses;
+
+ /** SharedPreferences file name for our persistent settings. */
+ private static final String SHARED_PREFERENCES_NAME = "respond_via_sms_prefs";
+
+ // Preference keys for the 4 "canned responses"; see RespondViaSmsManager$Settings.
+ // Since (for now at least) the number of messages is fixed at 4, and since
+ // SharedPreferences can't deal with arrays anyway, just store the messages
+ // as 4 separate strings.
+ private static final int NUM_CANNED_RESPONSES = 4;
+ private static final String KEY_CANNED_RESPONSE_PREF_1 = "canned_response_pref_1";
+ private static final String KEY_CANNED_RESPONSE_PREF_2 = "canned_response_pref_2";
+ private static final String KEY_CANNED_RESPONSE_PREF_3 = "canned_response_pref_3";
+ private static final String KEY_CANNED_RESPONSE_PREF_4 = "canned_response_pref_4";
+ private static final String KEY_PREFERRED_PACKAGE = "preferred_package_pref";
+ private static final String KEY_INSTANT_TEXT_DEFAULT_COMPONENT = "instant_text_def_component";
+
+ /**
+ * RespondViaSmsManager constructor.
+ */
+ public RespondViaSmsManager() {
+ }
+
+ public void setInCallScreenInstance(InCallScreen inCallScreen) {
+ mInCallScreen = inCallScreen;
+
+ if (mInCallScreen != null) {
+ // Prefetch shared preferences to make the first canned response lookup faster
+ // (and to prevent StrictMode violation)
+ mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ }
+ }
+
+ /**
+ * Brings up the "Respond via SMS" popup for an incoming call.
+ *
+ * @param ringingCall the current incoming call
+ */
+ public void showRespondViaSmsPopup(Call ringingCall) {
+ if (DBG) log("showRespondViaSmsPopup()...");
+
+ // Very quick succession of clicks can cause this to run twice.
+ // Stop here to avoid creating more than one popup.
+ if (isShowingPopup()) {
+ if (DBG) log("Skip showing popup when one is already shown.");
+ return;
+ }
+
+ ListView lv = new ListView(mInCallScreen);
+
+ // Refresh the array of "canned responses".
+ mCannedResponses = loadCannedResponses();
+
+ // Build the list: start with the canned responses, but manually add
+ // the write-your-own option as the last choice.
+ int numPopupItems = mCannedResponses.length + 1;
+ String[] popupItems = Arrays.copyOf(mCannedResponses, numPopupItems);
+ popupItems[numPopupItems - 1] = mInCallScreen.getResources()
+ .getString(R.string.respond_via_sms_custom_message);
+
+ ArrayAdapter<String> adapter =
+ new ArrayAdapter<String>(mInCallScreen,
+ android.R.layout.simple_list_item_1,
+ android.R.id.text1,
+ popupItems);
+ lv.setAdapter(adapter);
+
+ // Create a RespondViaSmsItemClickListener instance to handle item
+ // clicks from the popup.
+ // (Note we create a fresh instance for each incoming call, and
+ // stash away the call's phone number, since we can't necessarily
+ // assume this call will still be ringing when the user finally
+ // chooses a response.)
+
+ Connection c = ringingCall.getLatestConnection();
+ if (VDBG) log("- connection: " + c);
+
+ if (c == null) {
+ // Uh oh -- the "ringingCall" doesn't have any connections any more.
+ // (In other words, it's no longer ringing.) This is rare, but can
+ // happen if the caller hangs up right at the exact moment the user
+ // selects the "Respond via SMS" option.
+ // There's nothing to do here (since the incoming call is gone),
+ // so just bail out.
+ Log.i(TAG, "showRespondViaSmsPopup: null connection; bailing out...");
+ return;
+ }
+
+ // TODO: at this point we probably should re-check c.getAddress()
+ // and c.getNumberPresentation() for validity. (i.e. recheck the
+ // same cases in InCallTouchUi.showIncomingCallWidget() where we
+ // should have disallowed the "respond via SMS" feature in the
+ // first place.)
+
+ String phoneNumber = c.getAddress();
+ if (VDBG) log("- phoneNumber: " + phoneNumber);
+ lv.setOnItemClickListener(new RespondViaSmsItemClickListener(phoneNumber));
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen)
+ .setCancelable(true)
+ .setOnCancelListener(new RespondViaSmsCancelListener())
+ .setView(lv);
+ mCannedResponsePopup = builder.create();
+ mCannedResponsePopup.show();
+ }
+
+ /**
+ * Dismiss currently visible popups.
+ *
+ * This is safe to call even if the popup is already dismissed, and
+ * even if you never called showRespondViaSmsPopup() in the first
+ * place.
+ */
+ public void dismissPopup() {
+ if (mCannedResponsePopup != null) {
+ mCannedResponsePopup.dismiss(); // safe even if already dismissed
+ mCannedResponsePopup = null;
+ }
+ if (mPackageSelectionPopup != null) {
+ mPackageSelectionPopup.dismiss();
+ mPackageSelectionPopup = null;
+ }
+ }
+
+ public boolean isShowingPopup() {
+ return (mCannedResponsePopup != null && mCannedResponsePopup.isShowing())
+ || (mPackageSelectionPopup != null && mPackageSelectionPopup.isShowing());
+ }
+
+ /**
+ * OnItemClickListener for the "Respond via SMS" popup.
+ */
+ public class RespondViaSmsItemClickListener implements AdapterView.OnItemClickListener {
+ // Phone number to send the SMS to.
+ private String mPhoneNumber;
+
+ public RespondViaSmsItemClickListener(String phoneNumber) {
+ mPhoneNumber = phoneNumber;
+ }
+
+ /**
+ * Handles the user selecting an item from the popup.
+ */
+ @Override
+ public void onItemClick(AdapterView<?> parent, // The ListView
+ View view, // The TextView that was clicked
+ int position,
+ long id) {
+ if (DBG) log("RespondViaSmsItemClickListener.onItemClick(" + position + ")...");
+ String message = (String) parent.getItemAtPosition(position);
+ if (VDBG) log("- message: '" + message + "'");
+
+ // The "Custom" choice is a special case.
+ // (For now, it's guaranteed to be the last item.)
+ if (position == (parent.getCount() - 1)) {
+ // Take the user to the standard SMS compose UI.
+ launchSmsCompose(mPhoneNumber);
+ onPostMessageSent();
+ } else {
+ sendTextToDefaultActivity(mPhoneNumber, message);
+ }
+ }
+ }
+
+
+ /**
+ * OnCancelListener for the "Respond via SMS" popup.
+ */
+ public class RespondViaSmsCancelListener implements DialogInterface.OnCancelListener {
+ public RespondViaSmsCancelListener() {
+ }
+
+ /**
+ * Handles the user canceling the popup, either by touching
+ * outside the popup or by pressing Back.
+ */
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (DBG) log("RespondViaSmsCancelListener.onCancel()...");
+
+ dismissPopup();
+
+ final PhoneConstants.State state = PhoneGlobals.getInstance().mCM.getState();
+ if (state == PhoneConstants.State.IDLE) {
+ // This means the incoming call is already hung up when the user chooses not to
+ // use "Respond via SMS" feature. Let's just exit the whole in-call screen.
+ PhoneGlobals.getInstance().dismissCallScreen();
+ } else {
+
+ // If the user cancels the popup, this presumably means that
+ // they didn't actually mean to bring up the "Respond via SMS"
+ // UI in the first place (and instead want to go back to the
+ // state where they can either answer or reject the call.)
+ // So restart the ringer and bring back the regular incoming
+ // call UI.
+
+ // This will have no effect if the incoming call isn't still ringing.
+ PhoneGlobals.getInstance().notifier.restartRinger();
+
+ // We hid the GlowPadView widget way back in
+ // InCallTouchUi.onTrigger(), when the user first selected
+ // the "SMS" trigger.
+ //
+ // To bring it back, just force the entire InCallScreen to
+ // update itself based on the current telephony state.
+ // (Assuming the incoming call is still ringing, this will
+ // cause the incoming call widget to reappear.)
+ mInCallScreen.requestUpdateScreen();
+ }
+ }
+ }
+
+ private void sendTextToDefaultActivity(String phoneNumber, String message) {
+ if (DBG) log("sendTextToDefaultActivity()...");
+ final PackageManager packageManager = mInCallScreen.getPackageManager();
+
+ // Check to see if the default component to receive this intent is already saved
+ // and check to see if it still has the corrent permissions.
+ final SharedPreferences prefs = mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME,
+ Context.MODE_PRIVATE);
+ final String flattenedName = prefs.getString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, null);
+ if (flattenedName != null) {
+ if (DBG) log("Default package was found." + flattenedName);
+
+ final ComponentName componentName = ComponentName.unflattenFromString(flattenedName);
+ ServiceInfo serviceInfo = null;
+ try {
+ serviceInfo = packageManager.getServiceInfo(componentName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Default service does not have permission.");
+ }
+
+ if (serviceInfo != null &&
+ PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
+ sendTextAndExit(phoneNumber, message, componentName, false);
+ return;
+ } else {
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
+ editor.apply();
+ }
+ }
+
+ final ArrayList<ComponentName> componentsWithPermission =
+ getPackagesWithInstantTextPermission();
+
+ final int size = componentsWithPermission.size();
+ if (size == 0) {
+ Log.e(TAG, "No appropriate package receiving the Intent. Don't send anything");
+ onPostMessageSent();
+ } else if (size == 1) {
+ sendTextAndExit(phoneNumber, message, componentsWithPermission.get(0), false);
+ } else {
+ showPackageSelectionDialog(phoneNumber, message, componentsWithPermission);
+ }
+ }
+
+ /**
+ * Queries the System to determine what packages contain services that can handle the instant
+ * text response Action AND have permissions to do so.
+ */
+ private ArrayList<ComponentName> getPackagesWithInstantTextPermission() {
+ PackageManager packageManager = mInCallScreen.getPackageManager();
+
+ ArrayList<ComponentName> componentsWithPermission = Lists.newArrayList();
+
+ // Get list of all services set up to handle the Instant Text intent.
+ final List<ResolveInfo> infos = packageManager.queryIntentServices(
+ getInstantTextIntent("", null, null), 0);
+
+ // Collect all the valid services
+ for (ResolveInfo resolveInfo : infos) {
+ final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (serviceInfo == null) {
+ Log.w(TAG, "Ignore package without proper service.");
+ continue;
+ }
+
+ // A Service is valid only if it requires the permission
+ // PERMISSION_SEND_RESPOND_VIA_MESSAGE
+ if (PERMISSION_SEND_RESPOND_VIA_MESSAGE.equals(serviceInfo.permission)) {
+ componentsWithPermission.add(new ComponentName(serviceInfo.packageName,
+ serviceInfo.name));
+ }
+ }
+
+ return componentsWithPermission;
+ }
+
+ private void showPackageSelectionDialog(String phoneNumber, String message,
+ List<ComponentName> components) {
+ if (DBG) log("showPackageSelectionDialog()...");
+
+ dismissPopup();
+
+ BaseAdapter adapter = new PackageSelectionAdapter(mInCallScreen, components);
+
+ PackageClickListener clickListener =
+ new PackageClickListener(phoneNumber, message, components);
+
+ final CharSequence title = mInCallScreen.getResources().getText(
+ com.android.internal.R.string.whichApplication);
+ LayoutInflater inflater =
+ (LayoutInflater) mInCallScreen.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ final View view = inflater.inflate(com.android.internal.R.layout.always_use_checkbox, null);
+ final CheckBox alwaysUse = (CheckBox) view.findViewById(
+ com.android.internal.R.id.alwaysUse);
+ alwaysUse.setText(com.android.internal.R.string.alwaysUse);
+ alwaysUse.setOnCheckedChangeListener(clickListener);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(mInCallScreen)
+ .setTitle(title)
+ .setCancelable(true)
+ .setOnCancelListener(new RespondViaSmsCancelListener())
+ .setAdapter(adapter, clickListener)
+ .setView(view);
+ mPackageSelectionPopup = builder.create();
+ mPackageSelectionPopup.show();
+ }
+
+ private class PackageSelectionAdapter extends BaseAdapter {
+ private final LayoutInflater mInflater;
+ private final List<ComponentName> mComponents;
+
+ public PackageSelectionAdapter(Context context, List<ComponentName> components) {
+ mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ mComponents = components;
+ }
+
+ @Override
+ public int getCount() {
+ return mComponents.size();
+ }
+
+ @Override
+ public Object getItem(int position) {
+ return mComponents.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ if (convertView == null) {
+ convertView = mInflater.inflate(
+ com.android.internal.R.layout.resolve_list_item, parent, false);
+ }
+
+ final ComponentName component = mComponents.get(position);
+ final String packageName = component.getPackageName();
+ final PackageManager packageManager = mInCallScreen.getPackageManager();
+
+ // Set the application label
+ final TextView text = (TextView) convertView.findViewById(
+ com.android.internal.R.id.text1);
+ final TextView text2 = (TextView) convertView.findViewById(
+ com.android.internal.R.id.text2);
+
+ // Reset any previous values
+ text.setText("");
+ text2.setVisibility(View.GONE);
+ try {
+ final ApplicationInfo appInfo = packageManager.getApplicationInfo(packageName, 0);
+ final CharSequence label = packageManager.getApplicationLabel(appInfo);
+ if (label != null) {
+ text.setText(label);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Failed to load app label because package was not found.");
+ }
+
+ // Set the application icon
+ final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon);
+ Drawable drawable = null;
+ try {
+ drawable = mInCallScreen.getPackageManager().getApplicationIcon(packageName);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.w(TAG, "Failed to load icon because it wasn't found.");
+ }
+ if (drawable == null) {
+ drawable = mInCallScreen.getPackageManager().getDefaultActivityIcon();
+ }
+ icon.setImageDrawable(drawable);
+ ViewGroup.LayoutParams lp = (ViewGroup.LayoutParams) icon.getLayoutParams();
+ lp.width = lp.height = getIconSize();
+
+ return convertView;
+ }
+
+ }
+
+ private class PackageClickListener implements DialogInterface.OnClickListener,
+ CompoundButton.OnCheckedChangeListener {
+ /** Phone number to send the SMS to. */
+ final private String mPhoneNumber;
+ final private String mMessage;
+ final private List<ComponentName> mComponents;
+ private boolean mMakeDefault = false;
+
+ public PackageClickListener(String phoneNumber, String message,
+ List<ComponentName> components) {
+ mPhoneNumber = phoneNumber;
+ mMessage = message;
+ mComponents = components;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ ComponentName component = mComponents.get(which);
+ sendTextAndExit(mPhoneNumber, mMessage, component, mMakeDefault);
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ Log.i(TAG, "mMakeDefault : " + isChecked);
+ mMakeDefault = isChecked;
+ }
+ }
+
+ private void sendTextAndExit(String phoneNumber, String message, ComponentName component,
+ boolean setDefaultComponent) {
+ // Send the selected message immediately with no user interaction.
+ sendText(phoneNumber, message, component);
+
+ if (setDefaultComponent) {
+ final SharedPreferences prefs = mInCallScreen.getSharedPreferences(
+ SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ prefs.edit()
+ .putString(KEY_INSTANT_TEXT_DEFAULT_COMPONENT, component.flattenToString())
+ .apply();
+ }
+
+ // ...and show a brief confirmation to the user (since
+ // otherwise it's hard to be sure that anything actually
+ // happened.)
+ final Resources res = mInCallScreen.getResources();
+ final String formatString = res.getString(R.string.respond_via_sms_confirmation_format);
+ final String confirmationMsg = String.format(formatString, phoneNumber);
+ Toast.makeText(mInCallScreen,
+ confirmationMsg,
+ Toast.LENGTH_LONG).show();
+
+ // TODO: If the device is locked, this toast won't actually ever
+ // be visible! (That's because we're about to dismiss the call
+ // screen, which means that the device will return to the
+ // keyguard. But toasts aren't visible on top of the keyguard.)
+ // Possible fixes:
+ // (1) Is it possible to allow a specific Toast to be visible
+ // on top of the keyguard?
+ // (2) Artifically delay the dismissCallScreen() call by 3
+ // seconds to allow the toast to be seen?
+ // (3) Don't use a toast at all; instead use a transient state
+ // of the InCallScreen (perhaps via the InCallUiState
+ // progressIndication feature), and have that state be
+ // visible for 3 seconds before calling dismissCallScreen().
+
+ onPostMessageSent();
+ }
+
+ /**
+ * Sends a text message without any interaction from the user.
+ */
+ private void sendText(String phoneNumber, String message, ComponentName component) {
+ if (VDBG) log("sendText: number "
+ + phoneNumber + ", message '" + message + "'");
+
+ mInCallScreen.startService(getInstantTextIntent(phoneNumber, message, component));
+ }
+
+ private void onPostMessageSent() {
+ // At this point the user is done dealing with the incoming call, so
+ // there's no reason to keep it around. (It's also confusing for
+ // the "incoming call" icon in the status bar to still be visible.)
+ // So reject the call now.
+ mInCallScreen.hangupRingingCall();
+
+ dismissPopup();
+
+ final PhoneConstants.State state = PhoneGlobals.getInstance().mCM.getState();
+ if (state == PhoneConstants.State.IDLE) {
+ // There's no other phone call to interact. Exit the entire in-call screen.
+ PhoneGlobals.getInstance().dismissCallScreen();
+ } else {
+ // The user is still in the middle of other phone calls, so we should keep the
+ // in-call screen.
+ mInCallScreen.requestUpdateScreen();
+ }
+ }
+
+ /**
+ * Brings up the standard SMS compose UI.
+ */
+ private void launchSmsCompose(String phoneNumber) {
+ if (VDBG) log("launchSmsCompose: number " + phoneNumber);
+
+ Intent intent = getInstantTextIntent(phoneNumber, null, null);
+
+ if (VDBG) log("- Launching SMS compose UI: " + intent);
+ mInCallScreen.startService(intent);
+ }
+
+ /**
+ * @param phoneNumber Must not be null.
+ * @param message Can be null. If message is null, the returned Intent will be configured to
+ * launch the SMS compose UI. If non-null, the returned Intent will cause the specified message
+ * to be sent with no interaction from the user.
+ * @param component The component that should handle this intent.
+ * @return Service Intent for the instant response.
+ */
+ private static Intent getInstantTextIntent(String phoneNumber, String message,
+ ComponentName component) {
+ final Uri uri = Uri.fromParts(Constants.SCHEME_SMSTO, phoneNumber, null);
+ Intent intent = new Intent(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE, uri);
+ if (message != null) {
+ intent.putExtra(Intent.EXTRA_TEXT, message);
+ } else {
+ intent.putExtra("exit_on_sent", true);
+ intent.putExtra("showUI", true);
+ }
+ if (component != null) {
+ intent.setComponent(component);
+ }
+ return intent;
+ }
+
+ /**
+ * Settings activity under "Call settings" to let you manage the
+ * canned responses; see respond_via_sms_settings.xml
+ */
+ public static class Settings extends PreferenceActivity
+ implements Preference.OnPreferenceChangeListener {
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ if (DBG) log("Settings: onCreate()...");
+
+ getPreferenceManager().setSharedPreferencesName(SHARED_PREFERENCES_NAME);
+
+ // This preference screen is ultra-simple; it's just 4 plain
+ // <EditTextPreference>s, one for each of the 4 "canned responses".
+ //
+ // The only nontrivial thing we do here is copy the text value of
+ // each of those EditTextPreferences and use it as the preference's
+ // "title" as well, so that the user will immediately see all 4
+ // strings when they arrive here.
+ //
+ // Also, listen for change events (since we'll need to update the
+ // title any time the user edits one of the strings.)
+
+ addPreferencesFromResource(R.xml.respond_via_sms_settings);
+
+ EditTextPreference pref;
+ pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_1);
+ pref.setTitle(pref.getText());
+ pref.setOnPreferenceChangeListener(this);
+
+ pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_2);
+ pref.setTitle(pref.getText());
+ pref.setOnPreferenceChangeListener(this);
+
+ pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_3);
+ pref.setTitle(pref.getText());
+ pref.setOnPreferenceChangeListener(this);
+
+ pref = (EditTextPreference) findPreference(KEY_CANNED_RESPONSE_PREF_4);
+ pref.setTitle(pref.getText());
+ pref.setOnPreferenceChangeListener(this);
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ // android.R.id.home will be triggered in onOptionsItemSelected()
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ // Preference.OnPreferenceChangeListener implementation
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ if (DBG) log("onPreferenceChange: key = " + preference.getKey());
+ if (VDBG) log(" preference = '" + preference + "'");
+ if (VDBG) log(" newValue = '" + newValue + "'");
+
+ EditTextPreference pref = (EditTextPreference) preference;
+
+ // Copy the new text over to the title, just like in onCreate().
+ // (Watch out: onPreferenceChange() is called *before* the
+ // Preference itself gets updated, so we need to use newValue here
+ // rather than pref.getText().)
+ pref.setTitle((String) newValue);
+
+ return true; // means it's OK to update the state of the Preference with the new value
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ switch (itemId) {
+ case android.R.id.home:
+ // See ActionBar#setDisplayHomeAsUpEnabled()
+ CallFeaturesSetting.goUpToTopLevelSetting(this);
+ return true;
+ case R.id.respond_via_message_reset:
+ // Reset the preferences settings
+ SharedPreferences prefs = getSharedPreferences(
+ SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.remove(KEY_INSTANT_TEXT_DEFAULT_COMPONENT);
+ editor.apply();
+
+ return true;
+ default:
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.respond_via_message_settings_menu, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+ }
+
+ /**
+ * Read the (customizable) canned responses from SharedPreferences,
+ * or from defaults if the user has never actually brought up
+ * the Settings UI.
+ *
+ * This method does disk I/O (reading the SharedPreferences file)
+ * so don't call it from the main thread.
+ *
+ * @see RespondViaSmsManager.Settings
+ */
+ private String[] loadCannedResponses() {
+ if (DBG) log("loadCannedResponses()...");
+
+ SharedPreferences prefs = mInCallScreen.getSharedPreferences(SHARED_PREFERENCES_NAME,
+ Context.MODE_PRIVATE);
+ final Resources res = mInCallScreen.getResources();
+
+ String[] responses = new String[NUM_CANNED_RESPONSES];
+
+ // Note the default values here must agree with the corresponding
+ // android:defaultValue attributes in respond_via_sms_settings.xml.
+
+ responses[0] = prefs.getString(KEY_CANNED_RESPONSE_PREF_1,
+ res.getString(R.string.respond_via_sms_canned_response_1));
+ responses[1] = prefs.getString(KEY_CANNED_RESPONSE_PREF_2,
+ res.getString(R.string.respond_via_sms_canned_response_2));
+ responses[2] = prefs.getString(KEY_CANNED_RESPONSE_PREF_3,
+ res.getString(R.string.respond_via_sms_canned_response_3));
+ responses[3] = prefs.getString(KEY_CANNED_RESPONSE_PREF_4,
+ res.getString(R.string.respond_via_sms_canned_response_4));
+ return responses;
+ }
+
+ /**
+ * @return true if the "Respond via SMS" feature should be enabled
+ * for the specified incoming call.
+ *
+ * The general rule is that we *do* allow "Respond via SMS" except for
+ * the few (relatively rare) cases where we know for sure it won't
+ * work, namely:
+ * - a bogus or blank incoming number
+ * - a call from a SIP address
+ * - a "call presentation" that doesn't allow the number to be revealed
+ *
+ * In all other cases, we allow the user to respond via SMS.
+ *
+ * Note that this behavior isn't perfect; for example we have no way
+ * to detect whether the incoming call is from a landline (with most
+ * networks at least), so we still enable this feature even though
+ * SMSes to that number will silently fail.
+ */
+ public static boolean allowRespondViaSmsForCall(Context context, Call ringingCall) {
+ if (DBG) log("allowRespondViaSmsForCall(" + ringingCall + ")...");
+
+ // First some basic sanity checks:
+ if (ringingCall == null) {
+ Log.w(TAG, "allowRespondViaSmsForCall: null ringingCall!");
+ return false;
+ }
+ if (!ringingCall.isRinging()) {
+ // The call is in some state other than INCOMING or WAITING!
+ // (This should almost never happen, but it *could*
+ // conceivably happen if the ringing call got disconnected by
+ // the network just *after* we got it from the CallManager.)
+ Log.w(TAG, "allowRespondViaSmsForCall: ringingCall not ringing! state = "
+ + ringingCall.getState());
+ return false;
+ }
+ Connection conn = ringingCall.getLatestConnection();
+ if (conn == null) {
+ // The call doesn't have any connections! (Again, this can
+ // happen if the ringing call disconnects at the exact right
+ // moment, but should almost never happen in practice.)
+ Log.w(TAG, "allowRespondViaSmsForCall: null Connection!");
+ return false;
+ }
+
+ // Check the incoming number:
+ final String number = conn.getAddress();
+ if (DBG) log("- number: '" + number + "'");
+ if (TextUtils.isEmpty(number)) {
+ Log.w(TAG, "allowRespondViaSmsForCall: no incoming number!");
+ return false;
+ }
+ if (PhoneNumberUtils.isUriNumber(number)) {
+ // The incoming number is actually a URI (i.e. a SIP address),
+ // not a regular PSTN phone number, and we can't send SMSes to
+ // SIP addresses.
+ // (TODO: That might still be possible eventually, though. Is
+ // there some SIP-specific equivalent to sending a text message?)
+ Log.i(TAG, "allowRespondViaSmsForCall: incoming 'number' is a SIP address.");
+ return false;
+ }
+
+ // Finally, check the "call presentation":
+ int presentation = conn.getNumberPresentation();
+ if (DBG) log("- presentation: " + presentation);
+ if (presentation == PhoneConstants.PRESENTATION_RESTRICTED) {
+ // PRESENTATION_RESTRICTED means "caller-id blocked".
+ // The user isn't allowed to see the number in the first
+ // place, so obviously we can't let you send an SMS to it.
+ Log.i(TAG, "allowRespondViaSmsForCall: PRESENTATION_RESTRICTED.");
+ return false;
+ }
+
+ // Allow the feature only when there's a destination for it.
+ if (context.getPackageManager().resolveService(getInstantTextIntent(number, null, null) , 0)
+ == null) {
+ return false;
+ }
+
+ // TODO: with some carriers (in certain countries) you *can* actually
+ // tell whether a given number is a mobile phone or not. So in that
+ // case we could potentially return false here if the incoming call is
+ // from a land line.
+
+ // If none of the above special cases apply, it's OK to enable the
+ // "Respond via SMS" feature.
+ return true;
+ }
+
+ private int getIconSize() {
+ if (mIconSize < 0) {
+ final ActivityManager am =
+ (ActivityManager) mInCallScreen.getSystemService(Context.ACTIVITY_SERVICE);
+ mIconSize = am.getLauncherLargeIconSize();
+ }
+
+ return mIconSize;
+ }
+
+
+ private static void log(String msg) {
+ Log.d(TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/Ringer.java b/src/com/android/phone/Ringer.java
new file mode 100644
index 0000000..a882490
--- /dev/null
+++ b/src/com/android/phone/Ringer.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.media.Ringtone;
+import android.media.RingtoneManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.IPowerManager;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.SystemProperties;
+import android.os.SystemVibrator;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.internal.telephony.Phone;
+/**
+ * Ringer manager for the Phone app.
+ */
+public class Ringer {
+ private static final String LOG_TAG = "Ringer";
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+
+ private static final int PLAY_RING_ONCE = 1;
+ private static final int STOP_RING = 3;
+
+ private static final int VIBRATE_LENGTH = 1000; // ms
+ private static final int PAUSE_LENGTH = 1000; // ms
+
+ /** The singleton instance. */
+ private static Ringer sInstance;
+
+ // Uri for the ringtone.
+ Uri mCustomRingtoneUri = Settings.System.DEFAULT_RINGTONE_URI;
+
+ Ringtone mRingtone;
+ Vibrator mVibrator;
+ IPowerManager mPowerManager;
+ volatile boolean mContinueVibrating;
+ VibratorThread mVibratorThread;
+ Context mContext;
+ private Worker mRingThread;
+ private Handler mRingHandler;
+ private long mFirstRingEventTime = -1;
+ private long mFirstRingStartTime = -1;
+
+ /**
+ * Initialize the singleton Ringer instance.
+ * This is only done once, at startup, from PhoneApp.onCreate().
+ */
+ /* package */ static Ringer init(Context context) {
+ synchronized (Ringer.class) {
+ if (sInstance == null) {
+ sInstance = new Ringer(context);
+ } else {
+ Log.wtf(LOG_TAG, "init() called multiple times! sInstance = " + sInstance);
+ }
+ return sInstance;
+ }
+ }
+
+ /** Private constructor; @see init() */
+ private Ringer(Context context) {
+ mContext = context;
+ mPowerManager = IPowerManager.Stub.asInterface(ServiceManager.getService(Context.POWER_SERVICE));
+ // We don't rely on getSystemService(Context.VIBRATOR_SERVICE) to make sure this
+ // vibrator object will be isolated from others.
+ mVibrator = new SystemVibrator(context);
+ }
+
+ /**
+ * After a radio technology change, e.g. from CDMA to GSM or vice versa,
+ * the Context of the Ringer has to be updated. This is done by that function.
+ *
+ * @parameter Phone, the new active phone for the appropriate radio
+ * technology
+ */
+ void updateRingerContextAfterRadioTechnologyChange(Phone phone) {
+ if(DBG) Log.d(LOG_TAG, "updateRingerContextAfterRadioTechnologyChange...");
+ mContext = phone.getContext();
+ }
+
+ /**
+ * @return true if we're playing a ringtone and/or vibrating
+ * to indicate that there's an incoming call.
+ * ("Ringing" here is used in the general sense. If you literally
+ * need to know if we're playing a ringtone or vibrating, use
+ * isRingtonePlaying() or isVibrating() instead.)
+ *
+ * @see isVibrating
+ * @see isRingtonePlaying
+ */
+ boolean isRinging() {
+ synchronized (this) {
+ return (isRingtonePlaying() || isVibrating());
+ }
+ }
+
+ /**
+ * @return true if the ringtone is playing
+ * @see isVibrating
+ * @see isRinging
+ */
+ private boolean isRingtonePlaying() {
+ synchronized (this) {
+ return (mRingtone != null && mRingtone.isPlaying()) ||
+ (mRingHandler != null && mRingHandler.hasMessages(PLAY_RING_ONCE));
+ }
+ }
+
+ /**
+ * @return true if we're vibrating in response to an incoming call
+ * @see isVibrating
+ * @see isRinging
+ */
+ private boolean isVibrating() {
+ synchronized (this) {
+ return (mVibratorThread != null);
+ }
+ }
+
+ /**
+ * Starts the ringtone and/or vibrator
+ */
+ void ring() {
+ if (DBG) log("ring()...");
+
+ synchronized (this) {
+ try {
+ if (PhoneGlobals.getInstance().showBluetoothIndication()) {
+ mPowerManager.setAttentionLight(true, 0x000000ff);
+ } else {
+ mPowerManager.setAttentionLight(true, 0x00ffffff);
+ }
+ } catch (RemoteException ex) {
+ // the other end of this binder call is in the system process.
+ }
+
+ if (shouldVibrate() && mVibratorThread == null) {
+ mContinueVibrating = true;
+ mVibratorThread = new VibratorThread();
+ if (DBG) log("- starting vibrator...");
+ mVibratorThread.start();
+ }
+ AudioManager audioManager =
+ (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+
+ if (audioManager.getStreamVolume(AudioManager.STREAM_RING) == 0) {
+ if (DBG) log("skipping ring because volume is zero");
+ return;
+ }
+
+ makeLooper();
+ if (mFirstRingEventTime < 0) {
+ mFirstRingEventTime = SystemClock.elapsedRealtime();
+ mRingHandler.sendEmptyMessage(PLAY_RING_ONCE);
+ } else {
+ // For repeat rings, figure out by how much to delay
+ // the ring so that it happens the correct amount of
+ // time after the previous ring
+ if (mFirstRingStartTime > 0) {
+ // Delay subsequent rings by the delta between event
+ // and play time of the first ring
+ if (DBG) {
+ log("delaying ring by " + (mFirstRingStartTime - mFirstRingEventTime));
+ }
+ mRingHandler.sendEmptyMessageDelayed(PLAY_RING_ONCE,
+ mFirstRingStartTime - mFirstRingEventTime);
+ } else {
+ // We've gotten two ring events so far, but the ring
+ // still hasn't started. Reset the event time to the
+ // time of this event to maintain correct spacing.
+ mFirstRingEventTime = SystemClock.elapsedRealtime();
+ }
+ }
+ }
+ }
+
+ boolean shouldVibrate() {
+ AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+ int ringerMode = audioManager.getRingerMode();
+ if (CallFeaturesSetting.getVibrateWhenRinging(mContext)) {
+ return ringerMode != AudioManager.RINGER_MODE_SILENT;
+ } else {
+ return ringerMode == AudioManager.RINGER_MODE_VIBRATE;
+ }
+ }
+
+ /**
+ * Stops the ringtone and/or vibrator if any of these are actually
+ * ringing/vibrating.
+ */
+ void stopRing() {
+ synchronized (this) {
+ if (DBG) log("stopRing()...");
+
+ try {
+ mPowerManager.setAttentionLight(false, 0x00000000);
+ } catch (RemoteException ex) {
+ // the other end of this binder call is in the system process.
+ }
+
+ if (mRingHandler != null) {
+ mRingHandler.removeCallbacksAndMessages(null);
+ Message msg = mRingHandler.obtainMessage(STOP_RING);
+ msg.obj = mRingtone;
+ mRingHandler.sendMessage(msg);
+ PhoneUtils.setAudioMode();
+ mRingThread = null;
+ mRingHandler = null;
+ mRingtone = null;
+ mFirstRingEventTime = -1;
+ mFirstRingStartTime = -1;
+ } else {
+ if (DBG) log("- stopRing: null mRingHandler!");
+ }
+
+ if (mVibratorThread != null) {
+ if (DBG) log("- stopRing: cleaning up vibrator thread...");
+ mContinueVibrating = false;
+ mVibratorThread = null;
+ }
+ // Also immediately cancel any vibration in progress.
+ mVibrator.cancel();
+ }
+ }
+
+ private class VibratorThread extends Thread {
+ public void run() {
+ while (mContinueVibrating) {
+ mVibrator.vibrate(VIBRATE_LENGTH);
+ SystemClock.sleep(VIBRATE_LENGTH + PAUSE_LENGTH);
+ }
+ }
+ }
+ private class Worker implements Runnable {
+ private final Object mLock = new Object();
+ private Looper mLooper;
+
+ Worker(String name) {
+ Thread t = new Thread(null, this, name);
+ t.start();
+ synchronized (mLock) {
+ while (mLooper == null) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException ex) {
+ }
+ }
+ }
+ }
+
+ public Looper getLooper() {
+ return mLooper;
+ }
+
+ public void run() {
+ synchronized (mLock) {
+ Looper.prepare();
+ mLooper = Looper.myLooper();
+ mLock.notifyAll();
+ }
+ Looper.loop();
+ }
+
+ public void quit() {
+ mLooper.quit();
+ }
+ }
+
+ /**
+ * Sets the ringtone uri in preparation for ringtone creation
+ * in makeLooper(). This uri is defaulted to the phone-wide
+ * default ringtone.
+ */
+ void setCustomRingtoneUri (Uri uri) {
+ if (uri != null) {
+ mCustomRingtoneUri = uri;
+ }
+ }
+
+ private void makeLooper() {
+ if (mRingThread == null) {
+ mRingThread = new Worker("ringer");
+ mRingHandler = new Handler(mRingThread.getLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ Ringtone r = null;
+ switch (msg.what) {
+ case PLAY_RING_ONCE:
+ if (DBG) log("mRingHandler: PLAY_RING_ONCE...");
+ if (mRingtone == null && !hasMessages(STOP_RING)) {
+ // create the ringtone with the uri
+ if (DBG) log("creating ringtone: " + mCustomRingtoneUri);
+ r = RingtoneManager.getRingtone(mContext, mCustomRingtoneUri);
+ synchronized (Ringer.this) {
+ if (!hasMessages(STOP_RING)) {
+ mRingtone = r;
+ }
+ }
+ }
+ r = mRingtone;
+ if (r != null && !hasMessages(STOP_RING) && !r.isPlaying()) {
+ PhoneUtils.setAudioMode();
+ r.play();
+ synchronized (Ringer.this) {
+ if (mFirstRingStartTime < 0) {
+ mFirstRingStartTime = SystemClock.elapsedRealtime();
+ }
+ }
+ }
+ break;
+ case STOP_RING:
+ if (DBG) log("mRingHandler: STOP_RING...");
+ r = (Ringtone) msg.obj;
+ if (r != null) {
+ r.stop();
+ } else {
+ if (DBG) log("- STOP_RING with null ringtone! msg = " + msg);
+ }
+ getLooper().quit();
+ break;
+ }
+ }
+ };
+ }
+ }
+
+ private static void log(String msg) {
+ Log.d(LOG_TAG, msg);
+ }
+}
diff --git a/src/com/android/phone/SimContacts.java b/src/com/android/phone/SimContacts.java
new file mode 100644
index 0000000..ebfc775
--- /dev/null
+++ b/src/com/android/phone/SimContacts.java
@@ -0,0 +1,367 @@
+/*
+ * Copyright (C) 2007 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.phone;
+
+import android.accounts.Account;
+import android.app.ActionBar;
+import android.app.ProgressDialog;
+import android.content.ContentProviderOperation;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.OperationApplicationException;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.RawContacts;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.CursorAdapter;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+
+/**
+ * SIM Address Book UI for the Phone app.
+ */
+public class SimContacts extends ADNList {
+ private static final String LOG_TAG = "SimContacts";
+
+ private static final String UP_ACTIVITY_PACKAGE = "com.android.contacts";
+ private static final String UP_ACTIVITY_CLASS =
+ "com.android.contacts.activities.PeopleActivity";
+
+ static final ContentValues sEmptyContentValues = new ContentValues();
+
+ private static final int MENU_IMPORT_ONE = 1;
+ private static final int MENU_IMPORT_ALL = 2;
+ private ProgressDialog mProgressDialog;
+
+ private Account mAccount;
+
+ private static class NamePhoneTypePair {
+ final String name;
+ final int phoneType;
+ public NamePhoneTypePair(String nameWithPhoneType) {
+ // Look for /W /H /M or /O at the end of the name signifying the type
+ int nameLen = nameWithPhoneType.length();
+ if (nameLen - 2 >= 0 && nameWithPhoneType.charAt(nameLen - 2) == '/') {
+ char c = Character.toUpperCase(nameWithPhoneType.charAt(nameLen - 1));
+ if (c == 'W') {
+ phoneType = Phone.TYPE_WORK;
+ } else if (c == 'M' || c == 'O') {
+ phoneType = Phone.TYPE_MOBILE;
+ } else if (c == 'H') {
+ phoneType = Phone.TYPE_HOME;
+ } else {
+ phoneType = Phone.TYPE_OTHER;
+ }
+ name = nameWithPhoneType.substring(0, nameLen - 2);
+ } else {
+ phoneType = Phone.TYPE_OTHER;
+ name = nameWithPhoneType;
+ }
+ }
+ }
+
+ private class ImportAllSimContactsThread extends Thread
+ implements OnCancelListener, OnClickListener {
+
+ boolean mCanceled = false;
+
+ public ImportAllSimContactsThread() {
+ super("ImportAllSimContactsThread");
+ }
+
+ @Override
+ public void run() {
+ final ContentValues emptyContentValues = new ContentValues();
+ final ContentResolver resolver = getContentResolver();
+
+ mCursor.moveToPosition(-1);
+ while (!mCanceled && mCursor.moveToNext()) {
+ actuallyImportOneSimContact(mCursor, resolver, mAccount);
+ mProgressDialog.incrementProgressBy(1);
+ }
+
+ mProgressDialog.dismiss();
+ finish();
+ }
+
+ public void onCancel(DialogInterface dialog) {
+ mCanceled = true;
+ }
+
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_NEGATIVE) {
+ mCanceled = true;
+ mProgressDialog.dismiss();
+ } else {
+ Log.e(LOG_TAG, "Unknown button event has come: " + dialog.toString());
+ }
+ }
+ }
+
+ private static void actuallyImportOneSimContact(
+ final Cursor cursor, final ContentResolver resolver, Account account) {
+ final NamePhoneTypePair namePhoneTypePair =
+ new NamePhoneTypePair(cursor.getString(NAME_COLUMN));
+ final String name = namePhoneTypePair.name;
+ final int phoneType = namePhoneTypePair.phoneType;
+ final String phoneNumber = cursor.getString(NUMBER_COLUMN);
+ final String emailAddresses = cursor.getString(EMAILS_COLUMN);
+ final String[] emailAddressArray;
+ if (!TextUtils.isEmpty(emailAddresses)) {
+ emailAddressArray = emailAddresses.split(",");
+ } else {
+ emailAddressArray = null;
+ }
+
+ final ArrayList<ContentProviderOperation> operationList =
+ new ArrayList<ContentProviderOperation>();
+ ContentProviderOperation.Builder builder =
+ ContentProviderOperation.newInsert(RawContacts.CONTENT_URI);
+ String myGroupsId = null;
+ if (account != null) {
+ builder.withValue(RawContacts.ACCOUNT_NAME, account.name);
+ builder.withValue(RawContacts.ACCOUNT_TYPE, account.type);
+ } else {
+ builder.withValues(sEmptyContentValues);
+ }
+ operationList.add(builder.build());
+
+ builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+ builder.withValueBackReference(StructuredName.RAW_CONTACT_ID, 0);
+ builder.withValue(Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE);
+ builder.withValue(StructuredName.DISPLAY_NAME, name);
+ operationList.add(builder.build());
+
+ builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+ builder.withValueBackReference(Phone.RAW_CONTACT_ID, 0);
+ builder.withValue(Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE);
+ builder.withValue(Phone.TYPE, phoneType);
+ builder.withValue(Phone.NUMBER, phoneNumber);
+ builder.withValue(Data.IS_PRIMARY, 1);
+ operationList.add(builder.build());
+
+ if (emailAddresses != null) {
+ for (String emailAddress : emailAddressArray) {
+ builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+ builder.withValueBackReference(Email.RAW_CONTACT_ID, 0);
+ builder.withValue(Data.MIMETYPE, Email.CONTENT_ITEM_TYPE);
+ builder.withValue(Email.TYPE, Email.TYPE_MOBILE);
+ builder.withValue(Email.DATA, emailAddress);
+ operationList.add(builder.build());
+ }
+ }
+
+ if (myGroupsId != null) {
+ builder = ContentProviderOperation.newInsert(Data.CONTENT_URI);
+ builder.withValueBackReference(GroupMembership.RAW_CONTACT_ID, 0);
+ builder.withValue(Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
+ builder.withValue(GroupMembership.GROUP_SOURCE_ID, myGroupsId);
+ operationList.add(builder.build());
+ }
+
+ try {
+ resolver.applyBatch(ContactsContract.AUTHORITY, operationList);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage()));
+ } catch (OperationApplicationException e) {
+ Log.e(LOG_TAG, String.format("%s: %s", e.toString(), e.getMessage()));
+ }
+ }
+
+ private void importOneSimContact(int position) {
+ final ContentResolver resolver = getContentResolver();
+ if (mCursor.moveToPosition(position)) {
+ actuallyImportOneSimContact(mCursor, resolver, mAccount);
+ } else {
+ Log.e(LOG_TAG, "Failed to move the cursor to the position \"" + position + "\"");
+ }
+ }
+
+ /* Followings are overridden methods */
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ Intent intent = getIntent();
+ if (intent != null) {
+ final String accountName = intent.getStringExtra("account_name");
+ final String accountType = intent.getStringExtra("account_type");
+ if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
+ mAccount = new Account(accountName, accountType);
+ }
+ }
+
+ registerForContextMenu(getListView());
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ // android.R.id.home will be triggered in onOptionsItemSelected()
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ @Override
+ protected CursorAdapter newAdapter() {
+ return new SimpleCursorAdapter(this, R.layout.sim_import_list_entry, mCursor,
+ new String[] { "name" }, new int[] { android.R.id.text1 });
+ }
+
+ @Override
+ protected Uri resolveIntent() {
+ Intent intent = getIntent();
+ intent.setData(Uri.parse("content://icc/adn"));
+ if (Intent.ACTION_PICK.equals(intent.getAction())) {
+ // "index" is 1-based
+ mInitialSelection = intent.getIntExtra("index", 0) - 1;
+ }
+ return intent.getData();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ menu.add(0, MENU_IMPORT_ALL, 0, R.string.importAllSimEntries);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ MenuItem item = menu.findItem(MENU_IMPORT_ALL);
+ if (item != null) {
+ item.setVisible(mCursor != null && mCursor.getCount() > 0);
+ }
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ Intent intent = new Intent();
+ intent.setClassName(UP_ACTIVITY_PACKAGE, UP_ACTIVITY_CLASS);
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ finish();
+ return true;
+ case MENU_IMPORT_ALL:
+ CharSequence title = getString(R.string.importAllSimEntries);
+ CharSequence message = getString(R.string.importingSimContacts);
+
+ ImportAllSimContactsThread thread = new ImportAllSimContactsThread();
+
+ // TODO: need to show some error dialog.
+ if (mCursor == null) {
+ Log.e(LOG_TAG, "cursor is null. Ignore silently.");
+ break;
+ }
+ mProgressDialog = new ProgressDialog(this);
+ mProgressDialog.setTitle(title);
+ mProgressDialog.setMessage(message);
+ mProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
+ mProgressDialog.setButton(DialogInterface.BUTTON_NEGATIVE,
+ getString(R.string.cancel), thread);
+ mProgressDialog.setProgress(0);
+ mProgressDialog.setMax(mCursor.getCount());
+ mProgressDialog.show();
+
+ thread.start();
+
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case MENU_IMPORT_ONE:
+ ContextMenu.ContextMenuInfo menuInfo = item.getMenuInfo();
+ if (menuInfo instanceof AdapterView.AdapterContextMenuInfo) {
+ int position = ((AdapterView.AdapterContextMenuInfo)menuInfo).position;
+ importOneSimContact(position);
+ return true;
+ }
+ }
+ return super.onContextItemSelected(item);
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenu.ContextMenuInfo menuInfo) {
+ if (menuInfo instanceof AdapterView.AdapterContextMenuInfo) {
+ AdapterView.AdapterContextMenuInfo itemInfo =
+ (AdapterView.AdapterContextMenuInfo) menuInfo;
+ TextView textView = (TextView) itemInfo.targetView.findViewById(android.R.id.text1);
+ if (textView != null) {
+ menu.setHeaderTitle(textView.getText());
+ }
+ menu.add(0, MENU_IMPORT_ONE, 0, R.string.importSimEntry);
+ }
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ importOneSimContact(position);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_CALL: {
+ if (mCursor != null && mCursor.moveToPosition(getSelectedItemPosition())) {
+ String phoneNumber = mCursor.getString(NUMBER_COLUMN);
+ if (phoneNumber == null || !TextUtils.isGraphic(phoneNumber)) {
+ // There is no number entered.
+ //TODO play error sound or something...
+ return true;
+ }
+ Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
+ Uri.fromParts(Constants.SCHEME_TEL, phoneNumber, null));
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
+ | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ startActivity(intent);
+ finish();
+ return true;
+ }
+ }
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+}
diff --git a/src/com/android/phone/SipBroadcastReceiver.java b/src/com/android/phone/SipBroadcastReceiver.java
new file mode 100644
index 0000000..8fdc7f7
--- /dev/null
+++ b/src/com/android/phone/SipBroadcastReceiver.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2010 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.phone;
+
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.sip.SipPhone;
+import com.android.phone.sip.SipProfileDb;
+import com.android.phone.sip.SipSharedPreferences;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.sip.SipAudioCall;
+import android.net.sip.SipException;
+import android.net.sip.SipManager;
+import android.net.sip.SipProfile;
+import android.telephony.Rlog;
+import java.util.List;
+
+/**
+ * Broadcast receiver that handles SIP-related intents.
+ */
+public class SipBroadcastReceiver extends BroadcastReceiver {
+ private static final String TAG = SipBroadcastReceiver.class.getSimpleName();
+ private static final boolean DBG = true;
+ private SipSharedPreferences mSipSharedPreferences;
+
+ @Override
+ public void onReceive(Context context, final Intent intent) {
+ String action = intent.getAction();
+
+ if (!PhoneUtils.isVoipSupported()) {
+ if (DBG) log("SIP VOIP not supported: " + action);
+ return;
+ }
+ mSipSharedPreferences = new SipSharedPreferences(context);
+
+ if (action.equals(SipManager.ACTION_SIP_INCOMING_CALL)) {
+ takeCall(intent);
+ } else if (action.equals(SipManager.ACTION_SIP_ADD_PHONE)) {
+ String localSipUri = intent.getStringExtra(SipManager.EXTRA_LOCAL_URI);
+ SipPhone phone = PhoneFactory.makeSipPhone(localSipUri);
+ if (phone != null) {
+ CallManager.getInstance().registerPhone(phone);
+ }
+ if (DBG) log("onReceive: add phone" + localSipUri + " #phones="
+ + CallManager.getInstance().getAllPhones().size());
+ } else if (action.equals(SipManager.ACTION_SIP_REMOVE_PHONE)) {
+ String localSipUri = intent.getStringExtra(SipManager.EXTRA_LOCAL_URI);
+ removeSipPhone(localSipUri);
+ if (DBG) log("onReceive: remove phone: " + localSipUri + " #phones="
+ + CallManager.getInstance().getAllPhones().size());
+ } else if (action.equals(SipManager.ACTION_SIP_SERVICE_UP)) {
+ if (DBG) log("onReceive: start auto registration");
+ registerAllProfiles();
+ } else {
+ if (DBG) log("onReceive: action not processed: " + action);
+ return;
+ }
+ }
+
+ private void removeSipPhone(String sipUri) {
+ for (Phone phone : CallManager.getInstance().getAllPhones()) {
+ if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_SIP) {
+ if (((SipPhone) phone).getSipUri().equals(sipUri)) {
+ CallManager.getInstance().unregisterPhone(phone);
+ return;
+ }
+ }
+ }
+ if (DBG) log("RemoveSipPhone: failed:cannot find phone with uri " + sipUri);
+ }
+
+ private void takeCall(Intent intent) {
+ Context phoneContext = PhoneGlobals.getInstance();
+ try {
+ SipAudioCall sipAudioCall = SipManager.newInstance(phoneContext)
+ .takeAudioCall(intent, null);
+ for (Phone phone : CallManager.getInstance().getAllPhones()) {
+ if (phone.getPhoneType() == PhoneConstants.PHONE_TYPE_SIP) {
+ if (((SipPhone) phone).canTake(sipAudioCall)) {
+ if (DBG) log("takeCall: SIP call: " + intent);
+ return;
+ }
+ }
+ }
+ if (DBG) log("takeCall: not taken, drop SIP call: " + intent);
+ } catch (SipException e) {
+ loge("takeCall: error incoming SIP call", e);
+ }
+ }
+
+ private void registerAllProfiles() {
+ final Context context = PhoneGlobals.getInstance();
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ SipManager sipManager = SipManager.newInstance(context);
+ SipProfileDb profileDb = new SipProfileDb(context);
+ List<SipProfile> sipProfileList =
+ profileDb.retrieveSipProfileList();
+ for (SipProfile profile : sipProfileList) {
+ try {
+ if (!profile.getAutoRegistration() &&
+ !profile.getUriString().equals(
+ mSipSharedPreferences.getPrimaryAccount())) {
+ continue;
+ }
+ sipManager.open(profile,
+ SipUtil.createIncomingCallPendingIntent(),
+ null);
+ if (DBG) log("registerAllProfiles: profile=" + profile);
+ } catch (SipException e) {
+ loge("registerAllProfiles: failed" + profile.getProfileName(), e);
+ }
+ }
+ }}
+ ).start();
+ }
+
+ private void log(String s) {
+ Rlog.d(TAG, s);
+ }
+
+ private void loge(String s, Throwable t) {
+ Rlog.e(TAG, s, t);
+ }
+}
diff --git a/src/com/android/phone/SipCallOptionHandler.java b/src/com/android/phone/SipCallOptionHandler.java
new file mode 100644
index 0000000..500f322
--- /dev/null
+++ b/src/com/android/phone/SipCallOptionHandler.java
@@ -0,0 +1,448 @@
+/**
+ * Copyright (C) 2010 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.phone;
+
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.phone.sip.SipProfileDb;
+import com.android.phone.sip.SipSettings;
+import com.android.phone.sip.SipSharedPreferences;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.net.Uri;
+import android.net.sip.SipException;
+import android.net.sip.SipManager;
+import android.net.sip.SipProfile;
+import android.os.Bundle;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.CheckBox;
+import android.widget.CompoundButton;
+import android.widget.TextView;
+
+import java.util.List;
+
+/**
+ * Activity that selects the proper phone type for an outgoing call.
+ *
+ * This activity determines which Phone type (SIP or PSTN) should be used
+ * for an outgoing phone call, depending on the outgoing "number" (which
+ * may be either a PSTN number or a SIP address) as well as the user's SIP
+ * preferences. In some cases this activity has no interaction with the
+ * user, but in other cases it may (by bringing up a dialog if the user's
+ * preference is "Ask for each call".)
+ */
+public class SipCallOptionHandler extends Activity implements
+ DialogInterface.OnClickListener, DialogInterface.OnCancelListener,
+ CompoundButton.OnCheckedChangeListener {
+ static final String TAG = "SipCallOptionHandler";
+ private static final boolean DBG =
+ (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
+
+ static final int DIALOG_SELECT_PHONE_TYPE = 0;
+ static final int DIALOG_SELECT_OUTGOING_SIP_PHONE = 1;
+ static final int DIALOG_START_SIP_SETTINGS = 2;
+ static final int DIALOG_NO_INTERNET_ERROR = 3;
+ static final int DIALOG_NO_VOIP = 4;
+ static final int DIALOG_SIZE = 5;
+
+ private Intent mIntent;
+ private List<SipProfile> mProfileList;
+ private String mCallOption;
+ private String mNumber;
+ private SipSharedPreferences mSipSharedPreferences;
+ private SipProfileDb mSipProfileDb;
+ private Dialog[] mDialogs = new Dialog[DIALOG_SIZE];
+ private SipProfile mOutgoingSipProfile;
+ private TextView mUnsetPriamryHint;
+ private boolean mUseSipPhone = false;
+ private boolean mMakePrimary = false;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+ String action = intent.getAction();
+
+ // This activity is only ever launched with the
+ // ACTION_SIP_SELECT_PHONE action.
+ if (!OutgoingCallBroadcaster.ACTION_SIP_SELECT_PHONE.equals(action)) {
+ Log.wtf(TAG, "onCreate: got intent action '" + action + "', expected "
+ + OutgoingCallBroadcaster.ACTION_SIP_SELECT_PHONE);
+ finish();
+ return;
+ }
+
+ // mIntent is a copy of the original CALL intent that started the
+ // whole outgoing-call sequence. This intent will ultimately be
+ // passed to CallController.placeCall() after displaying the SIP
+ // call options dialog (if necessary).
+ mIntent = (Intent) intent.getParcelableExtra(OutgoingCallBroadcaster.EXTRA_NEW_CALL_INTENT);
+ if (mIntent == null) {
+ finish();
+ return;
+ }
+
+ // Allow this activity to be visible in front of the keyguard.
+ // (This is only necessary for obscure scenarios like the user
+ // initiating a call and then immediately pressing the Power
+ // button.)
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
+
+ // If we're trying to make a SIP call, return a SipPhone if one is
+ // available.
+ //
+ // - If it's a sip: URI, this is definitely a SIP call, regardless
+ // of whether the data is a SIP address or a regular phone
+ // number.
+ //
+ // - If this is a tel: URI but the data contains an "@" character
+ // (see PhoneNumberUtils.isUriNumber()) we consider that to be a
+ // SIP number too.
+ //
+ // TODO: Eventually we may want to disallow that latter case
+ // (e.g. "tel:foo@example.com").
+ //
+ // TODO: We should also consider moving this logic into the
+ // CallManager, where it could be made more generic.
+ // (For example, each "telephony provider" could be allowed
+ // to register the URI scheme(s) that it can handle, and the
+ // CallManager would then find the best match for every
+ // outgoing call.)
+
+ boolean voipSupported = PhoneUtils.isVoipSupported();
+ if (DBG) Log.v(TAG, "voipSupported: " + voipSupported);
+ mSipProfileDb = new SipProfileDb(this);
+ mSipSharedPreferences = new SipSharedPreferences(this);
+ mCallOption = mSipSharedPreferences.getSipCallOption();
+ if (DBG) Log.v(TAG, "Call option: " + mCallOption);
+ Uri uri = mIntent.getData();
+ String scheme = uri.getScheme();
+ mNumber = PhoneNumberUtils.getNumberFromIntent(mIntent, this);
+ boolean isInCellNetwork = PhoneGlobals.getInstance().phoneMgr.isRadioOn();
+ boolean isKnownCallScheme = Constants.SCHEME_TEL.equals(scheme)
+ || Constants.SCHEME_SIP.equals(scheme);
+ boolean isRegularCall = Constants.SCHEME_TEL.equals(scheme)
+ && !PhoneNumberUtils.isUriNumber(mNumber);
+
+ // Bypass the handler if the call scheme is not sip or tel.
+ if (!isKnownCallScheme) {
+ setResultAndFinish();
+ return;
+ }
+
+ // Check if VoIP feature is supported.
+ if (!voipSupported) {
+ if (!isRegularCall) {
+ showDialog(DIALOG_NO_VOIP);
+ } else {
+ setResultAndFinish();
+ }
+ return;
+ }
+
+ // Since we are not sure if anyone has touched the number during
+ // the NEW_OUTGOING_CALL broadcast, we just check if the provider
+ // put their gateway information in the intent. If so, it means
+ // someone has changed the destination number. We then make the
+ // call via the default pstn network. However, if one just alters
+ // the destination directly, then we still let it go through the
+ // Internet call option process.
+ if (!PhoneUtils.hasPhoneProviderExtras(mIntent)) {
+ if (!isNetworkConnected()) {
+ if (!isRegularCall) {
+ showDialog(DIALOG_NO_INTERNET_ERROR);
+ return;
+ }
+ } else {
+ if (mCallOption.equals(Settings.System.SIP_ASK_ME_EACH_TIME)
+ && isRegularCall && isInCellNetwork) {
+ showDialog(DIALOG_SELECT_PHONE_TYPE);
+ return;
+ }
+ if (!mCallOption.equals(Settings.System.SIP_ADDRESS_ONLY)
+ || !isRegularCall) {
+ mUseSipPhone = true;
+ }
+ }
+ }
+
+ if (mUseSipPhone) {
+ // If there is no sip profile and it is a regular call, then we
+ // should use pstn network instead.
+ if ((mSipProfileDb.getProfilesCount() > 0) || !isRegularCall) {
+ startGetPrimarySipPhoneThread();
+ return;
+ } else {
+ mUseSipPhone = false;
+ }
+ }
+ setResultAndFinish();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ if (isFinishing()) return;
+ for (Dialog dialog : mDialogs) {
+ if (dialog != null) dialog.dismiss();
+ }
+ finish();
+ }
+
+ protected Dialog onCreateDialog(int id) {
+ Dialog dialog;
+ switch(id) {
+ case DIALOG_SELECT_PHONE_TYPE:
+ dialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.pick_outgoing_call_phone_type)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setSingleChoiceItems(R.array.phone_type_values, -1, this)
+ .setNegativeButton(android.R.string.cancel, this)
+ .setOnCancelListener(this)
+ .create();
+ break;
+ case DIALOG_SELECT_OUTGOING_SIP_PHONE:
+ dialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.pick_outgoing_sip_phone)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setSingleChoiceItems(getProfileNameArray(), -1, this)
+ .setNegativeButton(android.R.string.cancel, this)
+ .setOnCancelListener(this)
+ .create();
+ addMakeDefaultCheckBox(dialog);
+ break;
+ case DIALOG_START_SIP_SETTINGS:
+ dialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.no_sip_account_found_title)
+ .setMessage(R.string.no_sip_account_found)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(R.string.sip_menu_add, this)
+ .setNegativeButton(android.R.string.cancel, this)
+ .setOnCancelListener(this)
+ .create();
+ break;
+ case DIALOG_NO_INTERNET_ERROR:
+ boolean wifiOnly = SipManager.isSipWifiOnly(this);
+ dialog = new AlertDialog.Builder(this)
+ .setTitle(wifiOnly ? R.string.no_wifi_available_title
+ : R.string.no_internet_available_title)
+ .setMessage(wifiOnly ? R.string.no_wifi_available
+ : R.string.no_internet_available)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(android.R.string.ok, this)
+ .setOnCancelListener(this)
+ .create();
+ break;
+ case DIALOG_NO_VOIP:
+ dialog = new AlertDialog.Builder(this)
+ .setTitle(R.string.no_voip)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(android.R.string.ok, this)
+ .setOnCancelListener(this)
+ .create();
+ break;
+ default:
+ dialog = null;
+ }
+ if (dialog != null) {
+ mDialogs[id] = dialog;
+ }
+ return dialog;
+ }
+
+ private void addMakeDefaultCheckBox(Dialog dialog) {
+ LayoutInflater inflater = (LayoutInflater) getSystemService(
+ Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(
+ com.android.internal.R.layout.always_use_checkbox, null);
+ CheckBox makePrimaryCheckBox =
+ (CheckBox)view.findViewById(com.android.internal.R.id.alwaysUse);
+ makePrimaryCheckBox.setText(R.string.remember_my_choice);
+ makePrimaryCheckBox.setOnCheckedChangeListener(this);
+ mUnsetPriamryHint = (TextView)view.findViewById(
+ com.android.internal.R.id.clearDefaultHint);
+ mUnsetPriamryHint.setText(R.string.reset_my_choice_hint);
+ mUnsetPriamryHint.setVisibility(View.GONE);
+ ((AlertDialog)dialog).setView(view);
+ }
+
+ private CharSequence[] getProfileNameArray() {
+ CharSequence[] entries = new CharSequence[mProfileList.size()];
+ int i = 0;
+ for (SipProfile p : mProfileList) {
+ entries[i++] = p.getProfileName();
+ }
+ return entries;
+ }
+
+ public void onClick(DialogInterface dialog, int id) {
+ if (id == DialogInterface.BUTTON_NEGATIVE) {
+ // button negative is cancel
+ finish();
+ return;
+ } else if(dialog == mDialogs[DIALOG_SELECT_PHONE_TYPE]) {
+ String selection = getResources().getStringArray(
+ R.array.phone_type_values)[id];
+ if (DBG) Log.v(TAG, "User pick phone " + selection);
+ if (selection.equals(getString(R.string.internet_phone))) {
+ mUseSipPhone = true;
+ startGetPrimarySipPhoneThread();
+ return;
+ }
+ } else if (dialog == mDialogs[DIALOG_SELECT_OUTGOING_SIP_PHONE]) {
+ mOutgoingSipProfile = mProfileList.get(id);
+ } else if ((dialog == mDialogs[DIALOG_NO_INTERNET_ERROR])
+ || (dialog == mDialogs[DIALOG_NO_VOIP])) {
+ finish();
+ return;
+ } else {
+ if (id == DialogInterface.BUTTON_POSITIVE) {
+ // Redirect to sip settings and drop the call.
+ Intent newIntent = new Intent(this, SipSettings.class);
+ newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(newIntent);
+ }
+ finish();
+ return;
+ }
+ setResultAndFinish();
+ }
+
+ public void onCancel(DialogInterface dialog) {
+ finish();
+ }
+
+ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+ mMakePrimary = isChecked;
+ if (isChecked) {
+ mUnsetPriamryHint.setVisibility(View.VISIBLE);
+ } else {
+ mUnsetPriamryHint.setVisibility(View.INVISIBLE);
+ }
+ }
+
+ private void createSipPhoneIfNeeded(SipProfile p) {
+ CallManager cm = PhoneGlobals.getInstance().mCM;
+ if (PhoneUtils.getSipPhoneFromUri(cm, p.getUriString()) != null) return;
+
+ // Create the phone since we can not find it in CallManager
+ try {
+ SipManager.newInstance(this).open(p);
+ Phone phone = PhoneFactory.makeSipPhone(p.getUriString());
+ if (phone != null) {
+ cm.registerPhone(phone);
+ } else {
+ Log.e(TAG, "cannot make sipphone profile" + p);
+ }
+ } catch (SipException e) {
+ Log.e(TAG, "cannot open sip profile" + p, e);
+ }
+ }
+
+ private void setResultAndFinish() {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ if (mOutgoingSipProfile != null) {
+ if (!isNetworkConnected()) {
+ showDialog(DIALOG_NO_INTERNET_ERROR);
+ return;
+ }
+ if (DBG) Log.v(TAG, "primary SIP URI is " +
+ mOutgoingSipProfile.getUriString());
+ createSipPhoneIfNeeded(mOutgoingSipProfile);
+ mIntent.putExtra(OutgoingCallBroadcaster.EXTRA_SIP_PHONE_URI,
+ mOutgoingSipProfile.getUriString());
+ if (mMakePrimary) {
+ mSipSharedPreferences.setPrimaryAccount(
+ mOutgoingSipProfile.getUriString());
+ }
+ }
+
+ if (mUseSipPhone && mOutgoingSipProfile == null) {
+ showDialog(DIALOG_START_SIP_SETTINGS);
+ return;
+ } else {
+ // Woo hoo -- it's finally OK to initiate the outgoing call!
+ PhoneGlobals.getInstance().callController.placeCall(mIntent);
+ }
+ finish();
+ }
+ });
+ }
+
+ private boolean isNetworkConnected() {
+ ConnectivityManager cm = (ConnectivityManager) getSystemService(
+ Context.CONNECTIVITY_SERVICE);
+ if (cm != null) {
+ NetworkInfo ni = cm.getActiveNetworkInfo();
+ if ((ni == null) || !ni.isConnected()) return false;
+
+ return ((ni.getType() == ConnectivityManager.TYPE_WIFI)
+ || !SipManager.isSipWifiOnly(this));
+ }
+ return false;
+ }
+
+ private void startGetPrimarySipPhoneThread() {
+ new Thread(new Runnable() {
+ public void run() {
+ getPrimarySipPhone();
+ }
+ }).start();
+ }
+
+ private void getPrimarySipPhone() {
+ String primarySipUri = mSipSharedPreferences.getPrimaryAccount();
+
+ mOutgoingSipProfile = getPrimaryFromExistingProfiles(primarySipUri);
+ if (mOutgoingSipProfile == null) {
+ if ((mProfileList != null) && (mProfileList.size() > 0)) {
+ runOnUiThread(new Runnable() {
+ public void run() {
+ showDialog(DIALOG_SELECT_OUTGOING_SIP_PHONE);
+ }
+ });
+ return;
+ }
+ }
+ setResultAndFinish();
+ }
+
+ private SipProfile getPrimaryFromExistingProfiles(String primarySipUri) {
+ mProfileList = mSipProfileDb.retrieveSipProfileList();
+ if (mProfileList == null) return null;
+ for (SipProfile p : mProfileList) {
+ if (p.getUriString().equals(primarySipUri)) return p;
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/phone/SipUtil.java b/src/com/android/phone/SipUtil.java
new file mode 100644
index 0000000..a901d58
--- /dev/null
+++ b/src/com/android/phone/SipUtil.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2010 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.phone;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.net.sip.SipManager;
+
+public class SipUtil {
+ private SipUtil() {
+ }
+
+ public static PendingIntent createIncomingCallPendingIntent() {
+ Context phoneContext = PhoneGlobals.getInstance();
+ Intent intent = new Intent(phoneContext, SipBroadcastReceiver.class);
+ intent.setAction(SipManager.ACTION_SIP_INCOMING_CALL);
+ return PendingIntent.getBroadcast(phoneContext, 0, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+}
diff --git a/src/com/android/phone/SmallerHitTargetTouchListener.java b/src/com/android/phone/SmallerHitTargetTouchListener.java
new file mode 100644
index 0000000..8e1bf16
--- /dev/null
+++ b/src/com/android/phone/SmallerHitTargetTouchListener.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * OnTouchListener used to shrink the "hit target" of some onscreen buttons.
+ *
+ * We do this for a few specific buttons which are vulnerable to
+ * "false touches" because either (1) they're near the edge of the
+ * screen and might be unintentionally touched while holding the
+ * device in your hand, (2) they're in the upper corners and might
+ * be touched by the user's ear before the prox sensor has a chance to
+ * kick in, or (3) they are close to other buttons.
+ */
+public class SmallerHitTargetTouchListener implements View.OnTouchListener {
+ private static final String TAG = "SmallerHitTargetTouchListener";
+
+ /**
+ * Edge dimensions where a touch does not register an action (in DIP).
+ */
+ private static final int HIT_TARGET_EDGE_IGNORE_DP_X = 30;
+ private static final int HIT_TARGET_EDGE_IGNORE_DP_Y = 10;
+ private static final int HIT_TARGET_MIN_SIZE_DP_X = HIT_TARGET_EDGE_IGNORE_DP_X * 3;
+ private static final int HIT_TARGET_MIN_SIZE_DP_Y = HIT_TARGET_EDGE_IGNORE_DP_Y * 3;
+
+ // True if the most recent DOWN event was a "hit".
+ boolean mDownEventHit;
+
+ /**
+ * Called when a touch event is dispatched to a view. This allows listeners to
+ * get a chance to respond before the target view.
+ *
+ * @return True if the listener has consumed the event, false otherwise.
+ * (In other words, we return true when the touch is *outside*
+ * the "smaller hit target", which will prevent the actual
+ * button from handling these events.)
+ */
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ // if (DBG) log("SmallerHitTargetTouchListener: " + v + ", event " + event);
+
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ // Note that event.getX() and event.getY() are already
+ // translated into the View's coordinates. (In other words,
+ // "0,0" is a touch on the upper-left-most corner of the view.)
+ final int touchX = (int) event.getX();
+ final int touchY = (int) event.getY();
+
+ final int viewWidth = v.getWidth();
+ final int viewHeight = v.getHeight();
+
+ final float pixelDensity = v.getResources().getDisplayMetrics().density;
+ final int targetMinSizeX = (int) (HIT_TARGET_MIN_SIZE_DP_X * pixelDensity);
+ final int targetMinSizeY = (int) (HIT_TARGET_MIN_SIZE_DP_Y * pixelDensity);
+
+ int edgeIgnoreX = (int) (HIT_TARGET_EDGE_IGNORE_DP_X * pixelDensity);
+ int edgeIgnoreY = (int) (HIT_TARGET_EDGE_IGNORE_DP_Y * pixelDensity);
+
+ // If we are dealing with smaller buttons where the dead zone defined by
+ // HIT_TARGET_EDGE_IGNORE_DP_[X|Y] is too large.
+ if (viewWidth < targetMinSizeX || viewHeight < targetMinSizeY) {
+ // This really should not happen given our two use cases (as of this writing)
+ // in the call edge button and secondary calling card. However, we leave
+ // this is as a precautionary measure.
+ Log.w(TAG, "onTouch: view is too small for SmallerHitTargetTouchListener");
+ edgeIgnoreX = 0;
+ edgeIgnoreY = 0;
+ }
+
+ final int minTouchX = edgeIgnoreX;
+ final int maxTouchX = viewWidth - edgeIgnoreX;
+ final int minTouchY = edgeIgnoreY;
+ final int maxTouchY = viewHeight - edgeIgnoreY;
+
+ if (touchX < minTouchX || touchX > maxTouchX ||
+ touchY < minTouchY || touchY > maxTouchY) {
+ // Missed!
+ // if (DBG) log(" -> MISSED!");
+ mDownEventHit = false;
+ return true; // Consume this event; don't let the button see it
+ } else {
+ // Hit!
+ // if (DBG) log(" -> HIT!");
+ mDownEventHit = true;
+ return false; // Let this event through to the actual button
+ }
+ } else {
+ // This is a MOVE, UP or CANCEL event.
+ //
+ // We only do the "smaller hit target" check on DOWN events.
+ // For the subsequent MOVE/UP/CANCEL events, we let them
+ // through to the actual button IFF the previous DOWN event
+ // got through to the actual button (i.e. it was a "hit".)
+ return !mDownEventHit;
+ }
+ }
+}
diff --git a/src/com/android/phone/SpecialCharSequenceMgr.java b/src/com/android/phone/SpecialCharSequenceMgr.java
new file mode 100644
index 0000000..9b5373c
--- /dev/null
+++ b/src/com/android/phone/SpecialCharSequenceMgr.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2006 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.phone;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import com.android.internal.telephony.TelephonyIntents;
+import com.android.internal.telephony.Phone;
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+import android.view.WindowManager;
+
+import com.android.internal.telephony.TelephonyCapabilities;
+
+/**
+ * Helper class to listen for some magic dialpad character sequences
+ * that are handled specially by the Phone app.
+ *
+ * Note the Contacts app also handles these sequences too, so there's a
+ * separate version of this class under apps/Contacts.
+ *
+ * In fact, the most common use case for these special sequences is typing
+ * them from the regular "Dialer" used for outgoing calls, which is part
+ * of the contacts app; see DialtactsActivity and DialpadFragment.
+ * *This* version of SpecialCharSequenceMgr is used for only a few
+ * relatively obscure places in the UI:
+ * - The "SIM network unlock" PIN entry screen (see
+ * IccNetworkDepersonalizationPanel.java)
+ * - The emergency dialer (see EmergencyDialer.java).
+ *
+ * TODO: there's lots of duplicated code between this class and the
+ * corresponding class under apps/Contacts. Let's figure out a way to
+ * unify these two classes (in the framework? in a common shared library?)
+ */
+public class SpecialCharSequenceMgr {
+ private static final String TAG = PhoneGlobals.LOG_TAG;
+ private static final boolean DBG = false;
+
+ private static final String MMI_IMEI_DISPLAY = "*#06#";
+ private static final String MMI_REGULATORY_INFO_DISPLAY = "*#07#";
+
+ /** This class is never instantiated. */
+ private SpecialCharSequenceMgr() {
+ }
+
+ /**
+ * Check for special strings of digits from an input
+ * string.
+ * @param context input Context for the events we handle.
+ * @param input the dial string to be examined.
+ */
+ static boolean handleChars(Context context, String input) {
+ return handleChars(context, input, null);
+ }
+
+ /**
+ * Generally used for the Personal Unblocking Key (PUK) unlocking
+ * case, where we want to be able to maintain a handle to the
+ * calling activity so that we can close it or otherwise display
+ * indication if the PUK code is recognized.
+ *
+ * NOTE: The counterpart to this file in Contacts does
+ * NOT contain the special PUK handling code, since it
+ * does NOT need it. When the device gets into PUK-
+ * locked state, the keyguard comes up and the only way
+ * to unlock the device is through the Emergency dialer,
+ * which is still in the Phone App.
+ *
+ * @param context input Context for the events we handle.
+ * @param input the dial string to be examined.
+ * @param pukInputActivity activity that originated this
+ * PUK call, tracked so that we can close it or otherwise
+ * indicate that special character sequence is
+ * successfully processed. Can be null.
+ * @return true if the input was a special string which has been
+ * handled.
+ */
+ static boolean handleChars(Context context,
+ String input,
+ Activity pukInputActivity) {
+
+ //get rid of the separators so that the string gets parsed correctly
+ String dialString = PhoneNumberUtils.stripSeparators(input);
+
+ if (handleIMEIDisplay(context, dialString)
+ || handleRegulatoryInfoDisplay(context, dialString)
+ || handlePinEntry(context, dialString, pukInputActivity)
+ || handleAdnEntry(context, dialString)
+ || handleSecretCode(context, dialString)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Variant of handleChars() that looks for the subset of "special
+ * sequences" that are available even if the device is locked.
+ *
+ * (Specifically, these are the sequences that you're allowed to type
+ * in the Emergency Dialer, which is accessible *without* unlocking
+ * the device.)
+ */
+ static boolean handleCharsForLockedDevice(Context context,
+ String input,
+ Activity pukInputActivity) {
+ // Get rid of the separators so that the string gets parsed correctly
+ String dialString = PhoneNumberUtils.stripSeparators(input);
+
+ // The only sequences available on a locked device are the "**04"
+ // or "**05" sequences that allow you to enter PIN or PUK-related
+ // codes. (e.g. for the case where you're currently locked out of
+ // your phone, and need to change the PIN! The only way to do
+ // that is via the Emergency Dialer.)
+
+ if (handlePinEntry(context, dialString, pukInputActivity)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Handles secret codes to launch arbitrary activities in the form of *#*#<code>#*#*.
+ * If a secret code is encountered an Intent is started with the android_secret_code://<code>
+ * URI.
+ *
+ * @param context the context to use
+ * @param input the text to check for a secret code in
+ * @return true if a secret code was encountered
+ */
+ static private boolean handleSecretCode(Context context, String input) {
+ // Secret codes are in the form *#*#<code>#*#*
+ int len = input.length();
+ if (len > 8 && input.startsWith("*#*#") && input.endsWith("#*#*")) {
+ Intent intent = new Intent(TelephonyIntents.SECRET_CODE_ACTION,
+ Uri.parse("android_secret_code://" + input.substring(4, len - 4)));
+ context.sendBroadcast(intent);
+ return true;
+ }
+
+ return false;
+ }
+
+ static private boolean handleAdnEntry(Context context, String input) {
+ /* ADN entries are of the form "N(N)(N)#" */
+
+ // if the phone is keyguard-restricted, then just ignore this
+ // input. We want to make sure that sim card contacts are NOT
+ // exposed unless the phone is unlocked, and this code can be
+ // accessed from the emergency dialer.
+ if (PhoneGlobals.getInstance().getKeyguardManager().inKeyguardRestrictedInputMode()) {
+ return false;
+ }
+
+ int len = input.length();
+ if ((len > 1) && (len < 5) && (input.endsWith("#"))) {
+ try {
+ int index = Integer.parseInt(input.substring(0, len-1));
+ Intent intent = new Intent(Intent.ACTION_PICK);
+
+ intent.setClassName("com.android.phone",
+ "com.android.phone.SimContacts");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.putExtra("index", index);
+ PhoneGlobals.getInstance().startActivity(intent);
+
+ return true;
+ } catch (NumberFormatException ex) {}
+ }
+ return false;
+ }
+
+ static private boolean handlePinEntry(Context context, String input,
+ Activity pukInputActivity) {
+ // TODO: The string constants here should be removed in favor
+ // of some call to a static the MmiCode class that determines
+ // if a dialstring is an MMI code.
+ if ((input.startsWith("**04") || input.startsWith("**05"))
+ && input.endsWith("#")) {
+ PhoneGlobals app = PhoneGlobals.getInstance();
+ boolean isMMIHandled = app.phone.handlePinMmi(input);
+
+ // if the PUK code is recognized then indicate to the
+ // phone app that an attempt to unPUK the device was
+ // made with this activity. The PUK code may still
+ // fail though, but we won't know until the MMI code
+ // returns a result.
+ if (isMMIHandled && input.startsWith("**05")) {
+ app.setPukEntryActivity(pukInputActivity);
+ }
+ return isMMIHandled;
+ }
+ return false;
+ }
+
+ static private boolean handleIMEIDisplay(Context context,
+ String input) {
+ if (input.equals(MMI_IMEI_DISPLAY)) {
+ showDeviceIdPanel(context);
+ return true;
+ }
+
+ return false;
+ }
+
+ static private void showDeviceIdPanel(Context context) {
+ if (DBG) log("showDeviceIdPanel()...");
+
+ Phone phone = PhoneGlobals.getPhone();
+ int labelId = TelephonyCapabilities.getDeviceIdLabel(phone);
+ String deviceId = phone.getDeviceId();
+
+ AlertDialog alert = new AlertDialog.Builder(context)
+ .setTitle(labelId)
+ .setMessage(deviceId)
+ .setPositiveButton(R.string.ok, null)
+ .setCancelable(false)
+ .create();
+ alert.getWindow().setType(WindowManager.LayoutParams.TYPE_PRIORITY_PHONE);
+ alert.show();
+ }
+
+ private static boolean handleRegulatoryInfoDisplay(Context context, String input) {
+ if (input.equals(MMI_REGULATORY_INFO_DISPLAY)) {
+ log("handleRegulatoryInfoDisplay() sending intent to settings app");
+ ComponentName regInfoDisplayActivity = new ComponentName(
+ "com.android.settings", "com.android.settings.RegulatoryInfoDisplayActivity");
+ Intent showRegInfoIntent = new Intent("android.settings.SHOW_REGULATORY_INFO");
+ showRegInfoIntent.setComponent(regInfoDisplayActivity);
+ try {
+ context.startActivity(showRegInfoIntent);
+ } catch (ActivityNotFoundException e) {
+ Log.e(TAG, "startActivity() failed: " + e);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ private static void log(String msg) {
+ Log.d(TAG, "[SpecialCharSequenceMgr] " + msg);
+ }
+}
diff --git a/src/com/android/phone/TelephonyDebugService.java b/src/com/android/phone/TelephonyDebugService.java
new file mode 100644
index 0000000..fdfe8f5
--- /dev/null
+++ b/src/com/android/phone/TelephonyDebugService.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2012 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.phone;
+
+import com.android.internal.telephony.DebugService;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * A debug service for telephony.
+ */
+public class TelephonyDebugService extends Service {
+ private static String TAG = "TelephonyDebugService";
+ private DebugService mDebugService = new DebugService();
+
+ /** Constructor */
+ public TelephonyDebugService() {
+ Log.d(TAG, "TelephonyDebugService()");
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mDebugService.dump(fd, pw, args);
+ }
+}
+
diff --git a/src/com/android/phone/TimeConsumingPreferenceActivity.java b/src/com/android/phone/TimeConsumingPreferenceActivity.java
new file mode 100644
index 0000000..19c4dda
--- /dev/null
+++ b/src/com/android/phone/TimeConsumingPreferenceActivity.java
@@ -0,0 +1,211 @@
+package com.android.phone;
+
+import com.android.internal.telephony.CommandException;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.util.Log;
+import android.view.WindowManager;
+
+import java.util.ArrayList;
+
+interface TimeConsumingPreferenceListener {
+ public void onStarted(Preference preference, boolean reading);
+ public void onFinished(Preference preference, boolean reading);
+ public void onError(Preference preference, int error);
+ public void onException(Preference preference, CommandException exception);
+}
+
+public class TimeConsumingPreferenceActivity extends PreferenceActivity
+ implements TimeConsumingPreferenceListener,
+ DialogInterface.OnCancelListener {
+ private static final String LOG_TAG = "TimeConsumingPreferenceActivity";
+ private final boolean DBG = (PhoneGlobals.DBG_LEVEL >= 2);
+
+ private class DismissOnClickListener implements DialogInterface.OnClickListener {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ }
+ private class DismissAndFinishOnClickListener implements DialogInterface.OnClickListener {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ finish();
+ }
+ }
+ private final DialogInterface.OnClickListener mDismiss = new DismissOnClickListener();
+ private final DialogInterface.OnClickListener mDismissAndFinish
+ = new DismissAndFinishOnClickListener();
+
+ private static final int BUSY_READING_DIALOG = 100;
+ private static final int BUSY_SAVING_DIALOG = 200;
+
+ static final int EXCEPTION_ERROR = 300;
+ static final int RESPONSE_ERROR = 400;
+ static final int RADIO_OFF_ERROR = 500;
+ static final int FDN_CHECK_FAILURE = 600;
+
+ private final ArrayList<String> mBusyList = new ArrayList<String>();
+
+ protected boolean mIsForeground = false;
+
+ @Override
+ protected Dialog onCreateDialog(int id) {
+ if (id == BUSY_READING_DIALOG || id == BUSY_SAVING_DIALOG) {
+ ProgressDialog dialog = new ProgressDialog(this);
+ dialog.setTitle(getText(R.string.updating_title));
+ dialog.setIndeterminate(true);
+
+ switch(id) {
+ case BUSY_READING_DIALOG:
+ dialog.setCancelable(true);
+ dialog.setOnCancelListener(this);
+ dialog.setMessage(getText(R.string.reading_settings));
+ return dialog;
+ case BUSY_SAVING_DIALOG:
+ dialog.setCancelable(false);
+ dialog.setMessage(getText(R.string.updating_settings));
+ return dialog;
+ }
+ return null;
+ }
+
+ if (id == RESPONSE_ERROR || id == RADIO_OFF_ERROR || id == EXCEPTION_ERROR
+ || id == FDN_CHECK_FAILURE) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+
+ int msgId;
+ int titleId = R.string.error_updating_title;
+
+ switch (id) {
+ case RESPONSE_ERROR:
+ msgId = R.string.response_error;
+ builder.setPositiveButton(R.string.close_dialog, mDismiss);
+ break;
+ case RADIO_OFF_ERROR:
+ msgId = R.string.radio_off_error;
+ // The error is not recoverable on dialog exit.
+ builder.setPositiveButton(R.string.close_dialog, mDismissAndFinish);
+ break;
+ case FDN_CHECK_FAILURE:
+ msgId = R.string.fdn_check_failure;
+ builder.setPositiveButton(R.string.close_dialog, mDismiss);
+ break;
+ case EXCEPTION_ERROR:
+ default:
+ msgId = R.string.exception_error;
+ // The error is not recoverable on dialog exit.
+ builder.setPositiveButton(R.string.close_dialog, mDismissAndFinish);
+ break;
+ }
+
+ builder.setTitle(getText(titleId));
+ builder.setMessage(getText(msgId));
+ builder.setCancelable(false);
+ AlertDialog dialog = builder.create();
+
+ // make the dialog more obvious by blurring the background.
+ dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_BLUR_BEHIND);
+
+ return dialog;
+ }
+ return null;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mIsForeground = true;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mIsForeground = false;
+ }
+
+ @Override
+ public void onStarted(Preference preference, boolean reading) {
+ if (DBG) dumpState();
+ if (DBG) Log.d(LOG_TAG, "onStarted, preference=" + preference.getKey()
+ + ", reading=" + reading);
+ mBusyList.add(preference.getKey());
+
+ if (mIsForeground) {
+ if (reading) {
+ showDialog(BUSY_READING_DIALOG);
+ } else {
+ showDialog(BUSY_SAVING_DIALOG);
+ }
+ }
+
+ }
+
+ @Override
+ public void onFinished(Preference preference, boolean reading) {
+ if (DBG) dumpState();
+ if (DBG) Log.d(LOG_TAG, "onFinished, preference=" + preference.getKey()
+ + ", reading=" + reading);
+ mBusyList.remove(preference.getKey());
+
+ if (mBusyList.isEmpty()) {
+ if (reading) {
+ dismissDialogSafely(BUSY_READING_DIALOG);
+ } else {
+ dismissDialogSafely(BUSY_SAVING_DIALOG);
+ }
+ }
+ preference.setEnabled(true);
+ }
+
+ @Override
+ public void onError(Preference preference, int error) {
+ if (DBG) dumpState();
+ if (DBG) Log.d(LOG_TAG, "onError, preference=" + preference.getKey() + ", error=" + error);
+
+ if (mIsForeground) {
+ showDialog(error);
+ }
+ preference.setEnabled(false);
+ }
+
+ @Override
+ public void onException(Preference preference, CommandException exception) {
+ if (exception.getCommandError() == CommandException.Error.FDN_CHECK_FAILURE) {
+ onError(preference, FDN_CHECK_FAILURE);
+ } else {
+ preference.setEnabled(false);
+ onError(preference, EXCEPTION_ERROR);
+ }
+ }
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ if (DBG) dumpState();
+ finish();
+ }
+
+ private void dismissDialogSafely(int id) {
+ try {
+ dismissDialog(id);
+ } catch (IllegalArgumentException e) {
+ // This is expected in the case where we were in the background
+ // at the time we would normally have shown the dialog, so we didn't
+ // show it.
+ }
+ }
+
+ /* package */ void dumpState() {
+ Log.d(LOG_TAG, "dumpState begin");
+ for (String key : mBusyList) {
+ Log.d(LOG_TAG, "mBusyList: key=" + key);
+ }
+ Log.d(LOG_TAG, "dumpState end");
+ }
+}
diff --git a/src/com/android/phone/Use2GOnlyCheckBoxPreference.java b/src/com/android/phone/Use2GOnlyCheckBoxPreference.java
new file mode 100644
index 0000000..a5a7a67
--- /dev/null
+++ b/src/com/android/phone/Use2GOnlyCheckBoxPreference.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2009 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.phone;
+
+import android.content.Context;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemProperties;
+import android.preference.CheckBoxPreference;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import com.android.internal.telephony.Phone;
+
+public class Use2GOnlyCheckBoxPreference extends CheckBoxPreference {
+ private static final String LOG_TAG = "Use2GOnlyCheckBoxPreference";
+
+ private Phone mPhone;
+ private MyHandler mHandler;
+
+ public Use2GOnlyCheckBoxPreference(Context context) {
+ this(context, null);
+ }
+
+ public Use2GOnlyCheckBoxPreference(Context context, AttributeSet attrs) {
+ this(context, attrs,com.android.internal.R.attr.checkBoxPreferenceStyle);
+ }
+
+ public Use2GOnlyCheckBoxPreference(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ mPhone = PhoneGlobals.getPhone();
+ mHandler = new MyHandler();
+ mPhone.getPreferredNetworkType(
+ mHandler.obtainMessage(MyHandler.MESSAGE_GET_PREFERRED_NETWORK_TYPE));
+ }
+
+ private int getDefaultNetworkMode() {
+ int mode = SystemProperties.getInt("ro.telephony.default_network",
+ Phone.PREFERRED_NT_MODE);
+ Log.i(LOG_TAG, "getDefaultNetworkMode: mode=" + mode);
+ return mode;
+ }
+
+ @Override
+ protected void onClick() {
+ super.onClick();
+
+ int networkType = isChecked() ? Phone.NT_MODE_GSM_ONLY : getDefaultNetworkMode();
+ Log.i(LOG_TAG, "set preferred network type="+networkType);
+ android.provider.Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.PREFERRED_NETWORK_MODE, networkType);
+ mPhone.setPreferredNetworkType(networkType, mHandler
+ .obtainMessage(MyHandler.MESSAGE_SET_PREFERRED_NETWORK_TYPE));
+ }
+
+ private class MyHandler extends Handler {
+
+ static final int MESSAGE_GET_PREFERRED_NETWORK_TYPE = 0;
+ static final int MESSAGE_SET_PREFERRED_NETWORK_TYPE = 1;
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MESSAGE_GET_PREFERRED_NETWORK_TYPE:
+ handleGetPreferredNetworkTypeResponse(msg);
+ break;
+
+ case MESSAGE_SET_PREFERRED_NETWORK_TYPE:
+ handleSetPreferredNetworkTypeResponse(msg);
+ break;
+ }
+ }
+
+ private void handleGetPreferredNetworkTypeResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (ar.exception == null) {
+ int type = ((int[])ar.result)[0];
+ if (type != Phone.NT_MODE_GSM_ONLY) {
+ // Back to default
+ type = getDefaultNetworkMode();
+ }
+ Log.i(LOG_TAG, "get preferred network type="+type);
+ setChecked(type == Phone.NT_MODE_GSM_ONLY);
+ android.provider.Settings.Global.putInt(mPhone.getContext().getContentResolver(),
+ android.provider.Settings.Global.PREFERRED_NETWORK_MODE, type);
+ } else {
+ // Weird state, disable the setting
+ Log.i(LOG_TAG, "get preferred network type, exception="+ar.exception);
+ setEnabled(false);
+ }
+ }
+
+ private void handleSetPreferredNetworkTypeResponse(Message msg) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+
+ if (ar.exception != null) {
+ // Yikes, error, disable the setting
+ setEnabled(false);
+ // Set UI to current state
+ Log.i(LOG_TAG, "set preferred network type, exception=" + ar.exception);
+ mPhone.getPreferredNetworkType(obtainMessage(MESSAGE_GET_PREFERRED_NETWORK_TYPE));
+ } else {
+ Log.i(LOG_TAG, "set preferred network type done");
+ }
+ }
+ }
+}
diff --git a/src/com/android/phone/sip/SipEditor.java b/src/com/android/phone/sip/SipEditor.java
new file mode 100644
index 0000000..8145c94
--- /dev/null
+++ b/src/com/android/phone/sip/SipEditor.java
@@ -0,0 +1,657 @@
+/*
+ * Copyright (C) 2010 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.phone.sip;
+
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.phone.R;
+import com.android.phone.SipUtil;
+
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.content.Intent;
+import android.net.sip.SipManager;
+import android.net.sip.SipProfile;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.preference.CheckBoxPreference;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceGroup;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Button;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+
+/**
+ * The activity class for editing a new or existing SIP profile.
+ */
+public class SipEditor extends PreferenceActivity
+ implements Preference.OnPreferenceChangeListener {
+ private static final int MENU_SAVE = Menu.FIRST;
+ private static final int MENU_DISCARD = Menu.FIRST + 1;
+ private static final int MENU_REMOVE = Menu.FIRST + 2;
+
+ private static final String TAG = SipEditor.class.getSimpleName();
+ private static final String KEY_PROFILE = "profile";
+ private static final String GET_METHOD_PREFIX = "get";
+ private static final char SCRAMBLED = '*';
+ private static final int NA = 0;
+
+ private PrimaryAccountSelector mPrimaryAccountSelector;
+ private AdvancedSettings mAdvancedSettings;
+ private SipSharedPreferences mSharedPreferences;
+ private boolean mDisplayNameSet;
+ private boolean mHomeButtonClicked;
+ private boolean mUpdateRequired;
+ private boolean mUpdatedFieldIsEmpty;
+
+ private SipManager mSipManager;
+ private SipProfileDb mProfileDb;
+ private SipProfile mOldProfile;
+ private CallManager mCallManager;
+ private Button mRemoveButton;
+
+ enum PreferenceKey {
+ Username(R.string.username, 0, R.string.default_preference_summary),
+ Password(R.string.password, 0, R.string.default_preference_summary),
+ DomainAddress(R.string.domain_address, 0, R.string.default_preference_summary),
+ DisplayName(R.string.display_name, 0, R.string.display_name_summary),
+ ProxyAddress(R.string.proxy_address, 0, R.string.optional_summary),
+ Port(R.string.port, R.string.default_port, R.string.default_port),
+ Transport(R.string.transport, R.string.default_transport, NA),
+ SendKeepAlive(R.string.send_keepalive, R.string.sip_system_decide, NA),
+ AuthUserName(R.string.auth_username, 0, R.string.optional_summary);
+
+ final int text;
+ final int initValue;
+ final int defaultSummary;
+ Preference preference;
+
+ /**
+ * @param key The key name of the preference.
+ * @param initValue The initial value of the preference.
+ * @param defaultSummary The default summary value of the preference
+ * when the preference value is empty.
+ */
+ PreferenceKey(int text, int initValue, int defaultSummary) {
+ this.text = text;
+ this.initValue = initValue;
+ this.defaultSummary = defaultSummary;
+ }
+
+ String getValue() {
+ if (preference instanceof EditTextPreference) {
+ return ((EditTextPreference) preference).getText();
+ } else if (preference instanceof ListPreference) {
+ return ((ListPreference) preference).getValue();
+ }
+ throw new RuntimeException("getValue() for the preference " + this);
+ }
+
+ void setValue(String value) {
+ if (preference instanceof EditTextPreference) {
+ String oldValue = getValue();
+ ((EditTextPreference) preference).setText(value);
+ if (this != Password) {
+ Log.v(TAG, this + ": setValue() " + value + ": " + oldValue
+ + " --> " + getValue());
+ }
+ } else if (preference instanceof ListPreference) {
+ ((ListPreference) preference).setValue(value);
+ }
+
+ if (TextUtils.isEmpty(value)) {
+ preference.setSummary(defaultSummary);
+ } else if (this == Password) {
+ preference.setSummary(scramble(value));
+ } else if ((this == DisplayName)
+ && value.equals(getDefaultDisplayName())) {
+ preference.setSummary(defaultSummary);
+ } else {
+ preference.setSummary(value);
+ }
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mHomeButtonClicked = false;
+ if (mCallManager.getState() != PhoneConstants.State.IDLE) {
+ mAdvancedSettings.show();
+ getPreferenceScreen().setEnabled(false);
+ if (mRemoveButton != null) mRemoveButton.setEnabled(false);
+ } else {
+ getPreferenceScreen().setEnabled(true);
+ if (mRemoveButton != null) mRemoveButton.setEnabled(true);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ Log.v(TAG, "start profile editor");
+ super.onCreate(savedInstanceState);
+
+ mSipManager = SipManager.newInstance(this);
+ mSharedPreferences = new SipSharedPreferences(this);
+ mProfileDb = new SipProfileDb(this);
+ mCallManager = CallManager.getInstance();
+
+ setContentView(R.layout.sip_settings_ui);
+ addPreferencesFromResource(R.xml.sip_edit);
+
+ SipProfile p = mOldProfile = (SipProfile) ((savedInstanceState == null)
+ ? getIntent().getParcelableExtra(SipSettings.KEY_SIP_PROFILE)
+ : savedInstanceState.getParcelable(KEY_PROFILE));
+
+ PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen();
+ for (int i = 0, n = screen.getPreferenceCount(); i < n; i++) {
+ setupPreference(screen.getPreference(i));
+ }
+
+ if (p == null) {
+ screen.setTitle(R.string.sip_edit_new_title);
+ }
+
+ mAdvancedSettings = new AdvancedSettings();
+ mPrimaryAccountSelector = new PrimaryAccountSelector(p);
+
+ loadPreferencesFromProfile(p);
+ }
+
+ @Override
+ public void onPause() {
+ Log.v(TAG, "SipEditor onPause(): finishing? " + isFinishing());
+ if (!isFinishing()) {
+ mHomeButtonClicked = true;
+ validateAndSetResult();
+ }
+ super.onPause();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ menu.add(0, MENU_DISCARD, 0, R.string.sip_menu_discard)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ menu.add(0, MENU_SAVE, 0, R.string.sip_menu_save)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ menu.add(0, MENU_REMOVE, 0, R.string.remove_sip_account)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ MenuItem removeMenu = menu.findItem(MENU_REMOVE);
+ removeMenu.setVisible(mOldProfile != null);
+ menu.findItem(MENU_SAVE).setEnabled(mUpdateRequired);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case MENU_SAVE:
+ validateAndSetResult();
+ return true;
+
+ case MENU_DISCARD:
+ finish();
+ return true;
+
+ case MENU_REMOVE: {
+ setRemovedProfileAndFinish();
+ return true;
+ }
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ switch (keyCode) {
+ case KeyEvent.KEYCODE_BACK:
+ validateAndSetResult();
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ private void saveAndRegisterProfile(SipProfile p) throws IOException {
+ if (p == null) return;
+ mProfileDb.saveProfile(p);
+ if (p.getAutoRegistration()
+ || mSharedPreferences.isPrimaryAccount(p.getUriString())) {
+ try {
+ mSipManager.open(p, SipUtil.createIncomingCallPendingIntent(),
+ null);
+ } catch (Exception e) {
+ Log.e(TAG, "register failed: " + p.getUriString(), e);
+ }
+ }
+ }
+
+ private void deleteAndUnregisterProfile(SipProfile p) {
+ if (p == null) return;
+ mProfileDb.deleteProfile(p);
+ unregisterProfile(p.getUriString());
+ }
+
+ private void unregisterProfile(String uri) {
+ try {
+ mSipManager.close(uri);
+ } catch (Exception e) {
+ Log.e(TAG, "unregister failed: " + uri, e);
+ }
+ }
+
+ private void setRemovedProfileAndFinish() {
+ Intent intent = new Intent(this, SipSettings.class);
+ setResult(RESULT_FIRST_USER, intent);
+ Toast.makeText(this, R.string.removing_account, Toast.LENGTH_SHORT)
+ .show();
+ replaceProfile(mOldProfile, null);
+ // do finish() in replaceProfile() in a background thread
+ }
+
+ private void showAlert(Throwable e) {
+ String msg = e.getMessage();
+ if (TextUtils.isEmpty(msg)) msg = e.toString();
+ showAlert(msg);
+ }
+
+ private void showAlert(final String message) {
+ if (mHomeButtonClicked) {
+ Log.v(TAG, "Home button clicked, don't show dialog: " + message);
+ return;
+ }
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ new AlertDialog.Builder(SipEditor.this)
+ .setTitle(android.R.string.dialog_alert_title)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setMessage(message)
+ .setPositiveButton(R.string.alert_dialog_ok, null)
+ .show();
+ }
+ });
+ }
+
+ private boolean isEditTextEmpty(PreferenceKey key) {
+ EditTextPreference pref = (EditTextPreference) key.preference;
+ return TextUtils.isEmpty(pref.getText())
+ || pref.getSummary().equals(getString(key.defaultSummary));
+ }
+
+ private void validateAndSetResult() {
+ boolean allEmpty = true;
+ CharSequence firstEmptyFieldTitle = null;
+ for (PreferenceKey key : PreferenceKey.values()) {
+ Preference p = key.preference;
+ if (p instanceof EditTextPreference) {
+ EditTextPreference pref = (EditTextPreference) p;
+ boolean fieldEmpty = isEditTextEmpty(key);
+ if (allEmpty && !fieldEmpty) allEmpty = false;
+
+ // use default value if display name is empty
+ if (fieldEmpty) {
+ switch (key) {
+ case DisplayName:
+ pref.setText(getDefaultDisplayName());
+ break;
+ case AuthUserName:
+ case ProxyAddress:
+ // optional; do nothing
+ break;
+ case Port:
+ pref.setText(getString(R.string.default_port));
+ break;
+ default:
+ if (firstEmptyFieldTitle == null) {
+ firstEmptyFieldTitle = pref.getTitle();
+ }
+ }
+ } else if (key == PreferenceKey.Port) {
+ int port = Integer.parseInt(PreferenceKey.Port.getValue());
+ if ((port < 1000) || (port > 65534)) {
+ showAlert(getString(R.string.not_a_valid_port));
+ return;
+ }
+ }
+ }
+ }
+
+ if (allEmpty || !mUpdateRequired) {
+ finish();
+ return;
+ } else if (firstEmptyFieldTitle != null) {
+ showAlert(getString(R.string.empty_alert, firstEmptyFieldTitle));
+ return;
+ }
+ try {
+ SipProfile profile = createSipProfile();
+ Intent intent = new Intent(this, SipSettings.class);
+ intent.putExtra(SipSettings.KEY_SIP_PROFILE, (Parcelable) profile);
+ setResult(RESULT_OK, intent);
+ Toast.makeText(this, R.string.saving_account, Toast.LENGTH_SHORT)
+ .show();
+
+ replaceProfile(mOldProfile, profile);
+ // do finish() in replaceProfile() in a background thread
+ } catch (Exception e) {
+ Log.w(TAG, "Can not create new SipProfile", e);
+ showAlert(e);
+ }
+ }
+
+ private void unregisterOldPrimaryAccount() {
+ String primaryAccountUri = mSharedPreferences.getPrimaryAccount();
+ Log.v(TAG, "old primary: " + primaryAccountUri);
+ if ((primaryAccountUri != null)
+ && !mSharedPreferences.isReceivingCallsEnabled()) {
+ Log.v(TAG, "unregister old primary: " + primaryAccountUri);
+ unregisterProfile(primaryAccountUri);
+ }
+ }
+
+ private void replaceProfile(final SipProfile oldProfile,
+ final SipProfile newProfile) {
+ // Replace profile in a background thread as it takes time to access the
+ // storage; do finish() once everything goes fine.
+ // newProfile may be null if the old profile is to be deleted rather
+ // than being modified.
+ new Thread(new Runnable() {
+ public void run() {
+ try {
+ // if new profile is primary, unregister the old primary account
+ if ((newProfile != null) && mPrimaryAccountSelector.isSelected()) {
+ unregisterOldPrimaryAccount();
+ }
+
+ mPrimaryAccountSelector.commit(newProfile);
+ deleteAndUnregisterProfile(oldProfile);
+ saveAndRegisterProfile(newProfile);
+ finish();
+ } catch (Exception e) {
+ Log.e(TAG, "Can not save/register new SipProfile", e);
+ showAlert(e);
+ }
+ }
+ }, "SipEditor").start();
+ }
+
+ private String getProfileName() {
+ return PreferenceKey.Username.getValue() + "@"
+ + PreferenceKey.DomainAddress.getValue();
+ }
+
+ private SipProfile createSipProfile() throws Exception {
+ return new SipProfile.Builder(
+ PreferenceKey.Username.getValue(),
+ PreferenceKey.DomainAddress.getValue())
+ .setProfileName(getProfileName())
+ .setPassword(PreferenceKey.Password.getValue())
+ .setOutboundProxy(PreferenceKey.ProxyAddress.getValue())
+ .setProtocol(PreferenceKey.Transport.getValue())
+ .setDisplayName(PreferenceKey.DisplayName.getValue())
+ .setPort(Integer.parseInt(PreferenceKey.Port.getValue()))
+ .setSendKeepAlive(isAlwaysSendKeepAlive())
+ .setAutoRegistration(
+ mSharedPreferences.isReceivingCallsEnabled())
+ .setAuthUserName(PreferenceKey.AuthUserName.getValue())
+ .build();
+ }
+
+ public boolean onPreferenceChange(Preference pref, Object newValue) {
+ if (!mUpdateRequired) {
+ mUpdateRequired = true;
+ if (mOldProfile != null) {
+ unregisterProfile(mOldProfile.getUriString());
+ }
+ }
+ if (pref instanceof CheckBoxPreference) {
+ invalidateOptionsMenu();
+ return true;
+ }
+ String value = (newValue == null) ? "" : newValue.toString();
+ if (TextUtils.isEmpty(value)) {
+ pref.setSummary(getPreferenceKey(pref).defaultSummary);
+ } else if (pref == PreferenceKey.Password.preference) {
+ pref.setSummary(scramble(value));
+ } else {
+ pref.setSummary(value);
+ }
+
+ if (pref == PreferenceKey.DisplayName.preference) {
+ ((EditTextPreference) pref).setText(value);
+ checkIfDisplayNameSet();
+ }
+
+ // SAVE menu should be enabled once the user modified some preference.
+ invalidateOptionsMenu();
+ return true;
+ }
+
+ private PreferenceKey getPreferenceKey(Preference pref) {
+ for (PreferenceKey key : PreferenceKey.values()) {
+ if (key.preference == pref) return key;
+ }
+ throw new RuntimeException("not possible to reach here");
+ }
+
+ private void loadPreferencesFromProfile(SipProfile p) {
+ if (p != null) {
+ Log.v(TAG, "Edit the existing profile : " + p.getProfileName());
+ try {
+ Class profileClass = SipProfile.class;
+ for (PreferenceKey key : PreferenceKey.values()) {
+ Method meth = profileClass.getMethod(GET_METHOD_PREFIX
+ + getString(key.text), (Class[])null);
+ if (key == PreferenceKey.SendKeepAlive) {
+ boolean value = ((Boolean)
+ meth.invoke(p, (Object[]) null)).booleanValue();
+ key.setValue(getString(value
+ ? R.string.sip_always_send_keepalive
+ : R.string.sip_system_decide));
+ } else {
+ Object value = meth.invoke(p, (Object[])null);
+ key.setValue((value == null) ? "" : value.toString());
+ }
+ }
+ checkIfDisplayNameSet();
+ } catch (Exception e) {
+ Log.e(TAG, "Can not load pref from profile", e);
+ }
+ } else {
+ Log.v(TAG, "Edit a new profile");
+ for (PreferenceKey key : PreferenceKey.values()) {
+ key.preference.setOnPreferenceChangeListener(this);
+
+ // FIXME: android:defaultValue in preference xml file doesn't
+ // work. Even if we setValue() for each preference in the case
+ // of (p != null), the dialog still shows android:defaultValue,
+ // not the value set by setValue(). This happens if
+ // android:defaultValue is not empty. Is it a bug?
+ if (key.initValue != 0) {
+ key.setValue(getString(key.initValue));
+ }
+ }
+ mDisplayNameSet = false;
+ }
+ }
+
+ private boolean isAlwaysSendKeepAlive() {
+ ListPreference pref = (ListPreference)
+ PreferenceKey.SendKeepAlive.preference;
+ return getString(R.string.sip_always_send_keepalive).equals(
+ pref.getValue());
+ }
+
+ private void setCheckBox(PreferenceKey key, boolean checked) {
+ CheckBoxPreference pref = (CheckBoxPreference) key.preference;
+ pref.setChecked(checked);
+ }
+
+ private void setupPreference(Preference pref) {
+ pref.setOnPreferenceChangeListener(this);
+ for (PreferenceKey key : PreferenceKey.values()) {
+ String name = getString(key.text);
+ if (name.equals(pref.getKey())) {
+ key.preference = pref;
+ return;
+ }
+ }
+ }
+
+ private void checkIfDisplayNameSet() {
+ String displayName = PreferenceKey.DisplayName.getValue();
+ mDisplayNameSet = !TextUtils.isEmpty(displayName)
+ && !displayName.equals(getDefaultDisplayName());
+ Log.d(TAG, "displayName set? " + mDisplayNameSet);
+ if (mDisplayNameSet) {
+ PreferenceKey.DisplayName.preference.setSummary(displayName);
+ } else {
+ PreferenceKey.DisplayName.setValue("");
+ }
+ }
+
+ private static String getDefaultDisplayName() {
+ return PreferenceKey.Username.getValue();
+ }
+
+ private static String scramble(String s) {
+ char[] cc = new char[s.length()];
+ Arrays.fill(cc, SCRAMBLED);
+ return new String(cc);
+ }
+
+ // only takes care of the primary account setting in SipSharedSettings
+ private class PrimaryAccountSelector {
+ private CheckBoxPreference mCheckbox;
+ private final boolean mWasPrimaryAccount;
+
+ // @param profile profile to be edited; null if adding new profile
+ PrimaryAccountSelector(SipProfile profile) {
+ mCheckbox = (CheckBoxPreference) getPreferenceScreen()
+ .findPreference(getString(R.string.set_primary));
+ boolean noPrimaryAccountSet =
+ !mSharedPreferences.hasPrimaryAccount();
+ boolean editNewProfile = (profile == null);
+ mWasPrimaryAccount = !editNewProfile
+ && mSharedPreferences.isPrimaryAccount(
+ profile.getUriString());
+
+ Log.v(TAG, " noPrimaryAccountSet: " + noPrimaryAccountSet);
+ Log.v(TAG, " editNewProfile: " + editNewProfile);
+ Log.v(TAG, " mWasPrimaryAccount: " + mWasPrimaryAccount);
+
+ mCheckbox.setChecked(mWasPrimaryAccount
+ || (editNewProfile && noPrimaryAccountSet));
+ }
+
+ boolean isSelected() {
+ return mCheckbox.isChecked();
+ }
+
+ // profile is null if the user removes it
+ void commit(SipProfile profile) {
+ if ((profile != null) && mCheckbox.isChecked()) {
+ mSharedPreferences.setPrimaryAccount(profile.getUriString());
+ } else if (mWasPrimaryAccount) {
+ mSharedPreferences.unsetPrimaryAccount();
+ }
+ Log.d(TAG, " primary account changed to : "
+ + mSharedPreferences.getPrimaryAccount());
+ }
+ }
+
+ private class AdvancedSettings
+ implements Preference.OnPreferenceClickListener {
+ private Preference mAdvancedSettingsTrigger;
+ private Preference[] mPreferences;
+ private boolean mShowing = false;
+
+ AdvancedSettings() {
+ mAdvancedSettingsTrigger = getPreferenceScreen().findPreference(
+ getString(R.string.advanced_settings));
+ mAdvancedSettingsTrigger.setOnPreferenceClickListener(this);
+
+ loadAdvancedPreferences();
+ }
+
+ private void loadAdvancedPreferences() {
+ PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen();
+
+ addPreferencesFromResource(R.xml.sip_advanced_edit);
+ PreferenceGroup group = (PreferenceGroup) screen.findPreference(
+ getString(R.string.advanced_settings_container));
+ screen.removePreference(group);
+
+ mPreferences = new Preference[group.getPreferenceCount()];
+ int order = screen.getPreferenceCount();
+ for (int i = 0, n = mPreferences.length; i < n; i++) {
+ Preference pref = group.getPreference(i);
+ pref.setOrder(order++);
+ setupPreference(pref);
+ mPreferences[i] = pref;
+ }
+ }
+
+ void show() {
+ mShowing = true;
+ mAdvancedSettingsTrigger.setSummary(R.string.advanced_settings_hide);
+ PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen();
+ for (Preference pref : mPreferences) {
+ screen.addPreference(pref);
+ Log.v(TAG, "add pref " + pref.getKey() + ": order=" + pref.getOrder());
+ }
+ }
+
+ private void hide() {
+ mShowing = false;
+ mAdvancedSettingsTrigger.setSummary(R.string.advanced_settings_show);
+ PreferenceGroup screen = (PreferenceGroup) getPreferenceScreen();
+ for (Preference pref : mPreferences) {
+ screen.removePreference(pref);
+ }
+ }
+
+ public boolean onPreferenceClick(Preference preference) {
+ Log.v(TAG, "optional settings clicked");
+ if (!mShowing) {
+ show();
+ } else {
+ hide();
+ }
+ return true;
+ }
+ }
+}
diff --git a/src/com/android/phone/sip/SipProfileDb.java b/src/com/android/phone/sip/SipProfileDb.java
new file mode 100644
index 0000000..a51dfb9
--- /dev/null
+++ b/src/com/android/phone/sip/SipProfileDb.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2010 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.phone.sip;
+
+import com.android.internal.os.AtomicFile;
+
+import android.content.Context;
+import android.net.sip.SipProfile;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility class that helps perform operations on the SipProfile database.
+ */
+public class SipProfileDb {
+ private static final String TAG = SipProfileDb.class.getSimpleName();
+
+ private static final String PROFILES_DIR = "/profiles/";
+ private static final String PROFILE_OBJ_FILE = ".pobj";
+
+ private String mProfilesDirectory;
+ private SipSharedPreferences mSipSharedPreferences;
+ private int mProfilesCount = -1;
+
+ public SipProfileDb(Context context) {
+ mProfilesDirectory = context.getFilesDir().getAbsolutePath()
+ + PROFILES_DIR;
+ mSipSharedPreferences = new SipSharedPreferences(context);
+ }
+
+ public void deleteProfile(SipProfile p) {
+ synchronized(SipProfileDb.class) {
+ deleteProfile(new File(mProfilesDirectory + p.getProfileName()));
+ if (mProfilesCount < 0) retrieveSipProfileListInternal();
+ mSipSharedPreferences.setProfilesCount(--mProfilesCount);
+ }
+ }
+
+ private void deleteProfile(File file) {
+ if (file.isDirectory()) {
+ for (File child : file.listFiles()) deleteProfile(child);
+ }
+ file.delete();
+ }
+
+ public void saveProfile(SipProfile p) throws IOException {
+ synchronized(SipProfileDb.class) {
+ if (mProfilesCount < 0) retrieveSipProfileListInternal();
+ File f = new File(mProfilesDirectory + p.getProfileName());
+ if (!f.exists()) f.mkdirs();
+ AtomicFile atomicFile =
+ new AtomicFile(new File(f, PROFILE_OBJ_FILE));
+ FileOutputStream fos = null;
+ ObjectOutputStream oos = null;
+ try {
+ fos = atomicFile.startWrite();
+ oos = new ObjectOutputStream(fos);
+ oos.writeObject(p);
+ oos.flush();
+ mSipSharedPreferences.setProfilesCount(++mProfilesCount);
+ atomicFile.finishWrite(fos);
+ } catch (IOException e) {
+ atomicFile.failWrite(fos);
+ throw e;
+ } finally {
+ if (oos != null) oos.close();
+ }
+ }
+ }
+
+ public int getProfilesCount() {
+ return (mProfilesCount < 0) ?
+ mSipSharedPreferences.getProfilesCount() : mProfilesCount;
+ }
+
+ public List<SipProfile> retrieveSipProfileList() {
+ synchronized(SipProfileDb.class) {
+ return retrieveSipProfileListInternal();
+ }
+ }
+
+ private List<SipProfile> retrieveSipProfileListInternal() {
+ List<SipProfile> sipProfileList = Collections.synchronizedList(
+ new ArrayList<SipProfile>());
+
+ File root = new File(mProfilesDirectory);
+ String[] dirs = root.list();
+ if (dirs == null) return sipProfileList;
+ for (String dir : dirs) {
+ File f = new File(new File(root, dir), PROFILE_OBJ_FILE);
+ if (!f.exists()) continue;
+ try {
+ SipProfile p = deserialize(f);
+ if (p == null) continue;
+ if (!dir.equals(p.getProfileName())) continue;
+
+ sipProfileList.add(p);
+ } catch (IOException e) {
+ Log.e(TAG, "retrieveProfileListFromStorage()", e);
+ }
+ }
+ mProfilesCount = sipProfileList.size();
+ mSipSharedPreferences.setProfilesCount(mProfilesCount);
+ return sipProfileList;
+ }
+
+ private SipProfile deserialize(File profileObjectFile) throws IOException {
+ AtomicFile atomicFile = new AtomicFile(profileObjectFile);
+ ObjectInputStream ois = null;
+ try {
+ ois = new ObjectInputStream(atomicFile.openRead());
+ SipProfile p = (SipProfile) ois.readObject();
+ return p;
+ } catch (ClassNotFoundException e) {
+ Log.w(TAG, "deserialize a profile: " + e);
+ } finally {
+ if (ois!= null) ois.close();
+ }
+ return null;
+ }
+}
diff --git a/src/com/android/phone/sip/SipSettings.java b/src/com/android/phone/sip/SipSettings.java
new file mode 100644
index 0000000..d58386c
--- /dev/null
+++ b/src/com/android/phone/sip/SipSettings.java
@@ -0,0 +1,514 @@
+/*
+ * Copyright (C) 2010 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.phone.sip;
+
+import com.android.internal.telephony.CallManager;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.phone.CallFeaturesSetting;
+import com.android.phone.R;
+import com.android.phone.SipUtil;
+
+import android.app.ActionBar;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.net.sip.SipErrorCode;
+import android.net.sip.SipException;
+import android.net.sip.SipManager;
+import android.net.sip.SipProfile;
+import android.net.sip.SipRegistrationListener;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.Process;
+import android.preference.CheckBoxPreference;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceCategory;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The PreferenceActivity class for managing sip profile preferences.
+ */
+public class SipSettings extends PreferenceActivity {
+ public static final String SIP_SHARED_PREFERENCES = "SIP_PREFERENCES";
+
+ private static final int MENU_ADD_ACCOUNT = Menu.FIRST;
+
+ static final String KEY_SIP_PROFILE = "sip_profile";
+
+ private static final String BUTTON_SIP_RECEIVE_CALLS =
+ "sip_receive_calls_key";
+ private static final String PREF_SIP_LIST = "sip_account_list";
+ private static final String TAG = "SipSettings";
+
+ private static final int REQUEST_ADD_OR_EDIT_SIP_PROFILE = 1;
+
+ private PackageManager mPackageManager;
+ private SipManager mSipManager;
+ private CallManager mCallManager;
+ private SipProfileDb mProfileDb;
+
+ private SipProfile mProfile; // profile that's being edited
+
+ private CheckBoxPreference mButtonSipReceiveCalls;
+ private PreferenceCategory mSipListContainer;
+ private Map<String, SipPreference> mSipPreferenceMap;
+ private List<SipProfile> mSipProfileList;
+ private SipSharedPreferences mSipSharedPreferences;
+ private int mUid = Process.myUid();
+
+ private class SipPreference extends Preference {
+ SipProfile mProfile;
+ SipPreference(Context c, SipProfile p) {
+ super(c);
+ setProfile(p);
+ }
+
+ SipProfile getProfile() {
+ return mProfile;
+ }
+
+ void setProfile(SipProfile p) {
+ mProfile = p;
+ setTitle(getProfileName(p));
+ updateSummary(mSipSharedPreferences.isReceivingCallsEnabled()
+ ? getString(R.string.registration_status_checking_status)
+ : getString(R.string.registration_status_not_receiving));
+ }
+
+ void updateSummary(String registrationStatus) {
+ int profileUid = mProfile.getCallingUid();
+ boolean isPrimary = mProfile.getUriString().equals(
+ mSipSharedPreferences.getPrimaryAccount());
+ Log.v(TAG, "profile uid is " + profileUid + " isPrimary:"
+ + isPrimary + " registration:" + registrationStatus
+ + " Primary:" + mSipSharedPreferences.getPrimaryAccount()
+ + " status:" + registrationStatus);
+ String summary = "";
+ if ((profileUid > 0) && (profileUid != mUid)) {
+ // from third party apps
+ summary = getString(R.string.third_party_account_summary,
+ getPackageNameFromUid(profileUid));
+ } else if (isPrimary) {
+ summary = getString(R.string.primary_account_summary_with,
+ registrationStatus);
+ } else {
+ summary = registrationStatus;
+ }
+ setSummary(summary);
+ }
+ }
+
+ private String getPackageNameFromUid(int uid) {
+ try {
+ String[] pkgs = mPackageManager.getPackagesForUid(uid);
+ ApplicationInfo ai =
+ mPackageManager.getApplicationInfo(pkgs[0], 0);
+ return ai.loadLabel(mPackageManager).toString();
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "cannot find name of uid " + uid, e);
+ }
+ return "uid:" + uid;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mSipManager = SipManager.newInstance(this);
+ mSipSharedPreferences = new SipSharedPreferences(this);
+ mProfileDb = new SipProfileDb(this);
+
+ mPackageManager = getPackageManager();
+ setContentView(R.layout.sip_settings_ui);
+ addPreferencesFromResource(R.xml.sip_setting);
+ mSipListContainer = (PreferenceCategory) findPreference(PREF_SIP_LIST);
+ registerForReceiveCallsCheckBox();
+ mCallManager = CallManager.getInstance();
+
+ updateProfilesStatus();
+
+ ActionBar actionBar = getActionBar();
+ if (actionBar != null) {
+ // android.R.id.home will be triggered in onOptionsItemSelected()
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (mCallManager.getState() != PhoneConstants.State.IDLE) {
+ mButtonSipReceiveCalls.setEnabled(false);
+ } else {
+ mButtonSipReceiveCalls.setEnabled(true);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ unregisterForContextMenu(getListView());
+ }
+
+ @Override
+ protected void onActivityResult(final int requestCode, final int resultCode,
+ final Intent intent) {
+ if (resultCode != RESULT_OK && resultCode != RESULT_FIRST_USER) return;
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ if (mProfile != null) {
+ Log.v(TAG, "Removed Profile:" + mProfile.getProfileName());
+ deleteProfile(mProfile);
+ }
+
+ SipProfile profile = intent.getParcelableExtra(KEY_SIP_PROFILE);
+ if (resultCode == RESULT_OK) {
+ Log.v(TAG, "New Profile Name:" + profile.getProfileName());
+ addProfile(profile);
+ }
+ updateProfilesStatus();
+ } catch (IOException e) {
+ Log.v(TAG, "Can not handle the profile : " + e.getMessage());
+ }
+ }
+ }.start();
+ }
+
+ private void registerForReceiveCallsCheckBox() {
+ mButtonSipReceiveCalls = (CheckBoxPreference) findPreference
+ (BUTTON_SIP_RECEIVE_CALLS);
+ mButtonSipReceiveCalls.setChecked(
+ mSipSharedPreferences.isReceivingCallsEnabled());
+ mButtonSipReceiveCalls.setOnPreferenceClickListener(
+ new OnPreferenceClickListener() {
+ public boolean onPreferenceClick(Preference preference) {
+ final boolean enabled =
+ ((CheckBoxPreference) preference).isChecked();
+ new Thread(new Runnable() {
+ public void run() {
+ handleSipReceiveCallsOption(enabled);
+ }
+ }).start();
+ return true;
+ }
+ });
+ }
+
+ private synchronized void handleSipReceiveCallsOption(boolean enabled) {
+ mSipSharedPreferences.setReceivingCallsEnabled(enabled);
+ List<SipProfile> sipProfileList = mProfileDb.retrieveSipProfileList();
+ for (SipProfile p : sipProfileList) {
+ String sipUri = p.getUriString();
+ p = updateAutoRegistrationFlag(p, enabled);
+ try {
+ if (enabled) {
+ mSipManager.open(p,
+ SipUtil.createIncomingCallPendingIntent(), null);
+ } else {
+ mSipManager.close(sipUri);
+ if (mSipSharedPreferences.isPrimaryAccount(sipUri)) {
+ // re-open in order to make calls
+ mSipManager.open(p);
+ }
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "register failed", e);
+ }
+ }
+ updateProfilesStatus();
+ }
+
+ private SipProfile updateAutoRegistrationFlag(
+ SipProfile p, boolean enabled) {
+ SipProfile newProfile = new SipProfile.Builder(p)
+ .setAutoRegistration(enabled)
+ .build();
+ try {
+ mProfileDb.deleteProfile(p);
+ mProfileDb.saveProfile(newProfile);
+ } catch (Exception e) {
+ Log.e(TAG, "updateAutoRegistrationFlag error", e);
+ }
+ return newProfile;
+ }
+
+ private void updateProfilesStatus() {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ retrieveSipLists();
+ } catch (Exception e) {
+ Log.e(TAG, "isRegistered", e);
+ }
+ }
+ }).start();
+ }
+
+ private String getProfileName(SipProfile profile) {
+ String profileName = profile.getProfileName();
+ if (TextUtils.isEmpty(profileName)) {
+ profileName = profile.getUserName() + "@" + profile.getSipDomain();
+ }
+ return profileName;
+ }
+
+ private void retrieveSipLists() {
+ mSipPreferenceMap = new LinkedHashMap<String, SipPreference>();
+ mSipProfileList = mProfileDb.retrieveSipProfileList();
+ processActiveProfilesFromSipService();
+ Collections.sort(mSipProfileList, new Comparator<SipProfile>() {
+ @Override
+ public int compare(SipProfile p1, SipProfile p2) {
+ return getProfileName(p1).compareTo(getProfileName(p2));
+ }
+
+ public boolean equals(SipProfile p) {
+ // not used
+ return false;
+ }
+ });
+ mSipListContainer.removeAll();
+ for (SipProfile p : mSipProfileList) {
+ addPreferenceFor(p);
+ }
+
+ if (!mSipSharedPreferences.isReceivingCallsEnabled()) return;
+ for (SipProfile p : mSipProfileList) {
+ if (mUid == p.getCallingUid()) {
+ try {
+ mSipManager.setRegistrationListener(
+ p.getUriString(), createRegistrationListener());
+ } catch (SipException e) {
+ Log.e(TAG, "cannot set registration listener", e);
+ }
+ }
+ }
+ }
+
+ private void processActiveProfilesFromSipService() {
+ SipProfile[] activeList = mSipManager.getListOfProfiles();
+ for (SipProfile activeProfile : activeList) {
+ SipProfile profile = getProfileFromList(activeProfile);
+ if (profile == null) {
+ mSipProfileList.add(activeProfile);
+ } else {
+ profile.setCallingUid(activeProfile.getCallingUid());
+ }
+ }
+ }
+
+ private SipProfile getProfileFromList(SipProfile activeProfile) {
+ for (SipProfile p : mSipProfileList) {
+ if (p.getUriString().equals(activeProfile.getUriString())) {
+ return p;
+ }
+ }
+ return null;
+ }
+
+ private void addPreferenceFor(SipProfile p) {
+ String status;
+ Log.v(TAG, "addPreferenceFor profile uri" + p.getUri());
+ SipPreference pref = new SipPreference(this, p);
+ mSipPreferenceMap.put(p.getUriString(), pref);
+ mSipListContainer.addPreference(pref);
+
+ pref.setOnPreferenceClickListener(
+ new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference pref) {
+ handleProfileClick(((SipPreference) pref).mProfile);
+ return true;
+ }
+ });
+ }
+
+ private void handleProfileClick(final SipProfile profile) {
+ int uid = profile.getCallingUid();
+ if (uid == mUid || uid == 0) {
+ startSipEditor(profile);
+ return;
+ }
+ new AlertDialog.Builder(this)
+ .setTitle(R.string.alert_dialog_close)
+ .setIconAttribute(android.R.attr.alertDialogIcon)
+ .setPositiveButton(R.string.close_profile,
+ new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int w) {
+ deleteProfile(profile);
+ unregisterProfile(profile);
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ }
+
+ private void unregisterProfile(final SipProfile p) {
+ // run it on background thread for better UI response
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ mSipManager.close(p.getUriString());
+ } catch (Exception e) {
+ Log.e(TAG, "unregister failed, SipService died?", e);
+ }
+ }
+ }, "unregisterProfile").start();
+ }
+
+ void deleteProfile(SipProfile p) {
+ mSipProfileList.remove(p);
+ SipPreference pref = mSipPreferenceMap.remove(p.getUriString());
+ mSipListContainer.removePreference(pref);
+ }
+
+ private void addProfile(SipProfile p) throws IOException {
+ try {
+ mSipManager.setRegistrationListener(p.getUriString(),
+ createRegistrationListener());
+ } catch (Exception e) {
+ Log.e(TAG, "cannot set registration listener", e);
+ }
+ mSipProfileList.add(p);
+ addPreferenceFor(p);
+ }
+
+ private void startSipEditor(final SipProfile profile) {
+ mProfile = profile;
+ Intent intent = new Intent(this, SipEditor.class);
+ intent.putExtra(KEY_SIP_PROFILE, (Parcelable) profile);
+ startActivityForResult(intent, REQUEST_ADD_OR_EDIT_SIP_PROFILE);
+ }
+
+ private void showRegistrationMessage(final String profileUri,
+ final String message) {
+ runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ SipPreference pref = mSipPreferenceMap.get(profileUri);
+ if (pref != null) {
+ pref.updateSummary(message);
+ }
+ }
+ });
+ }
+
+ private SipRegistrationListener createRegistrationListener() {
+ return new SipRegistrationListener() {
+ @Override
+ public void onRegistrationDone(String profileUri, long expiryTime) {
+ showRegistrationMessage(profileUri, getString(
+ R.string.registration_status_done));
+ }
+
+ @Override
+ public void onRegistering(String profileUri) {
+ showRegistrationMessage(profileUri, getString(
+ R.string.registration_status_registering));
+ }
+
+ @Override
+ public void onRegistrationFailed(String profileUri, int errorCode,
+ String message) {
+ switch (errorCode) {
+ case SipErrorCode.IN_PROGRESS:
+ showRegistrationMessage(profileUri, getString(
+ R.string.registration_status_still_trying));
+ break;
+ case SipErrorCode.INVALID_CREDENTIALS:
+ showRegistrationMessage(profileUri, getString(
+ R.string.registration_status_invalid_credentials));
+ break;
+ case SipErrorCode.SERVER_UNREACHABLE:
+ showRegistrationMessage(profileUri, getString(
+ R.string.registration_status_server_unreachable));
+ break;
+ case SipErrorCode.DATA_CONNECTION_LOST:
+ if (SipManager.isSipWifiOnly(getApplicationContext())){
+ showRegistrationMessage(profileUri, getString(
+ R.string.registration_status_no_wifi_data));
+ } else {
+ showRegistrationMessage(profileUri, getString(
+ R.string.registration_status_no_data));
+ }
+ break;
+ case SipErrorCode.CLIENT_ERROR:
+ showRegistrationMessage(profileUri, getString(
+ R.string.registration_status_not_running));
+ break;
+ default:
+ showRegistrationMessage(profileUri, getString(
+ R.string.registration_status_failed_try_later,
+ message));
+ }
+ }
+ };
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+ menu.add(0, MENU_ADD_ACCOUNT, 0, R.string.add_sip_account)
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ menu.findItem(MENU_ADD_ACCOUNT).setEnabled(
+ mCallManager.getState() == PhoneConstants.State.IDLE);
+ return super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ switch (itemId) {
+ case android.R.id.home: {
+ CallFeaturesSetting.goUpToTopLevelSetting(this);
+ return true;
+ }
+ case MENU_ADD_ACCOUNT: {
+ startSipEditor(null);
+ return true;
+ }
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/src/com/android/phone/sip/SipSharedPreferences.java b/src/com/android/phone/sip/SipSharedPreferences.java
new file mode 100644
index 0000000..e15db64
--- /dev/null
+++ b/src/com/android/phone/sip/SipSharedPreferences.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2010 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.phone.sip;
+
+import com.android.phone.R;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.provider.Settings;
+import android.provider.Settings.SettingNotFoundException;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * Wrapper for SIP's shared preferences.
+ */
+public class SipSharedPreferences {
+ private static final String SIP_SHARED_PREFERENCES = "SIP_PREFERENCES";
+ private static final String KEY_PRIMARY_ACCOUNT = "primary";
+ private static final String KEY_NUMBER_OF_PROFILES = "profiles";
+
+ private SharedPreferences mPreferences;
+ private Context mContext;
+
+ public SipSharedPreferences(Context context) {
+ mPreferences = context.getSharedPreferences(
+ SIP_SHARED_PREFERENCES, Context.MODE_WORLD_READABLE);
+ mContext = context;
+ }
+
+ public void setPrimaryAccount(String accountUri) {
+ SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putString(KEY_PRIMARY_ACCOUNT, accountUri);
+ editor.apply();
+ }
+
+ public void unsetPrimaryAccount() {
+ setPrimaryAccount(null);
+ }
+
+ /** Returns the primary account URI or null if it does not exist. */
+ public String getPrimaryAccount() {
+ return mPreferences.getString(KEY_PRIMARY_ACCOUNT, null);
+ }
+
+ public boolean isPrimaryAccount(String accountUri) {
+ return accountUri.equals(
+ mPreferences.getString(KEY_PRIMARY_ACCOUNT, null));
+ }
+
+ public boolean hasPrimaryAccount() {
+ return !TextUtils.isEmpty(
+ mPreferences.getString(KEY_PRIMARY_ACCOUNT, null));
+ }
+
+ public void setProfilesCount(int number) {
+ SharedPreferences.Editor editor = mPreferences.edit();
+ editor.putInt(KEY_NUMBER_OF_PROFILES, number);
+ editor.apply();
+ }
+
+ public int getProfilesCount() {
+ return mPreferences.getInt(KEY_NUMBER_OF_PROFILES, 0);
+ }
+
+ public void setSipCallOption(String option) {
+ Settings.System.putString(mContext.getContentResolver(),
+ Settings.System.SIP_CALL_OPTIONS, option);
+ }
+
+ public String getSipCallOption() {
+ String option = Settings.System.getString(mContext.getContentResolver(),
+ Settings.System.SIP_CALL_OPTIONS);
+ return (option != null) ? option
+ : mContext.getString(R.string.sip_address_only);
+ }
+
+ public void setReceivingCallsEnabled(boolean enabled) {
+ Settings.System.putInt(mContext.getContentResolver(),
+ Settings.System.SIP_RECEIVE_CALLS, (enabled ? 1 : 0));
+ }
+
+ public boolean isReceivingCallsEnabled() {
+ try {
+ return (Settings.System.getInt(mContext.getContentResolver(),
+ Settings.System.SIP_RECEIVE_CALLS) != 0);
+ } catch (SettingNotFoundException e) {
+ Log.d("SIP", "ReceiveCall option is not set; use default value");
+ return false;
+ }
+ }
+
+ // TODO: back up to Android Backup
+}