Merge branch 'dev/13/fp3/lineage-20.0' into int/13/fp3
* dev/13/fp3/lineage-20.0: (23 commits)
proguard.flags: Don't optimize fragment constructors
fixup! Add a permission to guard receiving intents in StatusSmsFetcher.
Dialer: Notify content observers upon call log entry deletion
Dialer: improve search bar
Dialer: set statusbar color same as activity background
Dialer: update ripple color
Dialer: update dark theme colors
Dialer: use black/white navigation colors
Dialer: handle database downgrade from lineage-17.1
Dialer: Use a DayNight launch theme
Dialer: allow framework to do dark theming automatically
Dialer: AudioModeProvider: use wired route for usb headsets
Control dialer's incoming call proximity sensor check via an overlay
Re-add dialer lookup.
Re-add call statistics.
Allow per-call account selection.
Re-add call recording.
Add setting to enable Do Not Disturb during calls
Generalize the in-call vibration settings category
Add back in-call vibration features
...
Change-Id: I6bd56338a9c7aa05a575232471124cbe57475647
diff --git a/Android.mk b/Android.mk
index c842c5e..b58b72a 100644
--- a/Android.mk
+++ b/Android.mk
@@ -79,6 +79,8 @@
LOCAL_SRC_FILES += $(call all-Iaidl-files-under, $(BASE_DIR))
LOCAL_SRC_FILES := $(filter-out $(EXCLUDE_FILES),$(LOCAL_SRC_FILES))
+LOCAL_AIDL_INCLUDES := $(LOCAL_PATH)/java
+
LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)
LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(RES_DIRS))
@@ -176,6 +178,7 @@
LOCAL_PRODUCT_MODULE := true
LOCAL_USE_AAPT2 := true
LOCAL_REQUIRED_MODULES := privapp_whitelist_com.android.dialer
+LOCAL_REQUIRED_MODULES += privapp_whitelist_com.android.dialer-ext.xml
LOCAL_USES_LIBRARIES := org.apache.http.legacy
LOCAL_LICENSE_KINDS := SPDX-license-identifier-Apache-2.0
@@ -183,6 +186,15 @@
LOCAL_NOTICE_FILE := $(LOCAL_PATH)/LICENSE
include $(BUILD_PACKAGE)
+include $(CLEAR_VARS)
+LOCAL_MODULE := privapp_whitelist_com.android.dialer-ext.xml
+LOCAL_MODULE_CLASS := ETC
+LOCAL_MODULE_TAGS := optional
+LOCAL_MODULE_PATH := $(TARGET_OUT_PRODUCT_ETC)/permissions
+LOCAL_PRODUCT_MODULE := true
+LOCAL_SRC_FILES := $(LOCAL_MODULE)
+include $(BUILD_PREBUILT)
+
# Cleanup local state
BASE_DIR :=
EXCLUDE_FILES :=
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a106e12..39d1c1e 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -78,8 +78,9 @@
<uses-permission android:name="android.permission.STOP_APP_SWITCHES"/>
<!-- Used for sending PendingIntents to dynamically registered receivers -->
- <uses-permission android:name="com.android.dialer.permission.DIALER_ORIGIN"
+ <permission android:name="com.android.dialer.permission.DIALER_ORIGIN"
android:protectionLevel="signature" />
+ <uses-permission android:name="com.android.dialer.permission.DIALER_ORIGIN"/>
<!-- Permissions needed for badger count showing on launch icon. -->
@@ -118,9 +119,11 @@
android:icon="@mipmap/ic_launcher_phone"
android:label="@string/applicationLabel"
android:name="com.android.dialer.binary.aosp.AospDialerApplication"
+ android:appCategory="social"
android:supportsRtl="true"
android:usesCleartextTraffic="false"
- android:extractNativeLibs="false">
+ android:extractNativeLibs="false"
+ android:requestLegacyExternalStorage="true">
</application>
</manifest>
diff --git a/assets/quantum/res/drawable/quantum_ic_record_white_36.xml b/assets/quantum/res/drawable/quantum_ic_record_white_36.xml
new file mode 100644
index 0000000..35aaa41
--- /dev/null
+++ b/assets/quantum/res/drawable/quantum_ic_record_white_36.xml
@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:height="36dp"
+ android:width="36dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M19,12C19,15.86 15.86,19 12,19C8.14,19 5,15.86 5,12C5,8.14 8.14,5 12,5C15.86,5 19,8.14 19,12Z" />
+</vector>
diff --git a/java/com/android/dialer/app/AccountSelectionActivity.java b/java/com/android/dialer/app/AccountSelectionActivity.java
new file mode 100644
index 0000000..de84a36
--- /dev/null
+++ b/java/com/android/dialer/app/AccountSelectionActivity.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2018 The LineageOS 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.dialer.app;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+
+import com.android.contacts.common.widget.SelectPhoneAccountDialogFragment;
+import com.android.contacts.common.widget.SelectPhoneAccountDialogOptions;
+import com.android.contacts.common.widget.SelectPhoneAccountDialogOptionsUtil;
+import com.android.dialer.callintent.CallInitiationType;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.util.CallUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AccountSelectionActivity extends AppCompatActivity {
+ public static Intent createIntent(Context context, String number,
+ CallInitiationType.Type initiationType) {
+ if (TextUtils.isEmpty(number)) {
+ return null;
+ }
+
+ List<PhoneAccount> accounts =
+ CallUtil.getCallCapablePhoneAccounts(context, PhoneAccount.SCHEME_TEL);
+ if (accounts == null || accounts.size() <= 1) {
+ return null;
+ }
+ ArrayList<PhoneAccountHandle> accountHandles = new ArrayList<>();
+ for (PhoneAccount account : accounts) {
+ accountHandles.add(account.getAccountHandle());
+ }
+
+ return new Intent(context, AccountSelectionActivity.class)
+ .putExtra("number", number)
+ .putExtra("accountHandles", accountHandles)
+ .putExtra("type", initiationType.ordinal());
+ }
+
+ private String number;
+ private CallInitiationType.Type initiationType;
+
+ private SelectPhoneAccountDialogFragment.SelectPhoneAccountListener listener =
+ new SelectPhoneAccountDialogFragment.SelectPhoneAccountListener() {
+ @Override
+ public void onPhoneAccountSelected(PhoneAccountHandle selectedAccountHandle,
+ boolean setDefault, String callId) {
+ Intent intent = new CallIntentBuilder(number, initiationType)
+ .setPhoneAccountHandle(selectedAccountHandle)
+ .build();
+ startActivity(intent);
+ finish();
+ }
+
+ @Override
+ public void onDialogDismissed(String callId) {
+ finish();
+ }
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ number = getIntent().getStringExtra("number");
+ initiationType = CallInitiationType.Type.values()[getIntent().getIntExtra("type", 0)];
+
+ if (getFragmentManager().findFragmentByTag("dialog") == null) {
+ List<PhoneAccountHandle> handles = getIntent().getParcelableArrayListExtra("accountHandles");
+ SelectPhoneAccountDialogOptions options = SelectPhoneAccountDialogOptionsUtil
+ .builderWithAccounts(handles)
+ .setTitle(R.string.call_via_dialog_title)
+ .setCanSetDefault(false)
+ .build();
+ SelectPhoneAccountDialogFragment dialog =
+ SelectPhoneAccountDialogFragment.newInstance(options, listener);
+
+ dialog.show(getFragmentManager(), "dialog");
+ }
+ }
+}
diff --git a/java/com/android/dialer/app/AndroidManifest.xml b/java/com/android/dialer/app/AndroidManifest.xml
index ade5bd4..93dedb6 100644
--- a/java/com/android/dialer/app/AndroidManifest.xml
+++ b/java/com/android/dialer/app/AndroidManifest.xml
@@ -78,6 +78,10 @@
android:theme="@style/DialtactsTheme">
</activity>
+ <activity android:name="com.android.dialer.app.AccountSelectionActivity"
+ android:theme="@style/TransparentTheme"
+ android:exported="false" />
+
<receiver android:name="com.android.dialer.app.calllog.CallLogReceiver"
android:exported="true">
<intent-filter>
diff --git a/java/com/android/dialer/app/DialtactsActivity.java b/java/com/android/dialer/app/DialtactsActivity.java
index 25a6956..318f9a1 100644
--- a/java/com/android/dialer/app/DialtactsActivity.java
+++ b/java/com/android/dialer/app/DialtactsActivity.java
@@ -769,9 +769,10 @@
String number = data.getStringExtra(OldCallDetailsActivity.EXTRA_PHONE_NUMBER);
int snackbarDurationMillis = 5_000;
Snackbar.make(parentLayout, getString(R.string.ec_data_deleted), snackbarDurationMillis)
- .setAction(
- R.string.view_conversation,
- v -> startActivity(IntentProvider.getSendSmsIntentProvider(number).getIntent(this)))
+ .setAction(R.string.view_conversation, v -> {
+ IntentProvider provider = IntentProvider.getSendSmsIntentProvider(number);
+ startActivity(provider.getClickIntent(this));
+ })
.setActionTextColor(getResources().getColor(R.color.dialer_snackbar_action_text_color))
.show();
}
diff --git a/java/com/android/dialer/app/calllog/CallLogActivity.java b/java/com/android/dialer/app/calllog/CallLogActivity.java
index fdfb3ab..ff3ff55 100644
--- a/java/com/android/dialer/app/calllog/CallLogActivity.java
+++ b/java/com/android/dialer/app/calllog/CallLogActivity.java
@@ -33,6 +33,8 @@
import com.android.contacts.common.list.ViewPagerTabs;
import com.android.dialer.app.R;
import com.android.dialer.calldetails.OldCallDetailsActivity;
+import com.android.dialer.callstats.CallStatsFragment;
+import com.android.dialer.callstats.DoubleDatePickerDialog;
import com.android.dialer.common.Assert;
import com.android.dialer.constants.ActivityRequestCodes;
import com.android.dialer.database.CallLogQueryHandler;
@@ -45,17 +47,19 @@
import com.android.dialer.util.ViewUtil;
/** Activity for viewing call history. */
-public class CallLogActivity extends TransactionSafeActivity
- implements ViewPager.OnPageChangeListener {
+public class CallLogActivity extends TransactionSafeActivity implements
+ ViewPager.OnPageChangeListener, DoubleDatePickerDialog.OnDateSetListener {
@VisibleForTesting static final int TAB_INDEX_ALL = 0;
@VisibleForTesting static final int TAB_INDEX_MISSED = 1;
- private static final int TAB_INDEX_COUNT = 2;
+ private static final int TAB_INDEX_STATS = 2;
+ private static final int TAB_INDEX_COUNT = 3;
private ViewPager viewPager;
private ViewPagerTabs viewPagerTabs;
private ViewPagerAdapter viewPagerAdapter;
private CallLogFragment allCallsFragment;
private CallLogFragment missedCallsFragment;
+ private CallStatsFragment statsFragment;
private String[] tabTitles;
private boolean isResumed;
private int selectedPageIndex;
@@ -86,6 +90,7 @@
tabTitles = new String[TAB_INDEX_COUNT];
tabTitles[0] = getString(R.string.call_log_all_title);
tabTitles[1] = getString(R.string.call_log_missed_title);
+ tabTitles[2] = getString(R.string.call_log_stats_title);
viewPager = (ViewPager) findViewById(R.id.call_log_pager);
@@ -187,6 +192,15 @@
viewPagerTabs.onPageScrollStateChanged(state);
}
+ @Override
+ public void onDateSet(long from, long to) {
+ switch (viewPager.getCurrentItem()) {
+ case TAB_INDEX_STATS:
+ statsFragment.onDateSet(from, to);
+ break;
+ }
+ }
+
private void sendScreenViewForChildFragment() {
Logger.get(this).logScreenView(ScreenEvent.Type.CALL_LOG_FILTER, this);
}
@@ -213,6 +227,8 @@
missedCallsFragment.markMissedCallsAsReadAndRemoveNotifications();
}
break;
+ case TAB_INDEX_STATS:
+ break;
default:
throw Assert.createIllegalStateFailException("Invalid position: " + position);
}
@@ -244,6 +260,8 @@
CallLogQueryHandler.CALL_TYPE_ALL, true /* isCallLogActivity */);
case TAB_INDEX_MISSED:
return new CallLogFragment(Calls.MISSED_TYPE, true /* isCallLogActivity */);
+ case TAB_INDEX_STATS:
+ return new CallStatsFragment();
default:
throw new IllegalStateException("No fragment at position " + position);
}
@@ -251,13 +269,16 @@
@Override
public Object instantiateItem(ViewGroup container, int position) {
- final CallLogFragment fragment = (CallLogFragment) super.instantiateItem(container, position);
+ final Object fragment = super.instantiateItem(container, position);
switch (getRtlPosition(position)) {
case TAB_INDEX_ALL:
- allCallsFragment = fragment;
+ allCallsFragment = (CallLogFragment) fragment;
break;
case TAB_INDEX_MISSED:
- missedCallsFragment = fragment;
+ missedCallsFragment = (CallLogFragment) fragment;
+ break;
+ case TAB_INDEX_STATS:
+ statsFragment = (CallStatsFragment) fragment;
break;
default:
throw Assert.createIllegalStateFailException("Invalid position: " + position);
@@ -284,9 +305,10 @@
&& data.getBooleanExtra(OldCallDetailsActivity.EXTRA_HAS_ENRICHED_CALL_DATA, false)) {
String number = data.getStringExtra(OldCallDetailsActivity.EXTRA_PHONE_NUMBER);
Snackbar.make(findViewById(R.id.calllog_frame), getString(R.string.ec_data_deleted), 5_000)
- .setAction(
- R.string.view_conversation,
- v -> startActivity(IntentProvider.getSendSmsIntentProvider(number).getIntent(this)))
+ .setAction(R.string.view_conversation, v -> {
+ IntentProvider provider = IntentProvider.getSendSmsIntentProvider(number);
+ startActivity(provider.getClickIntent(this));
+ })
.setActionTextColor(getResources().getColor(R.color.dialer_snackbar_action_text_color))
.show();
}
diff --git a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
index 05011d1..af4fb29 100644
--- a/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
+++ b/java/com/android/dialer/app/calllog/CallLogListItemViewHolder.java
@@ -106,6 +106,7 @@
*/
public final class CallLogListItemViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener,
+ View.OnLongClickListener,
MenuItem.OnMenuItemClickListener,
View.OnCreateContextMenuListener {
@@ -306,6 +307,7 @@
quickContactView.setOverlay(null);
quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
primaryActionButtonView.setOnClickListener(this);
+ primaryActionButtonView.setOnLongClickListener(this);
primaryActionView.setOnClickListener(this.expandCollapseListener);
if (this.voicemailPlaybackPresenter != null
&& ConfigProviderComponent.get(this.context)
@@ -519,6 +521,7 @@
primaryActionButtonView.setContentDescription(
TextUtils.expandTemplate(
context.getString(R.string.description_voicemail_action), validNameOrNumber));
+ primaryActionButtonView.setTag(null);
primaryActionButtonView.setVisibility(View.VISIBLE);
} else {
primaryActionButtonView.setVisibility(View.GONE);
@@ -1053,7 +1056,8 @@
return;
}
intentProvider.logInteraction(context);
- final Intent intent = intentProvider.getIntent(context);
+
+ final Intent intent = intentProvider.getClickIntent(context);
// See IntentProvider.getCallDetailIntentProvider() for why this may be null.
if (intent == null) {
return;
@@ -1073,6 +1077,18 @@
}
}
+ @Override
+ public boolean onLongClick(View view) {
+ final IntentProvider intentProvider = (IntentProvider) view.getTag();
+ final Intent intent = intentProvider != null
+ ? intentProvider.getLongClickIntent(context) : null;
+ if (intent != null) {
+ DialerUtils.startActivityWithErrorToast(context, intent);
+ return true;
+ }
+ return false;
+ }
+
private static boolean isNonContactEntry(ContactInfo info) {
if (info == null || info.sourceType != Type.SOURCE_TYPE_DIRECTORY) {
return true;
@@ -1379,6 +1395,9 @@
Calls.CONTENT_URI,
CallLog.Calls._ID + " IN (" + callIdsStr + ")" /* where */,
null /* selectionArgs */);
+ context
+ .getContentResolver()
+ .notifyChange(Calls.CONTENT_URI, null);
}
return null;
diff --git a/java/com/android/dialer/app/calllog/ClearCallLogDialog.java b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java
index 197d2da..bea06d3 100644
--- a/java/com/android/dialer/app/calllog/ClearCallLogDialog.java
+++ b/java/com/android/dialer/app/calllog/ClearCallLogDialog.java
@@ -101,6 +101,7 @@
@Override
public Void doInBackground(@Nullable Void unused) throws Throwable {
appContext.getContentResolver().delete(Calls.CONTENT_URI, null, null);
+ appContext.getContentResolver().notifyChange(Calls.CONTENT_URI, null);
CachedNumberLookupService cachedNumberLookupService =
PhoneNumberCache.get(appContext).getCachedNumberLookupService();
if (cachedNumberLookupService != null) {
diff --git a/java/com/android/dialer/app/calllog/IntentProvider.java b/java/com/android/dialer/app/calllog/IntentProvider.java
index 423b49c..4c5a98b 100644
--- a/java/com/android/dialer/app/calllog/IntentProvider.java
+++ b/java/com/android/dialer/app/calllog/IntentProvider.java
@@ -26,6 +26,7 @@
import android.telephony.TelephonyManager;
import com.android.contacts.common.model.Contact;
import com.android.contacts.common.model.ContactLoader;
+import com.android.dialer.app.AccountSelectionActivity;
import com.android.dialer.calldetails.CallDetailsEntries;
import com.android.dialer.calldetails.OldCallDetailsActivity;
import com.android.dialer.callintent.CallInitiationType;
@@ -55,12 +56,18 @@
final String number, final PhoneAccountHandle accountHandle) {
return new IntentProvider() {
@Override
- public Intent getIntent(Context context) {
+ public Intent getClickIntent(Context context) {
return PreCall.getIntent(
context,
new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG)
.setPhoneAccountHandle(accountHandle));
}
+
+ @Override
+ public Intent getLongClickIntent(Context context) {
+ return AccountSelectionActivity.createIntent(context, number,
+ CallInitiationType.Type.CALL_LOG);
+ }
};
}
@@ -68,12 +75,13 @@
final String number, final Context context, final TelephonyManager telephonyManager) {
return new IntentProvider() {
@Override
- public Intent getIntent(Context context) {
+ public Intent getClickIntent(Context context) {
return PreCall.getIntent(
context,
new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG)
.setAllowAssistedDial(true));
}
+
};
}
@@ -85,7 +93,7 @@
final String number, final PhoneAccountHandle accountHandle) {
return new IntentProvider() {
@Override
- public Intent getIntent(Context context) {
+ public Intent getClickIntent(Context context) {
return PreCall.getIntent(
context,
new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG)
@@ -98,7 +106,7 @@
public static IntentProvider getDuoVideoIntentProvider(String number, boolean isNonContact) {
return new IntentProvider() {
@Override
- public Intent getIntent(Context context) {
+ public Intent getClickIntent(Context context) {
return PreCall.getIntent(
context,
new CallIntentBuilder(number, CallInitiationType.Type.CALL_LOG)
@@ -122,7 +130,7 @@
public static IntentProvider getInstallDuoIntentProvider() {
return new IntentProvider() {
@Override
- public Intent getIntent(Context context) {
+ public Intent getClickIntent(Context context) {
return DuoComponent.get(context).getDuo().getInstallDuoIntent().orNull();
}
@@ -136,7 +144,7 @@
public static IntentProvider getSetUpDuoIntentProvider() {
return new IntentProvider() {
@Override
- public Intent getIntent(Context context) {
+ public Intent getClickIntent(Context context) {
return DuoComponent.get(context).getDuo().getActivateIntent().orNull();
}
@@ -150,7 +158,7 @@
public static IntentProvider getDuoInviteIntentProvider(String number) {
return new IntentProvider() {
@Override
- public Intent getIntent(Context context) {
+ public Intent getClickIntent(Context context) {
return DuoComponent.get(context).getDuo().getInviteIntent(number).orNull();
}
@@ -164,7 +172,7 @@
public static IntentProvider getReturnVoicemailCallIntentProvider() {
return new IntentProvider() {
@Override
- public Intent getIntent(Context context) {
+ public Intent getClickIntent(Context context) {
return PreCall.getIntent(
context,
CallIntentBuilder.forVoicemail(CallInitiationType.Type.CALL_LOG));
@@ -175,7 +183,7 @@
public static IntentProvider getSendSmsIntentProvider(final String number) {
return new IntentProvider() {
@Override
- public Intent getIntent(Context context) {
+ public Intent getClickIntent(Context context) {
return IntentUtil.getSendSmsIntent(number);
}
};
@@ -197,7 +205,7 @@
boolean canSupportAssistedDialing) {
return new IntentProvider() {
@Override
- public Intent getIntent(Context context) {
+ public Intent getClickIntent(Context context) {
return OldCallDetailsActivity.newInstance(
context, callDetailsEntries, contact, canReportCallerId, canSupportAssistedDialing);
}
@@ -213,7 +221,7 @@
final boolean isNewContact) {
return new IntentProvider() {
@Override
- public Intent getIntent(Context context) {
+ public Intent getClickIntent(Context context) {
Contact contactToSave = null;
if (lookupUri != null) {
@@ -274,7 +282,9 @@
};
}
- public abstract Intent getIntent(Context context);
-
+ public abstract Intent getClickIntent(Context context);
+ public Intent getLongClickIntent(Context context) {
+ return null;
+ }
public void logInteraction(Context context) {}
}
diff --git a/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java b/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java
index aed51b5..267dc62 100644
--- a/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java
+++ b/java/com/android/dialer/app/contactinfo/ExpirableCacheHeadlessFragment.java
@@ -34,7 +34,8 @@
private static final String FRAGMENT_TAG = "ExpirableCacheHeadlessFragment";
private static final int CONTACT_INFO_CACHE_SIZE = 100;
- private ExpirableCache<NumberWithCountryIso, ContactInfo> retainedCache;
+ private ExpirableCache<NumberWithCountryIso, ContactInfo> retainedCache =
+ ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
@NonNull
public static ExpirableCacheHeadlessFragment attach(@NonNull AppCompatActivity parentActivity) {
@@ -57,7 +58,6 @@
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- retainedCache = ExpirableCache.create(CONTACT_INFO_CACHE_SIZE);
setRetainInstance(true);
}
diff --git a/java/com/android/dialer/app/res/values-night/colors.xml b/java/com/android/dialer/app/res/values-night/colors.xml
new file mode 100644
index 0000000..1d165af
--- /dev/null
+++ b/java/com/android/dialer/app/res/values-night/colors.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ 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
+-->
+
+<resources>
+ <color name="nav_item">#80FFFFFF</color>
+ <color name="nav_item_selected">#FFFFFF</color>
+</resources>
diff --git a/java/com/android/dialer/app/res/values/cm_arrays.xml b/java/com/android/dialer/app/res/values/cm_arrays.xml
new file mode 100644
index 0000000..a788fd3
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/cm_arrays.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013-2014 The CyanogenMod Project
+ Copyright (C) 2018 The LineageOS 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="call_recording_encoder_entries" translatable="false">
+ <item>@string/wb_amr_format</item>
+ <item>@string/aac_format</item>
+ </string-array>
+
+ <string-array name="call_recording_encoder_values" translatable="false">
+ <item>"0"</item>
+ <item>"1"</item>
+ </string-array>
+
+</resources>
diff --git a/java/com/android/dialer/app/res/values/cm_attrs.xml b/java/com/android/dialer/app/res/values/cm_attrs.xml
new file mode 100644
index 0000000..3155845
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/cm_attrs.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013-2014 The CyanogenMod Project
+ Copyright (C) 2018 The LineageOS 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.
+-->
+<resources>
+ <declare-styleable name="LinearColorBar">
+ <attr name="redColor" format="color" />
+ <attr name="greenColor" format="color" />
+ <attr name="blueColor" format="color" />
+ <attr name="orangeColor" format="color" />
+ <attr name="backgroundColor" format="color" />
+ </declare-styleable>
+</resources>
diff --git a/java/com/android/dialer/app/res/values/cm_strings.xml b/java/com/android/dialer/app/res/values/cm_strings.xml
new file mode 100644
index 0000000..1dcdb2b
--- /dev/null
+++ b/java/com/android/dialer/app/res/values/cm_strings.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013-2014 The CyanogenMod Project
+ Copyright (C) 2018 The LineageOS 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="incall_category_key" translatable="false">dialer_general_incall_category_key</string>
+ <string name="incall_category_title">In-call</string>
+ <string name="incall_enable_dnd_title">Enable Do Not Disturb during calls</string>
+ <string name="incall_vibrate_outgoing_key" translatable="false">incall_vibrate_outgoing</string>
+ <string name="incall_vibrate_outgoing_title">Vibrate on answer</string>
+ <string name="incall_vibrate_call_waiting_key" translatable="false">incall_vibrate_call_waiting</string>
+ <string name="incall_vibrate_call_waiting_title">Vibrate on call waiting</string>
+ <string name="incall_vibrate_hangup_key" translatable="false">incall_vibrate_hangup</string>
+ <string name="incall_vibrate_hangup_title">Vibrate on hang up</string>
+ <string name="incall_vibrate_45_key" translatable="false">incall_vibrate_45secs</string>
+ <string name="incall_vibrate_45_title">Vibrate every minute</string>
+ <string name="incall_vibrate_45_summary">Vibrates at the 45 second mark of every minute during outgoing calls</string>
+ <string name="incall_dnd_dialog_message">In order to enable Do Not Disturb, the Phone app needs to be granted the permission to control the Do Not Disturb status.\nPlease allow it.</string>
+ <string name="allow">Allow</string>
+ <string name="deny">Deny</string>
+
+ <string name="call_recording_category_key" translatable="false">call_recording_category</string>
+ <string name="call_recording_category_title">Call recording</string>
+ <string name="call_recording_format_key" translatable="false">call_recording_format</string>
+ <string name="call_recording_format">Audio format</string>
+ <string name="wb_amr_format" translatable="false">AMR-WB</string>
+ <string name="aac_format" translatable="false">AAC</string>
+
+ <string name="call_via">Call via</string>
+ <string name="call_via_dialog_title">Call via\u2026</string>
+
+ <string name="call_log_stats_title">Statistics</string>
+</resources>
diff --git a/java/com/android/dialer/app/res/values/colors.xml b/java/com/android/dialer/app/res/values/colors.xml
index a971347..f59f4f1 100644
--- a/java/com/android/dialer/app/res/values/colors.xml
+++ b/java/com/android/dialer/app/res/values/colors.xml
@@ -18,9 +18,14 @@
<color name="voicemail_icon_disabled_tint">#80000000</color>
<color name="voicemail_playpause_icon_tint">?colorIcon</color>
+ <color name="call_record_playback_icon_color">#8a000000</color>
+
<!-- Text color for the "Remove" text when a contact is dragged on top of the remove view -->
<color name="remove_highlighted_text_color">#FF3F3B</color>
<!-- Colors for blocked numbers list -->
<color name="blocked_number_block_color">#F44336</color>
+
+ <color name="nav_item">#80666666</color>
+ <color name="nav_item_selected">#666666</color>
</resources>
diff --git a/java/com/android/dialer/app/res/values/styles.xml b/java/com/android/dialer/app/res/values/styles.xml
index c2cad31..0fa3503 100644
--- a/java/com/android/dialer/app/res/values/styles.xml
+++ b/java/com/android/dialer/app/res/values/styles.xml
@@ -23,6 +23,16 @@
<item name="android:textColorPrimary">?android:attr/textColorPrimaryInverse</item>
</style>
+ <style name="TransparentTheme" parent="DialtactsTheme">
+ <item name="android:alertDialogTheme">@style/AlertDialogTheme</item>
+ <item name="android:windowBackground">@android:color/transparent</item>
+ <item name="android:windowAnimationStyle">@android:style/Animation.Translucent</item>
+ <item name="android:windowContentOverlay">@null</item>
+ <item name="android:windowIsTranslucent">true</item>
+ <item name="android:windowNoTitle">true</item>
+ <item name="android:windowIsFloating">true</item>
+ </style>
+
<style name="ActionModeStyle" parent="Widget.AppCompat.ActionMode">
<item name="android:background">?android:attr/colorPrimary</item>
<item name="background">?android:attr/colorPrimary</item>
diff --git a/java/com/android/dialer/app/res/xml/file_paths.xml b/java/com/android/dialer/app/res/xml/file_paths.xml
index 0dd41a0..b43f450 100644
--- a/java/com/android/dialer/app/res/xml/file_paths.xml
+++ b/java/com/android/dialer/app/res/xml/file_paths.xml
@@ -22,4 +22,8 @@
<files-path
name="voicemails"
path="voicemails/"/>
+ <!-- Offer access to saved call recordings -->
+ <external-path
+ name="recordings"
+ path="CallRecordings/"/>
</paths>
diff --git a/java/com/android/dialer/app/res/xml/sound_settings.xml b/java/com/android/dialer/app/res/xml/sound_settings.xml
index d9afb74..aa02587 100644
--- a/java/com/android/dialer/app/res/xml/sound_settings.xml
+++ b/java/com/android/dialer/app/res/xml/sound_settings.xml
@@ -43,4 +43,46 @@
android:key="@string/dtmf_tone_length_preference_key"
android:title="@string/dtmf_tone_length_title"/>
+ <PreferenceCategory
+ android:key="@string/incall_category_key"
+ android:title="@string/incall_category_title">
+
+ <SwitchPreference
+ android:defaultValue="false"
+ android:key="incall_enable_dnd"
+ android:title="@string/incall_enable_dnd_title"/>
+
+ <SwitchPreference
+ android:key="@string/incall_vibrate_outgoing_key"
+ android:title="@string/incall_vibrate_outgoing_title" />
+
+ <SwitchPreference
+ android:key="@string/incall_vibrate_call_waiting_key"
+ android:title="@string/incall_vibrate_call_waiting_title" />
+
+ <SwitchPreference
+ android:key="@string/incall_vibrate_hangup_key"
+ android:title="@string/incall_vibrate_hangup_title" />
+
+ <SwitchPreference
+ android:key="@string/incall_vibrate_45_key"
+ android:title="@string/incall_vibrate_45_title"
+ android:summary="@string/incall_vibrate_45_summary" />
+
+ </PreferenceCategory>
+
+ <PreferenceCategory
+ android:key="@string/call_recording_category_key"
+ android:title="@string/call_recording_category_title">
+
+ <ListPreference
+ android:key="@string/call_recording_format_key"
+ android:title="@string/call_recording_format"
+ android:summary="%s"
+ android:entries="@array/call_recording_encoder_entries"
+ android:entryValues="@array/call_recording_encoder_values"
+ android:defaultValue="0" />
+
+ </PreferenceCategory>
+
</PreferenceScreen>
diff --git a/java/com/android/dialer/app/settings/DialerSettingsActivity.java b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
index d9bcd6b..6ffa62a 100644
--- a/java/com/android/dialer/app/settings/DialerSettingsActivity.java
+++ b/java/com/android/dialer/app/settings/DialerSettingsActivity.java
@@ -39,6 +39,7 @@
import com.android.dialer.common.LogUtil;
import com.android.dialer.compat.telephony.TelephonyManagerCompat;
import com.android.dialer.configprovider.ConfigProviderComponent;
+import com.android.dialer.lookup.LookupSettingsFragment;
import com.android.dialer.proguard.UsedByReflection;
import com.android.dialer.util.PermissionsUtil;
import com.android.dialer.voicemail.settings.VoicemailSettingsFragment;
@@ -102,6 +103,7 @@
Header soundSettingsHeader = new Header();
soundSettingsHeader.titleRes = R.string.sounds_and_vibration_title;
+ soundSettingsHeader.fragment = SoundSettingsFragment.class.getName();
soundSettingsHeader.id = R.id.settings_header_sounds_and_vibration;
target.add(soundSettingsHeader);
@@ -112,6 +114,11 @@
quickResponseSettingsHeader.intent = quickResponseSettingsIntent;
target.add(quickResponseSettingsHeader);
+ final Header lookupSettingsHeader = new Header();
+ lookupSettingsHeader.titleRes = R.string.lookup_settings_label;
+ lookupSettingsHeader.fragment = LookupSettingsFragment.class.getName();
+ target.add(lookupSettingsHeader);
+
TelephonyManager telephonyManager =
(TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
@@ -271,32 +278,22 @@
&& getResources().getBoolean(R.bool.config_sort_order_user_changeable);
}
- /**
- * For the "sounds and vibration" setting, we go directly to the system sound settings fragment.
- * This helps since:
- * <li>We don't need a separate Dialer sounds and vibrations fragment, as everything we need is
- * present in the system sounds fragment.
- * <li>OEM's e.g Moto that support dual sim ring-tones no longer need to update the dialer sound
- * and settings fragment.
- *
- * <p>For all other settings, we launch our our preferences fragment.
- */
@Override
public void onHeaderClick(Header header, int position) {
if (header.id == R.id.settings_header_sounds_and_vibration) {
-
+ // If we don't have the permission to write to system settings, go to system sound
+ // settings instead. Otherwise, perform the super implementation (which launches our
+ // own preference fragment.
if (!Settings.System.canWrite(this)) {
Toast.makeText(
this,
getResources().getString(R.string.toast_cannot_write_system_settings),
Toast.LENGTH_SHORT)
.show();
+ startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
+ return;
}
-
- startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
- return;
}
-
super.onHeaderClick(header, position);
}
diff --git a/java/com/android/dialer/app/settings/SoundSettingsFragment.java b/java/com/android/dialer/app/settings/SoundSettingsFragment.java
new file mode 100644
index 0000000..f7fc0d0
--- /dev/null
+++ b/java/com/android/dialer/app/settings/SoundSettingsFragment.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2014 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.dialer.app.settings;
+
+import android.app.AlertDialog;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.media.RingtoneManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Vibrator;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceFragment;
+import android.preference.PreferenceScreen;
+import android.preference.SwitchPreference;
+import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
+import android.telephony.TelephonyManager;
+import android.widget.Toast;
+import com.android.dialer.app.R;
+import com.android.dialer.callrecord.impl.CallRecorderService;
+import com.android.dialer.util.SettingsUtil;
+
+public class SoundSettingsFragment extends PreferenceFragment
+ implements Preference.OnPreferenceChangeListener {
+
+ private static final int NO_DTMF_TONE = 0;
+ private static final int PLAY_DTMF_TONE = 1;
+
+ private static final int NO_VIBRATION_FOR_CALLS = 0;
+ private static final int DO_VIBRATION_FOR_CALLS = 1;
+
+ private static final int DTMF_TONE_TYPE_NORMAL = 0;
+
+ private static final int MSG_UPDATE_RINGTONE_SUMMARY = 1;
+
+ private Preference ringtonePreference;
+ private final Handler ringtoneLookupComplete =
+ new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_UPDATE_RINGTONE_SUMMARY:
+ ringtonePreference.setSummary((CharSequence) msg.obj);
+ break;
+ }
+ }
+ };
+ private final Runnable ringtoneLookupRunnable =
+ new Runnable() {
+ @Override
+ public void run() {
+ updateRingtonePreferenceSummary();
+ }
+ };
+ private SwitchPreference vibrateWhenRinging;
+ private SwitchPreference playDtmfTone;
+ private ListPreference dtmfToneLength;
+ private SwitchPreference enableDndInCall;
+
+ private NotificationManager notificationManager;
+
+ @Override
+ public Context getContext() {
+ return getActivity();
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.sound_settings);
+
+ Context context = getActivity();
+
+ ringtonePreference = findPreference(context.getString(R.string.ringtone_preference_key));
+ vibrateWhenRinging =
+ (SwitchPreference) findPreference(context.getString(R.string.vibrate_on_preference_key));
+ playDtmfTone =
+ (SwitchPreference) findPreference(context.getString(R.string.play_dtmf_preference_key));
+ dtmfToneLength =
+ (ListPreference)
+ findPreference(context.getString(R.string.dtmf_tone_length_preference_key));
+ enableDndInCall = (SwitchPreference) findPreference("incall_enable_dnd");
+
+ if (hasVibrator()) {
+ vibrateWhenRinging.setOnPreferenceChangeListener(this);
+ } else {
+ PreferenceScreen ps = getPreferenceScreen();
+ Preference inCallVibrateOutgoing = findPreference(
+ context.getString(R.string.incall_vibrate_outgoing_key));
+ Preference inCallVibrateCallWaiting = findPreference(
+ context.getString(R.string.incall_vibrate_call_waiting_key));
+ Preference inCallVibrateHangup = findPreference(
+ context.getString(R.string.incall_vibrate_hangup_key));
+ Preference inCallVibrate45Secs = findPreference(
+ context.getString(R.string.incall_vibrate_45_key));
+ ps.removePreference(vibrateWhenRinging);
+ ps.removePreference(inCallVibrateOutgoing);
+ ps.removePreference(inCallVibrateCallWaiting);
+ ps.removePreference(inCallVibrateHangup);
+ ps.removePreference(inCallVibrate45Secs);
+ vibrateWhenRinging = null;
+ }
+
+ playDtmfTone.setOnPreferenceChangeListener(this);
+ playDtmfTone.setChecked(shouldPlayDtmfTone());
+
+ enableDndInCall.setOnPreferenceChangeListener(this);
+
+ TelephonyManager telephonyManager =
+ (TelephonyManager) getActivity().getSystemService(Context.TELEPHONY_SERVICE);
+ if (telephonyManager.canChangeDtmfToneLength()
+ && (telephonyManager.isWorldPhone() || !shouldHideCarrierSettings())) {
+ dtmfToneLength.setOnPreferenceChangeListener(this);
+ dtmfToneLength.setValueIndex(
+ Settings.System.getInt(
+ context.getContentResolver(),
+ Settings.System.DTMF_TONE_TYPE_WHEN_DIALING,
+ DTMF_TONE_TYPE_NORMAL));
+ } else {
+ getPreferenceScreen().removePreference(dtmfToneLength);
+ dtmfToneLength = null;
+ }
+ if (!CallRecorderService.isEnabled(getActivity())) {
+ getPreferenceScreen().removePreference(
+ findPreference(context.getString(R.string.call_recording_category_key)));
+ }
+ notificationManager = context.getSystemService(NotificationManager.class);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ if (!Settings.System.canWrite(getContext())) {
+ // If the user launches this setting fragment, then toggles the WRITE_SYSTEM_SETTINGS
+ // AppOp, then close the fragment since there is nothing useful to do.
+ getActivity().onBackPressed();
+ return;
+ }
+
+ if (vibrateWhenRinging != null) {
+ vibrateWhenRinging.setChecked(shouldVibrateWhenRinging());
+ }
+
+ // Lookup the ringtone name asynchronously.
+ new Thread(ringtoneLookupRunnable).start();
+ }
+
+ /**
+ * Supports onPreferenceChangeListener to look for preference changes.
+ *
+ * @param preference The preference to be changed
+ * @param objValue The value of the selection, NOT its localized display value.
+ */
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object objValue) {
+ if (!Settings.System.canWrite(getContext())) {
+ // A user shouldn't be able to get here, but this protects against monkey crashes.
+ Toast.makeText(
+ getContext(),
+ getResources().getString(R.string.toast_cannot_write_system_settings),
+ Toast.LENGTH_SHORT)
+ .show();
+ return true;
+ }
+ if (preference == vibrateWhenRinging) {
+ boolean doVibrate = (Boolean) objValue;
+ Settings.System.putInt(
+ getActivity().getContentResolver(),
+ Settings.System.VIBRATE_WHEN_RINGING,
+ doVibrate ? DO_VIBRATION_FOR_CALLS : NO_VIBRATION_FOR_CALLS);
+ } else if (preference == dtmfToneLength) {
+ int index = dtmfToneLength.findIndexOfValue((String) objValue);
+ Settings.System.putInt(
+ getActivity().getContentResolver(), Settings.System.DTMF_TONE_TYPE_WHEN_DIALING, index);
+ } else if (preference == enableDndInCall) {
+ boolean newValue = (Boolean) objValue;
+ if (newValue && !notificationManager.isNotificationPolicyAccessGranted()) {
+ new AlertDialog.Builder(getContext())
+ .setMessage(R.string.incall_dnd_dialog_message)
+ .setPositiveButton(R.string.allow, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ Intent intent = new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS);
+ startActivity(intent);
+ }
+ })
+ .setNegativeButton(R.string.deny, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.dismiss();
+ }
+ })
+ .show();
+
+ // At this time, it is unknown whether the user granted the permission
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** Click listener for toggle events. */
+ @Override
+ public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, Preference preference) {
+ if (!Settings.System.canWrite(getContext())) {
+ Toast.makeText(
+ getContext(),
+ getResources().getString(R.string.toast_cannot_write_system_settings),
+ Toast.LENGTH_SHORT)
+ .show();
+ return true;
+ }
+ if (preference == playDtmfTone) {
+ Settings.System.putInt(
+ getActivity().getContentResolver(),
+ Settings.System.DTMF_TONE_WHEN_DIALING,
+ playDtmfTone.isChecked() ? PLAY_DTMF_TONE : NO_DTMF_TONE);
+ }
+ return true;
+ }
+
+ /** Updates the summary text on the ringtone preference with the name of the ringtone. */
+ private void updateRingtonePreferenceSummary() {
+ SettingsUtil.updateRingtoneName(
+ getActivity(),
+ ringtoneLookupComplete,
+ RingtoneManager.TYPE_RINGTONE,
+ ringtonePreference.getKey(),
+ MSG_UPDATE_RINGTONE_SUMMARY);
+ }
+
+ /**
+ * Obtain the value for "vibrate when ringing" setting. The default value is false.
+ *
+ * <p>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.
+ */
+ private boolean shouldVibrateWhenRinging() {
+ int vibrateWhenRingingSetting =
+ Settings.System.getInt(
+ getActivity().getContentResolver(),
+ Settings.System.VIBRATE_WHEN_RINGING,
+ NO_VIBRATION_FOR_CALLS);
+ return hasVibrator() && (vibrateWhenRingingSetting == DO_VIBRATION_FOR_CALLS);
+ }
+
+ /** Obtains the value for dialpad/DTMF tones. The default value is true. */
+ private boolean shouldPlayDtmfTone() {
+ int dtmfToneSetting =
+ Settings.System.getInt(
+ getActivity().getContentResolver(),
+ Settings.System.DTMF_TONE_WHEN_DIALING,
+ PLAY_DTMF_TONE);
+ return dtmfToneSetting == PLAY_DTMF_TONE;
+ }
+
+ /** Whether the device hardware has a vibrator. */
+ private boolean hasVibrator() {
+ Vibrator vibrator = (Vibrator) getActivity().getSystemService(Context.VIBRATOR_SERVICE);
+ return vibrator != null && vibrator.hasVibrator();
+ }
+
+ private boolean shouldHideCarrierSettings() {
+ CarrierConfigManager configManager =
+ (CarrierConfigManager) getActivity().getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ return configManager
+ .getConfig()
+ .getBoolean(CarrierConfigManager.KEY_HIDE_CARRIER_NETWORK_SETTINGS_BOOL);
+ }
+}
diff --git a/java/com/android/dialer/binary/aosp/AospDialerApplication.java b/java/com/android/dialer/binary/aosp/AospDialerApplication.java
index 4ca94e2..8d94bb9 100644
--- a/java/com/android/dialer/binary/aosp/AospDialerApplication.java
+++ b/java/com/android/dialer/binary/aosp/AospDialerApplication.java
@@ -16,15 +16,34 @@
package com.android.dialer.binary.aosp;
+import android.content.Context;
+import android.net.Uri;
import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import com.android.contacts.common.extensions.PhoneDirectoryExtender;
+import com.android.contacts.common.extensions.PhoneDirectoryExtenderFactory;
import com.android.dialer.binary.common.DialerApplication;
import com.android.dialer.inject.ContextModule;
+import com.android.dialer.lookup.LookupCacheService;
+import com.android.dialer.lookup.LookupProvider;
+import com.android.dialer.lookup.LookupSettings;
+import com.android.dialer.lookup.ReverseLookupService;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.PhoneNumberCacheBindings;
+import com.android.dialer.phonenumbercache.PhoneNumberCacheBindingsFactory;
+import com.android.incallui.bindings.InCallUiBindings;
+import com.android.incallui.bindings.InCallUiBindingsFactory;
+import com.android.incallui.bindings.InCallUiBindingsStub;
+import com.android.incallui.bindings.PhoneNumberService;
+
+import java.util.List;
/**
* The application class for the AOSP Dialer. This is a version of the Dialer app that has no
* dependency on Google Play Services.
*/
-public class AospDialerApplication extends DialerApplication {
+public class AospDialerApplication extends DialerApplication implements
+ PhoneNumberCacheBindingsFactory, PhoneDirectoryExtenderFactory, InCallUiBindingsFactory {
/** Returns a new instance of the root component for the AOSP Dialer. */
@Override
@@ -32,4 +51,43 @@
protected Object buildRootComponent() {
return DaggerAospDialerRootComponent.builder().contextModule(new ContextModule(this)).build();
}
+
+ @Override
+ public PhoneDirectoryExtender newPhoneDirectoryExtender() {
+ return new PhoneDirectoryExtender() {
+ @Override
+ public boolean isEnabled(Context context) {
+ return LookupSettings.isForwardLookupEnabled(AospDialerApplication.this)
+ || LookupSettings.isPeopleLookupEnabled(AospDialerApplication.this);
+ }
+
+ @Override
+ @Nullable
+ public Uri getContentUri() {
+ return LookupProvider.NEARBY_AND_PEOPLE_LOOKUP_URI;
+ }
+ };
+ }
+
+ @Override
+ public InCallUiBindings newInCallUiBindings() {
+ return new InCallUiBindingsStub() {
+ @Override
+ @Nullable
+ public PhoneNumberService newPhoneNumberService(Context context) {
+ return new ReverseLookupService(context);
+ }
+ };
+ }
+
+ @Override
+ public PhoneNumberCacheBindings newPhoneNumberCacheBindings() {
+ return new PhoneNumberCacheBindings() {
+ @Override
+ @Nullable
+ public CachedNumberLookupService getCachedNumberLookupService() {
+ return new LookupCacheService();
+ }
+ };
+ }
}
diff --git a/java/com/android/dialer/binary/common/DialerApplication.java b/java/com/android/dialer/binary/common/DialerApplication.java
index 31d4d82..0f15025 100644
--- a/java/com/android/dialer/binary/common/DialerApplication.java
+++ b/java/com/android/dialer/binary/common/DialerApplication.java
@@ -26,6 +26,7 @@
import com.android.dialer.calllog.CallLogFramework;
import com.android.dialer.calllog.config.CallLogConfig;
import com.android.dialer.calllog.config.CallLogConfigComponent;
+import com.android.dialer.callrecord.CallRecordingAutoMigrator;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DialerExecutorComponent;
import com.android.dialer.inject.HasRootComponent;
@@ -48,6 +49,10 @@
new FilteredNumberAsyncQueryHandler(this),
DialerExecutorComponent.get(this).dialerExecutorFactory())
.asyncAutoMigrate();
+ new CallRecordingAutoMigrator(
+ this.getApplicationContext(),
+ DialerExecutorComponent.get(this).dialerExecutorFactory())
+ .asyncAutoMigrate();
initializeAnnotatedCallLog();
PersistentLogger.initialize(this);
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivity.java b/java/com/android/dialer/calldetails/CallDetailsActivity.java
index 36b8308..c1acbc3 100644
--- a/java/com/android/dialer/calldetails/CallDetailsActivity.java
+++ b/java/com/android/dialer/calldetails/CallDetailsActivity.java
@@ -28,6 +28,7 @@
import com.android.dialer.calldetails.CallDetailsFooterViewHolder.ReportCallIdListener;
import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener;
import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
+import com.android.dialer.callrecord.CallRecordingDataStore;
import com.android.dialer.common.Assert;
import com.android.dialer.enrichedcall.EnrichedCallComponent;
import com.android.dialer.protos.ProtoParsers;
@@ -93,7 +94,8 @@
CallDetailsEntryListener callDetailsEntryListener,
CallDetailsHeaderListener callDetailsHeaderListener,
ReportCallIdListener reportCallIdListener,
- DeleteCallDetailsListener deleteCallDetailsListener) {
+ DeleteCallDetailsListener deleteCallDetailsListener,
+ CallRecordingDataStore callRecordingDataStore) {
return new CallDetailsAdapter(
this,
headerInfo,
@@ -101,7 +103,8 @@
callDetailsEntryListener,
callDetailsHeaderListener,
reportCallIdListener,
- deleteCallDetailsListener);
+ deleteCallDetailsListener,
+ callRecordingDataStore);
}
@Override
diff --git a/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java b/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java
index a26f322..808d08c 100644
--- a/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java
+++ b/java/com/android/dialer/calldetails/CallDetailsActivityCommon.java
@@ -38,6 +38,7 @@
import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
import com.android.dialer.callintent.CallInitiationType;
import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.callrecord.CallRecordingDataStore;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
import com.android.dialer.common.concurrent.DialerExecutor.FailureListener;
@@ -96,6 +97,7 @@
private CallDetailsAdapterCommon adapter;
private CallDetailsEntries callDetailsEntries;
private UiListener<ImmutableSet<String>> checkRttTranscriptAvailabilityListener;
+ private CallRecordingDataStore callRecordingDataStore;
/**
* Handles the intent that launches {@link OldCallDetailsActivity} or {@link CallDetailsActivity},
@@ -108,7 +110,8 @@
CallDetailsEntryViewHolder.CallDetailsEntryListener callDetailsEntryListener,
CallDetailsHeaderViewHolder.CallDetailsHeaderListener callDetailsHeaderListener,
CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener,
- CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener);
+ CallDetailsFooterViewHolder.DeleteCallDetailsListener deleteCallDetailsListener,
+ CallRecordingDataStore callRecordingDataStore);
/** Returns the phone number of the call details. */
protected abstract String getNumber();
@@ -129,12 +132,20 @@
checkRttTranscriptAvailabilityListener =
DialerExecutorComponent.get(this)
.createUiListener(getFragmentManager(), "Query RTT transcript availability");
+ callRecordingDataStore = new CallRecordingDataStore();
handleIntent(getIntent());
setupRecyclerViewForEntries();
}
@Override
@CallSuper
+ protected void onDestroy() {
+ super.onDestroy();
+ callRecordingDataStore.close();
+ }
+
+ @Override
+ @CallSuper
protected void onResume() {
super.onResume();
@@ -205,7 +216,8 @@
callDetailsEntryListener,
callDetailsHeaderListener,
reportCallIdListener,
- deleteCallDetailsListener);
+ deleteCallDetailsListener,
+ callRecordingDataStore);
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
@@ -265,6 +277,9 @@
context
.getContentResolver()
.delete(Calls.CONTENT_URI, selection.getSelection(), selection.getSelectionArgs());
+ context
+ .getContentResolver()
+ .notifyChange(Calls.CONTENT_URI, null);
return null;
}
diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapter.java b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
index 40d856f..7e5ebe1 100644
--- a/java/com/android/dialer/calldetails/CallDetailsAdapter.java
+++ b/java/com/android/dialer/calldetails/CallDetailsAdapter.java
@@ -23,6 +23,7 @@
import com.android.dialer.calldetails.CallDetailsEntryViewHolder.CallDetailsEntryListener;
import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener;
import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener;
+import com.android.dialer.callrecord.CallRecordingDataStore;
import com.android.dialer.glidephotomanager.PhotoInfo;
/**
@@ -43,14 +44,16 @@
CallDetailsEntryListener callDetailsEntryListener,
CallDetailsHeaderListener callDetailsHeaderListener,
CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener,
- DeleteCallDetailsListener deleteCallDetailsListener) {
+ DeleteCallDetailsListener deleteCallDetailsListener,
+ CallRecordingDataStore callRecordingDataStore) {
super(
context,
callDetailsEntries,
callDetailsEntryListener,
callDetailsHeaderListener,
reportCallIdListener,
- deleteCallDetailsListener);
+ deleteCallDetailsListener,
+ callRecordingDataStore);
this.headerInfo = calldetailsHeaderInfo;
}
diff --git a/java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java b/java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java
index ec9263f..d33fea8 100644
--- a/java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java
+++ b/java/com/android/dialer/calldetails/CallDetailsAdapterCommon.java
@@ -32,6 +32,7 @@
import com.android.dialer.calllogutils.CallTypeHelper;
import com.android.dialer.calllogutils.CallbackActionHelper;
import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
+import com.android.dialer.callrecord.CallRecordingDataStore;
import com.android.dialer.common.Assert;
import com.android.dialer.duo.DuoComponent;
import com.android.dialer.glidephotomanager.PhotoInfo;
@@ -51,6 +52,7 @@
private final ReportCallIdListener reportCallIdListener;
private final DeleteCallDetailsListener deleteCallDetailsListener;
private final CallTypeHelper callTypeHelper;
+ private final CallRecordingDataStore callRecordingDataStore;
private CallDetailsEntries callDetailsEntries;
@@ -75,12 +77,14 @@
CallDetailsEntryListener callDetailsEntryListener,
CallDetailsHeaderListener callDetailsHeaderListener,
ReportCallIdListener reportCallIdListener,
- DeleteCallDetailsListener deleteCallDetailsListener) {
+ DeleteCallDetailsListener deleteCallDetailsListener,
+ CallRecordingDataStore callRecordingDataStore) {
this.callDetailsEntries = callDetailsEntries;
this.callDetailsEntryListener = callDetailsEntryListener;
this.callDetailsHeaderListener = callDetailsHeaderListener;
this.reportCallIdListener = reportCallIdListener;
this.deleteCallDetailsListener = deleteCallDetailsListener;
+ this.callRecordingDataStore = callRecordingDataStore;
this.callTypeHelper =
new CallTypeHelper(context.getResources(), DuoComponent.get(context).getDuo());
}
@@ -123,6 +127,7 @@
getPhotoInfo(),
entry,
callTypeHelper,
+ callRecordingDataStore,
!entry.getHistoryResultsList().isEmpty() && position != getItemCount() - 2);
}
}
diff --git a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
index 05957ae..a9be544 100644
--- a/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
+++ b/java/com/android/dialer/calldetails/CallDetailsEntryViewHolder.java
@@ -16,23 +16,36 @@
package com.android.dialer.calldetails;
+import android.content.ActivityNotFoundException;
+import android.content.ContentUris;
import android.content.Context;
+import android.content.Intent;
import android.net.Uri;
import android.provider.CallLog.Calls;
+import android.provider.MediaStore;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
+import android.support.v4.content.FileProvider;
import android.support.v4.os.BuildCompat;
import android.support.v7.widget.RecyclerView.ViewHolder;
import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.view.Menu;
import android.view.View;
+import android.webkit.MimeTypeMap;
import android.widget.ImageView;
+import android.widget.PopupMenu;
import android.widget.TextView;
+import android.widget.Toast;
import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
import com.android.dialer.calllogutils.CallLogDates;
import com.android.dialer.calllogutils.CallLogDurations;
import com.android.dialer.calllogutils.CallTypeHelper;
import com.android.dialer.calllogutils.CallTypeIconsView;
+import com.android.dialer.callrecord.CallRecording;
+import com.android.dialer.callrecord.CallRecordingDataStore;
+import com.android.dialer.callrecord.impl.CallRecorderService;
import com.android.dialer.common.LogUtil;
import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult;
import com.android.dialer.enrichedcall.historyquery.proto.HistoryResult.Type;
@@ -41,6 +54,11 @@
import com.android.dialer.util.DialerUtils;
import com.android.dialer.util.IntentUtil;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
/** ViewHolder for call entries in {@link OldCallDetailsActivity} or {@link CallDetailsActivity}. */
public class CallDetailsEntryViewHolder extends ViewHolder {
@@ -66,6 +84,7 @@
private final TextView rttTranscript;
private final ImageView multimediaImage;
+ private final TextView playbackButton;
// TODO(maxwelb): Display this when location is stored - a bug
@SuppressWarnings("unused")
@@ -83,6 +102,7 @@
callTime = (TextView) container.findViewById(R.id.call_time);
callDuration = (TextView) container.findViewById(R.id.call_duration);
+ playbackButton = (TextView) container.findViewById(R.id.play_recordings);
multimediaImageContainer = container.findViewById(R.id.multimedia_image_container);
multimediaDetailsContainer = container.findViewById(R.id.ec_container);
multimediaDivider = container.findViewById(R.id.divider);
@@ -101,6 +121,7 @@
PhotoInfo photoInfo,
CallDetailsEntry entry,
CallTypeHelper callTypeHelper,
+ CallRecordingDataStore callRecordingDataStore,
boolean showMultimediaDivider) {
int callType = entry.getCallType();
boolean isVideoCall = (entry.getFeatures() & Calls.FEATURES_VIDEO) == Calls.FEATURES_VIDEO;
@@ -139,6 +160,22 @@
CallLogDurations.formatDurationAndDataUsageA11y(
context, entry.getDuration(), entry.getDataUsage()));
}
+
+ // do this synchronously to prevent recordings from "popping in" after detail item is displayed
+ final List<CallRecording> recordings;
+ if (CallRecorderService.isEnabled(context)) {
+ callRecordingDataStore.open(context); // opens unless already open
+ recordings = callRecordingDataStore.getRecordings(number, entry.getDate());
+ } else {
+ recordings = null;
+ }
+
+ int count = recordings != null ? recordings.size() : 0;
+ playbackButton.setOnClickListener(v -> handleRecordingClick(v, recordings));
+ playbackButton.setText(
+ context.getResources().getQuantityString(R.plurals.play_recordings, count, count));
+ playbackButton.setVisibility(count > 0 ? View.VISIBLE : View.GONE);
+
setMultimediaDetails(number, entry, showMultimediaDivider);
if (isRttCall) {
if (entry.getHasRttTranscript()) {
@@ -209,6 +246,46 @@
DialerUtils.startActivityWithErrorToast(context, IntentUtil.getSendSmsIntent(number));
}
+ private void handleRecordingClick(View v, List<CallRecording> recordings) {
+ final Context context = v.getContext();
+ if (recordings.size() == 1) {
+ playRecording(context, recordings.get(0));
+ } else {
+ PopupMenu menu = new PopupMenu(context, v);
+ String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(),
+ DateFormat.is24HourFormat(context) ? "Hmss" : "hmssa");
+ SimpleDateFormat format = new SimpleDateFormat(pattern);
+
+ for (int i = 0; i < recordings.size(); i++) {
+ final long startTime = recordings.get(i).startRecordingTime;
+ final String formattedDate = format.format(new Date(startTime));
+ menu.getMenu().add(Menu.NONE, i, i, formattedDate);
+ }
+ menu.setOnMenuItemClickListener(item -> {
+ playRecording(context, recordings.get(item.getItemId()));
+ return true;
+ });
+ menu.show();
+ }
+ }
+
+ private void playRecording(Context context, CallRecording recording) {
+ Uri uri = ContentUris.withAppendedId(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, recording.mediaId);
+ String extension = MimeTypeMap.getFileExtensionFromUrl(recording.fileName);
+ String mime = !TextUtils.isEmpty(extension)
+ ? MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) : "audio/*";
+ try {
+ Intent intent = new Intent(Intent.ACTION_VIEW)
+ .setDataAndType(uri, mime)
+ .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ context.startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(context, R.string.call_playback_no_app_found_toast, Toast.LENGTH_LONG)
+ .show();
+ }
+ }
+
private static boolean isIncoming(@NonNull HistoryResult historyResult) {
return historyResult.getType() == Type.INCOMING_POST_CALL
|| historyResult.getType() == Type.INCOMING_CALL_COMPOSER;
diff --git a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
index f678061..13b03d0 100644
--- a/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
+++ b/java/com/android/dialer/calldetails/CallDetailsHeaderViewHolder.java
@@ -17,18 +17,22 @@
package com.android.dialer.calldetails;
import android.content.Context;
+import android.content.Intent;
import android.net.Uri;
import android.support.v7.widget.RecyclerView;
import android.telecom.PhoneAccount;
import android.text.TextUtils;
import android.view.View;
import android.view.View.OnClickListener;
+import android.view.View.OnLongClickListener;
import android.widget.ImageView;
import android.widget.QuickContactBadge;
import android.widget.RelativeLayout;
import android.widget.TextView;
+import com.android.dialer.app.AccountSelectionActivity;
import com.android.dialer.calldetails.CallDetailsActivityCommon.AssistedDialingNumberParseWorker;
import com.android.dialer.calldetails.CallDetailsEntries.CallDetailsEntry;
+import com.android.dialer.callintent.CallInitiationType;
import com.android.dialer.calllogutils.CallbackActionHelper.CallbackAction;
import com.android.dialer.common.Assert;
import com.android.dialer.common.LogUtil;
@@ -41,6 +45,7 @@
import com.android.dialer.logging.InteractionEvent;
import com.android.dialer.logging.Logger;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.DialerUtils;
import com.android.dialer.widget.BidiTextView;
/**
@@ -49,7 +54,7 @@
* <p>The header contains contact info and the primary callback button.
*/
public class CallDetailsHeaderViewHolder extends RecyclerView.ViewHolder
- implements OnClickListener, FailureListener {
+ implements OnClickListener, OnLongClickListener, FailureListener {
private final CallDetailsHeaderListener callDetailsHeaderListener;
private final ImageView callbackButton;
@@ -86,6 +91,7 @@
callDetailsHeaderListener::openAssistedDialingSettings);
callbackButton.setOnClickListener(this);
+ callbackButton.setOnLongClickListener(this);
this.number = number;
this.postDialDigits = postDialDigits;
@@ -250,6 +256,19 @@
}
}
+ @Override
+ public boolean onLongClick(View view) {
+ if (view == callbackButton) {
+ Intent intent = AccountSelectionActivity.createIntent(view.getContext(),
+ number, CallInitiationType.Type.CALL_DETAILS);
+ if (intent != null) {
+ DialerUtils.startActivityWithErrorToast(view.getContext(), intent);
+ return true;
+ }
+ }
+ return false;
+ }
+
/** Listener for the call details header */
interface CallDetailsHeaderListener {
diff --git a/java/com/android/dialer/calldetails/OldCallDetailsActivity.java b/java/com/android/dialer/calldetails/OldCallDetailsActivity.java
index 26217ab..0f53d69 100644
--- a/java/com/android/dialer/calldetails/OldCallDetailsActivity.java
+++ b/java/com/android/dialer/calldetails/OldCallDetailsActivity.java
@@ -22,6 +22,7 @@
import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener;
import com.android.dialer.calldetails.CallDetailsFooterViewHolder.ReportCallIdListener;
import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener;
+import com.android.dialer.callrecord.CallRecordingDataStore;
import com.android.dialer.common.Assert;
import com.android.dialer.dialercontact.DialerContact;
import com.android.dialer.protos.ProtoParsers;
@@ -80,7 +81,8 @@
CallDetailsEntryListener callDetailsEntryListener,
CallDetailsHeaderListener callDetailsHeaderListener,
ReportCallIdListener reportCallIdListener,
- DeleteCallDetailsListener deleteCallDetailsListener) {
+ DeleteCallDetailsListener deleteCallDetailsListener,
+ CallRecordingDataStore callRecordingDataStore) {
return new OldCallDetailsAdapter(
/* context = */ this,
contact,
@@ -88,7 +90,8 @@
callDetailsEntryListener,
callDetailsHeaderListener,
reportCallIdListener,
- deleteCallDetailsListener);
+ deleteCallDetailsListener,
+ callRecordingDataStore);
}
@Override
diff --git a/java/com/android/dialer/calldetails/OldCallDetailsAdapter.java b/java/com/android/dialer/calldetails/OldCallDetailsAdapter.java
index 878803c..af54538 100644
--- a/java/com/android/dialer/calldetails/OldCallDetailsAdapter.java
+++ b/java/com/android/dialer/calldetails/OldCallDetailsAdapter.java
@@ -23,6 +23,7 @@
import com.android.dialer.calldetails.CallDetailsEntryViewHolder.CallDetailsEntryListener;
import com.android.dialer.calldetails.CallDetailsFooterViewHolder.DeleteCallDetailsListener;
import com.android.dialer.calldetails.CallDetailsHeaderViewHolder.CallDetailsHeaderListener;
+import com.android.dialer.callrecord.CallRecordingDataStore;
import com.android.dialer.dialercontact.DialerContact;
import com.android.dialer.glidephotomanager.PhotoInfo;
import com.android.dialer.lettertile.LetterTileDrawable;
@@ -45,14 +46,16 @@
CallDetailsEntryListener callDetailsEntryListener,
CallDetailsHeaderListener callDetailsHeaderListener,
CallDetailsFooterViewHolder.ReportCallIdListener reportCallIdListener,
- DeleteCallDetailsListener deleteCallDetailsListener) {
+ DeleteCallDetailsListener deleteCallDetailsListener,
+ CallRecordingDataStore callRecordingDataStore) {
super(
context,
callDetailsEntries,
callDetailsEntryListener,
callDetailsHeaderListener,
reportCallIdListener,
- deleteCallDetailsListener);
+ deleteCallDetailsListener,
+ callRecordingDataStore);
this.contact = contact;
}
diff --git a/java/com/android/dialer/calldetails/res/drawable/recording_playback_button.xml b/java/com/android/dialer/calldetails/res/drawable/recording_playback_button.xml
new file mode 100644
index 0000000..c6fb87f
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/drawable/recording_playback_button.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2016 The CyanogenMod 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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="48"
+ android:viewportHeight="48">
+
+ <path
+ android:fillColor="@color/call_record_playback_icon_color"
+ android:pathData="M 21,30.75 L 30,24 21,17.25 21,30.75 Z M 24,9 C 15.7125,9 9,15.7125 9,24 9,32.2875 15.7125,39 24,39 32.2875,39 39,32.2875 39,24 39,15.7125 32.2875,9 24,9 Z m 0,27 c -6.615,0 -12,-5.385 -12,-12 0,-6.615 5.385,-12 12,-12 6.615,0 12,5.385 12,12 0,6.615 -5.385,12 -12,12 z" />
+</vector>
+
diff --git a/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml b/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
index bfbb4f8..ffe3ade 100644
--- a/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
+++ b/java/com/android/dialer/calldetails/res/layout/call_details_entry.xml
@@ -19,7 +19,8 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:paddingTop="@dimen/call_entry_padding">
+ android:paddingTop="@dimen/call_entry_padding"
+ android:paddingBottom="@dimen/call_entry_bottom_padding">
<com.android.dialer.calllogutils.CallTypeIconsView
android:id="@+id/call_direction"
@@ -43,7 +44,6 @@
style="@style/Dialer.TextAppearance.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="@dimen/call_entry_bottom_padding"
android:layout_marginStart="@dimen/call_entry_text_left_margin"
android:layout_marginEnd="16dp"
android:layout_below="@+id/call_type"/>
@@ -56,12 +56,27 @@
android:layout_marginEnd="@dimen/call_entry_padding"
android:layout_alignParentEnd="true"/>
+ <TextView
+ android:id="@+id/play_recordings"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/call_time"
+ android:paddingStart="@dimen/call_entry_text_left_margin"
+ android:paddingTop="8dp"
+ android:paddingBottom="8dp"
+ android:gravity="center_vertical"
+ android:drawableStart="@drawable/recording_playback_button"
+ android:drawablePadding="4dp"
+ android:background="?attr/selectableItemBackground"
+ android:visibility="gone"
+ style="@style/Dialer.TextAppearance.Secondary"/>
+
<include
android:id="@+id/ec_container"
layout="@layout/ec_data_container"
android:layout_width="match_parent"
android:layout_height="@dimen/ec_container_height"
- android:layout_below="@+id/call_time"
+ android:layout_below="@id/play_recordings"
android:visibility="gone"/>
<TextView
@@ -97,4 +112,4 @@
android:layout_below="@id/rtt_transcript"
android:background="@color/dialer_divider_line_color"
android:visibility="gone"/>
-</RelativeLayout>
\ No newline at end of file
+</RelativeLayout>
diff --git a/java/com/android/dialer/calldetails/res/values/cm_strings.xml b/java/com/android/dialer/calldetails/res/values/cm_strings.xml
new file mode 100644
index 0000000..076a494
--- /dev/null
+++ b/java/com/android/dialer/calldetails/res/values/cm_strings.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013-2014 The CyanogenMod 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <plurals name="play_recordings">
+ <item quantity="one">Play recording</item>
+ <item quantity="other">Play recordings</item>
+ </plurals>
+ <string name="call_playback_no_app_found_toast">No app could be found for playback of the selected recording.</string>
+</resources>
diff --git a/java/com/android/dialer/calllog/ui/menu/DeleteCallLogItemModule.java b/java/com/android/dialer/calllog/ui/menu/DeleteCallLogItemModule.java
index a929961..c5ade3d 100644
--- a/java/com/android/dialer/calllog/ui/menu/DeleteCallLogItemModule.java
+++ b/java/com/android/dialer/calllog/ui/menu/DeleteCallLogItemModule.java
@@ -100,6 +100,9 @@
context
.getContentResolver()
.delete(Calls.CONTENT_URI, selection.getSelection(), selection.getSelectionArgs());
+ context
+ .getContentResolver()
+ .notifyChange(Calls.CONTENT_URI, null);
if (numRowsDeleted != coalescedIds.getCoalescedIdCount()) {
LogUtil.e(
diff --git a/java/com/android/dialer/calllogutils/CallTypeIconsView.java b/java/com/android/dialer/calllogutils/CallTypeIconsView.java
index 19c30c5..79d1e6e 100644
--- a/java/com/android/dialer/calllogutils/CallTypeIconsView.java
+++ b/java/com/android/dialer/calllogutils/CallTypeIconsView.java
@@ -290,13 +290,15 @@
int iconId = R.drawable.quantum_ic_call_received_white_24;
Drawable drawable = largeIcons ? r.getDrawable(iconId) : getScaledBitmap(context, iconId);
incoming = drawable.mutate();
- incoming.setColorFilter(r.getColor(R.color.dialer_call_green), PorterDuff.Mode.MULTIPLY);
+ incoming.setColorFilter(r.getColor(R.color.answered_incoming_call),
+ PorterDuff.Mode.MULTIPLY);
// Create a rotated instance of the call arrow for outgoing calls.
iconId = R.drawable.quantum_ic_call_made_white_24;
drawable = largeIcons ? r.getDrawable(iconId) : getScaledBitmap(context, iconId);
outgoing = drawable.mutate();
- outgoing.setColorFilter(r.getColor(R.color.dialer_call_green), PorterDuff.Mode.MULTIPLY);
+ outgoing.setColorFilter(r.getColor(R.color.answered_outgoing_call),
+ PorterDuff.Mode.MULTIPLY);
// Need to make a copy of the arrow drawable, otherwise the same instance colored
// above will be recolored here.
diff --git a/java/com/android/dialer/calllogutils/FilterSpinnerHelper.java b/java/com/android/dialer/calllogutils/FilterSpinnerHelper.java
new file mode 100644
index 0000000..a6ae552
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/FilterSpinnerHelper.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.dialer.calllogutils;
+
+import android.content.Context;
+import android.provider.CallLog;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+
+import com.android.dialer.R;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.ArrayList;
+import java.util.List;
+
+public class FilterSpinnerHelper implements AdapterView.OnItemSelectedListener {
+ private static String TAG = FilterSpinnerHelper.class.getSimpleName();
+
+ public interface OnFilterChangedListener {
+ void onFilterChanged(PhoneAccountHandle account, int callType);
+ }
+
+ private OnFilterChangedListener mListener;
+ private Spinner mAccountSpinner;
+ private ArrayAdapter<AccountItem> mAccountAdapter;
+ private Spinner mTypeSpinner;
+ private ArrayAdapter<TypeItem> mTypeAdapter;
+
+ public FilterSpinnerHelper(View rootView, boolean includeVoicemailType,
+ OnFilterChangedListener listener) {
+ mListener = listener;
+
+ mAccountAdapter = createAccountAdapter(rootView.getContext());
+ mAccountSpinner = initSpinner(rootView, R.id.filter_account_spinner, mAccountAdapter);
+
+ mTypeAdapter = createTypeAdapter(rootView.getContext(), includeVoicemailType);
+ mTypeSpinner = initSpinner(rootView, R.id.filter_status_spinner, mTypeAdapter);
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+ int selectedAccountPos = Math.max(mAccountSpinner.getSelectedItemPosition(), 0);
+ int selectedTypePos = Math.max(mTypeSpinner.getSelectedItemPosition(), 0);
+ PhoneAccountHandle selectedAccount = mAccountAdapter.getItem(selectedAccountPos).account;
+ int selectedType = mTypeAdapter.getItem(selectedTypePos).value;
+ mListener.onFilterChanged(selectedAccount, selectedType);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+
+ private Spinner initSpinner(View rootView, int spinnerResId, ArrayAdapter<?> adapter) {
+ Spinner spinner = rootView.findViewById(spinnerResId);
+ if (spinner == null) {
+ throw new IllegalArgumentException("Could not find spinner "
+ + rootView.getContext().getResources().getResourceName(spinnerResId));
+ }
+ spinner.setAdapter(adapter);
+ spinner.setOnItemSelectedListener(this);
+ if (adapter.getCount() <= 1) {
+ spinner.setVisibility(View.GONE);
+ }
+ return spinner;
+ }
+
+ private ArrayAdapter<AccountItem> createAccountAdapter(Context context) {
+ ArrayList<AccountItem> items = new ArrayList<>();
+ items.add(new AccountItem(null, context.getString(R.string.call_log_show_all_accounts)));
+ if (PermissionsUtil.hasPermission(context, android.Manifest.permission.READ_PHONE_STATE)) {
+ TelecomManager tm = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+ for (PhoneAccountHandle account : tm.getCallCapablePhoneAccounts()) {
+ String displayName = PhoneAccountUtils.getAccountLabel(context, account);
+ if (!TextUtils.isEmpty(displayName)) {
+ items.add(new AccountItem(account, displayName));
+ }
+ }
+ }
+
+ return new ArrayAdapter<AccountItem>(context, R.layout.call_log_filter_spinner_item, items);
+ }
+
+ private ArrayAdapter<TypeItem> createTypeAdapter(Context context, boolean includeVoicemail) {
+ ArrayList<TypeItem> items = new ArrayList<>();
+ items.add(new TypeItem(-1, context.getString(R.string.call_log_all_calls_header)));
+ items.add(new TypeItem(CallLog.Calls.INCOMING_TYPE,
+ context.getString(R.string.call_log_incoming_header)));
+ items.add(new TypeItem(CallLog.Calls.OUTGOING_TYPE,
+ context.getString(R.string.call_log_outgoing_header)));
+ items.add(new TypeItem(CallLog.Calls.MISSED_TYPE,
+ context.getString(R.string.call_log_missed_header)));
+ items.add(new TypeItem(CallLog.Calls.BLOCKED_TYPE,
+ context.getString(R.string.call_log_blacklist_header)));
+ if (includeVoicemail) {
+ items.add(new TypeItem(CallLog.Calls.VOICEMAIL_TYPE,
+ context.getString(R.string.call_log_voicemail_header)));
+ }
+
+ return new ArrayAdapter<TypeItem>(context, R.layout.call_log_filter_spinner_item, items);
+ }
+
+ private final class AccountItem {
+ public final PhoneAccountHandle account;
+ public final String label;
+
+ private AccountItem(PhoneAccountHandle account, String label) {
+ this.account = account;
+ this.label = label;
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+ }
+
+ private final class TypeItem {
+ public final int value;
+ public final String label;
+
+ private TypeItem(int value, String label) {
+ this.value = value;
+ this.label = label;
+ }
+
+ @Override
+ public String toString() {
+ return label;
+ }
+ }
+}
diff --git a/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java b/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java
index 6fe3a82..6509af3 100644
--- a/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java
+++ b/java/com/android/dialer/calllogutils/PhoneNumberDisplayUtil.java
@@ -64,7 +64,7 @@
* @param number the number to display
* @param formattedNumber the formatted number if available, may be null
*/
- static CharSequence getDisplayNumber(
+ public static CharSequence getDisplayNumber(
Context context,
CharSequence number,
int presentation,
diff --git a/java/com/android/dialer/calllogutils/res/layout/call_log_filter_spinner_item.xml b/java/com/android/dialer/calllogutils/res/layout/call_log_filter_spinner_item.xml
new file mode 100644
index 0000000..d7fdb46
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/res/layout/call_log_filter_spinner_item.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (c) 2013-2014, The Linux Foundation. All rights reserved.
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the name of The Linux Foundation, Inc. nor the names of its
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@style/Dialer.TextAppearance.Primary"
+ android:layout_width="match_parent"
+ android:layout_height="40dip"
+ android:paddingLeft="8dip"
+ android:paddingRight="8dip"
+ android:textSize="14sp"
+ android:textStyle="bold"
+ android:gravity="center_vertical"
+ android:singleLine="true"
+ android:textAllCaps="true" />
diff --git a/java/com/android/dialer/calllogutils/res/layout/call_log_filter_spinners.xml b/java/com/android/dialer/calllogutils/res/layout/call_log_filter_spinners.xml
new file mode 100644
index 0000000..ecaf1d1
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/res/layout/call_log_filter_spinners.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingStart="@dimen/call_log_outer_margin"
+ android:paddingEnd="@dimen/call_log_outer_margin">
+ <Spinner
+ android:id="@+id/filter_account_spinner"
+ android:layout_width="0dip"
+ android:layout_height="@dimen/list_section_divider_min_height"
+ android:layout_weight="1.2"
+ android:layout_marginTop="6dip"
+ android:layout_marginBottom="6dip" />
+ <Spinner
+ android:id="@+id/filter_status_spinner"
+ android:layout_width="0dip"
+ android:layout_height="@dimen/list_section_divider_min_height"
+ android:layout_weight="1.8"
+ android:layout_marginTop="6dip"
+ android:layout_marginBottom="6dip" />
+</LinearLayout>
diff --git a/java/com/android/dialer/calllogutils/res/values/cm_strings.xml b/java/com/android/dialer/calllogutils/res/values/cm_strings.xml
new file mode 100644
index 0000000..d30aa17
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/res/values/cm_strings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013-2014 The CyanogenMod Project
+ Copyright (C) 2018 The LineageOS 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="call_log_show_all_accounts">All accounts</string>
+ <string name="call_log_all_calls_header">All calls</string>
+ <string name="call_log_incoming_header">Incoming calls only</string>
+ <string name="call_log_outgoing_header">Outgoing calls only</string>
+ <string name="call_log_missed_header">Missed calls only</string>
+ <string name="call_log_voicemail_header">Calls with voicemail only</string>
+ <string name="call_log_blacklist_header">Blocked calls only</string>
+</resources>
diff --git a/java/com/android/dialer/calllogutils/res/values/colors.xml b/java/com/android/dialer/calllogutils/res/values/colors.xml
index 3a9e3ae..40a522b 100644
--- a/java/com/android/dialer/calllogutils/res/values/colors.xml
+++ b/java/com/android/dialer/calllogutils/res/values/colors.xml
@@ -15,4 +15,12 @@
~ limitations under the License
-->
<resources>
-</resources>
\ No newline at end of file
+ <!-- Color for missed call icons. -->
+ <color name="missed_call">#C53929</color>
+ <!-- Color for answered call icons. -->
+ <color name="answered_incoming_call">#00a8ff</color>
+ <!-- Color for outgoing call icons. -->
+ <color name="answered_outgoing_call">#00c853</color>
+ <!-- Color for blocked call icons. -->
+ <color name="blocked_call">@color/dialer_secondary_text_color</color>
+</resources>
diff --git a/java/com/android/dialer/callrecord/AndroidManifest.xml b/java/com/android/dialer/callrecord/AndroidManifest.xml
new file mode 100644
index 0000000..5e25c73
--- /dev/null
+++ b/java/com/android/dialer/callrecord/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<!-- Copyright (C) 2018 The LineageOS 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer">
+
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
+ <uses-permission android:name="android.permission.CAPTURE_AUDIO_OUTPUT" />
+
+ <application>
+ <service android:name="com.android.dialer.callrecord.impl.CallRecorderService"
+ android:process="com.android.incallui" />
+ </application>
+</manifest>
diff --git a/java/com/android/dialer/callrecord/CallRecording.aidl b/java/com/android/dialer/callrecord/CallRecording.aidl
new file mode 100644
index 0000000..e8d65be
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecording.aidl
@@ -0,0 +1,3 @@
+package com.android.dialer.callrecord;
+
+parcelable CallRecording;
diff --git a/java/com/android/dialer/callrecord/CallRecording.java b/java/com/android/dialer/callrecord/CallRecording.java
new file mode 100644
index 0000000..a594213
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecording.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.dialer.callrecord;
+
+import android.content.ContentValues;
+import android.os.Environment;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.webkit.MimeTypeMap;
+
+import java.io.File;
+
+public final class CallRecording implements Parcelable {
+ public String phoneNumber;
+ public long creationTime;
+ public String fileName;
+ public long startRecordingTime;
+ public long mediaId;
+
+ public static final Parcelable.Creator<CallRecording> CREATOR =
+ new Parcelable.Creator<CallRecording>() {
+ @Override
+ public CallRecording createFromParcel(Parcel in) {
+ return new CallRecording(in);
+ }
+
+ @Override
+ public CallRecording[] newArray(int size) {
+ return new CallRecording[size];
+ }
+ };
+
+ public CallRecording(String phoneNumber, long creationTime,
+ String fileName, long startRecordingTime, long mediaId) {
+ this.phoneNumber = phoneNumber;
+ this.creationTime = creationTime;
+ this.fileName = fileName;
+ this.startRecordingTime = startRecordingTime;
+ this.mediaId = mediaId;
+ }
+
+ public CallRecording(Parcel in) {
+ phoneNumber = in.readString();
+ creationTime = in.readLong();
+ fileName = in.readString();
+ startRecordingTime = in.readLong();
+ mediaId = in.readLong();
+ }
+
+ public static ContentValues generateMediaInsertValues(String fileName, long creationTime) {
+ final ContentValues cv = new ContentValues(5);
+
+ cv.put(MediaStore.Audio.Media.RELATIVE_PATH, "Recordings/Call recordings");
+ cv.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName);
+ cv.put(MediaStore.Audio.Media.DATE_TAKEN, creationTime);
+ cv.put(MediaStore.Audio.Media.IS_PENDING, 1);
+
+ final String extension = MimeTypeMap.getFileExtensionFromUrl(fileName);
+ final String mime = !TextUtils.isEmpty(extension)
+ ? MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) : "audio/*";
+ cv.put(MediaStore.Audio.Media.MIME_TYPE, mime);
+
+ return cv;
+ }
+
+ public static ContentValues generateCompletedValues() {
+ final ContentValues cv = new ContentValues(1);
+ cv.put(MediaStore.Audio.Media.IS_PENDING, 0);
+ return cv;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(phoneNumber);
+ out.writeLong(creationTime);
+ out.writeString(fileName);
+ out.writeLong(startRecordingTime);
+ out.writeLong(mediaId);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return "phoneNumber=" + phoneNumber + ", creationTime=" + creationTime +
+ ", fileName=" + fileName + ", startRecordingTime=" + startRecordingTime;
+ }
+}
diff --git a/java/com/android/dialer/callrecord/CallRecordingAutoMigrator.java b/java/com/android/dialer/callrecord/CallRecordingAutoMigrator.java
new file mode 100644
index 0000000..81c1612
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecordingAutoMigrator.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ * Copyright (C) 2020 The LineageOS 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.dialer.callrecord;
+
+import android.annotation.TargetApi;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.MediaStore;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.DialerExecutor.Worker;
+import com.android.dialer.common.concurrent.DialerExecutorFactory;
+import com.android.voicemail.impl.mail.utils.LogUtils;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.file.Files;
+
+public class CallRecordingAutoMigrator {
+ private static final String TAG = "CallRecordingAutoMigrator";
+
+ @NonNull
+ private final Context appContext;
+ @NonNull private final DialerExecutorFactory dialerExecutorFactory;
+
+ public CallRecordingAutoMigrator(
+ @NonNull Context appContext,
+ @NonNull DialerExecutorFactory dialerExecutorFactory) {
+ this.appContext = Assert.isNotNull(appContext);
+ this.dialerExecutorFactory = Assert.isNotNull(dialerExecutorFactory);
+ }
+
+ public void asyncAutoMigrate() {
+ dialerExecutorFactory
+ .createNonUiTaskBuilder(new ShouldAttemptAutoMigrate(appContext))
+ .onSuccess(this::autoMigrate)
+ .build()
+ .executeParallel(null);
+ }
+
+ @TargetApi(26)
+ private void autoMigrate(boolean shouldAttemptAutoMigrate) {
+ if (!shouldAttemptAutoMigrate) {
+ return;
+ }
+
+ final CallRecordingDataStore store = new CallRecordingDataStore();
+ store.open(appContext);
+
+ final ContentResolver cr = appContext.getContentResolver();
+ final SparseArray<CallRecording> oldRecordingData = store.getUnmigratedRecordingData();
+ final File dir = Environment.getExternalStoragePublicDirectory("CallRecordings");
+ for (File recording : dir.listFiles()) {
+ OutputStream os = null;
+ try {
+ // determine data store ID and call creation time of recording
+ int id = -1;
+ long creationTime = System.currentTimeMillis();
+ for (int i = 0; i < oldRecordingData.size(); i++) {
+ if (TextUtils.equals(recording.getName(), oldRecordingData.valueAt(i).fileName)) {
+ creationTime = oldRecordingData.valueAt(i).creationTime;
+ id = oldRecordingData.keyAt(i);
+ break;
+ }
+ }
+
+ // create media store entry for recording
+ Uri uri = cr.insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ CallRecording.generateMediaInsertValues(recording.getName(), creationTime));
+ os = cr.openOutputStream(uri);
+
+ // copy file contents to media store stream
+ Files.copy(recording.toPath(), os);
+
+ // insert media store id to store
+ if (id >= 0) {
+ store.updateMigratedRecording(id, Integer.parseInt(uri.getLastPathSegment()));
+ }
+
+ // mark recording as complete
+ cr.update(uri, CallRecording.generateCompletedValues(), null, null);
+
+ // delete file
+ LogUtils.i(TAG, "Successfully migrated recording " + recording + " (ID " + id + ")");
+ recording.delete();
+ } catch (IOException e) {
+ LogUtils.w(TAG, "Failed migrating call recording " + recording, e);
+ } finally {
+ if (os != null) {
+ IOUtils.closeQuietly(os);
+ }
+ }
+ }
+
+ if (dir.listFiles().length == 0) {
+ dir.delete();
+ }
+
+ store.close();
+ }
+
+ private static class ShouldAttemptAutoMigrate implements Worker<Void, Boolean> {
+ private final Context appContext;
+
+ ShouldAttemptAutoMigrate(Context appContext) {
+ this.appContext = appContext;
+ }
+
+ @Nullable
+ @Override
+ public Boolean doInBackground(@Nullable Void input) {
+ if (Build.VERSION.SDK_INT < 26) {
+ return false;
+ }
+ if (appContext.checkSelfPermission(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ LogUtil.i(TAG, "not attempting auto-migrate: no storage permission");
+ return false;
+ }
+
+ final File dir = Environment.getExternalStoragePublicDirectory("CallRecordings");
+ if (!dir.exists()) {
+ LogUtil.i(TAG, "not attempting auto-migrate: no recordings present");
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/java/com/android/dialer/callrecord/CallRecordingDataStore.java b/java/com/android/dialer/callrecord/CallRecordingDataStore.java
new file mode 100644
index 0000000..88b603b
--- /dev/null
+++ b/java/com/android/dialer/callrecord/CallRecordingDataStore.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.dialer.callrecord;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteStatement;
+import android.provider.BaseColumns;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Persistent data store for call recordings. Usage:
+ * open()
+ * read/write operations
+ * close()
+ */
+public class CallRecordingDataStore {
+ private static final String TAG = "CallRecordingStore";
+ private SQLiteOpenHelper mOpenHelper = null;
+ private SQLiteDatabase mDatabase = null;
+
+ /**
+ * Open before reading/writing. Will not open handle if one is already open.
+ */
+ public void open(Context context) {
+ if (mDatabase == null) {
+ mOpenHelper = new CallRecordingSQLiteOpenHelper(context);
+ mDatabase = mOpenHelper.getWritableDatabase();
+ }
+ }
+
+ /**
+ * close when finished reading/writing
+ */
+ public void close() {
+ if (mDatabase != null) {
+ mDatabase.close();
+ }
+ if (mOpenHelper != null) {
+ mOpenHelper.close();
+ }
+ mDatabase = null;
+ mOpenHelper = null;
+ }
+
+ /**
+ * Save a recording in the data store
+ *
+ * @param recording the recording to store
+ */
+ public void putRecording(CallRecording recording) {
+ final String insertSql = "INSERT INTO " +
+ CallRecordingsContract.CallRecording.TABLE_NAME + " (" +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + ", " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + ", " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + ", " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE + ", " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID + ") " +
+ " VALUES (?, ?, ?, ?, ?)";
+
+ try {
+ SQLiteStatement stmt = mDatabase.compileStatement(insertSql);
+ int idx = 1;
+ stmt.bindString(idx++, recording.phoneNumber);
+ stmt.bindLong(idx++, recording.creationTime);
+ stmt.bindString(idx++, recording.fileName);
+ stmt.bindLong(idx++, System.currentTimeMillis());
+ stmt.bindLong(idx++, recording.mediaId);
+ long id = stmt.executeInsert();
+ Log.i(TAG, "Saved recording " + recording + " with id " + id);
+ } catch (SQLiteException e) {
+ Log.w(TAG, "Failed to save recording " + recording, e);
+ }
+ }
+
+ /**
+ * Get all recordings associated with a phone call
+ *
+ * @param phoneNumber phone number no spaces
+ * @param callCreationDate time that the call was created
+ * @return list of recordings
+ */
+ public List<CallRecording> getRecordings(String phoneNumber, long callCreationDate) {
+ List<CallRecording> resultList = new ArrayList<CallRecording>();
+
+ final String query = "SELECT " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + "," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE + "," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID +
+ " FROM " + CallRecordingsContract.CallRecording.TABLE_NAME +
+ " WHERE " + CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + " = ?" +
+ " AND " + CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + " = ?" +
+ " AND " + CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID + " != 0" +
+ " ORDER BY " + CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE;
+
+ String args[] = {
+ phoneNumber, String.valueOf(callCreationDate)
+ };
+
+ try {
+ Cursor cursor = mDatabase.rawQuery(query, args);
+ while (cursor.moveToNext()) {
+ String fileName = cursor.getString(0);
+ long creationDate = cursor.getLong(1);
+ long mediaId = cursor.getLong(2);
+ // FIXME: need to check whether media entry still exists?
+ resultList.add(
+ new CallRecording(phoneNumber, callCreationDate, fileName, creationDate, mediaId));
+ }
+ cursor.close();
+ } catch (SQLiteException e) {
+ Log.w(TAG, "Failed to fetch recordings for number " + phoneNumber +
+ ", date " + callCreationDate, e);
+ }
+
+ return resultList;
+ }
+
+ public SparseArray<CallRecording> getUnmigratedRecordingData() {
+ final String query = "SELECT " +
+ CallRecordingsContract.CallRecording._ID + "," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + "," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + "," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE +
+ " FROM " + CallRecordingsContract.CallRecording.TABLE_NAME +
+ " WHERE " + CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID + " == 0";
+ final SparseArray<CallRecording> result = new SparseArray<>();
+
+ try {
+ Cursor cursor = mDatabase.rawQuery(query, null);
+ while (cursor.moveToNext()) {
+ int id = cursor.getInt(0);
+ String phoneNumber = cursor.getString(1);
+ String fileName = cursor.getString(2);
+ long creationDate = cursor.getLong(3);
+ CallRecording recording = new CallRecording(
+ phoneNumber, creationDate, fileName, creationDate, 0);
+ result.put(id, recording);
+ }
+ cursor.close();
+ } catch (SQLiteException e) {
+ Log.w(TAG, "Failed to fetch recordings for migration", e);
+ }
+
+ return result;
+ }
+
+ public void updateMigratedRecording(int id, int mediaId) {
+ ContentValues cv = new ContentValues(1);
+ cv.put(CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID, mediaId);
+ mDatabase.update(CallRecordingsContract.CallRecording.TABLE_NAME, cv,
+ CallRecordingsContract.CallRecording._ID + " = ?", new String[] { String.valueOf(id) });
+ }
+
+ static class CallRecordingsContract {
+ static interface CallRecording extends BaseColumns {
+ static final String TABLE_NAME = "call_recordings";
+ static final String COLUMN_NAME_PHONE_NUMBER = "phone_number";
+ static final String COLUMN_NAME_CALL_DATE = "call_date";
+ static final String COLUMN_NAME_RECORDING_FILENAME = "recording_filename";
+ static final String COLUMN_NAME_CREATION_DATE = "creation_date";
+ static final String COLUMN_NAME_MEDIA_ID = "media_id";
+ }
+ }
+
+ static class CallRecordingSQLiteOpenHelper extends SQLiteOpenHelper {
+ private static final int VERSION = 2;
+ private static final String DB_NAME = "callrecordings.db";
+
+ public CallRecordingSQLiteOpenHelper(Context context) {
+ super(context, DB_NAME, null, VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE " + CallRecordingsContract.CallRecording.TABLE_NAME + " (" +
+ CallRecordingsContract.CallRecording._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + " TEXT," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + " LONG," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_RECORDING_FILENAME + " TEXT, " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CREATION_DATE + " LONG," +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID + " INTEGER DEFAULT 0" +
+ ");"
+ );
+
+ db.execSQL("CREATE INDEX IF NOT EXISTS phone_number_call_date_index ON " +
+ CallRecordingsContract.CallRecording.TABLE_NAME + " (" +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_PHONE_NUMBER + ", " +
+ CallRecordingsContract.CallRecording.COLUMN_NAME_CALL_DATE + ");"
+ );
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion < 2) {
+ db.execSQL("ALTER TABLE " + CallRecordingsContract.CallRecording.TABLE_NAME +
+ " ADD COLUMN " + CallRecordingsContract.CallRecording.COLUMN_NAME_MEDIA_ID +
+ " INTEGER DEFAULT 0;");
+ }
+ }
+ }
+}
diff --git a/java/com/android/dialer/callrecord/ICallRecorderService.aidl b/java/com/android/dialer/callrecord/ICallRecorderService.aidl
new file mode 100644
index 0000000..acbd5f8
--- /dev/null
+++ b/java/com/android/dialer/callrecord/ICallRecorderService.aidl
@@ -0,0 +1,37 @@
+package com.android.dialer.callrecord;
+
+import com.android.dialer.callrecord.CallRecording;
+
+/**
+ * Service for recording phone calls. Only one recording may be active at a time
+ * (i.e. every call to startRecording should be followed by a call to stopRecording).
+ */
+interface ICallRecorderService {
+ /**
+ * Start a recording.
+ *
+ * @return true if recording started successfully
+ */
+ boolean startRecording(String phoneNumber, long creationTime);
+
+ /**
+ * stops the current recording
+ *
+ * @return call recording data including the output filename
+ */
+ CallRecording stopRecording();
+
+ /**
+ * Recording status
+ *
+ * @return true if there is an active recording
+ */
+ boolean isRecording();
+
+ /**
+ * Get recording currently in progress
+ *
+ * @return call recording object
+ */
+ CallRecording getActiveRecording();
+}
diff --git a/java/com/android/dialer/callrecord/impl/CallRecorderService.java b/java/com/android/dialer/callrecord/impl/CallRecorderService.java
new file mode 100644
index 0000000..298e8ad
--- /dev/null
+++ b/java/com/android/dialer/callrecord/impl/CallRecorderService.java
@@ -0,0 +1,222 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.dialer.callrecord.impl;
+
+import android.app.Service;
+import android.content.ContentUris;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.media.MediaRecorder;
+import android.net.Uri;
+import android.os.IBinder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.provider.MediaStore;
+import android.text.TextUtils;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.dialer.callrecord.CallRecording;
+import com.android.dialer.callrecord.ICallRecorderService;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+import com.android.dialer.R;
+
+public class CallRecorderService extends Service {
+ private static final String TAG = "CallRecorderService";
+ private static final boolean DBG = false;
+
+ private MediaRecorder mMediaRecorder = null;
+ private CallRecording mCurrentRecording = null;
+
+ private SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyMMdd_HHmmssSSS");
+
+ private final ICallRecorderService.Stub mBinder = new ICallRecorderService.Stub() {
+ @Override
+ public CallRecording stopRecording() {
+ return stopRecordingInternal();
+ }
+
+ @Override
+ public boolean startRecording(String phoneNumber, long creationTime) throws RemoteException {
+ return startRecordingInternal(phoneNumber, creationTime);
+ }
+
+ @Override
+ public boolean isRecording() throws RemoteException {
+ return mMediaRecorder != null;
+ }
+
+ @Override
+ public CallRecording getActiveRecording() throws RemoteException {
+ return mCurrentRecording;
+ }
+ };
+
+ @Override
+ public void onCreate() {
+ if (DBG) Log.d(TAG, "Creating CallRecorderService");
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ private int getAudioSource() {
+ return getResources().getInteger(R.integer.call_recording_audio_source);
+ }
+
+ private int getAudioFormatChoice() {
+ // This replicates PreferenceManager.getDefaultSharedPreferences, except
+ // that we need multi process preferences, as the pref is written in a separate
+ // process (com.android.dialer vs. com.android.incallui)
+ final String prefName = getPackageName() + "_preferences";
+ final SharedPreferences prefs = getSharedPreferences(prefName, MODE_MULTI_PROCESS);
+
+ try {
+ String value = prefs.getString(getString(R.string.call_recording_format_key), null);
+ if (value != null) {
+ return Integer.parseInt(value);
+ }
+ } catch (NumberFormatException e) {
+ // ignore and fall through
+ }
+ return 0;
+ }
+
+ private synchronized boolean startRecordingInternal(String phoneNumber, long creationTime) {
+ if (mMediaRecorder != null) {
+ if (DBG) {
+ Log.d(TAG, "Start called with recording in progress, stopping current recording");
+ }
+ stopRecordingInternal();
+ }
+
+ if (checkSelfPermission(android.Manifest.permission.RECORD_AUDIO)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.w(TAG, "Record audio permission not granted, can't record call");
+ return false;
+ }
+
+ if (DBG) Log.d(TAG, "Starting recording");
+
+ mMediaRecorder = new MediaRecorder();
+ try {
+ int audioSource = getAudioSource();
+ int formatChoice = getAudioFormatChoice();
+ if (DBG) Log.d(TAG, "Creating media recorder with audio source " + audioSource);
+ mMediaRecorder.setAudioSource(audioSource);
+ mMediaRecorder.setOutputFormat(formatChoice == 0
+ ? MediaRecorder.OutputFormat.AMR_WB : MediaRecorder.OutputFormat.MPEG_4);
+ mMediaRecorder.setAudioEncoder(formatChoice == 0
+ ? MediaRecorder.AudioEncoder.AMR_WB : MediaRecorder.AudioEncoder.AAC);
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Error initializing media recorder", e);
+ mMediaRecorder.reset();
+ mMediaRecorder.release();
+ mMediaRecorder = null;
+ return false;
+ }
+
+ String fileName = generateFilename(phoneNumber);
+ Uri uri = getContentResolver().insert(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+ CallRecording.generateMediaInsertValues(fileName, creationTime));
+
+ try {
+ ParcelFileDescriptor pfd = getContentResolver().openFileDescriptor(uri, "w");
+ if (pfd == null) {
+ throw new IOException("Opening file for URI " + uri + " failed");
+ }
+ mMediaRecorder.setOutputFile(pfd.getFileDescriptor());
+ mMediaRecorder.prepare();
+ mMediaRecorder.start();
+
+ long mediaId = Long.parseLong(uri.getLastPathSegment());
+ mCurrentRecording = new CallRecording(phoneNumber, creationTime,
+ fileName, System.currentTimeMillis(), mediaId);
+ return true;
+ } catch (IOException | IllegalStateException e) {
+ Log.w(TAG, "Could not start recording", e);
+ getContentResolver().delete(uri, null, null);
+ } catch (RuntimeException e) {
+ getContentResolver().delete(uri, null, null);
+ // only catch exceptions thrown by the MediaRecorder JNI code
+ if (e.getMessage().indexOf("start failed") >= 0) {
+ Log.w(TAG, "Could not start recording", e);
+ } else {
+ throw e;
+ }
+ }
+
+ mMediaRecorder.reset();
+ mMediaRecorder.release();
+ mMediaRecorder = null;
+
+ return false;
+ }
+
+ private synchronized CallRecording stopRecordingInternal() {
+ CallRecording recording = mCurrentRecording;
+ if (DBG) Log.d(TAG, "Stopping current recording");
+ if (mMediaRecorder != null) {
+ try {
+ mMediaRecorder.stop();
+ mMediaRecorder.reset();
+ mMediaRecorder.release();
+ } catch (IllegalStateException e) {
+ Log.e(TAG, "Exception closing media recorder", e);
+ }
+
+ Uri uri = ContentUris.withAppendedId(
+ MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, mCurrentRecording.mediaId);
+ getContentResolver().update(uri, CallRecording.generateCompletedValues(), null, null);
+
+ mMediaRecorder = null;
+ mCurrentRecording = null;
+ }
+ return recording;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (DBG) Log.d(TAG, "Destroying CallRecorderService");
+ }
+
+ private String generateFilename(String number) {
+ String timestamp = DATE_FORMAT.format(new Date());
+
+ if (TextUtils.isEmpty(number)) {
+ number = "unknown";
+ }
+
+ int formatChoice = getAudioFormatChoice();
+ String extension = formatChoice == 0 ? ".amr" : ".m4a";
+ return number + "_" + timestamp + extension;
+ }
+
+ public static boolean isEnabled(Context context) {
+ return context.getResources().getBoolean(R.bool.call_recording_enabled);
+ }
+}
diff --git a/java/com/android/dialer/callrecord/res/values/config.xml b/java/com/android/dialer/callrecord/res/values/config.xml
new file mode 100644
index 0000000..7aabd6c
--- /dev/null
+++ b/java/com/android/dialer/callrecord/res/values/config.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (C) 2015 The CyanogenMod 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.
+-->
+
+<resources>
+ <bool name="call_recording_enabled">false</bool>
+ <!-- 1 (MIC) for microphone audio source (default)
+ 4 (VOICE_CALL) if supported by device for voice call uplink + downlink audio source -->
+ <integer name="call_recording_audio_source">1</integer>
+</resources>
diff --git a/java/com/android/dialer/callrecord/res/xml/call_record_states.xml b/java/com/android/dialer/callrecord/res/xml/call_record_states.xml
new file mode 100644
index 0000000..06309d7
--- /dev/null
+++ b/java/com/android/dialer/callrecord/res/xml/call_record_states.xml
@@ -0,0 +1,1341 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright (C) 2019 The LineageOS 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.
+ */
+-->
+<call-record-allowed-flags>
+ <!-- Disable recording for Andorra:
+ Article 183 of the Andorran Penal Code sets a prison sentence of one to four years as the
+ punishment for attempting or succeeding to infringe on the privacy of another person
+ without his or her consent. This includes intercepting calls or using technical means to
+ listen, transmit, record or reproduce their calls. Article 185 and 188 of the Andorran
+ Penal Code describe the length of imprisonment when sharing recordings with third parties,
+ even if one has not partaken in their creation, unless the party was unaware of their
+ illicit origin. The attempt to do any of the above is also punishable by law.
+
+ Penal Code, as of 2014:
+ https://sherloc.unodc.org/res/cld/document/codi_penal_andorra_as_of_2014_html/Andorra_codi_penal_as_of_2014.pdf
+ -->
+ <country iso="ad" allowed="false" />
+
+ <!-- Enable recording for Albania:
+ Relevant laws and/or legal precedents:
+ Article 121, 122 and 123 of the Albanian Penal Code cover the right to privacy, wiretapping
+ and harming other via dissemination of their secrets. Based on these, it is not a criminal
+ offense to record your own calls, but sharing them with a third party or using them to harm
+ the other party in said calls is a criminal offense.
+
+ Penal Code: (nonencrypted link)
+ http://rai-see.org/wp-content/uploads/2015/08/Criminal-Code-11-06-2015-EN.pdf
+ -->
+ <country iso="al" allowed="true" />
+
+ <!-- Enable recording for Armenia:
+ The Armenian Criminal Code discusses the legality and punishment of call recordings, when
+ recorded by a third party, otherwise known as wiretapping. Since the Criminal Code does not
+ specifically mention call recording done by a person that is a party to the call, with or
+ without consent, then the act of doing so is not a criminal offense, as it carries no
+ punishment whatsoever.
+
+ Criminal Code:
+ https://www.unodc.org/res/cld/document/armenia_criminal_code_html/Armenia_Criminal_Code_of_the_Republic_of_Armenia_2009.pdf
+
+ Two examples of usage of call recordings, without persecution:
+ https://fip.am/803
+ https://fip.am/4500
+ -->
+ <country iso="am" allowed="true" />
+
+ <!-- Enable recording for Argentina:
+ Argentina follows the continental law system. If a law does not exist, which defines
+ something as a crime, it is not a crime. Judges in Argentina make decisions based on their
+ reading of the law, and not on precedents. Call recording is currently not defined as a
+ crime in any law. This is defined in Section 19 of the Constitution of Argentina.
+
+ Constitution: (nonencrypted link)
+ http://www.senadoctes.gov.ar/constitucion-arg/Constitution%20of%20the%20Argentine%20Nation.htm
+
+ Example of usage of call recordings as legal evidence:
+ https://www.scribd.com/document/326647534/H-P-C-F-s-recurso-de-casacion
+ -->
+ <country iso="ar" allowed="true" />
+
+ <!-- Enable recording for American Samoa:
+ Federal Law 18 USC § 2511(2)(d) defines the recording of a call as legal when one party to
+ the call agrees to it, if said call recording is not done with the intention of committing
+ a crime. This territory of the United States conforms with its Federal legislation.
+ For further information, check 'us'.
+
+ U.S. Code: Title 18 - Crimes and Criminal Procedure:
+ https://www.law.cornell.edu/uscode/text/18/2511
+ -->
+ <country iso="as" allowed="true" />
+
+ <!-- Enable recording for Austria
+ According to Article 93 (3) of Austrian Communications Law, known as TKG 2003
+ Kommunikationsgeheimnis, it is illegal to recordor pass on information about a call, unless
+ you are one of the parties in that call. While recording is not illegal, sharing the
+ recording might be a punishable offense, without the consent of both sides.
+
+ Communications Law:
+ https://www.jusline.at/gesetz/tkg/paragraf/93
+ https://www.ris.bka.gv.at/GeltendeFassung.wxe?Abfrage=Bundesnormen&Gesetzesnummer=20002849
+ -->
+ <country iso="at" allowed="true" />
+
+ <!-- Disable recording for Australia:
+ Australian Capital Territory:
+ Subsection 4(3)(b) Listening Devices Act 1992 (ACT)
+ https://www8.austlii.edu.au/cgi-bin/viewdoc/au/legis/act/consol_act/lda1992181/s4.html
+ A person must not use a listening device with the intention of recording a private
+ conversation to which the person is a party. This does not apply when said listening
+ device is used by, or on behalf of, a party to a private conversation if a principal
+ party to the conversation consents to the listening device being so used, and the
+ recording of the conversation is considered by that principal party, on reasonable
+ grounds, to be necessary for the protection of that principal party's lawful interests;
+ or the recording is not made for the purpose of communicating or publishing the
+ conversation, or a report of the conversation, to any person who is not a party to the
+ conversation.
+
+ New South Wales:
+ Subsection 7(3)(b) Surveillance Devices Act 2007 (NSW)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/nsw/consol_act/sda2007210/s7.html
+ A person must not knowingly use a listening device to record a private conversation to
+ which the person is a party. This does not apply when a principal party to the
+ conversation consents to the listening device being so used and the recording of the
+ conversation is reasonably necessary for the protection of the lawful interests of that
+ principal party, or is not made for the purpose of communicating or publishing the
+ conversation, or a report of the conversation, to persons who are not parties to the
+ conversation.
+
+ Northern Territory:
+ Subsection 11(1)(a) Surveillance Devices Act 2007 (NT)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/nt/num_act/sda200719o2007256/s11.html
+ Subsection 43, Emergency use of listening device in public interest
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/nt/num_act/sda200719o2007256/s43.html
+ It is an offence to use a listening device to record a private conversation to which the
+ person is not a party and the device is used without the express or implied consent of
+ each party to the conversation. Under Section 43, a person may use a listening device to
+ record a private conversation if at the time of use there are reasonable grounds for
+ believing the circumstances are so serious and the matter is of such urgency that the use
+ of the device is in the public interest.
+
+ Queensland:
+ Subsection 43(2)(a) Invasion of Privacy Act 1971 (Qld)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/qld/consol_act/iopa1971222/s43.html
+ A person is guilty of an offence, if the person uses a listening device to record a
+ private conversation and is liable to a maximum penalty of 40 penalty units or
+ imprisonment for 2 years. This does not apply when the person using the listening device
+ is a party to the private conversation.
+
+ South Australia:
+ Subsection 4(2)(a)(ii) Surveillance Devices Act 2016 (SA)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/sa/consol_act/sda2016210/s4.html
+ A person must not knowingly use a listening device to record a private conversation to
+ which the person is, or is not a party. The maximum penalty is $15 000 or imprisonment
+ for 3 years. This does not apply if the use of a listening device is done by a party to
+ the private conversation if the use of the device is reasonably necessary for the
+ protection of the lawful interests of that person.
+
+ Tasmania:
+ Subsection 5(3)(b) Listening Devices Act 1991 (TAS)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/tas/consol_act/lda1991181/s5.html
+ A person shall not use a listening device to record a private conversation to which the
+ person is, or is not, a party. This does not apply when the listening device is used to
+ obtain evidence or information in connection with an imminent threat of serious violence
+ to persons or of substantial damage to property, or a serious narcotics offence, if the
+ person using the listening device believes on reasonable grounds that it was necessary to
+ use the device immediately to obtain that evidence or information. This does not apply if
+ a principal party to the conversation consents to the listening device being so used and
+ the recording of the conversation is reasonably necessary for the protection of the
+ lawful interests of that principal party or the recording of the conversation is not made
+ for the purpose of communicating or publishing the conversation, or a report of the
+ conversation, to persons who are not parties to the conversation.
+
+ Victoria:
+ Subsection 6(1) Surveillance Devices Act 1999 (NSW)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/vic/consol_act/sda1999210/s6.html
+ A person must not knowingly use a listening device to record a private conversation to
+ which the person is not a party. The penalty is up to 2 years imprisonment and up to 240
+ penalty units, or both.
+
+ Western Australia:
+ Subsection 5(3)(d) Surveillance Devices Act 1998 (WA)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/wa/consol_act/sda1998210/s5.html
+ Subsection 26(1)(2)(3)(b)
+ https://www.austlii.edu.au/cgi-bin/viewdoc/au/legis/wa/consol_act/sda1998210/s26.html
+ A person shall not use a listening device to record a conversation to which that person
+ is, or is not, a party. The penalty is $5 000 or imprisonment for 12 months, or both.
+ This does not apply to the use of a listening device by a party to a private conversation
+ if that principal party consents to its use, as reasonably necessary for the protection
+ of the lawful interests of that principal party. It is also not applicable in cases where
+ a person who is a party to a private conversation, or is acting on behalf of a party to a
+ private conversation, uses a listening device to record said private conversation,
+ believing that the use of the listening device is in the public interest.
+
+ Summary:
+ Most states and territories allow to make a recording of your personal conversations
+ under specific circumstances. The wording of the laws themselves are open to
+ legal interpretation and can be used against users. Until the laws above are presented
+ in a more clear way or enough evidence is shown to substantiate how the courts interpret
+ them when prosecuting private citizens, Australia shall not have call recording enabled.
+ -->
+ <country iso="au" allowed="false" />
+
+ <!-- Enable recording for Bosnia and Herzegovina:
+ Republika Srpska:
+ Article 155 - Unauthorized eavesdropping and tone recording.
+ Paragraph 1 specifically states 'which is not intended for him', which in context means
+ that a person may record anything which is intended for him or her. Paragraph 2 defines
+ the creation of a record with the intent to abuse and/or misuse it, or the act of sharing
+ it with a third party as a criminal offense.
+ Other Articles that might be relevant:
+ Article 153, Privacy of letters, telegrams and others.
+ Article 157, Unauthorized use of Personal Data.
+
+ Federation of Bosnia and Herzegovina:
+ Article 188, Unauthorized Tapping and Sound Recording, defines the recording of any call
+ 'which is not intended for public or private knowledge', as a criminal offense.
+
+ Brčko District:
+ Article 185 of the Criminal Code specifically states 'which was not intended for him',
+ which in context means that a party may record any call which is indended for said party.
+ Wiretapping is considered a crime and is criminally punishable.
+
+ Further information:
+ The Criminal Codes of the legal entities of Bosnia and Herzegovina, namely Republika
+ Srpska, the Federation of Bosnia and Herzegovina and the Brčko District, can all be found
+ through the link below. Please note that these three entities currently have separate
+ laws, due to administrative and/or judicial autonomy. It should be noted that these
+ different entities have over 15 police forces, each with its own jurisdiction.
+ https://www.legislationline.org/documents/section/criminal-codes/country/40/Bosnia%20and%20Herzegovina/show
+ -->
+ <country iso="ba" allowed="true" />
+
+ <!-- Enable recording for Bangladesh:
+ Based on the Evidence Act of 1872, audio recordings may be admissible as a form of
+ evidence. In civil cases, call recording may be used, but judges decide on when they are
+ admissable. Recordings are considered admissible evidence when a witness is deposed and
+ testifies to their validity before a court. Parties may rely on recordings during civil
+ cases, in order to support their side's version of events. While the use of audio
+ recordings as a form of evidence is common, the Supreme Court has not stated whether
+ recordings can be used as evidence blindly, thus their use is on a case-to-case basis.
+
+ Legal discussion:
+ https://www.thedailystar.net/news/your-advocate-2
+
+ Evidence Act of 1872:
+ https://acc.portal.gov.bd/sites/default/files/files/acc.portal.gov.bd/law/7e753733_a368_471e_aef9_b4525cb5082c/The_Evidence_Act,_1872_(Act_No._I_of_1872).pdf
+ -->
+ <country iso="bd" allowed="true" />
+
+ <!-- Enable recording for Belgium:
+ As stated in the official response below, Belgian law does not consider the recording of
+ one's personal communications as a punishable offense. Using said recordings in a
+ fraudulent and/or demeaning way does carry the potential for liability and/or prosecution
+ by the state. The recording of one's own calls might be regarded as a form of personal data
+ processing, depending on the specifics of the case. Specific laws and cases are quoted
+ within the official response. The Belgian municipality of Baarle-Hertog, consisting of a
+ number of exclaves, has territory which is within the Dutch province of North Brabant, and
+ as such it may not be within the confines of the Belgian ISO and its inherent laws.
+
+ Official Response by Belgian Minister (QRVA 50 157, pages 20199-20202, 24/02/2003):
+ https://www.lachambre.be/QRVA/pdf/50/50K0157.pdf
+ -->
+ <country iso="be" allowed="true" />
+
+ <!-- Enable recording for Bulgaria:
+ Article 32(2) of the Bulgarian Constitution states that it is an inviolable right for
+ people to not be followed, photographed, recorded (audio and/or video) without being
+ notified and/or despite his or her explicit disagreement to said actions, except where the
+ law allows for said actions. The Code of Criminal Procedure, Part III, Articles 125 and
+ 126, page 34, deal with the use of recordings as evidence. No law explicitly tackles the
+ issue of consent when recording one's personal telephone calls. Based on the available
+ documentation and the attached example of lack of state prosecution, recording one's own
+ calls is not legal, nor is it a criminal offense. Personal recordings are unlikely to be
+ a valid form of evidence in a court of law.
+
+ Constitution:
+ https://www.parliament.bg/bg/const
+
+ Code of Criminal Procedure:
+ https://www.mvr.bg/docs/default-source/normativnauredba/3da73fed-npk-pdf.pdf
+
+ Example of lack of state prosecution:
+ https://goo.gl/HQPUup
+ -->
+ <country iso="bg" allowed="true" />
+
+ <!-- Enable recording for Brazil:
+ Call recording is not a criminal offense when it the recording is made by one of the two
+ parties of said call. Interception by a third party is illegal and punishable by law,
+ unless done according to the requirements set out in Law 9296. There may be some debate, as
+ far as the use of a call recording as legitimate evidence. Further information is available
+ in the attached legal discussions below.
+
+ Law 9296 of the 24th of July 1996: (nonencrypted link)
+ http://www.planalto.gov.br/ccivil_03/leis/l9296.htm
+
+ Constitution of Brazil, Art 5º, X and XII:
+ https://www2.camara.leg.br/legin/fed/consti/1988/constituicao-1988-5-outubro-1988-322142-publicacaooriginal-1-pl.html
+
+ Legal discussions:
+ https://direitosbrasil.com/gravar-conversa-e-crime/
+ https://meusitejuridico.com.br/2018/04/02/stj-e-licita-gravacao-de-conversa-feita-pelo-destinatario-de-solicitacao-de-vantagem-indevida
+ https://moisesandrade.jusbrasil.com.br/artigos/121944095/constitucionalidade-do-uso-da-gravacao-clandestina-como-meio-de-prova
+ -->
+ <country iso="br" allowed="true" />
+
+ <!-- Enable recording for Belarus:
+ Article 28 of the Constitution of Belarus covers the right to privacy. Article 179 of the
+ Criminal Code of Belarus covers situations in which a person's privacy is violated by way
+ of any secret being shared without his or her consent, but no specific term of imprisonment
+ or fine is mentioned. The wording of the article is aimed at the collecting and sharing of
+ 'a personal or family secret of another person'. Creating a call recording for personal use
+ is not covered by this article, as privacy is not inherently guaranteed. The use of call
+ recordings as evidence in a court of law is dubious. The sharing of a call recording could
+ be considered as punishable by law, depending on the circumstances.
+
+ Belarusian Constitution: (nonencrypted link)
+ http://www.pravo.by/pravovaya-informatsiya/normativnye-dokumenty/konstitutsiya-respubliki-belarus
+
+ Belarusian Criminal Code:
+ https://etalonline.by/?type=text®num=HK9900275#load_text_none_1_
+ -->
+ <country iso="by" allowed="true" />
+
+ <!-- Enable recording for Canada:
+ Any intended recipient of a communication is entitled to record it, based on Section 184(2)
+ Subsection (1) of the Criminal Code of Canada. There are numerous legal cases that validate
+ the interception of private communications by parties to the conversation as not illegal.
+ For a more in-depth look, refer to the LegalTree article below.
+
+ Criminal Code:
+ https://laws-lois.justice.gc.ca/eng/acts/C-46/page-1.html
+
+ LegalTree article:
+ https://legaltree.ca/node/908
+
+ Legal articles:
+ https://lambertavocatinc.com/avocat-montreal/enregistrer-conversation-legal/
+ https://www.avocat.qc.ca/affaires/iitelephone.htm
+ -->
+ <country iso="ca" allowed="true" />
+
+ <!-- Disable recording for Switzerland:
+ According to Article 179 of the Swiss Criminal Code of 21 December 1937, it is a criminal
+ offense to store, record, or share the recording of a call, even when one is part of said
+ call. Explicit consent is required by both parties for a recording to be legal.
+
+ Criminal Code:
+ https://www.admin.ch/opc/en/classified-compilation/19370083/index.html#a179ter
+ -->
+ <country iso="ch" allowed="false" />
+
+ <!-- Enable recording for Chile:
+ The Chilean law is considered a type of civil law, hence judges base their decisions on
+ their own reading of the law. The Chilean Supreme Court ruled in favor of accepting
+ one-party consent call recording as a form of legal evidence, hence the act of recording
+ your own calls is not criminally punishable, as can be seen in the Chilean Penal Code.
+
+ Penal Code:
+ https://www.leychile.cl/Navegar?idNorma=1984
+
+ Article on one-party consent:
+ https://radio.uchile.cl/2018/04/22/grabacion-es-aceptada-como-prueba-en-juicio-por-practicas-antisindicales/
+ -->
+ <country iso="cl" allowed="true" />
+
+ <!-- Enable recording for China:
+ No clear definition exists on the matter of call recordings being made by a private
+ citizen within the Criminal Law of the People's Republic of China. Depending on whether
+ said call recording was made and/or published with malicious intent, it may or may not be
+ admissible in court. For further information
+
+ Criminal Law of the People's Republic of China: (nonencrypted link)
+ http://english.court.gov.cn/2015-12/01/content_22595464.htm
+
+ Supreme People's Court Provisions on Evidence in Civil Procedures: (nonencrypted link)
+ http://en.pkulaw.cn/display.aspx?cgid=38083&lib=law
+ -->
+ <country iso="cn" allowed="true" />
+
+ <!-- Enable recording for Costa Rica:
+ One may record one's own calls, as long as they are calls between said person and only one
+ other party, that is two say two sides. Calls between 3 or more people can not be legally
+ recorded without all sides agreeing to one person doing so, as long as said person is a
+ part of the call and not wiretapping or eavesdropping. Recording calls with more than 2
+ participants requires the express consent of all other parties. Article 29 of the
+ Communication Law of 1994 specifies under what circumstances one may or may not do so.
+
+ Communication Law: (nonencrypted link)
+ http://www.pgrweb.go.cr/scij/Busqueda/Normativa/Normas/nrm_texto_completo.aspx?param1=NRM&nValor1=1&nValor2=16466&strTipM=FN
+
+ Article:
+ https://www.laprensalibre.cr/Noticias/detalle/75929/ojo-conversaciones-grabadas-pueden-usarse-como-prueba-en-juicio
+ -->
+ <country iso="cr" allowed="true" />
+
+ <!-- Enable recording for Cyprus:
+ The Cypriot Penal Code does not explicitly cover the act of wiretapping or recording one's
+ own calls. Based on this, it is not a criminal offense to record personal calls. Article
+ 369 of the Cypriot Penal Code states that anyone who knows that another is planning to
+ commit a criminal offense, yet fails to use any reasonable means to prevent said crime, is
+ guilty of misconduct, which can be used as a reason for recording one's own calls, should
+ the need arise to quote a legal document.
+
+ Penal Code: (nonencrypted link)
+ http://www.cylaw.org/nomoi/enop/non-ind/0_154/index.html
+ -->
+ <country iso="cy" allowed="true" />
+
+ <!-- Enable recording for Czech Republic:
+ Case Law File Number 21 502/2000 of the Supreme Court specifies that even when evidence is
+ acquired or provided in contravention to legal regulations and/or personal rights, it shall
+ not be deemed as inadmissible. This, as well as other information, is accessible in the
+ Constitutional Court Finding 191/05 of the 13th of September 2006.
+
+ Constitutional Court Finding:
+ https://nalus.usoud.cz/Search/GetText.aspx?sz=1-191-05_2
+ -->
+ <country iso="cz" allowed="true" />
+
+ <!-- Disable recording for Germany:
+ According to Section 201 of the German Criminal Code - Violation of the privacy of the
+ spoken word, making an audio recording of the privately spoken words of another or making
+ such a recording accessible by a third party will result in up to three years of
+ imprisonment. Article 10 of the German Constitution explicitly states that the secrecy of
+ telecommunications is inviolable. There are notable exceptions, such as the use of
+ recordings when in a legitimate self-defense situation. Article 227 of the German Civil
+ Code notes that acting in one's own defense is not unlawful, which is also explained in
+ Article 32 of the German Criminal Code. Article 88 of the Telecommunications Act defines
+ telecommunications secrecy. The German municipality of Büsingen am Hochrhein is an exclave
+ within the territorial confines of Switzerland, and as such it may not be within the
+ confines of the German ISO and its inherent laws.
+
+ Civil Code:
+ https://www.gesetze-im-internet.de/bgb/__227.html
+
+ Criminal Code:
+ https://www.gesetze-im-internet.de/stgb/__32.html
+
+ Constitution:
+ https://www.gesetze-im-internet.de/gg/art_10.html
+
+ Telecommunications Act:
+ https://www.gesetze-im-internet.de/tkg_2004/__88.html
+
+ Wikipedia article on self-defense laws in Germany:
+ https://de.wikipedia.org/wiki/Notwehr_(Deutschland)
+
+ Explanation of lawful use of a recording in a legal dispute:
+ https://www.anwalt.de/rechtstipps/gespraechsmitschnitte-als-beweismittel-ungeeignet_057458.html
+ -->
+ <country iso="de" allowed="false" />
+
+ <!-- Enable recording for Denmark, Faroe Islands and Greeenland:
+ Chapter 27, Article 263(3) of the Criminal Code of Denmark denotes that a person is liable
+ for criminal punishment when he or she intercepts or records telephone conversations to
+ which he or she is not a party. The articles in Chapter 27 cover a lot of different
+ situations, including the dissemination of recordings, which may lead to a fine or prison
+ sentence. The act of recording a conversation that one is a part of is not covered
+ explicitly, hence it is not a criminal offense in the eyes of the law.
+
+ Criminal Code:
+ https://www.retsinformation.dk/Forms/r0710.aspx?id=164192#Kap27
+ -->
+ <country iso="dk,fo,gl" allowed="true" />
+
+ <!-- Enable recording for Estonia:
+ Recording your calls for personal use is not a criminal offense. Sharing said calls with a
+ third party is a criminal offence, hence punishable by law, except in cases where said
+ calls are shared by a journalist.
+
+ Constitution, Paragraph 43:
+ https://www.pohiseadus.ee/index.php?sid=1&p=43
+
+ Instructions for call recording (GDPR equivalent):
+ https://www.aki.ee/sites/www.aki.ee/files/elfinder/article_files/Telefonik%C3%B5nede%20salvestamise%20lubatavuse%20juhend.pdf
+
+ Legal article:
+ https://digi.geenius.ee/rubriik/uudis/millistel-juhtudel-tohib-eestis-telefonikone-salvestada-ja-selle-sisu-avaldada/
+ -->
+ <country iso="ee" allowed="true" />
+
+ <!-- Enable recording for Spain:
+ Based on the decision of the Spanish Constitutional Tribunal of November the 29th, 1984, it
+ is legal for a party to record his or her calls without notifying the other party. Sharing
+ said recording with a third party is not protected and may make the party that has shared
+ the recording liable to a civil suit, to be initiated by the aggrieved party. Unless done
+ so for judicial purposes, it is punishable to disclose or share the recording or the gist
+ of the recording to other parties. The town of Llívia is a Spanish exclave within the
+ territory of the Republic of France, and as such, it may not be within the confines of the
+ Spanish ISO and its inherent laws.
+
+ Decision of the Spanish Constitutional Tribunal:
+ https://hj.tribunalconstitucional.es/eu/Resolucion/Show/367
+
+ Legal articles: (nonencrypted link)
+ http://belegal.com/blog-by-antonio-flores/validity-of-recorded-telephone-conversations-in-spain/
+ https://www.fonvirtual.com/blog/la-grabacion-de-llamadas/
+ https://www.legalisconsultores.es/2014/04/es-legal-realizar-grabaciones-su-aportacion-en-juicios/
+ -->
+ <country iso="es" allowed="true" />
+
+ <!-- Enable recording for Finland:
+ As a private citizen, one may record any call they participate in. There is no requirement
+ to make other parties aware of the recording, but the use of said recordings, depending on
+ their content, may be subject to various laws, such as data protection (privacy)
+ legislation, libel laws, laws governing trade and national secrets, non-disclosure
+ agreements and so on.
+
+ Bureau of Data Ombudsman:
+ https://web.archive.org/web/20180517050133/http://www.tietosuoja.fi/sv/index/useinkysyttya/puheluidennauhoittaminen.html
+ -->
+ <country iso="fi" allowed="true" />
+
+ <!-- Enable recording for France, Saint Barthélemy, French Guayana, Guadeloupe, Saint Martin,
+ Martinique, New Caledonia, French Polynesia, Saint Pierre & Miquelon,
+ Réunion, Wallis-et-Futuna and Mayotte:
+ While recording calls without consent, as a third party, is punishable, it depends on
+ whether said recording was created or used with a malicious intent. Judges are free to view
+ said recordings as a form of evidence and base their final decisions with their help.
+ Recording your own calls as a private citizen is not a criminal offense. Sharing said
+ recordings, with the intent to harm the other party in any way, is a criminal offense.
+
+ Penal Code:
+ https://www.legifrance.gouv.fr/affichCode.do;jsessionid=3E84EAC0F63D49FC16A28B8D90EFF1D2.tplgfr44s_2?idSectionTA=LEGISCTA000006165309&cidTexte=LEGITEXT000006070719&dateTexte=20150413
+
+ Civil Code:
+ https://www.legifrance.gouv.fr/affichCode.do;jsessionid=1A7384A63066DBE1E1D8C732E698F844.tplgfr23s_3?idSectionTA=LEGISCTA000006117610&cidTexte=LEGITEXT000006070721&dateTexte=20190606
+
+ Legal article on call recordings as evidence:
+ https://www.annuaireavocats.fr/articles/enregistrer-une-conversation-a-linsu-dune-personne-est-ce-legal
+
+ Legal article on recording in the workplace:
+ https://www.cnil.fr/fr/lecoute-et-lenregistrement-des-appels-sur-le-lieu-de-travail
+ -->
+ <country iso="fr,bl,gf,gp,mf,mq,nc,pf,pm,re,wf,yt" allowed="true" />
+
+ <!-- Enable recording for United Kingdom:
+ Recording one's own calls is not a criminal offence and is not prohibited. As long as the
+ recording is for personal use, consent and/or notification of the other party are not
+ required. Call recordings can be used as evidence, since it is based on a trite law.
+ Sharing said call recordings with a third party, without consent, may be a criminal offence
+ and punishable.
+
+ Use as evidence (p. 3):
+ https://www.bailii.org/uk/cases/UKPC/1954/1954_43.pdf
+
+ Legal articles:
+ https://www.computertel.co.uk/article?ref=Call-Recording-Law-in-the-UK-2018-edition
+ https://www.dma-law.co.uk/is-it-illegal-to-record-conversations/
+ -->
+ <country iso="gb" allowed="true" />
+
+ <!-- Enable recording for Georgia:
+ The Constitution of Georgia, Chapter Two - Fundamental Human Rights, Article 15(2) states
+ that personal communication(s) are inviolable and that said right may only be restricted in
+ accordance with the law, to ensure national security or public safety, or to protect the
+ rights of other parties, insofar as it is necessary in a democratic society, based on a
+ court decision or without a court decision in cases of urgent necessity, as provided by the
+ law. Articles 157, 158 and 159 of the Criminal Code of Georgia deal with the disclosure of
+ private information, personal data, the violation of the secrecy of private communication
+ and the violation of secrecy of personal correspondence, phone conversations or other kinds
+ of communication. The document does not specify a situation in which one side of a
+ conversation records without the other side's knowledge or consent, thus the act of
+ recording one's conversations is in a legally gray area. All of the above articles
+ explicitly note that no criminal liability can be incurred if the gathered information is
+ submitted to investigative authorities.
+
+ Constitution:
+ https://matsne.gov.ge/en/document/view/30346?publication=35
+
+ Criminal Code of Georgia:
+ https://matsne.gov.ge/en/document/view/16426?publication=187&scroll=62067
+ -->
+ <country iso="ge" allowed="true" />
+
+ <!-- Enable recording for Greece:
+ Section 2 of Article 370A of the Greek Penal Code bans it, subarticle 4 offers exceptions
+ when no other evidence is present. Decision 53/2010 of the Supreme Criminal Court limits
+ evidence submitting to third parties that found the recording 'by accident'. Decision
+ 277/2014 of the Supreme Criminal Court acquitted a guilty party and deemed the presented
+ recordings admissable. Article 25 of the Penal Code states that, any action is not illegal
+ if it was done so to protect the property or safety of oneself or of another party,
+ provided that the crime of sharing the recording is a lesser one in comparison.
+
+ Legal discussion:
+ https://uk.practicallaw.thomsonreuters.com/w-010-1738
+ -->
+ <country iso="gr" allowed="true" />
+
+ <!-- Enable recording for Croatia:
+ Article 143 of the Croatian Criminal Code, Paragraph 1 notes that the recording of another
+ person's privately uttered words is a criminal offense, when said words are not 'intended
+ for his or her attention' and could lead to imprisonment not exceeding three years.
+ Paragraph 2, which holds the same punishment, indicated that situations in which the
+ recording, its transcription or the 'gist' of said recording being shared as an equal
+ crime. Paragraph 4 states that there is no criminal offence if said acts are committed in
+ 'the public interest or another interest prevailing over the interest to protect the
+ privacy of the person being recorded or eavesdropped on'. Prosecution is made per request
+ and the state does not initiate it, which renders the matter to the level of a civil case
+ and not to that of a criminal case.
+
+ Croatian Criminal Code:
+ https://www.legislationline.org/documents/section/criminal-codes/country/37/Croatia/show
+ -->
+ <country iso="hr" allowed="true" />
+
+ <!-- Enable recording for Hungary:
+ Sections 413 and 418 define the Breach of Trade and/or Business Secrecy as a criminal
+ offense. There is no mention of wiretapping and/or eavesdropping as a criminal offense.
+ The Hungarian Data Protection and Freedom of Information Agency (DPA) created a Guidance in
+ 2016, for cases concerning situations which include an individual as one side of the
+ conversation, and a data processing entity as the other side. This guidance should not be
+ considered relevant, as it does not deal with the communications of individuals. No
+ pertinent articles or paragraphs were found in the Hungarian Criminal Code, which in effect
+ equates to there being no punishment for the recording of personal calls.
+
+ DPA 2016 Guidance:
+ https://www.naih.hu/files/2016_05_09_tajekoztato_hangfelvetelekrol.pdf
+
+ Hungarian Criminal Code:
+ https://www.legislationline.org/documents/section/criminal-codes/country/25/Hungary/show
+ -->
+ <country iso="hu" allowed="true" />
+
+ <!-- Disable recording for Indonesia:
+ Based on Article 26 of both Law Number 11 of 2008 and its revision, Law Number 19 of 2016,
+ call recording is defined on its own and requires one to obtain consent from the other
+ party when recording calls, although it can be used as a form of evidence. Whether
+ recording another person without his or her consent is a criminal offense that is
+ prosecuted by the country itself is not clear and further information should be gathered by
+ a native speaker.
+
+ Law 11 of 2008 (first file):
+ https://www.hukumonline.com/pusatdata/detail/27912/nprt/1011/uu-no-11-tahun-2008-informasi-dan-transaksi-elektronik
+
+ Law 19 of 2016:
+ https://jdih.kominfo.go.id/produk_hukum/view/id/555/t/undangundang+nomor+19+tahun+2016+tanggal+25+november+2016
+ -->
+ <country iso="id" allowed="false" />
+
+ <!-- Enable recording for Ireland:
+ The Irish Constitution does not specifically state a right to privacy. Subsection (6) of
+ section 98 of the Interception of Postal Packets and Telecommunications Messages
+ (Regulation) Act of 1993 defines interception of a call in such a way, that deems the
+ recording of a call by one party to the call legal. Whether said call recording can be used
+ as evidence or infringes upon a person's privacy is a complicated matter that can only be
+ decided on a case-by-case basis. Subsection (2) of section 98 goes on to elabore on cases
+ in which call recordings are legal, such as in the interests of the security of the State
+ (c), for the prevention or detection of crime or for the purpose of any criminal
+ proceedings (b) and others.
+
+ Telecommunications Messages Act of 1993: (nonencrypted link)
+ http://www.irishstatutebook.ie/eli/1993/act/10/enacted/en/print.html
+
+ Legal discussions:
+ https://www.mhc.ie/latest/insights/big-brother-is-watching-but-is-he-listening-too
+ https://www.irishtimes.com/news/crime-and-law/q-a-what-are-the-legal-implications-1.1740070
+ -->
+ <country iso="ie" allowed="true" />
+
+ <!-- Enable recording for Israel and Palestine:
+ Israeli law specifies that call recording is illegal and punishable when neither party in
+ said conversation is aware of said act of recording. Either party in a conversation can
+ record his or her calls without being legally required to inform the other party.
+ Due to legal ambiguity, it is currently impossible to determine which set of laws should be
+ taken under consideration when recording personal calls within the Palestinian territories.
+ This is relevant as the Occupied Palestinian Territory makes use of the Mobile Country Code
+ registered to Israel. Palestine's ISO is set as disabled, since if it is in use there is no
+ legal way to determine which set of laws are being used, due to the differing laws used
+ in parts of it.
+
+ The Wiretapping Law, 5739-1979:
+ https://www.nevo.co.il/law_html/law01/077_001.htm
+
+ Information on State of Palestine:
+ https://en.wikipedia.org/wiki/Palestinian_law#Statutes_and_legislation
+
+ News articles:
+ https://www.globes.co.il/news/article.aspx?did=1001066185
+ https://www.ynet.co.il/articles/0,7340,L-3043583,00.html
+ -->
+ <country iso="il" allowed="true" />
+ <country iso="ps" allowed="false" />
+
+ <!--Enable recording for India:
+ No clear definition exists on the matter of call recordings being made by one side.
+ Depending on whether said call recording was made and/or published with malicious intent,
+ it may or may not be admissible in court, and/or punishable by law. There are a number of
+ precedents and legal definitions, which are available below.
+
+ Legal discussion:
+ https://copyright.lawmatters.in/2012/02/recording-telephonic-conversations.html
+ -->
+ <country iso="in" allowed="true" />
+
+ <!-- Disable recording for Iceland:
+ According to the Electronic Communications Act, No. 81, recording one's own telephone
+ conversations without notifying the other party can make the recording party liable to
+ fines or imprisonment of up to six months in the case of serious or repeated violations, as
+ explicitly stated in Article 74. Article 48 covers the Recording of telephone calls and
+ states that the party to a telephone conversation that wishes to record said conversation
+ shall, when it commences, notify the opposite party of his or her intent to do so. This is
+ not required when the opposite party can clearly be assumed to be aware of the recording.
+
+ Electronic Communications Act:
+ https://www.government.is/Publications/Legislation/Lex/?newsid=86c9a6a9-fab5-11e7-9423-005056bc4d74
+ -->
+ <country iso="is" allowed="false" />
+
+ <!-- Enable recording for Iran:
+ Based on Article 25 of the Iranian Constitution, recording one's own calls, as a private
+ citizen for archival reasons, is not illegal. The sharing of said recordings with a third
+ party is forbidden based on the aforementioned legal document. According to Article 730 of
+ the Iranian Cybercrime Law, wiretapping a call which can be defined as non-public is a
+ crime and may lead to a punishment in the form of imprisonment for a period of six months
+ to two years, or a fine of ten to forty million rials, or both. There is currently no
+ punishment for the act of recording one's own calls in the Iranian Penal Code, thus the act
+ itself is not criminally punishable. Sharing said recordings in a way that causes injury to
+ the other party might be criminally punishable. Caution is advised, due to the geopolitical
+ situation surrounding the Islamic Republic of Iran.
+
+ Constitution:
+ https://www.wipo.int/edocs/lexdocs/laws/en/ir/ir001en.pdf
+ Penal Code of 2013 (in Persian):
+ https://www.refworld.org/cgi-bin/texis/vtx/rwmain/opendocpdf.pdf?reldoc=y&docid=5447c9274
+ Cybercrime Law (in Persian):
+ https://www.cyberpolice.ir/page/42981
+ Legal article (in Persian):
+ https://www.irna.ir/news/83268974/%D8%B6%D8%A8%D8%B7-%D9%85%D9%83%D8%A7%D9%84%D9%85%D9%87-%D8%AA%D9%84%D9%81%D9%86%DB%8C-%D8%AC%D8%B1%D9%85-%D9%86%DB%8C%D8%B3%D8%AA
+ -->
+ <country iso="ir" allowed="true" />
+
+ <!-- Enable recording for Italy and Vatican City State:
+ It is not illegal to record a conversation, as parties to calls automatically accept the
+ risk that a call may be recorded. Making a recording available to other parties is a
+ criminal offense, when done so for reasons other than protecting either one's own rights or
+ other parties' rights. Articles 23 and 167, in the Privacy Code, deem that the crimes
+ provided for therein are punishable only if said acts result in harm. According to the
+ Supreme Court of Cassation, recorded conversations are legal and can be used as evidence in
+ court, even if the other party is unaware of being recorded, provided that it is not
+ recorded by a third party. The Italian comune of Campione d'Italia features an exclave,
+ situated within the Swiss canton of Ticino, and as such it may not be within the confines
+ of the Italian ISO and its inherent laws.
+
+ Legal articles:
+ https://www.altalex.com/index.php?idnot=53369
+ https://web.archive.org/web/20161011100301/http://notizie.tiscali.it/socialnews/articoli/polimeni/13230/registrare-di-nascosto-per-la-cassazione-e-legale/
+ -->
+ <country iso="it,va" allowed="true" />
+
+ <!-- Enable recording for Japan:
+ Recording one's own calls is neither a criminal offense, nor illegal. Wiretapping and
+ leaking information gained from a recording is illegal and may be criminally punishable.
+ Recording as a third party is a criminal offense, when done so without the consent of at
+ least one party to the conversation. Recordings obtained without consent from both sides
+ will not be admitted as evidence in a criminal case, but are admitted as such in most civil
+ cases, unless it was obtained in a method, which the court deems as unacceptable. If the
+ recording infringes one's personal rights or discloses trade secrets, sharing said
+ recording might lead to civil cases. In work-related instances, one may record and divulge
+ information under the protection of the Whistleblower Protection Act of 2004. The Supreme
+ Court of Japan's Decision of the 12th of July 2000, case number 1999 (A) 96, was in favor
+ of admitting a tape recording as evidence, which was made by one party to a conversation,
+ without the other party's consent.
+
+ Whistleblower Protection Act: (nonencrypted link)
+ http://drasuszodis.lt/userfiles/Japan%20Whistleblower%20Protection%20Act.pdf
+
+ Decision of the Supreme Court of Japan: (nonencrypted link)
+ http://www.courts.go.jp/app/hanrei_en/detail?id=494
+
+ Legal articles: (nonencrypted link)
+ https://www.moneypost.jp/292939
+ https://president.jp/articles/-/15666
+ https://www.hrpro.co.jp/trend_news.php?news_no=636
+ https://kumaben.com/recording-audio-without-consent/
+ https://www.mot-net.com/blog/efficiency-of-operations/6737
+ https://milight-partners-law.hatenablog.com/entry/2015/08/31/152333
+
+ Legal discussion:
+ https://blogs.yahoo.co.jp/unyieldingspirit2007/24529523.html
+ -->
+ <country iso="jp" allowed="true" />
+
+ <!-- Enable recording for South Korea:
+ According to Article 3(1) of the Protection of Communications Secrets Act, it is forbidden
+ to wiretap, record or listen to any conversation between other parties. Article 4 defines
+ recordings obtained by way of illegal recording or wiretapping as inadmissible, hence they
+ can not be used as evidence in a trial or disciplinary procedure. Article 14 goes on to
+ specify that no person shall record a conversation between other parties, that is not
+ public, or listen to said parties' conversation through the use of electronic or mechanical
+ devices. Definitions of recording, wiretapping and other such terms may be found in Article
+ 2. The Protection of Communications Secrets Act clearly defines that recording is not legal
+ when done by a third party, but does not specifically discuss whether whether both parties
+ to a conversation need to agree to a recording. Since there is no penalty listed, recording
+ one's own conversations should be in, at worst, a gray area that should still not make the
+ act punishable. Similarly, whether recordings made without consent can be used as evidence
+ is legally unclear.
+
+ Protection of Communications Secrets Act:
+ https://elaw.klri.re.kr/kor_service/lawView.do?hseq=31731&lang=ENG
+ -->
+ <country iso="kr" allowed="true" />
+
+ <!-- Enable recording for Liechtenstein:
+ Recording a call between an organization and an individual is illegal, when done without
+ notification and/or consent. Recording a call between individuals is illegal and punishable
+ when transmitting said recording or information to a third party, and/or when the person
+ that initiates the recording is not part of the conversation. This means that recording a
+ call when you are one of the two parties is legal, even without notifying the other party.
+ Legal action must be initiated by the aggrieved party. The following is defined in Article
+ 120 of the Criminal Code of 24 June 1987 (StGB), points 1, 2, 2a and 3. Article 100 of the
+ Constitution may be pertinent to use of call recordings as evidence.
+
+ Criminal Code of 24 June 1987 (StGB):
+ https://www.regierung.li/media/medienarchiv/311_0_11_07_2017_en.pdf.
+
+ Constitution:
+ https://www.regierung.li/media/medienarchiv/101_01_01_2012_en.pdf?t=2.
+ -->
+ <country iso="li" allowed="true" />
+
+ <!-- Enable recording for Sri Lanka:
+ Part IV/59 of the Sri Lankan Telecommunications Act defines the penalty for eavesdropping
+ on a call. The Sri Lankan Penal Code does not cover the act of recording one's own calls,
+ hence the act is not criminally punishable.
+
+ Telecommunications Act:
+ https://www.lawnet.gov.lk/1947/12/31/sri-lanka-telecommunications-2/
+
+ Penal Code:
+ https://www.lawnet.gov.lk/penal-code-consolidated-2/
+
+ Article: (nonencrypted link)
+ http://www.dailymirror.lk/article/PTL-tampered-with-phone-recording-system-ASG-135574.html
+ -->
+ <country iso="lk" allowed="true" />
+
+ <!-- Enable recording for Lithuania:
+ Article 166 of the Lithuanian Criminal Code defines that violations of a person's
+ correspondence, by unlawfully wiretapping a person's conversations as a criminal offense,
+ which could lead to a term of imprisonment of up to two years, a fine or community service.
+ The wording of said article is unclear and only mentions electronic communication networks
+ and recording and/or wiretapping as a third party, and not as one of the two parties.
+ Article 61 of the Law on Electronic Communications defines confidentiality of
+ communications, as far as situations like those covered by GDPR, as in the handling of
+ information between individuals and legal entities, and should therefore not be taken into
+ account.
+
+ Lithuanian Criminal Code:
+ https://e-seimas.lrs.lt/portal/legalActPrint/lt?jfwid=q8i88l10w&documentId=a84fa232877611e5bca4ce385a9b7048&category=TAD
+
+ Lithuanian Law on Electronic Communications:
+ https://e-seimas.lrs.lt/portal/legalActPrint/lt?jfwid=-wd7z7kkgy&documentId=05cd4e020f0a11e7b6c9f69dc4ecf19f&category=TAD
+ -->
+ <country iso="lt" allowed="true" />
+
+ <!-- Enable recording for Luxembourg:
+ The Luxembourgish Penal Code does not specifically cover the right to privacy and its
+ infringement. Based on this, it is not a criminal offense to record one's personal calls,
+ although doing so in a public manner may lead to a civil case from the aggrieved party. One
+ should consult further with a lawyer whether sharing said recording or recordings would
+ constitute a criminal offense.
+
+ Penal Code: (nonencrypted link)
+ http://legilux.public.lu/eli/etat/leg/code/penal/20181101
+ -->
+ <country iso="lu" allowed="true" />
+
+ <!-- Enable recording for Latvia
+ There is no clear definition of call recording by itself within the Criminal Law of Latvia.
+ Article 144 of said law covers breach of information secrecy, when said information is in
+ the form of correspondence or data relayed by way of electronic communications networks.
+ Paragraph (1) defines the punishment for violating the secret of a person's correspondence
+ as a term of imprisonment for up to two years, or a fine, or others. In a 2014
+ e-Consultation, the Deputy Head of the Public Relations Department of the State Police,
+ Tom Sadovsky, defined the recording of calls with the intent to use as evidence as legal.
+ The Personal Data Protection Law does not apply, as it considers the communication between
+ individuals and legal entitites.
+
+ Latvian Criminal Law:
+ https://likumi.lv/doc.php?id=88966
+
+ Latvian Personal Data Protection Law:
+ https://likumi.lv/doc.php?id=4042
+
+ Legal consultation:
+ https://lvportals.lv/e-konsultacijas/4460-sarunas-drikst-ierakstit-2014.
+ -->
+ <country iso="lv" allowed="true" />
+
+ <!-- Enable recording for Morocco:
+ Call recording is not punishable as one side of a two-party conversation. Recordings are
+ not admissible in court, if the other party is not aware of the recording. Article 447 of
+ the Criminal Law of Morocco, states that the premeditated and unconsented publication of
+ video and/or audio files is a punishable offense.
+
+ Personal Data Law 09-08:
+ https://www.afapdp.org/wp-content/uploads/2018/05/Maroc-Loi-09-08-relative-a-la-protection-des-personnes-physiques-a-legard-du-traitement-des-DCP-2009.pdf
+
+ Moroccan Criminal Law:
+ https://www.h24info.ma/maroc/la-loi-sur-la-protection-des-donnees-personnelles-entre-en-vigueur-le-13-septembre/
+ -->
+ <country iso="ma" allowed="true" />
+
+ <!-- Disable recording for Monaco:
+ According to the Penal Code of Monaco, Article 308-2, a person may be punished with a
+ prison sentence of six months to three years, as well as a fine, for infinging or
+ attempting to infringe on a person's rights to privacy. This includes wiretapping,
+ recording or transmitting the words spoken by a person in a private place. Consent will be
+ presumed when such an action is done during a meeting, with the knowledge of the person
+ that is being recorded. Article 344 of the Penal Code mentions the same punishment for
+ purposeful wiretapping.
+
+ Penal Code:
+ https://www.legimonaco.mc/305/legismclois.nsf/ViewCode!OpenView&Start=1&Count=300&RestrictToCategory=CODE%20P%C3%89NAL
+ -->
+ <country iso="mc" allowed="false" />
+
+ <!-- Enable recording for Moldova:
+ Article 30 of the Constitution of Moldova ensures the privacy of correspondence. No
+ specific law has been enacted that defines recording calls, as an individual, as a criminal
+ offense. There are laws which define this for legal entities and for the government. Please
+ read the attached legal discussion for further information on the subject.
+
+ Constitution of Moldova:
+ https://www.presedinte.md/eng/constitution
+
+ Code of Criminal Procedure:
+ https://www.seepag.info/download/rep_moldova/Criminal%20Procedure%20Code%20RM.pdf
+
+ Regulations for legal entities:
+ https://www.anrceti.md/files/filefield/hca%20nr.48%20din%2010.09.2013%20regulam%20priv%20serv%20CE.pdf
+
+ Legal discussion:
+ https://jsa.md/2017/02/06/inregistrarea-convorbirilor-telefonice-cit-de-legala-este/
+ -->
+ <country iso="md" allowed="true" />
+
+ <!-- Enable recording for Montenegro:
+ Article 173 of the Criminal Code of Montenegro marks call recording as legal if the content
+ of the conversation was 'intended for your use'. It is also legal when it concerns the
+ prevention of crimes, which carry a sentence of 5 years minimum. Sharing a conversation to
+ a third party is a criminal offense.
+
+ Criminal Code of Montenegro:
+ https://www.pravda.gov.me/ResourceManager/FileDownload.aspx?rid=256001&rType=2&file=Krivi%C4%8Dni%20zakonik%20Crne%20Gore.pdf
+ -->
+ <country iso="me" allowed="true" />
+
+ <!-- Enable recording for North Macedonia:
+ As stated in the Macedonian Penal Code, if the recording is made available to a third party
+ or is created and/or distributed with a malicious intent, then the other party can sue you.
+ The state does not prosecute in such cases, unless the act is done by an official state
+ representative of any kind, as mentioned in 151.4 and 151.5.
+
+ North Macedonian Penal Code:
+ https://www.wipo.int/edocs/lexdocs/laws/mk/mk/mk018mk.pdf
+ -->
+ <country iso="mk" allowed="true" />
+
+ <!-- Enable recording for Malta:
+ Relevant laws and/or legal precedents:
+ Article 34 (1)(f) of the Maltese Constitution states that a person may be deprived of his
+ rights in the case of there being suspicion of said person having commited, or being in the
+ process of committing a crime. Effectively this means that recording your own calls is
+ legal when done so to report a crime. There is no mention of the act of recording one's own
+ calls in the Maltese Criminal Code, which means that even if it were to be illegal, it is
+ not a criminal offense. The Media and Defamation Act of 2018 handles all cases of
+ defamation, which may or may not include the act of publishing one's call recordings
+ without the knowledge or consent of the other concerned party.
+
+ Constitution: (nonencrypted link)
+ http://www.justiceservices.gov.mt/DownloadDocument.aspx?app=lom&itemid=8566&l=1
+
+ Criminal Code: (nonencrypted link)
+ https://www.justiceservices.gov.mt/DownloadDocument.aspx?app=lom&itemid=8574&l=1
+
+ Media and Defamation Act: (nonencrypted link)
+ http://justiceservices.gov.mt/DownloadDocument.aspx?app=lp&itemid=29045&l=1
+ -->
+ <country iso="mt" allowed="true" />
+
+ <!-- Enable recording for Netherlands, Bonaire, Sint Eustatius, Saba, Sint Maarten, Curaçao,
+ Aruba:
+ Recording one's own conversations without the consent of the other party or parties is not
+ in itself punishable by law. Sharing recordings made without consent is punishable in the
+ form of a libel case. This in effect means that the government shall not prosecute anyone
+ for the recording of calls. Call recordings may be used as evidence in criminal and civil
+ cases.
+
+ Legal discussion:
+ https://blog.wetrecht.nl/telefoongesprekken-opnemen-als-bewijs-kan-dat
+ -->
+ <country iso="nl,bq,sx,cw,aw" allowed="true" />
+
+ <!-- Enable recording for Norway:
+ As a private citizen, one may record any call that they participate in. There is no
+ requirement to make other parties aware of the recording, but the use of said recording(s),
+ depending on the content, may be subject to various laws, such as data protection (privacy)
+ legislation, libel laws, laws governing trade and national secrets, non-disclosure
+ agreements and so on. It is, however, prohibited to record calls without the permission of
+ the other party or parties, if you are making the call on behalf of a company or
+ organization. All of the above is outlined in Article 205 of the Norwegian Penal Code.
+
+ Penal Code:
+ https://lovdata.no/dokument/NL/lov/2005-05-20-28/KAPITTEL_2-6#§205
+
+ Legal article:
+ https://www.datatilsynet.no/regelverk-og-verktoy/veiledere/lydopptak/
+ -->
+ <country iso="no" allowed="true" />
+
+ <!-- Enable recording for New Zealand:
+ According to the Crimes Act of 1961, Public Act 216B, Articles 1 and 2(a), anyone is liable
+ to imprisonment for a term not exceeding 2 years for intentionally intercepting any private
+ communication, unless he or she is a party to that private communication. Public Act 216C,
+ subsections (1) and (2) define the prohibition on disclosure of unlawfully intercepted
+ private communications. The recording of one's personal conversations and their publishing
+ or use as evidence without the other party's consent is not explicitly forbidden, nor is it
+ defined as a criminal offense.
+
+ Crimes Act of 1961, Part 9A, Crimes against personal privacy: (nonencrypted link)
+ http://www.legislation.govt.nz/act/public/1961/0043/latest/DLM327382.html#DLM329802
+ -->
+ <country iso="nz" allowed="true" />
+
+ <!-- Enable recording for Peru:
+ The Peruvian Constitution states that people own their own voice and images. If said images
+ or recordings are made for archival purposes, it is allowed. While wiretapping is illegal,
+ it has been used as legal evidence in a court of law. As long as one of the persons talking
+ agrees to the recording, said recording can be used in a court of law. There may be
+ exceptions if the communication contains information that may affect third parties, or if
+ it can be considered as information that should be blocked by medical or legal
+ confidentiality.
+
+ Wiretapping:
+ https://diariouno.pe/columna/chuponeo-prueba-prohibida-o-valida/
+
+ Legal article:
+ https://laley.pe/art/2679/una-grabacion-no-consentida-puede-ser-prueba-de-un-delito-
+
+ Legality of voice recordings and images:
+ https://commons.m.wikimedia.org/wiki/Special:MyLanguage/Commons:Country_specific_consent_requirements#Peru
+ -->
+ <country iso="pe" allowed="true" />
+
+ <!-- Enable recording for Poland:
+ Article 267 of the Polish Penal Code defines call recording as legal for private citizens,
+ when the recording is made by a party to the call.
+
+ Penal Code:
+ https://supertrans2014.files.wordpress.com/2014/06/the-criminal-code.pdf
+
+ Legal articles:
+ https://www.alfatronik.com.pl/info/nagrywanie-rozmow-legalne/
+ https://bezprawnik.pl/legalnosc-nagrywania-rozmowy/
+ -->
+ <country iso="pl" allowed="true" />
+
+ <!-- Disable recording for Puerto Rico:
+ Title Thirty-three of the Penal Code of 2004, Subtitle 5, Special Provisions, Part I:
+ Crimes Against the Person, Chapter 301: Crimes Against Civil Rights, Subchapter II: Crimes
+ Against the Right to Privacy, 33 L.P.R.A § 4809 defines the recording of a private personal
+ conversation, without the express authorization of all parties involved in it, as a
+ misdemeanor. This, in effect, means that recording a phone call as one of the two parties
+ is a criminal offense if done so without the explicit notification and consent of the other
+ party. This territory of the United States conforms with its State Laws. For further
+ information, check 'us'.
+
+ 33 L.P.R.A. § 4809. Recording of communications by a participant:
+ https://bit.ly/2UqbrRC
+ -->
+ <country iso="pr" allowed="false" />
+
+ <!-- Disable recording for Portugal:
+ Privacy is a fundamental right in Portuguese law, as it is defined in Articles 26(1) and 34
+ of the Portuguese Constitution. Infringing on said rights constitutes a crime, as defined
+ in Articles 192(1), 194(2) and 199(1) of the Portuguese Penal Code. The punishment is
+ imprisonment for a period of up to one year or a fine equaling 240 days of pay, either of
+ which may be increased by a third, based on Article 197. Lower courts and Higher courts
+ have been ruling both for and against recording one's own calls, no matter the reason, and
+ there have been numerous cases of exceptions being made, despite what the law says. A
+ complaint has been lodged with the European Court of Human Rights, which may lead to a
+ reversal in the current laws and prohibitions. It is noteworthy that in one case, the
+ Supreme Court rendered a decision, which can be translated as such: "The protection of
+ speech that embodies criminal practices or the image that portrays them must yield to the
+ interest of protecting the victim and the efficiency of criminal justice: protection ends
+ when what is protected is a crime."
+
+ Constitution:
+ https://www.parlamento.pt/Legislacao/Paginas/ConstituicaoRepublicaPortuguesa.aspx
+
+ Penal Code: (nonencrypted link)
+ http://www.pgdlisboa.pt/leis/lei_mostra_articulado.php?artigo_id=109A0199&nid=109&tabela=leis&pagina=1&ficha=1&nversao=
+
+ Examples of privacy as a fundamental right: (nonencrypted link)
+ http://www.dgsi.pt/jtrg.nsf/86c25a698e4e7cb7802579ec004d3832/ab509203321d898d802579ea00576d95?OpenDocument
+ http://www.dgsi.pt/jtre.nsf/134973db04f39bf2802579bf005f080b/be3732dc1664576d8025836100514c19
+
+ Examples of exceptions: (nonencrypted links)
+ https://portal.oa.pt/comunicacao/imprensa/2017/11/12/tribunais-aprovam-videos-de-telemovel-apesar-da-legislacao/
+ http://www.dgsi.pt/jtrp.nsf/-/CC3190F093E769FC80257F69004D9E7B
+ http://www.dgsi.pt/jtrl.nsf/0/44ed8c6ca2d940d580256f250052bfd8
+
+ Complaint to ECHR:
+ https://hudoc.echr.coe.int/eng#{%22itemid%22:[%22001-184193%22]}
+
+ Quoted Supreme Court Case: (nonencrypted link)
+ http://www.dgsi.pt/jstj.nsf/954f0ce6ad9dd8b980256b5f003fa814/25cd7aa80cc3adb0802579260032dd4a?OpenDocument
+
+ Legal alternatives: (nonencrypted links)
+ http://www.dgsi.pt/jtrc.nsf/c3fb530030ea1c61802568d9005cd5bb/c5bb36d9a0470bdd80257b400048f9f2?OpenDocument
+ http://www.dgsi.pt/jtrg.nsf/86c25a698e4e7cb7802579ec004d3832/ff947b8a3fda778780257c0000478b5a
+ -->
+ <country iso="pt" allowed="false" />
+
+ <!-- Enable recording for Romania:
+ The Telecommunications Act (506/2004) states that the recording of a conversation by a
+ party to that conversation is permitted and not a criminal offense. Nevertheless, while
+ such recordings are legal, making use of them may fall subject to further civil or criminal
+ laws. Admissibility as evidence depends on how the recording was obtained.
+
+ Telecommunications Act: (nonencrypted link)
+ http://legislatie.just.ro/Public/DetaliiDocument/56973#id_artA88_ttl
+
+ Civil Procedure Code:
+ https://www.dreptonline.ro/legislatie/codul_procedura_civila_consolidat.php
+
+ Criminal Procedure Code:
+ https://www.dreptonline.ro/legislatie/codul_procedura_penala_2007.php
+
+ Legal article:
+ https://www.dsclex.ro/coduri/cciv2.htm
+ -->
+ <country iso="ro" allowed="true" />
+
+ <!-- Enable recording for Serbia:
+ Article 143 of the Serbian Penal Code covers unauthorized wiretapping and recordings. While
+ it is criminally punishable to share call recordings or wiretap them, the law specifically
+ states recording is only punishable when said recording is 'not meant for him/her', hence
+ it is legal to record your own calls, but not to share them with third parties.
+
+ Serbian Penal Code:
+ https://www.paragraf.rs/propisi/krivicni_zakonik.html
+ -->
+ <country iso="rs" allowed="true" />
+
+ <!-- Enable recording for Russia:
+ Recording a phone call when not one of the two parties participating in said call is
+ illegal and punishable by law. As a party to a phone call, one may record it without
+ notifying the other side, as is evident in the decision of the Supreme Court of Russia for
+ case 35-KG16-18, which was rendered on the 6th of December 2016. This concerns civil cases
+ between two private citizens. Whether this covers cases involving legal entities or people
+ holding a public position has not been researched. The key laws to consider are the Federal
+ Law of 27th July 2006, N 149-FZ, (amended on 18th March 2019) "Information, Information
+ Technologies and Information Security" - Article 9, Subarticle 8, as well as the Civil
+ Procedure Code of 2002, N 138-FZ (amended on the 27th December 2018), article 55. Citations
+ of other pertinent laws may be found in the linked decision, starting at internal document
+ page number 4.
+
+ Supreme Court of Russia, Decision on case number 35-KG16-18: (nonencrypted link)
+ http://www.supcourt.ru/stor_pdf.php?id=1502686
+
+ Federal Law of 27th July 2006, N 149-FZ:
+ https://www.consultant.ru/document/cons_doc_LAW_61798/35f4fb38534799919febebd589466c9838f571b2/
+
+ Civil Procedure Code of 2002, N 138-FZ:
+ https://www.consultant.ru/document/cons_doc_LAW_39570/b48406042a309ee368f395fb6f3be1d43c7cbfc2/
+ -->
+ <country iso="ru" allowed="true" />
+
+ <!-- Enable recording for Sweden:
+ According to the Swedish Penal Code (Brottsbalken), Chapter 4, 8–9 §§, it is illegal to
+ make unauthorized recordings of telephone conversations as a third party. A court can grant
+ permission for law enforcement agencies to tap telephone lines. Anyone participating in the
+ telephone call may record the conversation. A recording is always admissible as evidence in
+ a court of law, even when obtained in an illegal way.
+
+ Criminal Code: (nonencrypted link)
+ https://lagen.nu/begrepp/Olovlig_avlyssning
+ http://www.riksdagen.se/sv/dokument-lagar/dokument/svensk-forfattningssamling/brottsbalk-1962700_sfs-1962-700
+ -->
+ <country iso="se" allowed="true" />
+
+ <!-- Enable recording for Singapore:
+ Singaporean law does not recognize privacy as a right that can be infringed upon. A party
+ can not be prosecuted or sued for recording a conversation he or she is a part of. The only
+ exception is when said recording contains confidential information, in which case the party
+ may or may not be liable for their actions, if said party makes use of said confidential
+ information in a way that clearly brings him or her gains of any sort, and/or harms the
+ other party in any perceivable way.
+
+ Legal discussion:
+ https://singaporelegaladvice.com/can-i-record-a-conversation-without-consent/
+
+ Personal Data Protection Act 2012:
+ https://sso.agc.gov.sg/Act/PDPA2012
+ https://www.pdpc.gov.sg/Legislation-and-Guidelines/Personal-Data-Protection-Act-Overview
+
+ Copyright Act, Revised Edition 2006:
+ https://sso.agc.gov.sg/Act/CA1987
+ -->
+ <country iso="sg" allowed="true" />
+
+ <!-- Enable recording for Slovenia:
+ Article 148 of the Slovenian Criminal Code covers the unlawful eavesdropping and sound
+ recording. Subarticle 1 defines a maximum punishment of no more than one year for the
+ unlawful eavesdropping or recording of a private conversation by use of special devices,
+ or directly transmitting said conversation to a third person. This also includes passing on
+ the gist of said conversation. Subarticle 2 states that recording another person's
+ statement with the intent to misuse it, without his or her consent, is punishable in the
+ way postulated in Subarticle 1. Prosecution is initiated by the aggrieved party for
+ Subarticle 1, while under Subarticle 2 it is initiated upon a private action. Based on
+ Article 148, it is legal to record one's own calls, when not done so with the intent to
+ misuse said recordings. Sharing said recordings in any way may be deemed a criminal or
+ civil offense.
+
+ Criminal Code:
+ https://www.wipo.int/edocs/lexdocs/laws/en/si/si046en.pdf
+ -->
+ <country iso="si" allowed="true" />
+
+ <!-- Enable recording for Slovakia:
+ Sections 376 and 377 of the Slovakian Criminal Code cover breach of confidentiality of
+ spoken utterance and other forms of personal expression and the breach of secrecy of all
+ types of instruments, recordings and documents. Section 376 states that the breach of
+ secrecy, by way of disclosing or making available to a third party and/or using it to cause
+ serious harm to another party, leads to an imprisonment of up to two years. Section 377
+ defines breach of confidentiality as the making of an unlawful recording accessible to a
+ third person or using it in any way that would hinder the other side's rights. This is
+ punishable with a term of imprisonment of up to two years.
+
+ Slovakian Criminal Code:
+ https://www.legislationline.org/documents/section/criminal-codes/country/4/Slovakia/show
+ -->
+ <country iso="sk" allowed="true" />
+
+ <!-- Enable recording for Turkey:
+ Article 132 of Law 5237, the Turkish Penal Code, sets the punishment for violating the
+ secrecy of communication as six months to two years of imprisonment. If the violation of
+ secrecy is done in the form of a recording, then the punishment is imprisonment of one to
+ three years. The act of unlawfully publishing the contents of a communication is
+ imprisonment of one to three years. Openly disclosing the content of a communication
+ between oneself and others, without the other party or parties' consent, is imprisonment of
+ six months to two years. If disclosure is done by way of the press or broadcast, the
+ punishment is increased by one half. One may listen and record conversations of other
+ parties, with the consent of at least one party. Doing so without consent is punishable
+ with imprisonment of two to six months. Article 135 of the Turkish Penal Code defines
+ punishment in the case of recording personal data and information. It stands to reason that
+ one may record one's own calls without the consent of the other party, but the act of
+ sharing those recordings in any way may result in persecution, either in the form of a
+ criminal or civil case against the party that has recorded his or hew own calls.
+
+ Turkish Penal Code:
+ https://www.wipo.int/edocs/lexdocs/laws/en/tr/tr171en.pdf
+ -->
+ <country iso="tr" allowed="true" />
+
+ <!-- Enable recording for Ukraine:
+ Sharing a call recording without consent is a punishable offense, and can not be used as
+ valid proof in a court of law. Call recordings may be handed over to the authorities, by
+ one of the two parties without the other party's consent, when a crime is mentioned in the
+ recording. In such cases the party that handed over the recording is not liable to fines or
+ punishment, as the authorities will use the recording to initiate an investigation, but not
+ as proof of a crime. The above information is covered in Articles 31 and 32 of the
+ Constitution, as well as Articles 163, 182 and 359 of the Criminal Code.
+
+ Constitution of Ukraine:
+ https://zakon.rada.gov.ua/cgi-bin/laws/main.cgi?nreg=254%EA%2F96%2D%E2%F0
+
+ Ukrainian Criminal Code:
+ https://www.legislationline.org/documents/section/criminal-codes/country/52
+
+ Radio Svoboda legal advice page:
+ https://www.radiosvoboda.org/a/details/28905674.html
+
+ Legal discussion:
+ https://sklaw.com.ua/ua/news/345_pro_naslidki_zapisu_telefonnoi_rozmovi_rozpovila_advokat_ao_spenser
+ -->
+ <country iso="ua" allowed="true" />
+
+ <!-- Disable recording for United States of America, Guam, the Northern Mariana Islands, the
+ United States Virgin Islands, Puerto Rico:
+ Currently federal laws state that call recording is legal. On the other hand, each state
+ has its own laws which take priority. Most states allow call recording when one sides
+ agrees, but over 10 require both sides to agree. Since there are no ISO country codes per
+ state, there is no way to differentiate whether the state you are currently in allows call
+ recording or not. Due to this, call recording is set as disabled for the United States of
+ America and some of its territories. These include Guam, the Northern Mariana Islands, the
+ United States Virgin Islands and Puerto Rico. Until a method for properly differentiating
+ between states is created, or a law or precedent emerges which would allow call recording
+ to be legal in all of the USA and its territories, all aforementioned countries and
+ territories should be set as false. Even if such a method is to be found, there is still
+ the question of Native American Reservations, territories that have a cumulative size of
+ over 200 000 square kilometers and might enforce their own set of laws for call recording.
+
+ Two-party consent state laws:
+ https://recordinglaw.com/party-two-party-consent-states/
+ -->
+ <country iso="us,vi,gu" allowed="false" />
+
+ <!-- Enable recording for Kosovo:
+ Article 36 of the Constitution of Kosovo defines the Right to Privacy. Article 202 of the
+ Criminal Code of Kosovo covers the infringing of privacy in corresepondence and computer
+ databases, which can be best explained as the act of violating and/or sharing a private
+ document with another person. Article 203 covers the unauthorized disclosure of
+ confidential information, when the person disclosing said information is under legal duty
+ to maintain it as confidential. Said person is not liable when the disclosure of the
+ confidential information is done so in the interest of the public. Paragraph 4 describes
+ public interest as the welfare of the general public outweighing the individual interest.
+ It is also permissible to use this as a defense when the disclosed information involves
+ plans, preparation or the commission of crimes against the constitutional order or
+ territorial integrity of the Republic of Kosovo or other criminal offenses that will cause
+ great bodily injury or death to another person. Article 204 covers the unauthorized
+ interception of a conversation or statement. Article 205 covers the unauthorized
+ photographing or video recording of a person 'in his or her personal premises or in any
+ other place where a person has a reasonable expectation of privacy', with paragraph 4
+ offering an exception for liability when the act is done to discover a criminal offence or
+ the perpetrators of a criminal offence, or to present as evidence to the police,
+ prosecution or court, and if the photos or recordings are submitted to these authorities.
+ It is beyond doubt that none of these articles deal with recording personal conversation
+ between two parties, hence the act of doing so is not explicitly punishable and is not a
+ criminal offense that would warrant a criminal case.
+
+ Constitution of Kosovo:
+ https://kuvendikosoves.org/?cid=2,1058
+ Criminal Code of Kosovo:
+ https://assembly-kosova.org/common/docs/ligjet/Criminal%20Code.pdf
+ -->
+ <country iso="xk" allowed="true" />
+
+ <!-- Enable recording for South Africa:
+ Under the Regulation of Interception of Communications and Provision of
+ Communication-related Information Act of 2003, 4(1)(a)(b) as well as 16(5)(a)(b), it is
+ legal for a party of a conversation to record said conversation, when there are reasonable
+ grounds to believe that said act will prevent a crime, prevent bodily harm, is in the
+ interest of public safety or one of the other reasons stated in the previously noted
+ paragraphs.
+
+ Regulation of Interception of Communications Act of 2003:
+ https://www.gov.za/sites/default/files/gcis_document/201409/a70-02.pdf
+ -->
+ <country iso="za" allowed="true" />
+
+</call-record-allowed-flags>
diff --git a/java/com/android/dialer/callstats/AndroidManifest.xml b/java/com/android/dialer/callstats/AndroidManifest.xml
new file mode 100644
index 0000000..7f76f78
--- /dev/null
+++ b/java/com/android/dialer/callstats/AndroidManifest.xml
@@ -0,0 +1,32 @@
+<!-- Copyright (C) 2018 The LineageOS 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer">
+
+ <application>
+
+ <activity android:name=".callstats.CallStatsDetailActivity"
+ android:label="@string/call_stats_detail_title"
+ android:theme="@style/Dialer.ThemeBase.NoActionBar"
+ android:exported="true"
+ android:screenOrientation="portrait">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/java/com/android/dialer/callstats/CallStatsAdapter.java b/java/com/android/dialer/callstats/CallStatsAdapter.java
new file mode 100644
index 0000000..1d673fc
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsAdapter.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.dialer.callstats;
+
+import android.content.Context;
+import android.content.Intent;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.ContextMenu;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.dialer.R;
+import com.android.dialer.app.DialtactsActivity;
+import com.android.dialer.app.contactinfo.ContactInfoCache;
+import com.android.dialer.app.contactinfo.NumberWithCountryIso;
+import com.android.dialer.clipboard.ClipboardUtils;
+import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences;
+import com.android.dialer.location.GeoUtil;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.ExpirableCache;
+import com.android.dialer.util.PermissionsUtil;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Adapter class to hold and handle call stat entries
+ */
+class CallStatsAdapter extends RecyclerView.Adapter {
+ private final Context mContext;
+ private final ContactInfoHelper mContactInfoHelper;
+ private final ContactInfoCache mContactInfoCache;
+ private final ContactDisplayPreferences mContactDisplayPreferences;
+
+ private ArrayList<CallStatsDetails> mAllItems;
+ private ArrayList<CallStatsDetails> mShownItems;
+ private CallStatsDetails mTotalItem;
+ private Map<CallStatsDetails, ContactInfo> mInfoLookup;
+
+ private int mType = -1;
+ private long mFilterFrom;
+ private long mFilterTo;
+ private boolean mSortByDuration;
+
+ /**
+ * Listener that is triggered to populate the context menu with actions to perform on the call's
+ * number, when the call log entry is long pressed.
+ */
+ private final View.OnCreateContextMenuListener mContextMenuListener = (menu, v, menuInfo) -> {
+ final CallStatsListItemViewHolder vh = (CallStatsListItemViewHolder) v.getTag();
+ if (TextUtils.isEmpty(vh.details.number)) {
+ return;
+ }
+
+ menu.setHeaderTitle(vh.details.number);
+
+ final MenuItem copyItem = menu.add(ContextMenu.NONE, R.id.context_menu_copy_to_clipboard,
+ ContextMenu.NONE, R.string.action_copy_number_text);
+
+ copyItem.setOnMenuItemClickListener(item -> {
+ ClipboardUtils.copyText(CallStatsAdapter.this.mContext, null, vh.details.number, true);
+ return true;
+ });
+
+ // The edit number before call does not show up if any of the conditions apply:
+ // 1) Number cannot be called
+ // 2) Number is the voicemail number
+ // 3) Number is a SIP address
+
+ boolean canPlaceCallsTo = PhoneNumberHelper.canPlaceCallsTo(vh.details.number,
+ vh.details.numberPresentation);
+ if (!canPlaceCallsTo || PhoneNumberHelper.isSipNumber(vh.details.number)) {
+ return;
+ }
+
+ final MenuItem editItem = menu.add(ContextMenu.NONE, R.id.context_menu_edit_before_call,
+ ContextMenu.NONE, R.string.action_edit_number_before_call);
+
+ editItem.setOnMenuItemClickListener(item -> {
+ final Intent intent = new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(vh.details.number));
+ DialerUtils.startActivityWithErrorToast(v.getContext(), intent);
+ return true;
+ });
+ };
+
+ private final Comparator<CallStatsDetails> mDurationComparator = (o1, o2) -> {
+ Long duration1 = o1.getRequestedDuration(mType);
+ Long duration2 = o2.getRequestedDuration(mType);
+ // sort descending
+ return duration2.compareTo(duration1);
+ };
+ private final Comparator<CallStatsDetails> mCountComparator = (o1, o2) -> {
+ Integer count1 = o1.getRequestedCount(mType);
+ Integer count2 = o2.getRequestedCount(mType);
+ // sort descending
+ return count2.compareTo(count1);
+ };
+
+ CallStatsAdapter(Context context, ContactDisplayPreferences prefs,
+ ExpirableCache<NumberWithCountryIso,ContactInfo> cache) {
+ mContext = context;
+ mContactDisplayPreferences = prefs;
+
+ final String currentCountryIso = GeoUtil.getCurrentCountryIso(mContext);
+ mContactInfoHelper = new ContactInfoHelper(mContext, currentCountryIso);
+
+ mAllItems = new ArrayList<CallStatsDetails>();
+ mShownItems = new ArrayList<CallStatsDetails>();
+ mTotalItem = new CallStatsDetails(null, 0, null, null, null, null, null, 0);
+ mInfoLookup = new ConcurrentHashMap<>();
+
+ mContactInfoCache = new ContactInfoCache(cache,
+ mContactInfoHelper, () -> notifyDataSetChanged());
+ if (!PermissionsUtil.hasContactsReadPermissions(context)) {
+ mContactInfoCache.disableRequestProcessing();
+ }
+ }
+
+ public void updateData(Map<ContactInfo, CallStatsDetails> calls, long from, long to) {
+ mInfoLookup.clear();
+ mFilterFrom = from;
+ mFilterTo = to;
+
+ mAllItems.clear();
+ mTotalItem.reset();
+
+ for (Map.Entry<ContactInfo, CallStatsDetails> entry : calls.entrySet()) {
+ final CallStatsDetails call = entry.getValue();
+ mAllItems.add(call);
+ mTotalItem.mergeWith(call);
+ mInfoLookup.put(call, entry.getKey());
+ }
+ }
+
+ public void updateDisplayedData(int type, boolean sortByDuration) {
+ mType = type;
+ mSortByDuration = sortByDuration;
+
+ mShownItems.clear();
+
+ for (CallStatsDetails call : mAllItems) {
+ if (sortByDuration && call.getRequestedDuration(type) > 0) {
+ mShownItems.add(call);
+ } else if (!sortByDuration && call.getRequestedCount(type) > 0) {
+ mShownItems.add(call);
+ }
+ }
+
+ Collections.sort(mShownItems, sortByDuration ? mDurationComparator : mCountComparator);
+ notifyDataSetChanged();
+ }
+
+ public void invalidateCache() {
+ mContactInfoCache.invalidate();
+ }
+
+ public void startCache() {
+ if (PermissionsUtil.hasPermission(mContext, android.Manifest.permission.READ_CONTACTS)) {
+ mContactInfoCache.start();
+ }
+ }
+
+ public void pauseCache() {
+ mContactInfoCache.stop();
+ }
+
+ @Override
+ public int getItemCount() {
+ return mShownItems.size();
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ LayoutInflater inflater = LayoutInflater.from(mContext);
+ View view = inflater.inflate(R.layout.call_stats_list_item, parent, false);
+ CallStatsListItemViewHolder viewHolder = CallStatsListItemViewHolder.create(view,
+ mContactInfoHelper);
+
+ viewHolder.mPrimaryActionView.setOnCreateContextMenuListener(mContextMenuListener);
+ viewHolder.mPrimaryActionView.setTag(viewHolder);
+
+ return viewHolder;
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int position) {
+ CallStatsDetails details = mShownItems.get(position);
+ CallStatsDetails first = mShownItems.get(0);
+ CallStatsListItemViewHolder views = (CallStatsListItemViewHolder) viewHolder;
+
+ if (PhoneNumberHelper.canPlaceCallsTo(details.number, details.numberPresentation)
+ && !details.isVoicemailNumber) {
+ ContactInfo info = mContactInfoCache.getValue(details.number + details.postDialDigits,
+ details.countryIso, mInfoLookup.get(details), false);
+ if (info != null) {
+ details.updateFromInfo(info);
+ }
+ }
+ views.setDetails(details, first, mTotalItem, mType,
+ mSortByDuration, mContactDisplayPreferences.getDisplayOrder());
+ views.clickIntent = getItemClickIntent(details);
+ }
+
+ private Intent getItemClickIntent(CallStatsDetails details) {
+ Intent intent = new Intent(mContext, CallStatsDetailActivity.class);
+ intent.putExtra(CallStatsDetailActivity.EXTRA_DETAILS, details);
+ intent.putExtra(CallStatsDetailActivity.EXTRA_TOTAL, mTotalItem);
+ intent.putExtra(CallStatsDetailActivity.EXTRA_FROM, mFilterFrom);
+ intent.putExtra(CallStatsDetailActivity.EXTRA_TO, mFilterTo);
+ return intent;
+ }
+
+ public String getTotalCallCountString() {
+ return CallStatsListItemViewHolder.getCallCountString(
+ mContext, mTotalItem.getRequestedCount(mType));
+ }
+
+ public String getFullDurationString(boolean withSeconds) {
+ final long duration = mTotalItem.getRequestedDuration(mType);
+ return CallStatsListItemViewHolder.getDurationString(
+ mContext, duration, withSeconds);
+ }
+}
diff --git a/java/com/android/dialer/callstats/CallStatsDetailActivity.java b/java/com/android/dialer/callstats/CallStatsDetailActivity.java
new file mode 100644
index 0000000..d25e24b
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsDetailActivity.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.dialer.callstats;
+
+import android.app.DialogFragment;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.view.View;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+import com.android.dialer.app.AccountSelectionActivity;
+import com.android.dialer.callintent.CallInitiationType;
+import com.android.dialer.callintent.CallIntentBuilder;
+import com.android.dialer.calllogutils.CallTypeIconsView;
+import com.android.dialer.clipboard.ClipboardUtils;
+import com.android.dialer.contactphoto.ContactPhotoManager;
+import com.android.dialer.contacts.ContactsComponent;
+import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences;
+import com.android.dialer.lettertile.LetterTileDrawable;
+import com.android.dialer.location.GeoUtil;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.CallUtil;
+import com.android.dialer.widget.LinearColorBar;
+
+/**
+ * Activity to display detailed information about a callstat item
+ */
+public class CallStatsDetailActivity extends AppCompatActivity implements
+ View.OnClickListener, View.OnLongClickListener {
+ private static final String TAG = "CallStatsDetailActivity";
+
+ public static final String EXTRA_DETAILS = "details";
+ public static final String EXTRA_TOTAL = "total";
+ public static final String EXTRA_FROM = "from";
+ public static final String EXTRA_TO = "to";
+
+ private ContactInfoHelper mContactInfoHelper;
+ private ContactDisplayPreferences mContactDisplayPreferences;
+ private Resources mResources;
+
+ private QuickContactBadge mQuickContactBadge;
+ private TextView mCallerName;
+ private TextView mCallerNumber;
+ private View mCallButton;
+ private View mSeparator;
+ private View mCopyButton;
+ private View mEditNumberButton;
+
+ private TextView mTotalDuration, mTotalCount;
+ private TextView mTotalTotalDuration, mTotalTotalCount;
+
+ private DetailLine mInDuration, mOutDuration;
+ private DetailLine mInCount, mOutCount;
+ private DetailLine mMissedCount, mBlockedCount;
+ private DetailLine mInAverage, mOutAverage;
+
+ private LinearColorBar mDurationBar, mCountBar;
+ private LinearColorBar mTotalDurationBar, mTotalCountBar;
+
+ private CallStatsDetails mData;
+ private CallStatsDetails mTotalData;
+ private String mNumber = null;
+
+ private class UpdateContactTask extends AsyncTask<String, Void, ContactInfo> {
+ @Override
+ protected ContactInfo doInBackground(String... strings) {
+ return mContactInfoHelper.lookupNumber(strings[0], strings[1]);
+ }
+
+ @Override
+ protected void onPostExecute(ContactInfo info) {
+ if (info != null) {
+ mData.updateFromInfo(info);
+ updateData();
+ }
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ setContentView(R.layout.call_stats_detail);
+
+ Toolbar toolbar = findViewById(R.id.toolbar);
+ toolbar.setNavigationOnClickListener(v -> finish());
+ toolbar.setTitle(R.string.call_stats_detail_title);
+
+ mResources = getResources();
+ mContactInfoHelper = new ContactInfoHelper(this, GeoUtil.getCurrentCountryIso(this));
+ mContactDisplayPreferences = ContactsComponent.get(this).contactDisplayPreferences();
+
+ mQuickContactBadge = (QuickContactBadge) findViewById(R.id.quick_contact_photo);
+ mQuickContactBadge.setOverlay(null);
+ mQuickContactBadge.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+ mCallerName = (TextView) findViewById(R.id.caller_name);
+ mCallerNumber = (TextView) findViewById(R.id.caller_number);
+
+ mCallButton = findViewById(R.id.call_back_button);
+ mCallButton.setOnClickListener(this);
+ mCallButton.setOnLongClickListener(this);
+
+ mSeparator = findViewById(R.id.separator);
+ mEditNumberButton = findViewById(R.id.call_detail_action_edit_before_call);
+ mEditNumberButton.setOnClickListener(this);
+ mCopyButton = findViewById(R.id.call_detail_action_copy);
+ mCopyButton.setOnClickListener(this);
+
+ mDurationBar = (LinearColorBar) findViewById(R.id.duration_number_percent_bar);
+ mTotalDurationBar = (LinearColorBar) findViewById(R.id.duration_total_percent_bar);
+ mTotalDuration = (TextView) findViewById(R.id.total_duration_number);
+ mTotalTotalDuration = (TextView) findViewById(R.id.total_duration_total);
+ mInDuration = new DetailLine(R.id.in_duration,
+ R.string.call_stats_incoming, Calls.INCOMING_TYPE);
+ mOutDuration = new DetailLine(R.id.out_duration,
+ R.string.call_stats_outgoing, Calls.OUTGOING_TYPE);
+
+ mCountBar = (LinearColorBar) findViewById(R.id.count_number_percent_bar);
+ mTotalCountBar = (LinearColorBar) findViewById(R.id.count_total_percent_bar);
+ mTotalCount = (TextView) findViewById(R.id.total_count_number);
+ mTotalTotalCount = (TextView) findViewById(R.id.total_count_total);
+ mInCount = new DetailLine(R.id.in_count, R.string.call_stats_incoming, Calls.INCOMING_TYPE);
+ mOutCount = new DetailLine(R.id.out_count, R.string.call_stats_outgoing, Calls.OUTGOING_TYPE);
+ mMissedCount = new DetailLine(R.id.missed_count,
+ R.string.call_stats_missed, Calls.MISSED_TYPE);
+ mBlockedCount = new DetailLine(R.id.blocked_count,
+ R.string.call_stats_blocked, Calls.BLOCKED_TYPE);
+
+ mInAverage = new DetailLine(R.id.in_average,
+ R.string.call_stats_incoming, Calls.INCOMING_TYPE);
+ mOutAverage = new DetailLine(R.id.out_average,
+ R.string.call_stats_outgoing, Calls.OUTGOING_TYPE);
+
+ Intent launchIntent = getIntent();
+ mData = (CallStatsDetails) launchIntent.getParcelableExtra(EXTRA_DETAILS);
+ mTotalData = (CallStatsDetails) launchIntent.getParcelableExtra(EXTRA_TOTAL);
+ updateData();
+
+ TextView dateFilterView = (TextView) findViewById(R.id.date_filter);
+ long filterFrom = launchIntent.getLongExtra(EXTRA_FROM, -1);
+ if (filterFrom == -1) {
+ dateFilterView.setVisibility(View.GONE);
+ } else {
+ long filterTo = launchIntent.getLongExtra(EXTRA_TO, -1);
+ dateFilterView.setText(DateUtils.formatDateRange(this, filterFrom, filterTo, 0));
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ new UpdateContactTask().execute(mData.number.toString(), mData.countryIso);
+ }
+
+ private void updateData() {
+ mNumber = mData.number.toString();
+
+ // Cache the details about the phone number.
+ boolean canPlaceCallsTo = PhoneNumberHelper.canPlaceCallsTo(mNumber, mData.numberPresentation);
+ final CharSequence callLocationOrType = !TextUtils.isEmpty(mData.displayName)
+ ? Phone.getTypeLabel(mResources, mData.numberType, mData.numberLabel)
+ : mData.geocode;
+
+ mData.updateDisplayProperties(this, mContactDisplayPreferences.getDisplayOrder());
+
+ final boolean isSipNumber = PhoneNumberHelper.isSipNumber(mNumber);
+ boolean hasEditNumberBeforeCallOption =
+ canPlaceCallsTo && !isSipNumber && !mData.isVoicemailNumber;
+
+ if (!TextUtils.isEmpty(mData.displayName)) {
+ mCallerName.setText(mData.displayName);
+ mCallerNumber.setText(callLocationOrType + " " + mData.displayNumber);
+ } else {
+ mCallerName.setText(mData.displayNumber);
+ if (!TextUtils.isEmpty(callLocationOrType)) {
+ mCallerNumber.setText(callLocationOrType);
+ mCallerNumber.setVisibility(View.VISIBLE);
+ } else {
+ mCallerNumber.setVisibility(View.GONE);
+ }
+ }
+
+ mCallButton.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
+ mCopyButton.setVisibility(canPlaceCallsTo ? View.VISIBLE : View.GONE);
+ mEditNumberButton.setVisibility(hasEditNumberBeforeCallOption ? View.VISIBLE : View.GONE);
+ mSeparator.setVisibility(canPlaceCallsTo || hasEditNumberBeforeCallOption
+ ? View.VISIBLE : View.GONE);
+
+ final boolean isBusiness = mContactInfoHelper.isBusiness(mData.sourceType);
+ final int contactType =
+ mData.isVoicemailNumber ? LetterTileDrawable.TYPE_VOICEMAIL :
+ isBusiness ? LetterTileDrawable.TYPE_BUSINESS :
+ LetterTileDrawable.TYPE_DEFAULT;
+ final String nameForDefaultImage = TextUtils.isEmpty(mData.name)
+ ? mData.displayNumber : mData.name;
+
+ ContactPhotoManager.getInstance(this).loadDialerThumbnailOrPhoto(mQuickContactBadge,
+ mData.contactUri, mData.photoId, mData.photoUri, nameForDefaultImage, contactType);
+
+ invalidateOptionsMenu();
+
+ long totalDuration = mData.getFullDuration();
+ mInDuration.updateFromDurations(mData.inDuration, totalDuration);
+ mOutDuration.updateFromDurations(mData.outDuration, totalDuration);
+ if (totalDuration != 0) {
+ mTotalDuration.setText(CallStatsListItemViewHolder.getDurationString(this,
+ totalDuration, true));
+ mTotalTotalDuration.setText(CallStatsListItemViewHolder.getDurationString(this,
+ mTotalData.getFullDuration(), true));
+ updateBar(mDurationBar, mData.inDuration, mData.outDuration, 0, 0, totalDuration);
+ updateBar(mTotalDurationBar, mData.inDuration, mData.outDuration,
+ 0, 0, mTotalData.getFullDuration());
+ findViewById(R.id.duration_container).setVisibility(View.VISIBLE);
+ } else {
+ findViewById(R.id.duration_container).setVisibility(View.GONE);
+ }
+
+ mInAverage.updateAsAverage(mData.inDuration, mData.incomingCount);
+ mOutAverage.updateAsAverage(mData.outDuration, mData.outgoingCount);
+
+ int totalCount = mData.getTotalCount();
+ mTotalCount.setText(CallStatsListItemViewHolder.getCallCountString(this, totalCount));
+ mTotalTotalCount.setText(
+ CallStatsListItemViewHolder.getCallCountString(this, mTotalData.getTotalCount()));
+ mInCount.updateFromCounts(mData.incomingCount, totalCount);
+ mOutCount.updateFromCounts(mData.outgoingCount, totalCount);
+ mMissedCount.updateFromCounts(mData.missedCount, totalCount);
+ mBlockedCount.updateFromCounts(mData.blockedCount, totalCount);
+ updateBar(mCountBar, mData.incomingCount, mData.outgoingCount,
+ mData.missedCount, mData.blockedCount, totalCount);
+ updateBar(mTotalCountBar, mData.incomingCount, mData.outgoingCount,
+ mData.missedCount, mData.blockedCount, mTotalData.getTotalCount());
+ }
+
+ private void updateBar(LinearColorBar bar,
+ long value1, long value2, long value3, long value4, long total) {
+ bar.setRatios((float) value1 / total, (float) value2 / total,
+ (float) value3 / total, (float) value4 / total);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == mCallButton) {
+ Intent intent = new CallIntentBuilder(mNumber, CallInitiationType.Type.CALL_LOG).build();
+ startActivity(intent);
+ } else if (view == mCopyButton) {
+ ClipboardUtils.copyText(this, null, mNumber, true);
+ } else if (view == mEditNumberButton) {
+ startActivity(new Intent(Intent.ACTION_DIAL, CallUtil.getCallUri(mNumber)));
+ }
+ }
+
+ @Override
+ public boolean onLongClick(View view) {
+ if (view == mCallButton) {
+ Intent intent = AccountSelectionActivity.createIntent(
+ CallStatsDetailActivity.this, mNumber, CallInitiationType.Type.CALL_LOG);
+ if (intent != null) {
+ startActivity(intent);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void onHomeSelected() {
+ Intent intent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI);
+ // This will open the call log even if the detail view has been opened directly.
+ intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ startActivity(intent);
+ finish();
+ }
+
+ private class DetailLine {
+ private int mValueTemplateResId;
+ private View mRootView;
+ private TextView mTextView;
+ private TextView mPercentView;
+
+ public DetailLine(int rootViewId, int valueTemplateResId, int iconType) {
+ mValueTemplateResId = valueTemplateResId;
+ mRootView = findViewById(rootViewId);
+ mTextView = (TextView) mRootView.findViewById(R.id.value);
+ mPercentView = (TextView) mRootView.findViewById(R.id.percent);
+
+ CallTypeIconsView icon = (CallTypeIconsView) mRootView.findViewById(R.id.icon);
+ icon.add(iconType);
+ }
+
+ public void updateFromCounts(int count, int totalCount) {
+ if (count == 0 && totalCount > 0) {
+ mRootView.setVisibility(View.GONE);
+ return;
+ }
+
+ mRootView.setVisibility(View.VISIBLE);
+ String value = CallStatsListItemViewHolder.getCallCountString(mTextView.getContext(), count);
+ mTextView.setText(getString(mValueTemplateResId, value));
+ updatePercent(count, totalCount);
+ }
+
+ public void updateFromDurations(long duration, long totalDuration) {
+ if (duration == 0 && totalDuration >= 0) {
+ mRootView.setVisibility(View.GONE);
+ return;
+ }
+
+ mRootView.setVisibility(View.VISIBLE);
+ String value = CallStatsListItemViewHolder.getDurationString(
+ mTextView.getContext(), duration, true);
+ mTextView.setText(getString(mValueTemplateResId, value));
+ updatePercent(duration, totalDuration);
+ }
+
+ public void updateAsAverage(long duration, int count) {
+ if (count == 0) {
+ mRootView.setVisibility(View.GONE);
+ return;
+ }
+
+ mRootView.setVisibility(View.VISIBLE);
+ mPercentView.setVisibility(View.GONE);
+
+ long averageDuration = (long) Math.round((float) duration / (float) count);
+ String value = CallStatsListItemViewHolder.getDurationString(
+ mTextView.getContext(), averageDuration, true);
+ mTextView.setText(getString(mValueTemplateResId, value));
+ }
+
+ private void updatePercent(long value, long total) {
+ int percent = (int) Math.round(100F * value / total);
+ mPercentView.setText(getString(R.string.call_stats_percent, percent));
+ }
+ }
+}
diff --git a/java/com/android/dialer/callstats/CallStatsDetails.java b/java/com/android/dialer/callstats/CallStatsDetails.java
new file mode 100644
index 0000000..cfa6dfa
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsDetails.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.dialer.callstats;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.CallLog.Calls;
+import android.telecom.PhoneAccountHandle;
+import android.text.BidiFormatter;
+import android.text.TextDirectionHeuristics;
+import android.text.TextUtils;
+
+import com.android.dialer.calllogutils.PhoneNumberDisplayUtil;
+import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences.DisplayOrder;
+import com.android.dialer.logging.ContactSource;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+
+/**
+ * Class to store statistical details for a given contact/number.
+ */
+public class CallStatsDetails implements Parcelable {
+ public final String number;
+ public final String postDialDigits;
+ public final int numberPresentation;
+ public String formattedNumber;
+ public final String countryIso;
+ public final String geocode;
+ public final long date;
+ public String name;
+ public String nameAlternative;
+ public int numberType;
+ public String numberLabel;
+ public Uri contactUri;
+ public Uri photoUri;
+ public long photoId;
+ public long inDuration;
+ public long outDuration;
+ public int incomingCount;
+ public int outgoingCount;
+ public int missedCount;
+ public int blockedCount;
+ public PhoneAccountHandle accountHandle;
+ public ContactSource.Type sourceType = ContactSource.Type.UNKNOWN_SOURCE_TYPE;
+
+ public boolean isVoicemailNumber;
+ public String displayNumber;
+ public String displayName;
+
+ public CallStatsDetails(CharSequence number, int numberPresentation,
+ String postDialDigits, PhoneAccountHandle accountHandle,
+ ContactInfo info, String countryIso, String geocode, long date) {
+ this.number = number != null ? number.toString() : null;
+ this.numberPresentation = numberPresentation;
+ this.postDialDigits = postDialDigits;
+ this.countryIso = countryIso;
+ this.geocode = geocode;
+ this.date = date;
+
+ reset();
+
+ if (info != null) {
+ updateFromInfo(info);
+ }
+ }
+
+ public void updateFromInfo(ContactInfo info) {
+ this.displayName = info.name;
+ this.nameAlternative = info.nameAlternative;
+ this.name = info.name;
+ this.numberType = info.type;
+ this.numberLabel = info.label;
+ this.photoId = info.photoId;
+ this.photoUri = info.photoUri;
+ this.formattedNumber = info.formattedNumber;
+ this.contactUri = info.lookupUri;
+ this.photoUri = info.photoUri;
+ this.photoId = info.photoId;
+ this.sourceType = info.sourceType;
+ this.displayNumber = null;
+ }
+
+
+ public void updateDisplayProperties(Context context, DisplayOrder nameDisplayOrder) {
+ if (nameDisplayOrder == DisplayOrder.PRIMARY || TextUtils.isEmpty(nameAlternative)) {
+ this.displayName = this.name;
+ } else {
+ this.displayName = this.nameAlternative;
+ }
+
+ if (displayNumber == null) {
+ isVoicemailNumber = PhoneNumberHelper.isVoicemailNumber(context, accountHandle, number);
+ final CharSequence displayNumber = PhoneNumberDisplayUtil.getDisplayNumber(context,
+ number, numberPresentation, formattedNumber, postDialDigits, isVoicemailNumber);
+ this.displayNumber = BidiFormatter.getInstance().unicodeWrap(
+ displayNumber.toString(), TextDirectionHeuristics.LTR);
+ }
+ }
+
+ public long getFullDuration() {
+ return inDuration + outDuration;
+ }
+
+ public int getTotalCount() {
+ return incomingCount + outgoingCount + missedCount + blockedCount;
+ }
+
+ public void addTimeOrMissed(int type, long time) {
+ switch (type) {
+ case Calls.INCOMING_TYPE:
+ incomingCount++;
+ inDuration += time;
+ break;
+ case Calls.OUTGOING_TYPE:
+ outgoingCount++;
+ outDuration += time;
+ break;
+ case Calls.MISSED_TYPE:
+ missedCount++;
+ break;
+ case Calls.BLOCKED_TYPE:
+ blockedCount++;
+ break;
+ }
+ }
+
+ public int getDurationPercentage(int type) {
+ long duration = getRequestedDuration(type);
+ return Math.round((float) duration * 100F / getFullDuration());
+ }
+
+ public int getCountPercentage(int type) {
+ int count = getRequestedCount(type);
+ return Math.round((float) count * 100F / getTotalCount());
+ }
+
+ public long getRequestedDuration(int type) {
+ switch (type) {
+ case Calls.INCOMING_TYPE:
+ return inDuration;
+ case Calls.OUTGOING_TYPE:
+ return outDuration;
+ case Calls.MISSED_TYPE:
+ return (long) missedCount;
+ case Calls.BLOCKED_TYPE:
+ return (long) blockedCount;
+ default:
+ return getFullDuration();
+ }
+ }
+
+ public int getRequestedCount(int type) {
+ switch (type) {
+ case Calls.INCOMING_TYPE:
+ return incomingCount;
+ case Calls.OUTGOING_TYPE:
+ return outgoingCount;
+ case Calls.MISSED_TYPE:
+ return missedCount;
+ case Calls.BLOCKED_TYPE:
+ return blockedCount;
+ default:
+ return getTotalCount();
+ }
+ }
+
+ public void mergeWith(CallStatsDetails other) {
+ this.inDuration += other.inDuration;
+ this.outDuration += other.outDuration;
+ this.incomingCount += other.incomingCount;
+ this.outgoingCount += other.outgoingCount;
+ this.missedCount += other.missedCount;
+ this.blockedCount += other.blockedCount;
+ }
+
+ public void reset() {
+ this.inDuration = this.outDuration = 0;
+ this.incomingCount = this.outgoingCount = this.missedCount = this.blockedCount = 0;
+ }
+
+ /* Parcelable interface */
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(number);
+ out.writeInt(numberPresentation);
+ out.writeString(postDialDigits);
+ out.writeString(formattedNumber);
+ out.writeString(countryIso);
+ out.writeString(geocode);
+ out.writeLong(date);
+ out.writeString(name);
+ out.writeInt(numberType);
+ out.writeString(numberLabel);
+ out.writeParcelable(contactUri, flags);
+ out.writeParcelable(photoUri, flags);
+ out.writeLong(photoId);
+ out.writeLong(inDuration);
+ out.writeLong(outDuration);
+ out.writeInt(incomingCount);
+ out.writeInt(outgoingCount);
+ out.writeInt(missedCount);
+ out.writeInt(blockedCount);
+ }
+
+ public static final Parcelable.Creator<CallStatsDetails> CREATOR =
+ new Parcelable.Creator<CallStatsDetails>() {
+ public CallStatsDetails createFromParcel(Parcel in) {
+ return new CallStatsDetails(in);
+ }
+
+ public CallStatsDetails[] newArray(int size) {
+ return new CallStatsDetails[size];
+ }
+ };
+
+ private CallStatsDetails (Parcel in) {
+ number = in.readString();
+ numberPresentation = in.readInt();
+ postDialDigits = in.readString();
+ formattedNumber = in.readString();
+ countryIso = in.readString();
+ geocode = in.readString();
+ date = in.readLong();
+ name = in.readString();
+ numberType = in.readInt();
+ numberLabel = in.readString();
+ contactUri = in.readParcelable(null);
+ photoUri = in.readParcelable(null);
+ photoId = in.readLong();
+ inDuration = in.readLong();
+ outDuration = in.readLong();
+ incomingCount = in.readInt();
+ outgoingCount = in.readInt();
+ missedCount = in.readInt();
+ blockedCount = in.readInt();
+ }
+}
diff --git a/java/com/android/dialer/callstats/CallStatsFragment.java b/java/com/android/dialer/callstats/CallStatsFragment.java
new file mode 100644
index 0000000..3a90d93
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsFragment.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.dialer.callstats;
+
+import android.app.Fragment;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.database.ContentObserver;
+import android.os.Bundle;
+import android.os.Handler;
+import android.provider.CallLog;
+import android.provider.ContactsContract;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.RecyclerView;
+import android.support.v7.widget.LinearLayoutManager;
+import android.telecom.PhoneAccountHandle;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+import com.android.dialer.app.contactinfo.ExpirableCacheHeadlessFragment;
+import com.android.dialer.calllogutils.FilterSpinnerHelper;
+import com.android.dialer.contacts.ContactsComponent;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.dialer.widget.EmptyContentView;
+
+import java.util.Map;
+
+import static android.Manifest.permission.READ_CALL_LOG;
+
+public class CallStatsFragment extends Fragment implements
+ CallStatsQueryHandler.Listener, FilterSpinnerHelper.OnFilterChangedListener,
+ EmptyContentView.OnEmptyViewActionButtonClickedListener,
+ DoubleDatePickerDialog.OnDateSetListener {
+ private static final String TAG = "CallStatsFragment";
+
+ private static final int READ_CALL_LOG_PERMISSION_REQUEST_CODE = 1;
+
+ private PhoneAccountHandle mAccountFilter = null;
+ private int mCallTypeFilter = -1;
+ private long mFilterFrom = -1;
+ private long mFilterTo = -1;
+ private boolean mSortByDuration = true;
+ private boolean mDataLoaded = false;
+
+ private RecyclerView mRecyclerView;
+ private EmptyContentView mEmptyListView;
+ private LinearLayoutManager mLayoutManager;
+ private CallStatsAdapter mAdapter;
+ private CallStatsQueryHandler mCallStatsQueryHandler;
+ private FilterSpinnerHelper mFilterHelper;
+
+ private TextView mSumHeaderView;
+ private TextView mDateFilterView;
+
+ private boolean mHasReadCallLogPermission = false;
+
+ private boolean mRefreshDataRequired = true;
+ private final ContentObserver mObserver = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ mRefreshDataRequired = true;
+ }
+ };
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+
+ final ContentResolver cr = getActivity().getContentResolver();
+ mCallStatsQueryHandler = new CallStatsQueryHandler(cr, this);
+ cr.registerContentObserver(CallLog.CONTENT_URI, true, mObserver);
+ cr.registerContentObserver(ContactsContract.Contacts.CONTENT_URI, true, mObserver);
+
+ setHasOptionsMenu(true);
+
+ ExpirableCacheHeadlessFragment cacheFragment =
+ ExpirableCacheHeadlessFragment.attach((AppCompatActivity) getActivity());
+ mAdapter = new CallStatsAdapter(getActivity(),
+ ContactsComponent.get(getActivity()).contactDisplayPreferences(),
+ cacheFragment.getRetainedCache());
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
+ View view = inflater.inflate(R.layout.call_stats_fragment, container, false);
+
+ mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view);
+ mRecyclerView.setHasFixedSize(true);
+ mLayoutManager = new LinearLayoutManager(getActivity());
+ mRecyclerView.setLayoutManager(mLayoutManager);
+ mEmptyListView = (EmptyContentView) view.findViewById(R.id.empty_list_view);
+ mEmptyListView.setImage(R.drawable.empty_call_log);
+ mEmptyListView.setActionClickedListener(this);
+
+ mSumHeaderView = (TextView) view.findViewById(R.id.sum_header);
+ mDateFilterView = (TextView) view.findViewById(R.id.date_filter);
+
+ return view;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mRecyclerView.setAdapter(mAdapter);
+ mFilterHelper = new FilterSpinnerHelper(view, false, this);
+ updateEmptyVisibilityAndMessage();
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ if (getUserVisibleHint() && PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG)) {
+ inflater.inflate(R.menu.call_stats_options, menu);
+
+ final MenuItem resetItem = menu.findItem(R.id.reset_date_filter);
+ final MenuItem sortDurationItem = menu.findItem(R.id.sort_by_duration);
+ final MenuItem sortCountItem = menu.findItem(R.id.sort_by_count);
+
+ resetItem.setVisible(mFilterFrom != -1);
+ sortDurationItem.setVisible(!mSortByDuration);
+ sortCountItem.setVisible(mSortByDuration);
+ }
+
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ final int itemId = item.getItemId();
+ switch (itemId) {
+ case R.id.date_filter: {
+ final DoubleDatePickerDialog.Fragment fragment =
+ new DoubleDatePickerDialog.Fragment();
+ fragment.setArguments(
+ DoubleDatePickerDialog.Fragment.createArguments(mFilterFrom, mFilterTo));
+ fragment.show(getFragmentManager(), "filter");
+ break;
+ }
+ case R.id.reset_date_filter: {
+ mFilterFrom = -1;
+ mFilterTo = -1;
+ fetchCalls();
+ updateEmptyVisibilityAndMessage();
+ getActivity().invalidateOptionsMenu();
+ break;
+ }
+ case R.id.sort_by_duration:
+ case R.id.sort_by_count: {
+ mSortByDuration = itemId == R.id.sort_by_duration;
+ mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration);
+ getActivity().invalidateOptionsMenu();
+ break;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public void onFilterChanged(PhoneAccountHandle account, int callType) {
+ if (account != mAccountFilter) {
+ mAccountFilter = account;
+ fetchCalls();
+ }
+ if (callType != mCallTypeFilter) {
+ mCallTypeFilter = callType;
+ mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration);
+ if (mDataLoaded) {
+ updateHeader();
+ updateEmptyVisibilityAndMessage();
+ }
+ }
+ }
+
+ @Override
+ public void onEmptyViewActionButtonClicked() {
+ if (!PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG)) {
+ requestPermissions(new String[] { READ_CALL_LOG },
+ READ_CALL_LOG_PERMISSION_REQUEST_CODE);
+ }
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions,
+ int[] grantResults) {
+ if (requestCode == READ_CALL_LOG_PERMISSION_REQUEST_CODE) {
+ if (grantResults.length >= 1 && PackageManager.PERMISSION_GRANTED == grantResults[0]) {
+ // Force a refresh of the data since we were missing the permission before this.
+ mRefreshDataRequired = true;
+ getActivity().invalidateOptionsMenu();
+ }
+ }
+ }
+
+ @Override
+ public void onDateSet(long from, long to) {
+ mFilterFrom = from;
+ mFilterTo = to;
+ getActivity().invalidateOptionsMenu();
+ fetchCalls();
+ updateEmptyVisibilityAndMessage();
+ }
+
+ /**
+ * Called by the CallStatsQueryHandler when the list of calls has been
+ * fetched or updated.
+ */
+ @Override
+ public void onCallsFetched(Map<ContactInfo, CallStatsDetails> calls) {
+ if (getActivity() == null || getActivity().isFinishing()) {
+ return;
+ }
+
+ mDataLoaded = true;
+ mAdapter.updateData(calls, mFilterFrom, mFilterTo);
+ mAdapter.updateDisplayedData(mCallTypeFilter, mSortByDuration);
+ updateHeader();
+ updateEmptyVisibilityAndMessage();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ final boolean hasReadCallLogPermission =
+ PermissionsUtil.hasPermission(getActivity(), READ_CALL_LOG);
+ if (!mHasReadCallLogPermission && hasReadCallLogPermission) {
+ // We didn't have the permission before, and now we do. Force a refresh of the call log.
+ // Note that this code path always happens on a fresh start, but mRefreshDataRequired
+ // is already true in that case anyway.
+ mRefreshDataRequired = true;
+ mDataLoaded = false;
+ updateEmptyVisibilityAndMessage();
+ getActivity().invalidateOptionsMenu();
+ }
+ mHasReadCallLogPermission = hasReadCallLogPermission;
+ refreshData();
+ mAdapter.startCache();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mAdapter.pauseCache();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mAdapter.pauseCache();
+ getActivity().getContentResolver().unregisterContentObserver(mObserver);
+ }
+
+ private void fetchCalls() {
+ mCallStatsQueryHandler.fetchCalls(mFilterFrom, mFilterTo, mAccountFilter);
+ }
+
+ private void updateHeader() {
+ final String callCount = mAdapter.getTotalCallCountString();
+ final String duration = mAdapter.getFullDurationString(false);
+
+ if (duration != null) {
+ mSumHeaderView.setText(getString(R.string.call_stats_header_total, callCount, duration));
+ } else {
+ mSumHeaderView.setText(getString(R.string.call_stats_header_total_callsonly, callCount));
+ }
+ mSumHeaderView.setVisibility(isListEmpty() ? View.GONE : View.VISIBLE);
+
+ if (mFilterFrom == -1) {
+ mDateFilterView.setVisibility(View.GONE);
+ } else {
+ mDateFilterView.setText(
+ DateUtils.formatDateRange(getActivity(), mFilterFrom, mFilterTo, 0));
+ mDateFilterView.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /** Requests updates to the data to be shown. */
+ private void refreshData() {
+ // Prevent unnecessary refresh.
+ if (mRefreshDataRequired) {
+ // Mark all entries in the contact info cache as out of date, so
+ // they will be looked up again once being shown.
+ mAdapter.invalidateCache();
+ fetchCalls();
+ mRefreshDataRequired = false;
+ }
+ }
+
+ private boolean isListEmpty() {
+ return mDataLoaded && mAdapter.getItemCount() == 0;
+ }
+
+ private void updateEmptyVisibilityAndMessage() {
+ final Context context = getActivity();
+ if (context == null) {
+ return;
+ }
+
+ boolean showListView = !isListEmpty();
+
+ if (!PermissionsUtil.hasPermission(context, READ_CALL_LOG)) {
+ mEmptyListView.setDescription(R.string.permission_no_calllog);
+ mEmptyListView.setActionLabel(R.string.permission_single_turn_on);
+ showListView = false;
+ } else if (mFilterFrom > 0 || mFilterTo > 0) {
+ mEmptyListView.setDescription(R.string.recent_calls_no_items_in_range);
+ mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
+ } else {
+ mEmptyListView.setDescription(R.string.call_log_all_empty);
+ mEmptyListView.setActionLabel(EmptyContentView.NO_LABEL);
+ }
+
+ mRecyclerView.setVisibility(showListView ? View.VISIBLE : View.GONE);
+ mEmptyListView.setVisibility(!showListView ? View.VISIBLE : View.GONE);
+ }
+}
diff --git a/java/com/android/dialer/callstats/CallStatsListItemViewHolder.java b/java/com/android/dialer/callstats/CallStatsListItemViewHolder.java
new file mode 100644
index 0000000..bda6b3e
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsListItemViewHolder.java
@@ -0,0 +1,245 @@
+/*
+ * 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.dialer.callstats;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.QuickContactBadge;
+import android.widget.TextView;
+
+import com.android.dialer.R;
+import com.android.dialer.contactphoto.ContactPhotoManager;
+import com.android.dialer.contacts.displaypreference.ContactDisplayPreferences;
+import com.android.dialer.lettertile.LetterTileDrawable;
+import com.android.dialer.location.GeoUtil;
+import com.android.dialer.phonenumbercache.ContactInfoHelper;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.widget.LinearColorBar;
+
+/**
+ * This is an object containing references to views contained by the call log list item. This
+ * improves performance by reducing the frequency with which we need to find views by IDs.
+ *
+ * This object also contains UI logic pertaining to the view, to isolate it from the CallLogAdapter.
+ */
+public final class CallStatsListItemViewHolder extends RecyclerView.ViewHolder
+ implements View.OnClickListener {
+
+ public CallStatsDetails details;
+ public Intent clickIntent;
+
+ public final View mRootView;
+ public final QuickContactBadge mQuickContactView;
+ public final View mPrimaryActionView;
+ public final TextView mNameView;
+ public final TextView mNumberView;
+ public final TextView mLabelView;
+ public final TextView mPercentView;
+ public final LinearColorBar mBarView;
+
+ private Context mContext;
+ private ContactInfoHelper mContactInfoHelper;
+ private final int mPhotoSize;
+
+ private CallStatsListItemViewHolder(View rootView,
+ QuickContactBadge quickContactView,
+ View primaryActionView,
+ TextView nameView,
+ TextView numberView,
+ TextView labelView,
+ TextView percentView,
+ LinearColorBar barView,
+ ContactInfoHelper contactInfoHelper) {
+ super(rootView);
+
+ mRootView = rootView;
+ mQuickContactView = quickContactView;
+ mPrimaryActionView = primaryActionView;
+ mNameView = nameView;
+ mNumberView = numberView;
+ mLabelView = labelView;
+ mPercentView = percentView;
+ mBarView = barView;
+
+ mPrimaryActionView.setOnClickListener(this);
+
+ quickContactView.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
+
+ mContext = rootView.getContext();
+ mPhotoSize = mContext.getResources().getDimensionPixelSize(R.dimen.contact_photo_size);
+ mContactInfoHelper = contactInfoHelper;
+ }
+
+ public static CallStatsListItemViewHolder create(View view,
+ ContactInfoHelper contactInfoHelper) {
+ return new CallStatsListItemViewHolder(view,
+ (QuickContactBadge) view.findViewById(R.id.quick_contact_photo),
+ view.findViewById(R.id.primary_action_view),
+ (TextView) view.findViewById(R.id.name),
+ (TextView) view.findViewById(R.id.number),
+ (TextView) view.findViewById(R.id.label),
+ (TextView) view.findViewById(R.id.percent),
+ (LinearColorBar) view.findViewById(R.id.percent_bar),
+ contactInfoHelper);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (clickIntent != null) {
+ DialerUtils.startActivityWithErrorToast(mContext, clickIntent);
+ }
+ }
+
+ public void setDetails(CallStatsDetails details, CallStatsDetails first,
+ CallStatsDetails total, int type, boolean byDuration,
+ ContactDisplayPreferences.DisplayOrder nameDisplayOrder) {
+ this.details = details;
+ details.updateDisplayProperties(mContext, nameDisplayOrder);
+
+ CharSequence numberFormattedLabel = null;
+ // Only show a label if the number is shown and it is not a SIP address.
+ if (!TextUtils.isEmpty(details.number)
+ && !PhoneNumberHelper.isUriNumber(details.number.toString())) {
+ numberFormattedLabel = Phone.getTypeLabel(mContext.getResources(),
+ details.numberType, details.numberLabel);
+ }
+
+ final CharSequence nameText;
+ final CharSequence numberText;
+ final CharSequence labelText;
+
+ if (TextUtils.isEmpty(details.displayName)) {
+ nameText = details.displayNumber;
+ if (TextUtils.isEmpty(details.geocode) || details.isVoicemailNumber) {
+ numberText = null;
+ } else {
+ numberText = details.geocode;
+ }
+ labelText = null;
+ } else {
+ nameText = details.displayName;
+ numberText = details.displayNumber;
+ labelText = numberFormattedLabel;
+ }
+
+ float in = 0, out = 0, missed = 0, blocked = 0;
+ float ratio = getDetailValue(details, type, byDuration) /
+ getDetailValue(first, type, byDuration);
+
+ if (type == Calls.INCOMING_TYPE) {
+ in = ratio;
+ } else if (type == Calls.OUTGOING_TYPE) {
+ out = ratio;
+ } else if (type == Calls.MISSED_TYPE) {
+ missed = ratio;
+ } else if (type == Calls.BLOCKED_TYPE) {
+ blocked = ratio;
+ } else {
+ float full = getDetailValue(details, type, byDuration);
+ in = getDetailValue(details, Calls.INCOMING_TYPE, byDuration) * ratio / full;
+ out = getDetailValue(details, Calls.OUTGOING_TYPE, byDuration) * ratio / full;
+ if (!byDuration) {
+ missed = getDetailValue(details, Calls.MISSED_TYPE, byDuration) * ratio / full;
+ blocked = getDetailValue(details, Calls.BLOCKED_TYPE, byDuration) * ratio / full;
+ }
+ }
+
+ mBarView.setRatios(in, out, missed, blocked);
+ mNameView.setText(nameText);
+ mNumberView.setText(numberText);
+ mLabelView.setText(labelText);
+ mLabelView.setVisibility(TextUtils.isEmpty(labelText) ? View.GONE : View.VISIBLE);
+
+ if (byDuration && type == Calls.MISSED_TYPE) {
+ mPercentView.setText(getCallCountString(mContext, details.missedCount));
+ } else if (byDuration && type == Calls.BLOCKED_TYPE) {
+ mPercentView.setText(getCallCountString(mContext, details.blockedCount));
+ } else {
+ float percent = getDetailValue(details, type, byDuration) * 100F /
+ getDetailValue(total, type, byDuration);
+ mPercentView.setText(String.format("%.1f%%", percent));
+ }
+
+ final String nameForDefaultImage = TextUtils.isEmpty(details.name)
+ ? details.displayNumber : details.name;
+
+ int contactType = LetterTileDrawable.TYPE_DEFAULT;
+ if (details.isVoicemailNumber) {
+ contactType = LetterTileDrawable.TYPE_VOICEMAIL;
+ } else if (mContactInfoHelper.isBusiness(details.sourceType)) {
+ contactType = LetterTileDrawable.TYPE_BUSINESS;
+ }
+
+ ContactPhotoManager.getInstance(mContext).loadDialerThumbnailOrPhoto(mQuickContactView,
+ details.contactUri, details.photoId, details.photoUri, nameForDefaultImage, contactType);
+ }
+
+ private float getDetailValue(CallStatsDetails details, int type, boolean byDuration) {
+ if (byDuration) {
+ return (float) details.getRequestedDuration(type);
+ } else {
+ return (float) details.getRequestedCount(type);
+ }
+ }
+
+ public static String getCallCountString(Context context, long count) {
+ return context.getResources().getQuantityString(R.plurals.call, (int) count, (int) count);
+ }
+
+ public static String getDurationString(Context context, long duration, boolean includeSeconds) {
+ int hours, minutes, seconds;
+
+ hours = (int) (duration / 3600);
+ duration -= (long) hours * 3600;
+ minutes = (int) (duration / 60);
+ duration -= (long) minutes * 60;
+ seconds = (int) duration;
+
+ if (!includeSeconds) {
+ if (seconds >= 30) {
+ minutes++;
+ }
+ if (minutes >= 60) {
+ hours++;
+ }
+ }
+
+ boolean dispHours = hours > 0;
+ boolean dispMinutes = minutes > 0 || (!includeSeconds && hours == 0);
+ boolean dispSeconds = includeSeconds && (seconds > 0 || (hours == 0 && minutes == 0));
+
+ final Resources res = context.getResources();
+ final String hourString = dispHours ?
+ res.getQuantityString(R.plurals.hour, hours, hours) : null;
+ final String minuteString = dispMinutes ?
+ res.getQuantityString(R.plurals.minute, minutes, minutes) : null;
+ final String secondString = dispSeconds ?
+ res.getQuantityString(R.plurals.second, seconds, seconds) : null;
+
+ int index = ((dispHours ? 4 : 0) | (dispMinutes ? 2 : 0) | (dispSeconds ? 1 : 0)) - 1;
+ String[] formats = res.getStringArray(R.array.call_stats_duration);
+ return String.format(formats[index], hourString, minuteString, secondString);
+ }
+}
diff --git a/java/com/android/dialer/callstats/CallStatsQuery.java b/java/com/android/dialer/callstats/CallStatsQuery.java
new file mode 100644
index 0000000..92bd9c7
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsQuery.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.dialer.callstats;
+
+import android.provider.CallLog.Calls;
+
+public class CallStatsQuery {
+ public static final String[] _PROJECTION = new String[] {
+ Calls._ID, // 0
+ Calls.NUMBER, // 1
+ Calls.DATE, // 2
+ Calls.DURATION, // 3
+ Calls.TYPE, // 4
+ Calls.COUNTRY_ISO, // 5
+ Calls.GEOCODED_LOCATION, // 6
+ Calls.CACHED_NAME, // 7
+ Calls.CACHED_NUMBER_TYPE, // 8
+ Calls.CACHED_NUMBER_LABEL, // 9
+ Calls.CACHED_LOOKUP_URI, // 10
+ Calls.CACHED_MATCHED_NUMBER, // 11
+ Calls.CACHED_NORMALIZED_NUMBER, // 12
+ Calls.CACHED_PHOTO_ID, // 13
+ Calls.CACHED_FORMATTED_NUMBER, // 14
+ Calls.NUMBER_PRESENTATION, // 15
+ Calls.PHONE_ACCOUNT_COMPONENT_NAME, // 16
+ Calls.PHONE_ACCOUNT_ID, // 17
+ Calls.POST_DIAL_DIGITS, // 18
+ };
+
+ public static final int ID = 0;
+ public static final int NUMBER = 1;
+ public static final int DATE = 2;
+ public static final int DURATION = 3;
+ public static final int CALL_TYPE = 4;
+ public static final int COUNTRY_ISO = 5;
+ public static final int GEOCODED_LOCATION = 6;
+ public static final int CACHED_NAME = 7;
+ public static final int CACHED_NUMBER_TYPE = 8;
+ public static final int CACHED_NUMBER_LABEL = 9;
+ public static final int CACHED_LOOKUP_URI = 10;
+ public static final int CACHED_MATCHED_NUMBER = 11;
+ public static final int CACHED_NORMALIZED_NUMBER = 12;
+ public static final int CACHED_PHOTO_ID = 13;
+ public static final int CACHED_FORMATTED_NUMBER = 14;
+ public static final int NUMBER_PRESENTATION = 15;
+ public static final int ACCOUNT_COMPONENT_NAME = 16;
+ public static final int ACCOUNT_ID = 17;
+ public static final int POST_DIAL_DIGITS = 18;
+}
diff --git a/java/com/android/dialer/callstats/CallStatsQueryHandler.java b/java/com/android/dialer/callstats/CallStatsQueryHandler.java
new file mode 100644
index 0000000..3c93be0
--- /dev/null
+++ b/java/com/android/dialer/callstats/CallStatsQueryHandler.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.dialer.callstats;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabaseCorruptException;
+import android.database.sqlite.SQLiteDiskIOException;
+import android.database.sqlite.SQLiteFullException;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.provider.CallLog.Calls;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.PhoneNumberUtils;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.UriUtils;
+
+import com.google.common.collect.Lists;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class to handle call-log queries, optionally with a date-range filter
+ */
+public class CallStatsQueryHandler extends AsyncQueryHandler {
+ private static final String[] EMPTY_STRING_ARRAY = new String[0];
+
+ private static final int EVENT_PROCESS_DATA = 10;
+
+ private static final int QUERY_CALLS_TOKEN = 100;
+
+ private static final String TAG = "CallStatsQueryHandler";
+
+ private final WeakReference<Listener> mListener;
+ private Handler mWorkerThreadHandler;
+
+ /**
+ * Simple handler that wraps background calls to catch
+ * {@link SQLiteException}, such as when the disk is full.
+ */
+ protected class CatchingWorkerHandler extends AsyncQueryHandler.WorkerHandler {
+ public CatchingWorkerHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.arg1 == EVENT_PROCESS_DATA) {
+ Cursor cursor = (Cursor) msg.obj;
+ Message reply = CallStatsQueryHandler.this.obtainMessage(msg.what);
+ reply.obj = processData(cursor);
+ reply.arg1 = msg.arg1;
+ reply.sendToTarget();
+ return;
+ }
+
+ try {
+ // Perform same query while catching any exceptions
+ super.handleMessage(msg);
+ } catch (SQLiteDiskIOException e) {
+ Log.w(TAG, "Exception on background worker thread", e);
+ } catch (SQLiteFullException e) {
+ Log.w(TAG, "Exception on background worker thread", e);
+ } catch (SQLiteDatabaseCorruptException e) {
+ Log.w(TAG, "Exception on background worker thread", e);
+ }
+ }
+ }
+
+ @Override
+ protected Handler createHandler(Looper looper) {
+ // Provide our special handler that catches exceptions
+ mWorkerThreadHandler = new CatchingWorkerHandler(looper);
+ return mWorkerThreadHandler;
+ }
+
+ public CallStatsQueryHandler(ContentResolver contentResolver, Listener listener) {
+ super(contentResolver);
+ mListener = new WeakReference<Listener>(listener);
+ }
+
+ public void fetchCalls(long from, long to, PhoneAccountHandle account) {
+ cancelOperation(QUERY_CALLS_TOKEN);
+
+ StringBuilder selection = new StringBuilder();
+ List<String> selectionArgs = Lists.newArrayList();
+
+ if (from != -1) {
+ selection.append(String.format("(%s > ?)", Calls.DATE));
+ selectionArgs.add(String.valueOf(from));
+ }
+ if (to != -1) {
+ if (selection.length() > 0) {
+ selection.append(" AND ");
+ }
+ selection.append(String.format("(%s < ?)", Calls.DATE));
+ selectionArgs.add(String.valueOf(to));
+ }
+ if (account != null) {
+ if (selection.length() > 0) {
+ selection.append(" AND ");
+ }
+ selection.append(String.format("(%s = ?)", Calls.PHONE_ACCOUNT_ID));
+ selectionArgs.add(account.getId());
+ }
+
+ startQuery(QUERY_CALLS_TOKEN, null, Calls.CONTENT_URI_WITH_VOICEMAIL,
+ CallStatsQuery._PROJECTION, selection.toString(),
+ selectionArgs.toArray(EMPTY_STRING_ARRAY), Calls.NUMBER + " ASC");
+ }
+
+ @Override
+ protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) {
+ if (token == QUERY_CALLS_TOKEN) {
+ Message msg = mWorkerThreadHandler.obtainMessage(token);
+ msg.arg1 = EVENT_PROCESS_DATA;
+ msg.obj = cursor;
+
+ mWorkerThreadHandler.sendMessage(msg);
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.arg1 == EVENT_PROCESS_DATA) {
+ final Map<ContactInfo, CallStatsDetails> calls =
+ (Map<ContactInfo, CallStatsDetails>) msg.obj;
+ final Listener listener = mListener.get();
+ if (listener != null) {
+ listener.onCallsFetched(calls);
+ }
+ } else {
+ super.handleMessage(msg);
+ }
+ }
+
+ private Map<ContactInfo, CallStatsDetails> processData(Cursor cursor) {
+ final Map<ContactInfo, CallStatsDetails> result = new HashMap<ContactInfo, CallStatsDetails>();
+ final ArrayList<ContactInfo> infos = new ArrayList<ContactInfo>();
+ final ArrayList<CallStatsDetails> calls = new ArrayList<CallStatsDetails>();
+ CallStatsDetails pending = null;
+
+ cursor.moveToFirst();
+
+ while (!cursor.isAfterLast()) {
+ final String number = cursor.getString(CallStatsQuery.NUMBER);
+ final long duration = cursor.getLong(CallStatsQuery.DURATION);
+ final int callType = cursor.getInt(CallStatsQuery.CALL_TYPE);
+
+ if (pending == null || !phoneNumbersEqual(pending.number.toString(), number)) {
+ final long date = cursor.getLong(CallStatsQuery.DATE);
+ final int numberPresentation = cursor.getInt(CallStatsQuery.NUMBER_PRESENTATION);
+ final String countryIso = cursor.getString(CallStatsQuery.COUNTRY_ISO);
+ final String geocode = cursor.getString(CallStatsQuery.GEOCODED_LOCATION);
+ final String postDialDigits = cursor.getString(CallStatsQuery.POST_DIAL_DIGITS);
+ final ContactInfo info = getContactInfoFromCallStats(cursor);
+ final PhoneAccountHandle accountHandle = TelecomUtil.composePhoneAccountHandle(
+ cursor.getString(CallStatsQuery.ACCOUNT_COMPONENT_NAME),
+ cursor.getString(CallStatsQuery.ACCOUNT_ID));
+
+ pending = new CallStatsDetails(number, numberPresentation, postDialDigits,
+ accountHandle, info, countryIso, geocode, date);
+ infos.add(info);
+ calls.add(pending);
+ }
+
+ pending.addTimeOrMissed(callType, duration);
+ cursor.moveToNext();
+ }
+
+ cursor.close();
+ mergeItemsByNumber(calls, infos);
+
+ for (int i = 0; i < calls.size(); i++) {
+ result.put(infos.get(i), calls.get(i));
+ }
+
+ return result;
+ }
+
+ private void mergeItemsByNumber(List<CallStatsDetails> calls, List<ContactInfo> infos) {
+ // temporarily store items marked for removal
+ final ArrayList<CallStatsDetails> callsToRemove = new ArrayList<CallStatsDetails>();
+ final ArrayList<ContactInfo> infosToRemove = new ArrayList<ContactInfo>();
+
+ for (int i = 0; i < calls.size(); i++) {
+ final CallStatsDetails outerItem = calls.get(i);
+ final String currentFormattedNumber = outerItem.number.toString();
+
+ for (int j = calls.size() - 1; j > i; j--) {
+ final CallStatsDetails innerItem = calls.get(j);
+ final String innerNumber = innerItem.number.toString();
+
+ if (phoneNumbersEqual(currentFormattedNumber, innerNumber)) {
+ outerItem.mergeWith(innerItem);
+ //make sure we're not counting twice in case we're dealing with
+ //multiple different formats
+ innerItem.reset();
+ callsToRemove.add(innerItem);
+ infosToRemove.add(infos.get(j));
+ }
+ }
+ }
+
+ for (CallStatsDetails call : callsToRemove) {
+ calls.remove(call);
+ }
+ for (ContactInfo info : infosToRemove) {
+ infos.remove(info);
+ }
+ }
+
+ private ContactInfo getContactInfoFromCallStats(Cursor c) {
+ ContactInfo info = new ContactInfo();
+ info.lookupUri = UriUtils.parseUriOrNull(c.getString(CallStatsQuery.CACHED_LOOKUP_URI));
+ info.name = c.getString(CallStatsQuery.CACHED_NAME);
+ info.type = c.getInt(CallStatsQuery.CACHED_NUMBER_TYPE);
+ info.label = c.getString(CallStatsQuery.CACHED_NUMBER_LABEL);
+
+ final String matchedNumber = c.getString(CallStatsQuery.CACHED_MATCHED_NUMBER);
+ info.number = matchedNumber == null ? c.getString(CallStatsQuery.NUMBER) : matchedNumber;
+ info.normalizedNumber = c.getString(CallStatsQuery.CACHED_NORMALIZED_NUMBER);
+ info.formattedNumber = c.getString(CallStatsQuery.CACHED_FORMATTED_NUMBER);
+
+ info.photoId = c.getLong(CallStatsQuery.CACHED_PHOTO_ID);
+ info.photoUri = null; // We do not cache the photo URI.
+
+ return info;
+ }
+
+ private static boolean phoneNumbersEqual(String number1, String number2) {
+ if (PhoneNumberUtils.isUriNumber(number1) || PhoneNumberUtils.isUriNumber(number2)) {
+ return sipAddressesEqual(number1, number2);
+ } else {
+ return PhoneNumberUtils.compare(number1, number2);
+ }
+ }
+
+ private static boolean sipAddressesEqual(String number1, String number2) {
+ if (number1 == null || number2 == null) {
+ return number1 == number2;
+ }
+
+ int index1 = number1.indexOf('@');
+ final String userinfo1;
+ final String rest1;
+ if (index1 != -1) {
+ userinfo1 = number1.substring(0, index1);
+ rest1 = number1.substring(index1);
+ } else {
+ userinfo1 = number1;
+ rest1 = "";
+ }
+
+ int index2 = number2.indexOf('@');
+ final String userinfo2;
+ final String rest2;
+ if (index2 != -1) {
+ userinfo2 = number2.substring(0, index2);
+ rest2 = number2.substring(index2);
+ } else {
+ userinfo2 = number2;
+ rest2 = "";
+ }
+
+ return userinfo1.equals(userinfo2) && rest1.equalsIgnoreCase(rest2);
+ }
+
+ public interface Listener {
+ void onCallsFetched(Map<ContactInfo, CallStatsDetails> calls);
+ }
+}
diff --git a/java/com/android/dialer/callstats/DoubleDatePickerDialog.java b/java/com/android/dialer/callstats/DoubleDatePickerDialog.java
new file mode 100644
index 0000000..5d11bec
--- /dev/null
+++ b/java/com/android/dialer/callstats/DoubleDatePickerDialog.java
@@ -0,0 +1,334 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.dialer.callstats;
+
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.os.Bundle;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.DatePicker;
+import android.widget.DatePicker.OnDateChangedListener;
+import android.widget.Spinner;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+
+import com.android.dialer.R;
+
+/**
+ * Alertdialog with two date pickers - one for a start and one for an end date.
+ * Used to filter the callstats query.
+ */
+public class DoubleDatePickerDialog extends AlertDialog
+ implements OnClickListener, OnDateChangedListener, OnItemSelectedListener {
+
+ private static final String TAG = "DoubleDatePickerDialog";
+
+ public interface OnDateSetListener {
+ void onDateSet(long from, long to);
+ }
+
+ public static class Fragment extends DialogFragment implements OnDateSetListener {
+ private DoubleDatePickerDialog mDialog;
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ mDialog = new DoubleDatePickerDialog(getActivity(), this);
+ return mDialog;
+ }
+
+ @Override
+ public void onStart() {
+ final Bundle args = getArguments();
+ final long from = args.getLong("from", -1);
+ final long to = args.getLong("to", -1);
+
+ if (from != -1) {
+ mDialog.setValues(from, to);
+ } else {
+ mDialog.resetPickers();
+ }
+ super.onStart();
+ }
+
+ @Override
+ public void onDateSet(long from, long to) {
+ ((DoubleDatePickerDialog.OnDateSetListener) getActivity()).onDateSet(from, to);
+ }
+
+ public static Bundle createArguments(long from, long to) {
+ final Bundle args = new Bundle();
+ args.putLong("from", from);
+ args.putLong("to", to);
+ return args;
+ }
+ }
+
+ private interface QuickSelection {
+ void adjustStartDate(Calendar date);
+ }
+
+ private static final int[] QUICKSELECTION_ENTRIES = new int[] {
+ R.string.date_qs_currentmonth,
+ R.string.date_qs_currentquarter,
+ R.string.date_qs_currentyear,
+ R.string.date_qs_lastweek,
+ R.string.date_qs_lastmonth,
+ R.string.date_qs_lastquarter,
+ R.string.date_qs_lastyear
+ };
+
+ private static final QuickSelection[] QUICKSELECTIONS = new QuickSelection[] {
+ new QuickSelection() {
+ @Override
+ public void adjustStartDate(Calendar date) {
+ date.set(Calendar.DAY_OF_MONTH, 1);
+ }
+ },
+ new QuickSelection() {
+ @Override
+ public void adjustStartDate(Calendar date) {
+ final int currentMonth = date.get(Calendar.MONTH);
+ date.set(Calendar.MONTH, currentMonth - (currentMonth % 3));
+ date.set(Calendar.DAY_OF_MONTH, 1);
+ }
+ },
+ new QuickSelection() {
+ @Override
+ public void adjustStartDate(Calendar date) {
+ date.set(Calendar.MONTH, 0);
+ date.set(Calendar.DAY_OF_MONTH, 1);
+ }
+ },
+ new QuickSelection() {
+ @Override
+ public void adjustStartDate(Calendar date) {
+ date.add(Calendar.WEEK_OF_YEAR, -1);
+ }
+ },
+ new QuickSelection() {
+ @Override
+ public void adjustStartDate(Calendar date) {
+ date.add(Calendar.MONTH, -1);
+ }
+ },
+ new QuickSelection() {
+ @Override
+ public void adjustStartDate(Calendar date) {
+ date.add(Calendar.MONTH, -3);
+ }
+ },
+ new QuickSelection() {
+ @Override
+ public void adjustStartDate(Calendar date) {
+ date.add(Calendar.YEAR, -1);
+ }
+ },
+ };
+
+ private static final String YEAR = "year";
+ private static final String MONTH = "month";
+ private static final String DAY = "day";
+
+ private final Spinner mQuickSelSpinner;
+ private final DatePicker mDatePickerFrom;
+ private final DatePicker mDatePickerTo;
+ private final OnDateSetListener mCallBack;
+ private Button mOkButton;
+ private int mQuickSelSelection = -1;
+
+ public DoubleDatePickerDialog(final Context context, OnDateSetListener callBack) {
+ super(context);
+
+ mCallBack = callBack;
+
+ setTitle(R.string.call_stats_filter_picker_title);
+ setButton(BUTTON_NEGATIVE, context.getString(android.R.string.cancel), this);
+ setButton(BUTTON_POSITIVE, context.getString(android.R.string.ok), this);
+ setIcon(0);
+
+ LayoutInflater inflater =
+ (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ View view = inflater.inflate(R.layout.double_date_picker_dialog, null);
+ setView(view);
+
+ mDatePickerFrom = (DatePicker) view.findViewById(R.id.date_picker_from);
+ mDatePickerTo = (DatePicker) view.findViewById(R.id.date_picker_to);
+
+ ArrayList<CharSequence> quickSelEntries = new ArrayList<CharSequence>();
+ for (int entryId : QUICKSELECTION_ENTRIES) {
+ quickSelEntries.add(context.getString(entryId));
+ }
+ ArrayAdapter<CharSequence> quickSelAdapter = new ArrayAdapter<CharSequence>(
+ context, android.R.layout.simple_spinner_item, android.R.id.text1, quickSelEntries) {
+ @Override
+ public View getView(int position, View convertView, android.view.ViewGroup parent) {
+ final TextView v = (TextView) super.getView(position, convertView, parent);
+ v.setText(context.getString(R.string.date_quick_selection));
+ return v;
+ }
+ };
+ quickSelAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+
+ mQuickSelSpinner = (Spinner) view.findViewById(R.id.date_quick_selection);
+ mQuickSelSpinner.setOnItemSelectedListener(this);
+ mQuickSelSpinner.setAdapter(quickSelAdapter);
+
+ resetPickers();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mOkButton = getButton(DialogInterface.BUTTON_POSITIVE);
+ updateOkButtonState();
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case BUTTON_POSITIVE:
+ tryNotifyDateSet();
+ break;
+ case BUTTON_NEGATIVE:
+ break;
+ }
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int pos, long id) {
+ if (mQuickSelSelection >= 0) {
+ QuickSelection sel = QUICKSELECTIONS[pos];
+ Calendar from = Calendar.getInstance();
+ long millisTo = from.getTimeInMillis();
+ sel.adjustStartDate(from);
+ long millisFrom = from.getTimeInMillis();
+
+ setValues(millisFrom, millisTo);
+ }
+ mQuickSelSelection = pos;
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ }
+
+ public void onDateChanged(DatePicker view, int year, int month, int day) {
+ view.init(year, month, day, this);
+ updateOkButtonState();
+ }
+
+ public void setValues(long millisFrom, long millisTo) {
+ setPicker(mDatePickerFrom, millisFrom);
+ setPicker(mDatePickerTo, millisTo);
+ updateOkButtonState();
+ }
+
+ public void resetPickers() {
+ long millis = System.currentTimeMillis();
+ setPicker(mDatePickerFrom, millis);
+ setPicker(mDatePickerTo, millis);
+ updateOkButtonState();
+ }
+
+ private void setPicker(DatePicker picker, long millis) {
+ Calendar c = Calendar.getInstance();
+ c.setTimeInMillis(millis);
+
+ int year = c.get(Calendar.YEAR);
+ int month = c.get(Calendar.MONTH);
+ int day = c.get(Calendar.DAY_OF_MONTH);
+
+ picker.init(year, month, day, this);
+ }
+
+ private long getMillisForPicker(DatePicker picker, boolean endOfDay) {
+ Calendar c = Calendar.getInstance();
+ c.set(Calendar.YEAR, picker.getYear());
+ c.set(Calendar.MONTH, picker.getMonth());
+ c.set(Calendar.DAY_OF_MONTH, picker.getDayOfMonth());
+ c.set(Calendar.HOUR_OF_DAY, 0);
+ c.set(Calendar.MINUTE, 0);
+ c.set(Calendar.SECOND, 0);
+
+ long millis = c.getTimeInMillis();
+ if (endOfDay) {
+ millis += 24L * 60L * 60L * 1000L - 1L;
+ }
+
+ return millis;
+ }
+
+ private void updateOkButtonState() {
+ if (mOkButton != null) {
+ long millisFrom = getMillisForPicker(mDatePickerFrom, false);
+ long millisTo = getMillisForPicker(mDatePickerTo, true);
+ mOkButton.setEnabled(millisFrom < millisTo);
+ }
+ }
+
+ private void tryNotifyDateSet() {
+ if (mCallBack != null) {
+ mDatePickerFrom.clearFocus();
+ mDatePickerTo.clearFocus();
+
+ long millisFrom = getMillisForPicker(mDatePickerFrom, false);
+ long millisTo = getMillisForPicker(mDatePickerTo, true);
+
+ mCallBack.onDateSet(millisFrom, millisTo);
+ }
+ }
+
+ // users like to play with it, so save the state and don't reset each time
+ @Override
+ public Bundle onSaveInstanceState() {
+ Bundle state = super.onSaveInstanceState();
+ state.putInt("F_" + YEAR, mDatePickerFrom.getYear());
+ state.putInt("F_" + MONTH, mDatePickerFrom.getMonth());
+ state.putInt("F_" + DAY, mDatePickerFrom.getDayOfMonth());
+ state.putInt("T_" + YEAR, mDatePickerTo.getYear());
+ state.putInt("T_" + MONTH, mDatePickerTo.getMonth());
+ state.putInt("T_" + DAY, mDatePickerTo.getDayOfMonth());
+ return state;
+ }
+
+ @Override
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ int fyear = savedInstanceState.getInt("F_" + YEAR);
+ int fmonth = savedInstanceState.getInt("F_" + MONTH);
+ int fday = savedInstanceState.getInt("F_" + DAY);
+ int tyear = savedInstanceState.getInt("T_" + YEAR);
+ int tmonth = savedInstanceState.getInt("T_" + MONTH);
+ int tday = savedInstanceState.getInt("T_" + DAY);
+ mDatePickerFrom.init(fyear, fmonth, fday, this);
+ mDatePickerTo.init(tyear, tmonth, tday, this);
+ }
+}
diff --git a/java/com/android/dialer/callstats/res/layout/call_stats_detail.xml b/java/com/android/dialer/callstats/res/layout/call_stats_detail.xml
new file mode 100644
index 0000000..c12dbfa
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/layout/call_stats_detail.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/call_stats_detail"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentTop="true"
+ android:background="?android:attr/colorBackground">
+
+ <com.android.dialer.widget.DialerToolbar
+ android:id="@+id/toolbar"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+ <!-- Caller information "card" -->
+ <LinearLayout
+ android:id="@+id/caller_information"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/call_details_top_margin"
+ android:paddingStart="@dimen/contact_container_padding_top_start"
+ android:paddingEnd="@dimen/contact_container_padding_bottom_end"
+ android:paddingTop="@dimen/contact_container_padding_top_start"
+ android:paddingBottom="@dimen/contact_container_padding_bottom_end"
+ android:baselineAligned="false"
+ android:orientation="horizontal"
+ android:focusable="true">
+
+ <QuickContactBadge
+ android:id="@+id/quick_contact_photo"
+ android:layout_width="@dimen/contact_photo_size"
+ android:layout_height="@dimen/contact_photo_size"
+ android:layout_gravity="center_vertical"
+ android:focusable="true" />
+
+ <LinearLayout
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:orientation="vertical"
+ android:layout_marginStart="@dimen/photo_text_margin">
+
+ <TextView
+ android:id="@+id/caller_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@style/Dialer.TextAppearance.Primary" />
+
+ <TextView
+ android:id="@+id/caller_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ style="@style/Dialer.TextAppearance.Secondary" />
+
+ </LinearLayout>
+
+ <ImageView
+ android:id="@+id/call_back_button"
+ android:layout_width="@dimen/call_back_button_size"
+ android:layout_height="@dimen/call_back_button_size"
+ android:background="?android:attr/selectableItemBackgroundBorderless"
+ android:src="@drawable/quantum_ic_call_white_24"
+ android:scaleType="center"
+ android:tint="?android:attr/textColorSecondary"
+ android:contentDescription="@string/call"
+ android:visibility="gone" />
+
+ </LinearLayout>
+
+ <ScrollView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <!-- The actual details -->
+ <include layout="@layout/call_stats_detail_info" />
+
+ <View
+ android:id="@+id/separator"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:layout_below="@id/out_average"
+ android:layout_marginTop="@dimen/ec_divider_top_bottom_margin"
+ android:layout_marginBottom="@dimen/ec_divider_top_bottom_margin"
+ android:background="#12000000" />
+
+ <TextView
+ android:id="@+id/call_detail_action_copy"
+ style="@style/CallDetailsActionItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/quantum_ic_content_copy_grey600_24"
+ android:text="@string/call_details_copy_number"/>
+
+ <TextView
+ android:id="@+id/call_detail_action_edit_before_call"
+ style="@style/CallDetailsActionItemStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:drawableStart="@drawable/quantum_ic_edit_grey600_24"
+ android:text="@string/call_details_edit_number"/>
+
+ </LinearLayout>
+
+ </ScrollView>
+
+</LinearLayout>
diff --git a/java/com/android/dialer/callstats/res/layout/call_stats_detail_info.xml b/java/com/android/dialer/callstats/res/layout/call_stats_detail_info.xml
new file mode 100644
index 0000000..4f4e59f
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/layout/call_stats_detail_info.xml
@@ -0,0 +1,230 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:ex="http://schemas.android.com/apk/res/com.android.dialer"
+ android:id="@+id/call_stats_info"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingStart="@dimen/call_log_list_item_info_margin_start"
+ android:paddingEnd="@dimen/call_log_list_item_info_margin_start"
+ android:paddingTop="@dimen/call_log_outer_margin"
+ android:paddingBottom="@dimen/call_log_outer_margin">
+
+ <TextView
+ android:id="@+id/date_filter"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/Dialer.TextAppearance.Secondary" />
+
+ <RelativeLayout
+ android:id="@+id/duration_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/date_filter">
+
+ <TextView
+ android:id="@+id/durations_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/call_stats_title_durations"
+ android:layout_marginBottom="@dimen/call_log_vertical_padding"
+ android:textColor="?android:attr/colorAccent"
+ style="@style/TextAppearance.CallStatsCategoryTitle" />
+
+ <TextView
+ android:id="@+id/total_duration_total"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_below="@id/durations_header"
+ style="@style/Dialer.TextAppearance.Primary" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@id/total_duration_total"
+ android:layout_alignBaseline="@id/total_duration_total"
+ android:text="@string/call_stats_title_of_total"
+ style="@style/Dialer.TextAppearance.Primary" />
+
+ <com.android.dialer.widget.LinearColorBar
+ android:id="@+id/duration_total_percent_bar"
+ android:layout_width="match_parent"
+ android:layout_below="@id/total_duration_total"
+ style="@style/CallStatsBarStyle" />
+
+ <TextView
+ android:id="@+id/total_duration_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_below="@id/duration_total_percent_bar"
+ style="@style/Dialer.TextAppearance.Primary" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@id/total_duration_number"
+ android:layout_alignBaseline="@id/total_duration_number"
+ android:text="@string/call_stats_title_for_number"
+ style="@style/Dialer.TextAppearance.Primary" />
+
+ <com.android.dialer.widget.LinearColorBar
+ android:id="@+id/duration_number_percent_bar"
+ android:layout_width="match_parent"
+ android:layout_below="@id/total_duration_number"
+ style="@style/CallStatsBarStyle" />
+
+ <include
+ android:id="@+id/in_duration"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/duration_number_percent_bar"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ layout="@layout/call_stats_detail_line" />
+
+ <include
+ android:id="@+id/out_duration"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/in_duration"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ layout="@layout/call_stats_detail_line" />
+
+ </RelativeLayout>
+
+ <TextView
+ android:id="@+id/count_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/duration_container"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="@dimen/call_log_vertical_padding"
+ android:text="@string/call_stats_title_count"
+ android:textColor="?android:attr/colorAccent"
+ style="@style/TextAppearance.CallStatsCategoryTitle" />
+
+ <TextView
+ android:id="@+id/total_count_total"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_below="@id/count_header"
+ style="@style/Dialer.TextAppearance.Primary" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@id/total_count_total"
+ android:layout_alignBaseline="@id/total_count_total"
+ android:text="@string/call_stats_title_of_total"
+ style="@style/Dialer.TextAppearance.Primary" />
+
+ <com.android.dialer.widget.LinearColorBar
+ android:id="@+id/count_total_percent_bar"
+ android:layout_width="match_parent"
+ android:layout_below="@id/total_count_total"
+ style="@style/CallStatsBarStyle" />
+
+ <TextView
+ android:id="@+id/total_count_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:layout_below="@id/count_total_percent_bar"
+ style="@style/Dialer.TextAppearance.Primary" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_toStartOf="@id/total_count_number"
+ android:layout_alignBaseline="@id/total_count_number"
+ android:text="@string/call_stats_title_for_number"
+ style="@style/Dialer.TextAppearance.Primary" />
+
+ <com.android.dialer.widget.LinearColorBar
+ android:id="@+id/count_number_percent_bar"
+ android:layout_width="match_parent"
+ android:layout_below="@id/total_count_number"
+ style="@style/CallStatsBarStyle" />
+
+ <include
+ android:id="@+id/in_count"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/count_number_percent_bar"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ layout="@layout/call_stats_detail_line" />
+
+ <include
+ android:id="@+id/out_count"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/in_count"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ layout="@layout/call_stats_detail_line" />
+
+ <include
+ android:id="@+id/missed_count"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/out_count"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ layout="@layout/call_stats_detail_line" />
+
+ <include
+ android:id="@+id/blocked_count"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/missed_count"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ layout="@layout/call_stats_detail_line" />
+
+ <TextView
+ android:id="@+id/average_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/blocked_count"
+ android:layout_marginTop="16dp"
+ android:layout_marginBottom="@dimen/call_log_vertical_padding"
+ android:text="@string/call_stats_title_average_duration"
+ android:textColor="?android:attr/colorAccent"
+ style="@style/TextAppearance.CallStatsCategoryTitle" />
+
+ <include
+ android:id="@+id/in_average"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/average_header"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ layout="@layout/call_stats_detail_line" />
+
+ <include
+ android:id="@+id/out_average"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/in_average"
+ android:layout_marginBottom="@dimen/call_log_icon_margin"
+ layout="@layout/call_stats_detail_line" />
+
+</RelativeLayout>
diff --git a/java/com/android/dialer/callstats/res/layout/call_stats_detail_line.xml b/java/com/android/dialer/callstats/res/layout/call_stats_detail_line.xml
new file mode 100644
index 0000000..5219f07
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/layout/call_stats_detail_line.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <com.android.dialer.calllogutils.CallTypeIconsView
+ android:id="@+id/icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical" />
+
+ <TextView
+ android:id="@+id/value"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_marginStart="@dimen/call_log_icon_margin"
+ style="@style/Dialer.TextAppearance.Primary" />
+
+ <TextView
+ android:id="@+id/percent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/call_log_icon_margin"
+ style="@style/Dialer.TextAppearance.Secondary" />
+
+</LinearLayout>
diff --git a/java/com/android/dialer/callstats/res/layout/call_stats_fragment.xml b/java/com/android/dialer/callstats/res/layout/call_stats_fragment.xml
new file mode 100644
index 0000000..017c3a4
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/layout/call_stats_fragment.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<!-- Layout parameters are set programmatically. -->
+<android.support.design.widget.CoordinatorLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:design="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ design:statusBarBackground="@null" >
+
+ <android.support.design.widget.AppBarLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:stateListAnimator="@null"
+ android:background="?android:attr/colorBackground"
+ android:elevation="2dp">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ design:layout_scrollFlags="scroll|enterAlways">
+
+ <include layout="@layout/call_log_filter_spinners" />
+
+ <TextView
+ android:id="@+id/date_filter"
+ style="@style/ContactListSeparatorTextViewStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/call_log_outer_margin"
+ android:layout_marginEnd="@dimen/call_log_outer_margin"
+ android:visibility="gone" />
+
+ <TextView
+ android:id="@+id/sum_header"
+ style="@style/ContactListSeparatorTextViewStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/call_log_outer_margin"
+ android:layout_marginEnd="@dimen/call_log_outer_margin"
+ android:paddingBottom="@dimen/call_log_outer_margin"
+ android:visibility="gone" />
+ </LinearLayout>
+
+ </android.support.design.widget.AppBarLayout>
+
+ <android.support.v7.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:paddingStart="@dimen/call_log_horizontal_margin"
+ android:paddingEnd="@dimen/call_log_horizontal_margin"
+ android:background="?android:attr/colorBackground"
+ design:layout_behavior="@string/appbar_scrolling_view_behavior" />
+
+ <com.android.dialer.widget.EmptyContentView
+ android:id="@+id/empty_list_view"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center_vertical"
+ android:background="?android:attr/colorBackground"
+ android:visibility="gone" />
+
+</android.support.design.widget.CoordinatorLayout>
diff --git a/java/com/android/dialer/callstats/res/layout/call_stats_list_item.xml b/java/com/android/dialer/callstats/res/layout/call_stats_list_item.xml
new file mode 100644
index 0000000..18600f4
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/layout/call_stats_list_item.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:ex="http://schemas.android.com/apk/res/com.android.dialer"
+ android:id="@+id/primary_action_view"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingStart="@dimen/call_log_start_margin"
+ android:paddingEnd="@dimen/call_log_outer_margin"
+ android:paddingTop="@dimen/call_log_vertical_padding"
+ android:paddingBottom="@dimen/call_log_vertical_padding"
+ android:background="?android:attr/selectableItemBackground">
+
+ <QuickContactBadge
+ android:id="@+id/quick_contact_photo"
+ android:layout_width="@dimen/contact_photo_size"
+ android:layout_height="@dimen/contact_photo_size"
+ android:layout_marginEnd="@dimen/call_log_start_margin"
+ android:paddingTop="2dp" />
+
+ <TextView
+ android:id="@+id/name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toEndOf="@id/quick_contact_photo"
+ android:ellipsize="marquee"
+ android:singleLine="true"
+ style="@style/Dialer.TextAppearance.Primary" />
+
+ <TextView
+ android:id="@+id/percent"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_toEndOf="@id/name"
+ android:layout_alignBaseline="@id/name"
+ android:layout_alignParentEnd="true"
+ android:gravity="end"
+ style="@style/Dialer.TextAppearance.Secondary" />
+
+ <com.android.dialer.widget.LinearColorBar
+ android:id="@+id/percent_bar"
+ android:layout_width="wrap_content"
+ android:layout_below="@id/name"
+ android:layout_toEndOf="@id/quick_contact_photo"
+ android:layout_alignParentEnd="true"
+ style="@style/CallStatsBarStyle" />
+
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/percent_bar"
+ android:layout_toEndOf="@id/quick_contact_photo"
+ android:layout_marginEnd="8dp"
+ android:singleLine="true"
+ style="@style/Dialer.TextAppearance.Secondary" />
+
+ <TextView
+ android:id="@+id/number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_below="@id/percent_bar"
+ android:layout_toEndOf="@id/label"
+ android:layout_alignBaseline="@id/label"
+ android:singleLine="true"
+ android:textSize="12sp"
+ style="@style/Dialer.TextAppearance.Secondary" />
+
+</RelativeLayout>
diff --git a/java/com/android/dialer/callstats/res/layout/double_date_picker_dialog.xml b/java/com/android/dialer/callstats/res/layout/double_date_picker_dialog.xml
new file mode 100644
index 0000000..ec4a1f2
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/layout/double_date_picker_dialog.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+**
+** Copyright 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.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="vertical" >
+
+ <Spinner
+ android:id="@+id/date_quick_selection"
+ android:layout_marginTop="3dp"
+ android:layout_marginBottom="3dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="-12dp"
+ android:layout_marginRight="3dp"
+ android:layout_marginTop="3dp"
+ android:text="@string/call_stats_filter_from"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <DatePicker
+ android:id="@+id/date_picker_from"
+ android:datePickerMode="spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:calendarViewShown="false"
+ android:spinnersShown="true" />
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="-12dp"
+ android:layout_marginRight="4dp"
+ android:text="@string/call_stats_filter_to"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+ <DatePicker
+ android:id="@+id/date_picker_to"
+ android:datePickerMode="spinner"
+ android:layout_width="wrap_content"
+ android:layout_height="0dp"
+ android:layout_weight="1"
+ android:calendarViewShown="false"
+ android:spinnersShown="true" />
+
+</LinearLayout>
diff --git a/java/com/android/dialer/callstats/res/menu/call_stats_options.xml b/java/com/android/dialer/callstats/res/menu/call_stats_options.xml
new file mode 100644
index 0000000..ae4b7eb
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/menu/call_stats_options.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ 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.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android" >
+ <item
+ android:id="@+id/date_filter"
+ android:showAsAction="never"
+ android:title="@string/call_stats_date_filter" />
+
+ <item
+ android:id="@+id/reset_date_filter"
+ android:showAsAction="never"
+ android:visible="false"
+ android:title="@string/call_stats_reset_filter" />
+
+ <item
+ android:id="@+id/sort_by_duration"
+ android:showAsAction="never"
+ android:visible="false"
+ android:title="@string/call_stats_sort_by_duration" />
+
+ <item
+ android:id="@+id/sort_by_count"
+ android:showAsAction="never"
+ android:title="@string/call_stats_sort_by_count" />
+
+</menu>
diff --git a/java/com/android/dialer/callstats/res/values/cm_arrays.xml b/java/com/android/dialer/callstats/res/values/cm_arrays.xml
new file mode 100644
index 0000000..ee49865
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/values/cm_arrays.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013-2014 The CyanogenMod Project
+ Copyright (C) 2018 The LineageOS 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- 0: sec
+ 1: min
+ 2: min sec
+ 3: hour
+ 4: hour sec
+ 5: hour min
+ 6: hour min sec -->
+
+ <string-array name="call_stats_duration">
+ <item><xliff:g id="seconds" example="2 secs">%3$s</xliff:g></item>
+ <item><xliff:g id="minutes" example="2 mins">%2$s</xliff:g></item>
+ <item><xliff:g id="minutes" example="2 mins">%2$s</xliff:g> <xliff:g id="seconds" example="2 secs">%3$s</xliff:g></item>
+ <item><xliff:g id="hours" example="2 hrs">%1$s</xliff:g></item>
+ <item><xliff:g id="hours" example="2 hrs">%1$s</xliff:g> <xliff:g id="seconds" example="2 secs">%3$s</xliff:g></item>
+ <item><xliff:g id="hours" example="2 hrs">%1$s</xliff:g> <xliff:g id="minutes" example="2 mins">%2$s</xliff:g></item>
+ <item><xliff:g id="hours" example="2 hrs">%1$s</xliff:g> <xliff:g id="minutes" example="2 mins">%2$s</xliff:g> <xliff:g id="seconds" example="2 secs">%3$s</xliff:g></item>
+ </string-array>
+
+</resources>
diff --git a/java/com/android/dialer/callstats/res/values/cm_plurals.xml b/java/com/android/dialer/callstats/res/values/cm_plurals.xml
new file mode 100644
index 0000000..536e3f8
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/values/cm_plurals.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013 The CyanogenMod 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <plurals name="hour">
+ <item quantity="one">1 hr</item>
+ <item quantity="other">%d hrs</item>
+ </plurals>
+ <plurals name="minute">
+ <item quantity="one">1 min</item>
+ <item quantity="other">%d mins</item>
+ </plurals>
+ <plurals name="second">
+ <item quantity="one">1 sec</item>
+ <item quantity="other">%d secs</item>
+ </plurals>
+
+ <plurals name="call">
+ <item quantity="one">1 call</item>
+ <item quantity="other">%d calls</item>
+ </plurals>
+</resources>
+
diff --git a/java/com/android/dialer/callstats/res/values/cm_strings.xml b/java/com/android/dialer/callstats/res/values/cm_strings.xml
new file mode 100644
index 0000000..0e3fd91
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/values/cm_strings.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013-2014 The CyanogenMod Project
+ Copyright (C) 2018 The LineageOS 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="call_stats_detail_title">Contact statistics details</string>
+
+ <string name="call_stats_incoming">Incoming: <xliff:g id="value">%s</xliff:g></string>
+ <string name="call_stats_outgoing">Outgoing: <xliff:g id="value">%s</xliff:g></string>
+ <string name="call_stats_missed">Missed: <xliff:g id="value">%s</xliff:g></string>
+ <string name="call_stats_blocked">Blocked: <xliff:g id="value">%s</xliff:g></string>
+ <string name="call_stats_percent"><xliff:g id="percent">%d</xliff:g>%%</string>
+ <string name="call_stats_header_total">Total: <xliff:g id="call_count">%1$s</xliff:g>, <xliff:g id="duration">%2$s</xliff:g></string>
+ <string name="call_stats_header_total_callsonly">Total: <xliff:g id="call_count">%s</xliff:g></string>
+ <string name="call_stats_filter_from">Start date</string>
+ <string name="call_stats_filter_to">End date</string>
+ <string name="call_stats_filter_picker_title">Filter range</string>
+
+ <string name="date_quick_selection">Quick selection</string>
+ <string name="date_qs_currentmonth">Current month</string>
+ <string name="date_qs_currentquarter">Current quarter</string>
+ <string name="date_qs_currentyear">Current year</string>
+ <string name="date_qs_lastweek">Last week</string>
+ <string name="date_qs_lastmonth">Last month</string>
+ <string name="date_qs_lastquarter">Last quarter</string>
+ <string name="date_qs_lastyear">Last year</string>
+
+ <string name="call_stats_date_filter">Adjust time range</string>
+ <string name="call_stats_reset_filter">Reset time range</string>
+ <string name="call_stats_sort_by_duration">Sort by call duration</string>
+ <string name="call_stats_sort_by_count">Sort by call count</string>
+
+ <string name="call_stats_title_for_number">This number</string>
+ <string name="call_stats_title_of_total">Of total</string>
+ <string name="call_stats_title_durations">Call durations</string>
+ <string name="call_stats_title_count">Call count</string>
+ <string name="call_stats_title_average_duration">Average call duration</string>
+
+ <!-- Text displayed when there are no call log entries in the selected time range. -->
+ <string name="recent_calls_no_items_in_range">Your call log does not contain any calls in the selected time range.</string>
+</resources>
diff --git a/java/com/android/dialer/callstats/res/values/colors.xml b/java/com/android/dialer/callstats/res/values/colors.xml
new file mode 100644
index 0000000..40472cf
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/values/colors.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright (C) 2018 The LineageOS 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
+-->
+
+<resources>
+ <!-- Colors for incoming and outgoing calls in the call statistics -->
+ <color name="call_stats_bar_background">#88888888</color>
+</resources>
diff --git a/java/com/android/dialer/callstats/res/values/styles.xml b/java/com/android/dialer/callstats/res/values/styles.xml
new file mode 100644
index 0000000..3aae797
--- /dev/null
+++ b/java/com/android/dialer/callstats/res/values/styles.xml
@@ -0,0 +1,36 @@
+<!--
+ ~ Copyright (C) 2018 The LineageOS 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
+ -->
+
+<resources>
+ <style name="CallStatsBarStyle">
+ <item name="android:layout_height">4dp</item>
+ <item name="android:layout_marginTop">6dp</item>
+ <item name="android:layout_marginBottom">6dp</item>
+ <item name="android:orientation">horizontal</item>
+ <item name="backgroundColor">@color/call_stats_bar_background</item>
+ <item name="blueColor">@color/answered_incoming_call</item>
+ <item name="greenColor">@color/answered_outgoing_call</item>
+ <item name="redColor">@color/missed_call</item>
+ <item name="orangeColor">@color/blocked_call</item>
+ </style>
+
+ <style name="TextAppearance.CallStatsCategoryTitle" parent="TextAppearance.AppCompat.Body2">
+ <item name="android:textAllCaps">true</item>
+ <item name="android:textSize">11sp</item>
+ <!-- 0.8 Spacing, 0.8/11 = 0.072727273 -->
+ <item name="android:letterSpacing">0.072727273</item>
+ </style>
+</resources>
diff --git a/java/com/android/dialer/database/DialerDatabaseHelper.java b/java/com/android/dialer/database/DialerDatabaseHelper.java
index 719492e..9d26398 100644
--- a/java/com/android/dialer/database/DialerDatabaseHelper.java
+++ b/java/com/android/dialer/database/DialerDatabaseHelper.java
@@ -280,6 +280,34 @@
db.execSQL("ALTER TABLE smartdial_table ADD carrier_presence INTEGER NOT NULL DEFAULT 0");
}
+ @Override
+ public void onDowngrade(SQLiteDatabase db, int oldNumber, int newNumber) {
+ // Disregard the old version and new versions provided by SQLiteOpenHelper, we will read
+ // our own from the database.
+
+ int oldVersion;
+
+ oldVersion = getPropertyAsInt(db, DATABASE_VERSION_PROPERTY, 0);
+
+ if (oldVersion == 0) {
+ LogUtil.e(
+ "DialerDatabaseHelper.onDowngrade", "malformed database version..recreating database");
+ setupTables(db);
+ return;
+ }
+
+ if (oldVersion == 70011) {
+ oldVersion = 10;
+ }
+
+ if (oldVersion != DATABASE_VERSION) {
+ throw new IllegalStateException(
+ "error downgrading the database to version " + DATABASE_VERSION);
+ }
+
+ setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
+ }
+
/** Stores a key-value pair in the {@link Tables#PROPERTIES} table. */
public void setProperty(String key, String value) {
setProperty(getWritableDatabase(), key, value);
diff --git a/java/com/android/dialer/dialpadview/DialpadFragment.java b/java/com/android/dialer/dialpadview/DialpadFragment.java
index 12c82c9..234457e 100644
--- a/java/com/android/dialer/dialpadview/DialpadFragment.java
+++ b/java/com/android/dialer/dialpadview/DialpadFragment.java
@@ -50,6 +50,7 @@
import android.support.design.widget.FloatingActionButton;
import android.telecom.PhoneAccount;
import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
import android.telephony.PhoneNumberFormattingTextWatcher;
import android.telephony.PhoneNumberUtils;
import android.telephony.ServiceState;
@@ -64,6 +65,7 @@
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
+import android.view.SubMenu;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
@@ -204,6 +206,8 @@
private boolean isLayoutRtl;
private boolean isLandscape;
+ private PhoneAccountHandle selectedAccount;
+
private DialerExecutor<String> initPhoneNumberFormattingTextWatcherExecutor;
private boolean isDialpadSlideUp;
@@ -1021,6 +1025,37 @@
item.setVisible(CallUtil.isCallWithSubjectSupported(getContext()));
}
}
+
+ final MenuItem callWithItem = menu.findItem(R.id.call_with);
+ List<PhoneAccount> accounts =
+ CallUtil.getCallCapablePhoneAccounts(getContext(), PhoneAccount.SCHEME_TEL);
+ if (accounts != null && accounts.size() > 1) {
+ final PhoneAccountHandle selected;
+ if (selectedAccount != null) {
+ selected = selectedAccount;
+ } else {
+ selected = TelecomUtil.getDefaultOutgoingPhoneAccount(getContext(),
+ PhoneAccount.SCHEME_TEL);
+ }
+
+ SubMenu callWithMenu = callWithItem.getSubMenu();
+ callWithMenu.clear();
+
+ for (PhoneAccount account : accounts) {
+ final PhoneAccountHandle handle = account.getAccountHandle();
+ final Intent intent = new Intent()
+ .putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, handle);
+
+ callWithMenu.add(Menu.FIRST, Menu.NONE, Menu.NONE, account.getLabel())
+ .setIntent(intent)
+ .setChecked(handle.equals(selected));
+ }
+ callWithMenu.setGroupCheckable(Menu.FIRST, true, true);
+ callWithItem.setVisible(callWithMenu.hasVisibleItems());
+ } else {
+ callWithItem.setVisible(false);
+ }
+
super.show();
}
};
@@ -1186,7 +1221,9 @@
// Clear the digits just in case.
clearDialpad();
} else {
- PreCall.start(getContext(), new CallIntentBuilder(number, CallInitiationType.Type.DIALPAD));
+ CallIntentBuilder builder = new CallIntentBuilder(number, CallInitiationType.Type.DIALPAD)
+ .setPhoneAccountHandle(selectedAccount);
+ PreCall.start(getContext(), builder);
hideAndClearDialpad();
}
}
@@ -1196,6 +1233,7 @@
if (digits != null) {
digits.getText().clear();
}
+ selectedAccount = null;
}
private void handleDialButtonClickWithEmptyDigits() {
@@ -1420,6 +1458,11 @@
@Override
public boolean onMenuItemClick(MenuItem item) {
+ if (item.getGroupId() == Menu.FIRST) {
+ Intent intent = item.getIntent();
+ selectedAccount = intent.getParcelableExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE);
+ return true;
+ }
int resId = item.getItemId();
if (resId == R.id.menu_2s_pause) {
updateDialString(PAUSE);
diff --git a/java/com/android/dialer/dialpadview/res/menu/dialpad_options.xml b/java/com/android/dialer/dialpadview/res/menu/dialpad_options.xml
index 2921ea3..760a004 100644
--- a/java/com/android/dialer/dialpadview/res/menu/dialpad_options.xml
+++ b/java/com/android/dialer/dialpadview/res/menu/dialpad_options.xml
@@ -14,7 +14,11 @@
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
-
+ <item
+ android:id="@+id/call_with"
+ android:title="@string/call_via">
+ <menu />
+ </item>
<item
android:id="@+id/menu_2s_pause"
android:showAsAction="withText"
diff --git a/java/com/android/dialer/lookup/AndroidManifest.xml b/java/com/android/dialer/lookup/AndroidManifest.xml
new file mode 100644
index 0000000..0a278db
--- /dev/null
+++ b/java/com/android/dialer/lookup/AndroidManifest.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2017 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
+ -->
+
+<manifest
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.dialer.lookup">
+
+ <uses-sdk android:minSdkVersion="23"/>
+ <uses-permission android:name="lineageos.permission.WRITE_SETTINGS"/>
+
+ <application>
+ <provider android:name="com.android.dialer.lookup.LookupProvider"
+ android:authorities="com.android.dialer.lookup"
+ android:exported="false"
+ android:multiprocess="false" />
+
+ </application>
+</manifest>
diff --git a/java/com/android/dialer/lookup/ContactBuilder.java b/java/com/android/dialer/lookup/ContactBuilder.java
new file mode 100644
index 0000000..e88f956
--- /dev/null
+++ b/java/com/android/dialer/lookup/ContactBuilder.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredName;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Directory;
+import android.provider.ContactsContract.DisplayNameSources;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.util.Constants;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.R;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.json.JSONTokener;
+import org.w3c.dom.Text;
+
+import java.sql.Struct;
+import java.util.ArrayList;
+
+public class ContactBuilder {
+ private static final String TAG = ContactBuilder.class.getSimpleName();
+
+ private static final boolean DEBUG = false;
+
+ /** Default photo for businesses if no other image is found */
+ public static final String PHOTO_URI_BUSINESS = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+ .authority("com.android.dialer")
+ .appendPath(String.valueOf(R.drawable.ic_places_picture_180_holo_light))
+ .build()
+ .toString();
+
+ private final ArrayList<Address> addresses = new ArrayList<>();
+ private final ArrayList<PhoneNumber> phoneNumbers = new ArrayList<>();
+ private final ArrayList<WebsiteUrl> websites = new ArrayList<>();
+
+ private final long directoryId;
+ private Name name;
+ private final String normalizedNumber;
+ private final String formattedNumber;
+ private Uri photoUri;
+
+ public static ContactBuilder forForwardLookup(String number) {
+ return new ContactBuilder(DirectoryId.NEARBY, null, number);
+ }
+
+ public static ContactBuilder forPeopleLookup(String number) {
+ return new ContactBuilder(DirectoryId.PEOPLE, null, number);
+ }
+
+ public static ContactBuilder forReverseLookup(String normalizedNumber, String formattedNumber) {
+ return new ContactBuilder(DirectoryId.NULL, normalizedNumber, formattedNumber);
+ }
+
+ private ContactBuilder(long directoryId, String normalizedNumber, String formattedNumber) {
+ this.directoryId = directoryId;
+ this.normalizedNumber = normalizedNumber;
+ this.formattedNumber = formattedNumber;
+ }
+
+ public ContactBuilder(Uri encodedContactUri) throws JSONException {
+ String jsonData = encodedContactUri.getEncodedFragment();
+ String directoryIdStr = encodedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+ long directoryId = DirectoryId.DEFAULT;
+
+ if (!TextUtils.isEmpty(directoryIdStr)) {
+ try {
+ directoryId = Long.parseLong(directoryIdStr);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "Error parsing directory id of uri " + encodedContactUri, e);
+ }
+ }
+
+ this.directoryId = directoryId;
+ this.formattedNumber = null;
+ this.normalizedNumber = null;
+
+ try {
+ // name
+ JSONObject json = new JSONObject(jsonData);
+ JSONObject contact = json.optJSONObject(Contacts.CONTENT_ITEM_TYPE);
+ JSONObject nameObj = contact.optJSONObject(StructuredName.CONTENT_ITEM_TYPE);
+ name = new Name(nameObj);
+
+ if (contact != null) {
+ // numbers
+ if (contact.has(Phone.CONTENT_ITEM_TYPE)) {
+ String phoneData = contact.getString(Phone.CONTENT_ITEM_TYPE);
+ Object phoneObject = new JSONTokener(phoneData).nextValue();
+ JSONArray phoneNumbersJson;
+ if (phoneObject instanceof JSONObject) {
+ phoneNumbersJson = new JSONArray();
+ phoneNumbersJson.put(phoneObject);
+ } else {
+ phoneNumbersJson = contact.getJSONArray(Phone.CONTENT_ITEM_TYPE);
+ }
+ for (int i = 0; i < phoneNumbersJson.length(); ++i) {
+ JSONObject phoneObj = phoneNumbersJson.getJSONObject(i);
+ phoneNumbers.add(new PhoneNumber(phoneObj));
+ }
+ }
+
+ // address
+ if (contact.has(StructuredPostal.CONTENT_ITEM_TYPE)) {
+ JSONArray addressesJson = contact.getJSONArray(StructuredPostal.CONTENT_ITEM_TYPE);
+ for (int i = 0; i < addressesJson.length(); ++i) {
+ JSONObject addrObj = addressesJson.getJSONObject(i);
+ addresses.add(new Address(addrObj));
+ }
+ }
+
+ // websites
+ if (contact.has(Website.CONTENT_ITEM_TYPE)) {
+ JSONArray websitesJson = contact.getJSONArray(Website.CONTENT_ITEM_TYPE);
+ for (int i = 0; i < websitesJson.length(); ++i) {
+ JSONObject websiteObj = websitesJson.getJSONObject(i);
+ final WebsiteUrl websiteUrl = new WebsiteUrl(websiteObj);
+ if (!TextUtils.isEmpty(websiteUrl.url)) {
+ websites.add(new WebsiteUrl(websiteObj));
+ }
+ }
+ }
+ }
+ } catch(JSONException e) {
+ Log.e(TAG, "Error parsing encoded fragment of uri " + encodedContactUri, e);
+ throw e;
+ }
+ }
+
+ public ContactBuilder addAddress(Address address) {
+ if (DEBUG) Log.d(TAG, "Adding address");
+ if (address != null) {
+ addresses.add(address);
+ }
+ return this;
+ }
+
+ public ContactBuilder addPhoneNumber(PhoneNumber phoneNumber) {
+ if (DEBUG) Log.d(TAG, "Adding phone number");
+ if (phoneNumber != null) {
+ phoneNumbers.add(phoneNumber);
+ }
+ return this;
+ }
+
+ public ContactBuilder addWebsite(WebsiteUrl website) {
+ if (DEBUG) Log.d(TAG, "Adding website");
+ if (website != null) {
+ websites.add(website);
+ }
+ return this;
+ }
+
+ public ContactBuilder setName(Name name) {
+ if (DEBUG) Log.d(TAG, "Setting name");
+ if (name != null) {
+ this.name = name;
+ }
+ return this;
+ }
+
+ public ContactBuilder setPhotoUri(String photoUri) {
+ if (photoUri != null) {
+ setPhotoUri(Uri.parse(photoUri));
+ }
+ return this;
+ }
+
+ public ContactBuilder setPhotoUri(Uri photoUri) {
+ if (DEBUG) Log.d(TAG, "Setting photo URI");
+ this.photoUri = photoUri;
+ return this;
+ }
+
+ public ContactInfo build() {
+ if (name == null) {
+ throw new IllegalStateException("Name has not been set");
+ }
+
+ // Use the incoming call's phone number if no other phone number
+ // is specified. The reverse lookup source could present the phone
+ // number differently (eg. without the area code).
+ if (phoneNumbers.isEmpty()) {
+ PhoneNumber pn = new PhoneNumber();
+ // Use the formatted number where possible
+ pn.number = formattedNumber != null
+ ? formattedNumber : normalizedNumber;
+ pn.type = Phone.TYPE_MAIN;
+ addPhoneNumber(pn);
+ }
+
+ try {
+ JSONObject contact = new JSONObject();
+
+ // Insert the name
+ contact.put(StructuredName.CONTENT_ITEM_TYPE, name.getJsonObject());
+
+ // Insert phone numbers
+ JSONArray phoneNumbersJson = new JSONArray();
+ for (PhoneNumber number : phoneNumbers) {
+ phoneNumbersJson.put(number.getJsonObject());
+ }
+ contact.put(Phone.CONTENT_ITEM_TYPE, phoneNumbersJson);
+
+ // Insert addresses if there are any
+ if (!addresses.isEmpty()) {
+ JSONArray addressesJson = new JSONArray();
+ for (Address address : addresses) {
+ addressesJson.put(address.getJsonObject());
+ }
+ contact.put(StructuredPostal.CONTENT_ITEM_TYPE, addressesJson);
+ }
+
+ // Insert websites if there are any
+ if (!websites.isEmpty()) {
+ JSONArray websitesJson = new JSONArray();
+ for (WebsiteUrl site : websites) {
+ websitesJson.put(site.getJsonObject());
+ }
+ contact.put(Website.CONTENT_ITEM_TYPE, websitesJson);
+ }
+
+ ContactInfo info = new ContactInfo();
+ info.name = name.displayName;
+ info.normalizedNumber = normalizedNumber;
+ info.number = phoneNumbers.get(0).number;
+ info.type = phoneNumbers.get(0).type;
+ info.label = phoneNumbers.get(0).label;
+ info.photoUri = photoUri;
+
+ String json = new JSONObject()
+ .put(Contacts.DISPLAY_NAME, name.displayName)
+ .put(Contacts.DISPLAY_NAME_SOURCE, DisplayNameSources.ORGANIZATION)
+ .put(Directory.EXPORT_SUPPORT, Directory.EXPORT_SUPPORT_ANY_ACCOUNT)
+ .put(Contacts.CONTENT_ITEM_TYPE, contact)
+ .toString();
+
+ if (json != null) {
+ info.lookupUri = Contacts.CONTENT_LOOKUP_URI
+ .buildUpon()
+ .appendPath(Constants.LOOKUP_URI_ENCODED)
+ .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
+ String.valueOf(directoryId))
+ .encodedFragment(json)
+ .build();
+ }
+
+ return info;
+ } catch (JSONException e) {
+ Log.e(TAG, "Failed to build contact", e);
+ return null;
+ }
+ }
+
+ // android.provider.ContactsContract.CommonDataKinds.StructuredPostal
+ public static class Address {
+ public String formattedAddress;
+ public int type;
+ public String label;
+ public String street;
+ public String poBox;
+ public String neighborhood;
+ public String city;
+ public String region;
+ public String postCode;
+ public String country;
+
+ public static Address createFormattedHome(String address) {
+ if (address == null) {
+ return null;
+ }
+ Address a = new Address();
+ a.formattedAddress = address;
+ a.type = StructuredPostal.TYPE_HOME;
+ return a;
+ }
+
+ public JSONObject getJsonObject() throws JSONException {
+ JSONObject json = new JSONObject();
+ json.putOpt(StructuredPostal.FORMATTED_ADDRESS, formattedAddress);
+ json.put(StructuredPostal.TYPE, type);
+ json.putOpt(StructuredPostal.LABEL, label);
+ json.putOpt(StructuredPostal.STREET, street);
+ json.putOpt(StructuredPostal.POBOX, poBox);
+ json.putOpt(StructuredPostal.NEIGHBORHOOD, neighborhood);
+ json.putOpt(StructuredPostal.CITY, city);
+ json.putOpt(StructuredPostal.REGION, region);
+ json.putOpt(StructuredPostal.POSTCODE, postCode);
+ json.putOpt(StructuredPostal.COUNTRY, country);
+ return json;
+ }
+
+ public Address() {}
+
+ public Address(JSONObject json) throws JSONException {
+ if (json.has(StructuredPostal.FORMATTED_ADDRESS)) {
+ formattedAddress = json.getString(StructuredPostal.FORMATTED_ADDRESS);
+ }
+ }
+
+ public String toString() {
+ return "formattedAddress: " + formattedAddress + "; " +
+ "type: " + type + "; " +
+ "label: " + label + "; " +
+ "street: " + street + "; " +
+ "poBox: " + poBox + "; " +
+ "neighborhood: " + neighborhood + "; " +
+ "city: " + city + "; " +
+ "region: " + region + "; " +
+ "postCode: " + postCode + "; " +
+ "country: " + country;
+ }
+ }
+
+ // android.provider.ContactsContract.CommonDataKinds.StructuredName
+ public static class Name {
+ public String displayName;
+ public String givenName;
+ public String familyName;
+ public String prefix;
+ public String middleName;
+ public String suffix;
+ public String phoneticGivenName;
+ public String phoneticMiddleName;
+ public String phoneticFamilyName;
+
+ public static Name createDisplayName(String displayName) {
+ Name name = new Name();
+ name.displayName = displayName;
+ return name;
+ }
+
+ public JSONObject getJsonObject() throws JSONException {
+ JSONObject json = new JSONObject();
+ json.putOpt(StructuredName.DISPLAY_NAME, displayName);
+ json.putOpt(StructuredName.GIVEN_NAME, givenName);
+ json.putOpt(StructuredName.FAMILY_NAME, familyName);
+ json.putOpt(StructuredName.PREFIX, prefix);
+ json.putOpt(StructuredName.MIDDLE_NAME, middleName);
+ json.putOpt(StructuredName.SUFFIX, suffix);
+ json.putOpt(StructuredName.PHONETIC_GIVEN_NAME, phoneticGivenName);
+ json.putOpt(StructuredName.PHONETIC_MIDDLE_NAME, phoneticMiddleName);
+ json.putOpt(StructuredName.PHONETIC_FAMILY_NAME, phoneticFamilyName);
+ return json;
+ }
+
+ public Name(JSONObject json) throws JSONException {
+ if (json != null) {
+ displayName = json.optString(StructuredName.DISPLAY_NAME, null);
+ }
+ }
+
+ public Name() {}
+
+ public String toString() {
+ return "displayName: " + displayName + "; " +
+ "givenName: " + givenName + "; " +
+ "familyName: " + familyName + "; " +
+ "prefix: " + prefix + "; " +
+ "middleName: " + middleName + "; " +
+ "suffix: " + suffix + "; " +
+ "phoneticGivenName: " + phoneticGivenName + "; " +
+ "phoneticMiddleName: " + phoneticMiddleName + "; " +
+ "phoneticFamilyName: " + phoneticFamilyName;
+ }
+ }
+
+ // android.provider.ContactsContract.CommonDataKinds.Phone
+ public static class PhoneNumber {
+ public String number;
+ public int type;
+ public String label;
+
+ public static PhoneNumber createMainNumber(String number) {
+ PhoneNumber n = new PhoneNumber();
+ n.number = number;
+ n.type = Phone.TYPE_MAIN;
+ return n;
+ }
+
+ public JSONObject getJsonObject() throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put(Phone.NUMBER, number);
+ json.put(Phone.TYPE, type);
+ json.putOpt(Phone.LABEL, label);
+ return json;
+ }
+
+ public PhoneNumber(JSONObject json) throws JSONException {
+ number = json.getString(Phone.NUMBER);
+ type = json.getInt(Phone.TYPE);
+ if (json.has(Phone.LABEL)) {
+ label = json.getString(Phone.LABEL);
+ }
+ }
+
+ public PhoneNumber() {}
+
+ public String toString() {
+ return "number: " + number + "; " +
+ "type: " + type + "; " +
+ "label: " + label;
+ }
+ }
+
+ // android.provider.ContactsContract.CommonDataKinds.Website
+ public static class WebsiteUrl {
+ public String url;
+ public int type;
+ public String label;
+
+ public static WebsiteUrl createProfile(String url) {
+ if (url == null) {
+ return null;
+ }
+ WebsiteUrl u = new WebsiteUrl();
+ u.url = url;
+ u.type = Website.TYPE_PROFILE;
+ return u;
+ }
+
+ public JSONObject getJsonObject() throws JSONException {
+ JSONObject json = new JSONObject();
+ json.put(Website.URL, url);
+ json.put(Website.TYPE, type);
+ json.putOpt(Website.LABEL, label);
+ return json;
+ }
+
+ public WebsiteUrl() {}
+
+ public WebsiteUrl(JSONObject json) throws JSONException {
+ if (json.has(Website.URL)) {
+ url = json.getString(Website.URL);
+ }
+ if (json.has(Website.TYPE)) {
+ type = json.getInt(Website.TYPE);
+ }
+ if (json.has(Website.LABEL)) {
+ label = json.getString(Website.LABEL);
+ }
+ }
+
+ public String toString() {
+ return "url: " + url + "; " +
+ "type: " + type + "; " +
+ "label: " + label;
+ }
+ }
+}
diff --git a/java/com/android/dialer/lookup/DirectoryId.java b/java/com/android/dialer/lookup/DirectoryId.java
new file mode 100644
index 0000000..023585c
--- /dev/null
+++ b/java/com/android/dialer/lookup/DirectoryId.java
@@ -0,0 +1,33 @@
+package com.android.dialer.lookup;
+
+import android.net.Uri;
+import android.provider.ContactsContract;
+
+public class DirectoryId {
+ // default contacts directory
+ public static final long DEFAULT = ContactsContract.Directory.DEFAULT;
+
+ // id for a non existant directory
+ public static final long NULL = Long.MAX_VALUE;
+
+ // id for nearby forward lookup results (not a real directory)
+ public static final long NEARBY = NULL - 1;
+
+ // id for people forward lookup results (not a real directory)
+ public static final long PEOPLE = NULL - 2;
+
+ public static boolean isFakeDirectory(long directory) {
+ return directory == NULL || directory == NEARBY || directory == PEOPLE;
+ }
+
+ public static long fromUri(Uri lookupUri) {
+ long directory = DirectoryId.DEFAULT;
+ if (lookupUri != null) {
+ String dqp = lookupUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY);
+ if (dqp != null) {
+ directory = Long.valueOf(dqp);
+ }
+ }
+ return directory;
+ }
+}
diff --git a/java/com/android/dialer/lookup/ForwardLookup.java b/java/com/android/dialer/lookup/ForwardLookup.java
new file mode 100644
index 0000000..2f59aeb
--- /dev/null
+++ b/java/com/android/dialer/lookup/ForwardLookup.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup;
+
+import android.content.Context;
+import android.location.Location;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.google.GoogleForwardLookup;
+import com.android.dialer.lookup.openstreetmap.OpenStreetMapForwardLookup;
+
+import java.util.List;
+
+public abstract class ForwardLookup {
+ private static final String TAG = ForwardLookup.class.getSimpleName();
+
+ private static ForwardLookup INSTANCE = null;
+
+ public static ForwardLookup getInstance(Context context) {
+ String provider = LookupSettings.getForwardLookupProvider(context);
+
+ if (INSTANCE == null || !isInstance(provider)) {
+ Log.d(TAG, "Chosen forward lookup provider: " + provider);
+
+ if (provider.equals(LookupSettings.FLP_GOOGLE)) {
+ INSTANCE = new GoogleForwardLookup(context);
+ } else if (provider.equals(LookupSettings.FLP_OPENSTREETMAP)) {
+ INSTANCE = new OpenStreetMapForwardLookup(context);
+ }
+ }
+
+ return INSTANCE;
+ }
+
+ private static boolean isInstance(String provider) {
+ if (provider.equals(LookupSettings.FLP_GOOGLE)
+ && INSTANCE instanceof GoogleForwardLookup) {
+ return true;
+ } else if (provider.equals(LookupSettings.FLP_OPENSTREETMAP)
+ && INSTANCE instanceof OpenStreetMapForwardLookup) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public abstract List<ContactInfo> lookup(Context context, String filter, Location lastLocation);
+}
diff --git a/java/com/android/dialer/lookup/LookupCache.java b/java/com/android/dialer/lookup/LookupCache.java
new file mode 100644
index 0000000..6fe3a9f
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupCache.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chenxiaolong@cxl.epac.to>
+ *
+ * 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.dialer.lookup;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+import android.util.JsonReader;
+import android.util.JsonWriter;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.util.DialerUtils;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStreamReader;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+public class LookupCache {
+ private static final String TAG = LookupCache.class.getSimpleName();
+
+ public static final String NAME = "Name";
+ public static final String TYPE = "Type";
+ public static final String LABEL = "Label";
+ public static final String NUMBER = "Number";
+ public static final String FORMATTED_NUMBER = "FormattedNumber";
+ public static final String NORMALIZED_NUMBER = "NormalizedNumber";
+ public static final String PHOTO_ID = "PhotoID";
+ public static final String LOOKUP_URI = "LookupURI";
+
+ public static boolean hasCachedContact(Context context, String number) {
+ String normalizedNumber = formatE164(context, number);
+ if (normalizedNumber == null) {
+ return false;
+ }
+
+ File file = getFilePath(context, normalizedNumber);
+ return file.exists();
+ }
+
+ public static void cacheContact(Context context, ContactInfo info) {
+ File file = getFilePath(context, info.normalizedNumber);
+
+ if (file.exists()) {
+ file.delete();
+ }
+
+ FileOutputStream out = null;
+ JsonWriter writer = null;
+
+ try {
+ out = new FileOutputStream(file);
+ writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
+ writer.setIndent(" ");
+ List messages = new ArrayList();
+
+ writer.beginObject();
+ if (info.name != null) {
+ writer.name(NAME).value(info.name);
+ }
+ writer.name(TYPE).value(info.type);
+ if (info.label != null) {
+ writer.name(LABEL).value(info.label);
+ }
+ if (info.number != null) {
+ writer.name(NUMBER).value(info.number);
+ }
+ if (info.formattedNumber != null) {
+ writer.name(FORMATTED_NUMBER).value(info.formattedNumber);
+ }
+ if (info.normalizedNumber != null) {
+ writer.name(NORMALIZED_NUMBER).value(info.normalizedNumber);
+ }
+ writer.name(PHOTO_ID).value(info.photoId);
+
+ if (info.lookupUri != null) {
+ writer.name(LOOKUP_URI).value(info.lookupUri.toString());
+ }
+
+ // We do not save the photo URI. If there's a cached image, that
+ // will be used when the contact is retrieved. Otherwise, photoUri
+ // will be set to null.
+
+ writer.endObject();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ DialerUtils.closeQuietly(writer);
+ DialerUtils.closeQuietly(out);
+ }
+ }
+
+ public static ContactInfo getCachedContact(Context context, String number) {
+ String normalizedNumber = formatE164(context, number);
+ if (normalizedNumber == null) {
+ return null;
+ }
+
+ File file = getFilePath(context, normalizedNumber);
+ if (!file.exists()) {
+ // Whatever is calling this should probably check anyway
+ return null;
+ }
+
+ ContactInfo info = new ContactInfo();
+
+ FileInputStream in = null;
+ JsonReader reader = null;
+
+ try {
+ in = new FileInputStream(file);
+ reader = new JsonReader(new InputStreamReader(in, "UTF-8"));
+
+ reader.beginObject();
+ while (reader.hasNext()) {
+ String name = reader.nextName();
+
+ if (NAME.equals(name)) {
+ info.name = reader.nextString();
+ } else if (TYPE.equals(name)) {
+ info.type = reader.nextInt();
+ } else if (LABEL.equals(name)) {
+ info.label = reader.nextString();
+ } else if (NUMBER.equals(name)) {
+ info.number = reader.nextString();
+ } else if (FORMATTED_NUMBER.equals(name)) {
+ info.formattedNumber = reader.nextString();
+ } else if (NORMALIZED_NUMBER.equals(name)) {
+ info.normalizedNumber = reader.nextString();
+ } else if (PHOTO_ID.equals(name)) {
+ info.photoId = reader.nextInt();
+ } else if (LOOKUP_URI.equals(name)) {
+ Uri lookupUri = Uri.parse(reader.nextString());
+
+ if (hasCachedImage(context, normalizedNumber)) {
+ // Insert cached photo URI
+ Uri image = Uri.withAppendedPath(LookupProvider.IMAGE_CACHE_URI,
+ Uri.encode(normalizedNumber));
+
+ String json = lookupUri.getEncodedFragment();
+ if (json != null) {
+ try {
+ JSONObject jsonObj = new JSONObject(json);
+ jsonObj.putOpt(Contacts.PHOTO_URI, image.toString());
+ lookupUri = lookupUri.buildUpon()
+ .encodedFragment(jsonObj.toString())
+ .build();
+ } catch (JSONException e) {
+ Log.e(TAG, "Failed to add image URI to json", e);
+ }
+ }
+
+ info.photoUri = image;
+ }
+
+ info.lookupUri = lookupUri;
+ }
+ }
+ reader.endObject();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } finally {
+ DialerUtils.closeQuietly(reader);
+ DialerUtils.closeQuietly(in);
+ }
+
+ return info;
+ }
+
+ public static void deleteCachedContacts(Context context) {
+ File dir = new File(context.getCacheDir(), "lookup");
+ if (!dir.exists()) {
+ Log.v(TAG, "Lookup cache directory does not exist. Not clearing it.");
+ return;
+ }
+
+ if (!dir.isDirectory()) {
+ Log.e(TAG, "Path " + dir + " is not a directory");
+ return;
+ }
+
+ File[] files = dir.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isFile()) {
+ file.delete();
+ }
+ }
+ }
+ }
+
+ public static void deleteCachedContact(Context context, String normalizedNumber) {
+ File f = getFilePath(context, normalizedNumber);
+ if (f.exists()) {
+ f.delete();
+ }
+
+ f = getImagePath(context, normalizedNumber);
+ if (f.exists()) {
+ f.delete();
+ }
+ }
+
+ public static boolean hasCachedImage(Context context, String number) {
+ String normalizedNumber = formatE164(context, number);
+ if (normalizedNumber == null) {
+ return false;
+ }
+
+ File file = getImagePath(context, normalizedNumber);
+ return file.exists();
+ }
+
+ public static Uri cacheImage(Context context, String normalizedNumber, Bitmap bmp) {
+ // Compress the cached images to save space
+ if (bmp == null) {
+ Log.e(TAG, "Failed to cache image");
+ return null;
+ }
+
+ File image = getImagePath(context, normalizedNumber);
+ FileOutputStream out = null;
+
+ try {
+ out = new FileOutputStream(image);
+ bmp.compress(Bitmap.CompressFormat.WEBP, 100, out);
+ return Uri.fromFile(image);
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ DialerUtils.closeQuietly(out);
+ }
+ return null;
+ }
+
+ public static Bitmap getCachedImage(Context context, String normalizedNumber) {
+ File image = getImagePath(context, normalizedNumber);
+ if (!image.exists()) {
+ return null;
+ }
+
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+ return BitmapFactory.decodeFile(image.getPath(), options);
+ }
+
+ private static String formatE164(Context context, String number) {
+ TelephonyManager tm = context.getSystemService(TelephonyManager.class);
+ String countryIso = tm.getSimCountryIso().toUpperCase();
+ return PhoneNumberUtils.formatNumberToE164(number, countryIso);
+ }
+
+ private static File getFilePath(Context context, String normalizedNumber) {
+ File dir = new File(context.getCacheDir(), "lookup");
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+
+ return new File(dir, normalizedNumber + ".json");
+ }
+
+ public static File getImagePath(Context context, String normalizedNumber) {
+ File dir = new File(context.getCacheDir(), "lookup");
+ if (!dir.exists()) {
+ dir.mkdirs();
+ }
+
+ return new File(dir, normalizedNumber + ".webp");
+ }
+}
diff --git a/java/com/android/dialer/lookup/LookupCacheService.java b/java/com/android/dialer/lookup/LookupCacheService.java
new file mode 100644
index 0000000..43dc9f0
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupCacheService.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2018 The LineageOS 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.dialer.lookup;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+
+import com.android.dialer.logging.ContactSource;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.ContactInfo;
+
+import java.io.InputStream;
+
+public class LookupCacheService implements CachedNumberLookupService {
+ @Override
+ public CachedContactInfo buildCachedContactInfo(ContactInfo info) {
+ return new LookupCachedContactInfo(info);
+ }
+
+ @Override
+ public void addContact(Context context, CachedContactInfo cachedInfo) {
+ LookupCache.cacheContact(context, cachedInfo.getContactInfo());
+ }
+
+ @Override
+ public CachedContactInfo lookupCachedContactFromNumber(Context context, String number) {
+ ContactInfo info = LookupCache.getCachedContact(context, number);
+ return info != null ? new LookupCachedContactInfo(info) : null;
+ }
+
+ @Override
+ public void clearAllCacheEntries(Context context) {
+ LookupCache.deleteCachedContacts(context);
+ }
+
+ @Override
+ public boolean isBusiness(ContactSource.Type sourceType) {
+ // We don't store source type, so assume false
+ return false;
+ }
+
+ @Override
+ public boolean canReportAsInvalid(ContactSource.Type sourceType, String objectId) {
+ return false;
+ }
+
+ @Override
+ public boolean reportAsInvalid(Context context, CachedContactInfo cachedContactInfo) {
+ return false;
+ }
+
+ @Override
+ public @Nullable Uri addPhoto(Context context, String number, InputStream in) {
+ TelephonyManager tm = context.getSystemService(TelephonyManager.class);
+ String countryIso = tm.getSimCountryIso().toUpperCase();
+ String normalized = number != null
+ ? PhoneNumberUtils.formatNumberToE164(number, countryIso) : null;
+ if (normalized != null) {
+ Bitmap bitmap = BitmapFactory.decodeStream(in, null, null);
+ if (bitmap != null) {
+ return LookupCache.cacheImage(context, normalized, bitmap);
+ }
+ }
+ return null;
+ }
+
+ private static class LookupCachedContactInfo implements CachedContactInfo {
+ private final ContactInfo info;
+
+ private LookupCachedContactInfo(ContactInfo info) {
+ this.info = info;
+ }
+
+ @Override
+ @NonNull public ContactInfo getContactInfo() {
+ return info;
+ }
+
+ @Override
+ public void setSource(ContactSource.Type sourceType, String name, long directoryId) {
+ }
+
+ @Override
+ public void setDirectorySource(String name, long directoryId) {
+ }
+
+ @Override
+ public void setLookupKey(String lookupKey) {
+ }
+ }
+}
diff --git a/java/com/android/dialer/lookup/LookupProvider.java b/java/com/android/dialer/lookup/LookupProvider.java
new file mode 100644
index 0000000..5e8fc1d
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupProvider.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup;
+
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.UriMatcher;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.location.Criteria;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.ParcelFileDescriptor;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.Contacts;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.dialer.searchfragment.common.Projections;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.dialer.R;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.TimeUnit;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+public class LookupProvider extends ContentProvider {
+ private static final String TAG = LookupProvider.class.getSimpleName();
+
+ private static final boolean DEBUG = false;
+
+ public static final String AUTHORITY = "com.android.dialer.lookup";
+ public static final Uri AUTHORITY_URI = Uri.parse("content://" + AUTHORITY);
+ public static final Uri NEARBY_LOOKUP_URI =
+ Uri.withAppendedPath(AUTHORITY_URI, "nearby");
+ public static final Uri PEOPLE_LOOKUP_URI =
+ Uri.withAppendedPath(AUTHORITY_URI, "people");
+ public static final Uri NEARBY_AND_PEOPLE_LOOKUP_URI =
+ Uri.withAppendedPath(AUTHORITY_URI, "nearby_and_people");
+ public static final Uri IMAGE_CACHE_URI =
+ Uri.withAppendedPath(AUTHORITY_URI, "images");
+
+ private static final UriMatcher uriMatcher = new UriMatcher(-1);
+ private final LinkedList<FutureTask> activeTasks = new LinkedList<>();
+
+ private static final int NEARBY = 0;
+ private static final int PEOPLE = 1;
+ private static final int NEARBY_AND_PEOPLE = 2;
+ private static final int IMAGE = 3;
+
+ static {
+ uriMatcher.addURI(AUTHORITY, "nearby/*", NEARBY);
+ uriMatcher.addURI(AUTHORITY, "people/*", PEOPLE);
+ uriMatcher.addURI(AUTHORITY, "nearby_and_people/*", NEARBY_AND_PEOPLE);
+ uriMatcher.addURI(AUTHORITY, "images/*", IMAGE);
+ }
+
+ private class FutureCallable<T> implements Callable<T> {
+ private final Callable<T> callable;
+ private volatile FutureTask<T> future;
+
+ public FutureCallable(Callable<T> callable) {
+ future = null;
+ this.callable = callable;
+ }
+
+ public T call() throws Exception {
+ Log.v(TAG, "Future called for " + Thread.currentThread().getName());
+
+ T result = callable.call();
+ if (future == null) {
+ return result;
+ }
+
+ synchronized (activeTasks) {
+ activeTasks.remove(future);
+ }
+
+ future = null;
+ return result;
+ }
+
+ public void setFuture(FutureTask<T> future) {
+ this.future = future;
+ }
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public Cursor query(Uri uri, final String[] projection, String selection,
+ String[] selectionArgs, String sortOrder) {
+ if (DEBUG) Log.v(TAG, "query: " + uri);
+
+ Location lastLocation = null;
+ final int match = uriMatcher.match(uri);
+
+ switch (match) {
+ case NEARBY:
+ case NEARBY_AND_PEOPLE:
+ if (!PermissionsUtil.hasLocationPermissions(getContext())) {
+ Log.v(TAG, "Location permission is missing, can not determine location.");
+ } else if (!isLocationEnabled()) {
+ Log.v(TAG, "Location settings is disabled, can no determine location.");
+ } else {
+ lastLocation = getLastLocation();
+ }
+ if (match == NEARBY && lastLocation == null) {
+ Log.v(TAG, "No location available, ignoring query.");
+ return null;
+ }
+ // fall through to the actual query
+
+ case PEOPLE:
+ final String filter = Uri.encode(uri.getLastPathSegment());
+ String limit = uri.getQueryParameter(ContactsContract.LIMIT_PARAM_KEY);
+
+ int maxResults = -1;
+
+ try {
+ if (limit != null) {
+ maxResults = Integer.parseInt(limit);
+ }
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "query: invalid limit parameter: '" + limit + "'");
+ }
+
+ final Location finalLastLocation = lastLocation;
+ final int finalMaxResults = maxResults;
+
+ return execute(new Callable<Cursor>() {
+ @Override
+ public Cursor call() {
+ return handleFilter(match, projection, filter, finalMaxResults, finalLastLocation);
+ }
+ }, "FilterThread");
+ }
+
+ return null;
+ }
+
+ @Override
+ public Uri insert(Uri uri, ContentValues values) {
+ throw new UnsupportedOperationException("insert() not supported");
+ }
+
+ @Override
+ public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException("update() not supported");
+ }
+
+ @Override
+ public int delete(Uri uri, String selection, String[] selectionArgs) {
+ throw new UnsupportedOperationException("delete() not supported");
+ }
+
+ @Override
+ public String getType(Uri uri) {
+ int match = uriMatcher.match(uri);
+
+ switch (match) {
+ case NEARBY:
+ case PEOPLE:
+ case NEARBY_AND_PEOPLE:
+ return Contacts.CONTENT_ITEM_TYPE;
+
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
+ switch (uriMatcher.match(uri)) {
+ case IMAGE:
+ String number = uri.getLastPathSegment();
+ File image = LookupCache.getImagePath(getContext(), number);
+
+ if (mode.equals("r")) {
+ if (image == null || !image.exists() || !image.isFile()) {
+ throw new FileNotFoundException("Cached image does not exist");
+ }
+
+ return ParcelFileDescriptor.open(image, ParcelFileDescriptor.MODE_READ_ONLY);
+ } else {
+ throw new FileNotFoundException("The URI is read only");
+ }
+
+ default:
+ throw new FileNotFoundException("Invalid URI: " + uri);
+ }
+ }
+
+ /**
+ * Check if the location services is on.
+ *
+ * @return Whether location services are enabled
+ */
+ private boolean isLocationEnabled() {
+ try {
+ int mode = Settings.Secure.getInt(getContext().getContentResolver(),
+ Settings.Secure.LOCATION_MODE);
+
+ return mode != Settings.Secure.LOCATION_MODE_OFF;
+ } catch (Settings.SettingNotFoundException e) {
+ Log.e(TAG, "Failed to get location mode", e);
+ return false;
+ }
+ }
+
+ /**
+ * Get location from last location query.
+ *
+ * @return The last location
+ */
+ private Location getLastLocation() {
+ LocationManager locationManager = getContext().getSystemService(LocationManager.class);
+
+ try {
+ locationManager.requestSingleUpdate(new Criteria(), new LocationListener() {
+ @Override
+ public void onLocationChanged(Location location) {
+ }
+
+ @Override
+ public void onProviderDisabled(String provider) {
+ }
+
+ @Override
+ public void onProviderEnabled(String provider) {
+ }
+
+ @Override
+ public void onStatusChanged(String provider, int status, Bundle extras) {
+ }
+ }, Looper.getMainLooper());
+
+ return locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
+ } catch (IllegalArgumentException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Process filter/query and perform the lookup.
+ *
+ * @param projection Columns to include in query
+ * @param filter String to lookup
+ * @param maxResults Maximum number of results
+ * @param lastLocation Coordinates of last location query
+ * @return Cursor for the results
+ */
+ private Cursor handleFilter(int type, String[] projection, String filter,
+ int maxResults, Location lastLocation) {
+ if (DEBUG) Log.v(TAG, "handleFilter(" + filter + ")");
+
+ if (filter == null) {
+ return null;
+ }
+
+ try {
+ filter = URLDecoder.decode(filter, "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ }
+
+ ArrayList<ContactInfo> results = null;
+ if ((type == NEARBY || type == NEARBY_AND_PEOPLE) && lastLocation != null) {
+ ForwardLookup fl = ForwardLookup.getInstance(getContext());
+ List<ContactInfo> nearby = fl.lookup(getContext(), filter, lastLocation);
+ if (nearby != null) {
+ results.addAll(nearby);
+ }
+ }
+ if (type == PEOPLE || type == NEARBY_AND_PEOPLE) {
+ PeopleLookup pl = PeopleLookup.getInstance(getContext());
+ List<ContactInfo> people = pl.lookup(getContext(), filter);
+ if (people != null) {
+ results.addAll(people);
+ }
+ }
+
+ if (results.isEmpty()) {
+ if (DEBUG) Log.v(TAG, "handleFilter(" + filter + "): No results");
+ return null;
+ }
+
+ Cursor cursor = null;
+ try {
+ cursor = buildResultCursor(projection, results, maxResults);
+ if (DEBUG) {
+ Log.v(TAG, "handleFilter(" + filter + "): " + cursor.getCount() + " matches");
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON failure", e);
+ }
+
+ return cursor;
+ }
+
+ /**
+ * Query results.
+ *
+ * @param projection Columns to include in query
+ * @param results Results for the forward lookup
+ * @param maxResults Maximum number of rows/results to add to cursor
+ * @return Cursor for forward lookup query results
+ */
+ private Cursor buildResultCursor(String[] projection, List<ContactInfo> results, int maxResults)
+ throws JSONException {
+ // Extended directories always use this projection
+ MatrixCursor cursor = new MatrixCursor(Projections.DATA_PROJECTION);
+
+ int id = 1;
+ for (ContactInfo result : results) {
+ Object[] row = new Object[Projections.DATA_PROJECTION.length];
+
+ row[Projections.ID] = id;
+ row[Projections.PHONE_TYPE] = result.type;
+ row[Projections.PHONE_LABEL] = getAddress(result);
+ row[Projections.PHONE_NUMBER] = result.number;
+ row[Projections.DISPLAY_NAME] = result.name;
+ row[Projections.PHOTO_ID] = 0;
+ row[Projections.PHOTO_URI] = result.photoUri;
+ row[Projections.LOOKUP_KEY] = result.lookupUri.getEncodedFragment();
+ row[Projections.CONTACT_ID] = id;
+
+ cursor.addRow(row);
+
+ if (maxResults != -1 && cursor.getCount() >= maxResults) {
+ break;
+ }
+
+ id++;
+ }
+
+ return cursor;
+ }
+
+ private String getAddress(ContactInfo info) {
+ // Hack: Show city or address for phone label, so they appear in the results list
+
+ String city = null;
+ String address = null;
+
+ try {
+ String jsonString = info.lookupUri.getEncodedFragment();
+ JSONObject json = new JSONObject(jsonString);
+ JSONObject contact = json.getJSONObject(Contacts.CONTENT_ITEM_TYPE);
+
+ if (!contact.has(StructuredPostal.CONTENT_ITEM_TYPE)) {
+ return null;
+ }
+
+ JSONArray addresses = contact.getJSONArray(StructuredPostal.CONTENT_ITEM_TYPE);
+ if (addresses.length() == 0) {
+ return null;
+ }
+
+ JSONObject addressEntry = addresses.getJSONObject(0);
+ if (addressEntry.has(StructuredPostal.CITY)) {
+ city = addressEntry.getString(StructuredPostal.CITY);
+ }
+ if (addressEntry.has(StructuredPostal.FORMATTED_ADDRESS)) {
+ address = addressEntry.getString(StructuredPostal.FORMATTED_ADDRESS);
+ }
+ } catch (JSONException e) {
+ Log.e(TAG, "Failed to get address", e);
+ }
+
+ if (city != null) {
+ return city;
+ } else if (address != null) {
+ return address;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Execute thread that is killed after a specified amount of time.
+ *
+ * @param callable The thread
+ * @param name Name of the thread
+ * @return Instance of the thread
+ */
+ private <T> T execute(Callable<T> callable, String name) {
+ FutureCallable<T> futureCallable = new FutureCallable<T>(callable);
+ FutureTask<T> future = new FutureTask<T>(futureCallable);
+ futureCallable.setFuture(future);
+
+ synchronized (activeTasks) {
+ activeTasks.addLast(future);
+ Log.v(TAG, "Currently running tasks: " + activeTasks.size());
+
+ while (activeTasks.size() > 8) {
+ Log.w(TAG, "Too many tasks, canceling one");
+ activeTasks.removeFirst().cancel(true);
+ }
+ }
+
+ Log.v(TAG, "Starting task " + name);
+
+ new Thread(future, name).start();
+
+ try {
+ Log.v(TAG, "Getting future " + name);
+ return future.get(10000, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Task was interrupted: " + name);
+ Thread.currentThread().interrupt();
+ } catch (ExecutionException e) {
+ Log.w(TAG, "Task threw an exception: " + name, e);
+ } catch (TimeoutException e) {
+ Log.w(TAG, "Task timed out: " + name);
+ future.cancel(true);
+ } catch (CancellationException e) {
+ Log.w(TAG, "Task was cancelled: " + name);
+ }
+
+ return null;
+ }
+}
diff --git a/java/com/android/dialer/lookup/LookupSettings.java b/java/com/android/dialer/lookup/LookupSettings.java
new file mode 100644
index 0000000..2686f68
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupSettings.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.provider.Settings;
+
+import java.util.List;
+
+public final class LookupSettings {
+ private static final String TAG = LookupSettings.class.getSimpleName();
+
+ /** Forward lookup providers */
+ public static final String FLP_GOOGLE = "Google";
+ public static final String FLP_OPENSTREETMAP = "OpenStreetMap";
+ public static final String FLP_DEFAULT = FLP_GOOGLE;
+
+ /** People lookup providers */
+ public static final String PLP_AUSKUNFT = "Auskunft";
+ public static final String PLP_DEFAULT = PLP_AUSKUNFT;
+
+ /** Reverse lookup providers */
+ public static final String RLP_OPENCNAM = "OpenCnam";
+ public static final String RLP_YELLOWPAGES = "YellowPages";
+ public static final String RLP_YELLOWPAGES_CA = "YellowPages_CA";
+ public static final String RLP_ZABASEARCH = "ZabaSearch";
+ public static final String RLP_CYNGN_CHINESE = "CyngnChinese";
+ public static final String RLP_DASTELEFONBUCH = "DasTelefonbuch";
+ public static final String RLP_AUSKUNFT = "Auskunft";
+ public static final String RLP_DEFAULT = RLP_OPENCNAM;
+
+ /** Preferences */
+ private static final String SHARED_PREFERENCES_NAME = "lookup_settings";
+ private static final String ENABLE_FORWARD_LOOKUP = "enable_forward_lookup";
+ private static final String ENABLE_PEOPLE_LOOKUP = "enable_people_lookup";
+ private static final String ENABLE_REVERSE_LOOKUP = "enable_reverse_lookup";
+ private static final String FORWARD_LOOKUP_PROVIDER = "forward_lookup_provider";
+ private static final String PEOPLE_LOOKUP_PROVIDER = "people_lookup_provider";
+ private static final String REVERSE_LOOKUP_PROVIDER = "reverse_lookup_provider";
+ private static final String OPENCNAM_ACCOUNT_SID = "opencnam_account_sid";
+ private static final String OPENCNAM_AUTH_TOKEN = "opencnam_auth_token";
+
+ private LookupSettings() {
+ }
+
+ private static SharedPreferences getSharedPreferences(Context context) {
+ return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
+ }
+
+ public static boolean isForwardLookupEnabled(Context context) {
+ return getSharedPreferences(context).getBoolean(ENABLE_FORWARD_LOOKUP, false);
+ }
+
+ public static void setForwardLookupEnabled(Context context, boolean value) {
+ getSharedPreferences(context).edit().putBoolean(ENABLE_FORWARD_LOOKUP, value).commit();
+ }
+
+ public static boolean isPeopleLookupEnabled(Context context) {
+ return getSharedPreferences(context).getBoolean(ENABLE_PEOPLE_LOOKUP, false);
+ }
+
+ public static void setPeopleLookupEnabled(Context context, boolean value) {
+ getSharedPreferences(context).edit().putBoolean(ENABLE_PEOPLE_LOOKUP, value).commit();
+ }
+
+ public static boolean isReverseLookupEnabled(Context context) {
+ return getSharedPreferences(context).getBoolean(ENABLE_REVERSE_LOOKUP, false);
+ }
+
+ public static void setReverseLookupEnabled(Context context, boolean value) {
+ getSharedPreferences(context).edit().putBoolean(ENABLE_REVERSE_LOOKUP, value).commit();
+ }
+
+ public static String getForwardLookupProvider(Context context) {
+ return getSharedPreferences(context).getString(FORWARD_LOOKUP_PROVIDER, FLP_DEFAULT);
+ }
+
+ public static void setForwardLookupProvider(Context context, String value) {
+ getSharedPreferences(context).edit().putString(FORWARD_LOOKUP_PROVIDER, value).commit();
+ }
+
+ public static String getPeopleLookupProvider(Context context) {
+ return getSharedPreferences(context).getString(PEOPLE_LOOKUP_PROVIDER, PLP_DEFAULT);
+ }
+
+ public static void setPeopleLookupProvider(Context context, String value) {
+ getSharedPreferences(context).edit().putString(PEOPLE_LOOKUP_PROVIDER, value).commit();
+ }
+
+ public static String getReverseLookupProvider(Context context) {
+ return getSharedPreferences(context).getString(REVERSE_LOOKUP_PROVIDER, RLP_DEFAULT);
+ }
+
+ public static void setReverseLookupProvider(Context context, String value) {
+ getSharedPreferences(context).edit().putString(REVERSE_LOOKUP_PROVIDER, value).commit();
+ }
+
+ public static String getOpenCnamAccountSid(Context context) {
+ return getSharedPreferences(context).getString(OPENCNAM_ACCOUNT_SID, null);
+ }
+
+ public static void setOpenCnamAccountSid(Context context, String value) {
+ getSharedPreferences(context).edit().putString(OPENCNAM_ACCOUNT_SID, value).commit();
+ }
+
+ public static String getOpenCnamAuthToken(Context context) {
+ return getSharedPreferences(context).getString(OPENCNAM_AUTH_TOKEN, null);
+ }
+
+ public static void setOpenCnamAuthToken(Context context, String value) {
+ getSharedPreferences(context).edit().putString(OPENCNAM_AUTH_TOKEN, value).commit();
+ }
+}
diff --git a/java/com/android/dialer/lookup/LookupSettingsFragment.java b/java/com/android/dialer/lookup/LookupSettingsFragment.java
new file mode 100644
index 0000000..bca92f9
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupSettingsFragment.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.dialer.lookup;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.ListPreference;
+import android.preference.PreferenceFragment;
+import android.preference.SwitchPreference;
+
+import com.android.dialer.R;
+
+import java.util.Arrays;
+
+public class LookupSettingsFragment extends PreferenceFragment
+ implements Preference.OnPreferenceChangeListener {
+
+ private static final String KEY_ENABLE_FORWARD_LOOKUP = "enable_forward_lookup";
+ private static final String KEY_ENABLE_PEOPLE_LOOKUP = "enable_people_lookup";
+ private static final String KEY_ENABLE_REVERSE_LOOKUP = "enable_reverse_lookup";
+ private static final String KEY_FORWARD_LOOKUP_PROVIDER = "forward_lookup_provider";
+ private static final String KEY_PEOPLE_LOOKUP_PROVIDER = "people_lookup_provider";
+ private static final String KEY_REVERSE_LOOKUP_PROVIDER = "reverse_lookup_provider";
+
+ private SwitchPreference enableForwardLookup;
+ private SwitchPreference enablePeopleLookup;
+ private SwitchPreference enableReverseLookup;
+ private ListPreference forwardLookupProvider;
+ private ListPreference peopleLookupProvider;
+ private ListPreference reverseLookupProvider;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ addPreferencesFromResource(R.xml.lookup_settings);
+
+ enableForwardLookup = (SwitchPreference) findPreference(KEY_ENABLE_FORWARD_LOOKUP);
+ enablePeopleLookup = (SwitchPreference) findPreference(KEY_ENABLE_PEOPLE_LOOKUP);
+ enableReverseLookup = (SwitchPreference) findPreference(KEY_ENABLE_REVERSE_LOOKUP);
+
+ enableForwardLookup.setOnPreferenceChangeListener(this);
+ enablePeopleLookup.setOnPreferenceChangeListener(this);
+ enableReverseLookup.setOnPreferenceChangeListener(this);
+
+ forwardLookupProvider = (ListPreference) findPreference(KEY_FORWARD_LOOKUP_PROVIDER);
+ peopleLookupProvider = (ListPreference) findPreference(KEY_PEOPLE_LOOKUP_PROVIDER);
+ reverseLookupProvider = (ListPreference) findPreference(KEY_REVERSE_LOOKUP_PROVIDER);
+
+ forwardLookupProvider.setOnPreferenceChangeListener(this);
+ peopleLookupProvider.setOnPreferenceChangeListener(this);
+ reverseLookupProvider.setOnPreferenceChangeListener(this);
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ restoreLookupProviderSwitches();
+ restoreLookupProviders();
+ }
+
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object newValue) {
+ Context context = getContext();
+
+ if (preference == enableForwardLookup) {
+ LookupSettings.setForwardLookupEnabled(context, (Boolean) newValue);
+ } else if (preference == enablePeopleLookup) {
+ LookupSettings.setPeopleLookupEnabled(context, (Boolean) newValue);
+ } else if (preference == enableReverseLookup) {
+ LookupSettings.setReverseLookupEnabled(context, (Boolean) newValue);
+ } else if (preference == forwardLookupProvider) {
+ LookupSettings.setForwardLookupProvider(context, (String) newValue);
+ } else if (preference == peopleLookupProvider) {
+ LookupSettings.setPeopleLookupProvider(context, (String) newValue);
+ } else if (preference == reverseLookupProvider) {
+ LookupSettings.setReverseLookupProvider(context, (String) newValue);
+ }
+
+ return true;
+ }
+
+ private void restoreLookupProviderSwitches() {
+ Context context = getContext();
+
+ enableForwardLookup.setChecked(LookupSettings.isForwardLookupEnabled(context));
+ enablePeopleLookup.setChecked(LookupSettings.isPeopleLookupEnabled(context));
+ enableReverseLookup.setChecked(LookupSettings.isReverseLookupEnabled(context));
+ }
+
+ private void restoreLookupProviders() {
+ Context context = getContext();
+
+ restoreLookupProvider(forwardLookupProvider, LookupSettings.getForwardLookupProvider(context));
+ restoreLookupProvider(peopleLookupProvider, LookupSettings.getPeopleLookupProvider(context));
+ restoreLookupProvider(reverseLookupProvider, LookupSettings.getReverseLookupProvider(context));
+ }
+
+ private void restoreLookupProvider(ListPreference pref, String provider) {
+ Context context = getContext();
+
+ if (pref.getEntries().length < 1) {
+ pref.setEnabled(false);
+ return;
+ }
+
+ if (provider == null) {
+ pref.setValueIndex(0);
+
+ if (pref == forwardLookupProvider) {
+ LookupSettings.setForwardLookupProvider(context, pref.getValue());
+ } else if (pref == peopleLookupProvider) {
+ LookupSettings.setPeopleLookupProvider(context, pref.getValue());
+ } else if (pref == reverseLookupProvider) {
+ LookupSettings.setReverseLookupProvider(context, pref.getValue());
+ }
+ } else {
+ pref.setValue(provider);
+ }
+ }
+}
diff --git a/java/com/android/dialer/lookup/LookupUtils.java b/java/com/android/dialer/lookup/LookupUtils.java
new file mode 100644
index 0000000..b6e4533
--- /dev/null
+++ b/java/com/android/dialer/lookup/LookupUtils.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.dialer.lookup;
+
+import android.text.Html;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.charset.IllegalCharsetNameException;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class LookupUtils {
+ private static final String USER_AGENT =
+ "Mozilla/5.0 (X11; Linux x86_64; rv:42.0) Gecko/20100101 Firefox/42.0";
+
+ private static HttpURLConnection prepareHttpConnection(String url, Map<String, String> headers)
+ throws IOException {
+ // open connection
+ HttpURLConnection urlConnection = (HttpURLConnection) new URL(url).openConnection();
+ // set user agent (default value is null)
+ urlConnection.setRequestProperty("User-Agent", USER_AGENT);
+ // set all other headers if not null
+ if (headers != null) {
+ for (Map.Entry<String, String> header : headers.entrySet()) {
+ urlConnection.setRequestProperty(header.getKey(), header.getValue());
+ }
+ }
+
+ return urlConnection;
+ }
+
+ private static byte[] httpFetch(HttpURLConnection urlConnection) throws IOException {
+ // query url, read and return buffered response body
+ // we want to make sure that the connection gets closed here
+ InputStream is = new BufferedInputStream(urlConnection.getInputStream());
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ byte[] result = null;
+ try {
+ byte[] partial = new byte[4096];
+ int read;
+ while ((read = is.read(partial, 0, 4096)) != -1) {
+ baos.write(partial, 0, read);
+ }
+ result = baos.toByteArray();
+ } finally {
+ is.close();
+ baos.close();
+ }
+ return result;
+ }
+
+ private static Charset determineCharset(HttpURLConnection connection) {
+ String contentType = connection.getContentType();
+ if (contentType != null) {
+ String[] split = contentType.split(";");
+ for (int i = 0; i < split.length; i++) {
+ String trimmed = split[i].trim();
+ if (trimmed.startsWith("charset=")) {
+ try {
+ return Charset.forName(trimmed.substring(8));
+ } catch (IllegalCharsetNameException | UnsupportedCharsetException e) {
+ // we don't know about this charset -> ignore
+ }
+ }
+ }
+ }
+ return Charset.defaultCharset();
+ }
+
+ public static String httpGet(String url, Map<String, String> headers) throws IOException {
+ HttpURLConnection connection = prepareHttpConnection(url, headers);
+ try {
+ byte[] response = httpFetch(connection);
+ return new String(response, determineCharset(connection));
+ } finally {
+ connection.disconnect();
+ }
+ }
+
+ public static byte[] httpGetBytes(String url, Map<String, String> headers) throws IOException {
+ HttpURLConnection connection = prepareHttpConnection(url, headers);
+ try {
+ return httpFetch(connection);
+ } finally {
+ connection.disconnect();
+ }
+ }
+
+ public static String httpPost(String url, Map<String, String> headers, String postData)
+ throws IOException {
+ HttpURLConnection connection = prepareHttpConnection(url, headers);
+
+ try {
+ // write postData to buffered output stream
+ if (postData != null) {
+ connection.setDoOutput(true);
+ BufferedWriter bw = new BufferedWriter(
+ new OutputStreamWriter(connection.getOutputStream()));
+ try {
+ bw.write(postData, 0, postData.length());
+ // close connection and re-throw exception
+ } finally {
+ bw.close();
+ }
+ }
+ byte[] response = httpFetch(connection);
+ return new String(response, determineCharset(connection));
+ } finally {
+ connection.disconnect();
+ }
+ }
+
+ public static List<String> allRegexResults(String input, String regex, boolean dotall) {
+ if (input == null) {
+ return null;
+ }
+ Pattern pattern = Pattern.compile(regex, dotall ? Pattern.DOTALL : 0);
+ Matcher matcher = pattern.matcher(input);
+
+ List<String> regexResults = new ArrayList<String>();
+ while (matcher.find()) {
+ regexResults.add(matcher.group(1).trim());
+ }
+ return regexResults;
+ }
+
+ public static String firstRegexResult(String input, String regex, boolean dotall) {
+ if (input == null) {
+ return null;
+ }
+ Pattern pattern = Pattern.compile(regex, dotall ? Pattern.DOTALL : 0);
+ Matcher m = pattern.matcher(input);
+ return m.find() ? m.group(1).trim() : null;
+ }
+
+ public static String fromHtml(String input) {
+ if (input == null) {
+ return null;
+ }
+ return Html.fromHtml(input).toString().trim();
+ }
+}
diff --git a/java/com/android/dialer/lookup/PeopleLookup.java b/java/com/android/dialer/lookup/PeopleLookup.java
new file mode 100644
index 0000000..c7e53df
--- /dev/null
+++ b/java/com/android/dialer/lookup/PeopleLookup.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.auskunft.AuskunftPeopleLookup;
+
+import java.util.List;
+
+public abstract class PeopleLookup {
+ private static final String TAG = PeopleLookup.class.getSimpleName();
+
+ private static PeopleLookup INSTANCE = null;
+
+ public static PeopleLookup getInstance(Context context) {
+ String provider = LookupSettings.getPeopleLookupProvider(context);
+
+ if (INSTANCE == null || !isInstance(provider)) {
+ Log.d(TAG, "Chosen people lookup provider: " + provider);
+
+ if (provider.equals(LookupSettings.PLP_AUSKUNFT)) {
+ INSTANCE = new AuskunftPeopleLookup(context);
+ }
+ }
+
+ return INSTANCE;
+ }
+
+ private static boolean isInstance(String provider) {
+ if (provider.equals(LookupSettings.PLP_AUSKUNFT)
+ && INSTANCE instanceof AuskunftPeopleLookup) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public abstract List<ContactInfo> lookup(Context context, String filter);
+}
diff --git a/java/com/android/dialer/lookup/ReverseLookup.java b/java/com/android/dialer/lookup/ReverseLookup.java
new file mode 100644
index 0000000..a2cc896
--- /dev/null
+++ b/java/com/android/dialer/lookup/ReverseLookup.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.auskunft.AuskunftReverseLookup;
+import com.android.dialer.lookup.dastelefonbuch.TelefonbuchReverseLookup;
+import com.android.dialer.lookup.opencnam.OpenCnamReverseLookup;
+import com.android.dialer.lookup.yellowpages.YellowPagesReverseLookup;
+import com.android.dialer.lookup.zabasearch.ZabaSearchReverseLookup;
+
+import java.io.IOException;
+
+public abstract class ReverseLookup {
+ private static final String TAG = ReverseLookup.class.getSimpleName();
+
+ private static ReverseLookup INSTANCE = null;
+
+ public static ReverseLookup getInstance(Context context) {
+ String provider = LookupSettings.getReverseLookupProvider(context);
+
+ if (INSTANCE == null || !isInstance(provider)) {
+ Log.d(TAG, "Chosen reverse lookup provider: " + provider);
+
+ if (provider.equals(LookupSettings.RLP_OPENCNAM)) {
+ INSTANCE = new OpenCnamReverseLookup(context);
+ } else if (provider.equals(LookupSettings.RLP_YELLOWPAGES)
+ || provider.equals(LookupSettings.RLP_YELLOWPAGES_CA)) {
+ INSTANCE = new YellowPagesReverseLookup(context, provider);
+ } else if (provider.equals(LookupSettings.RLP_ZABASEARCH)) {
+ INSTANCE = new ZabaSearchReverseLookup(context);
+ } else if (provider.equals(LookupSettings.RLP_DASTELEFONBUCH)) {
+ INSTANCE = new TelefonbuchReverseLookup(context);
+ } else if (provider.equals(LookupSettings.RLP_AUSKUNFT)) {
+ INSTANCE = new AuskunftReverseLookup(context);
+ }
+ }
+
+ return INSTANCE;
+ }
+
+ private static boolean isInstance(String provider) {
+ if (provider.equals(LookupSettings.RLP_OPENCNAM)
+ && INSTANCE instanceof OpenCnamReverseLookup) {
+ return true;
+ } else if ((provider.equals(LookupSettings.RLP_YELLOWPAGES)
+ || provider.equals(LookupSettings.RLP_YELLOWPAGES_CA))
+ && INSTANCE instanceof YellowPagesReverseLookup) {
+ return true;
+ } else if (provider.equals(LookupSettings.RLP_ZABASEARCH)
+ && INSTANCE instanceof ZabaSearchReverseLookup) {
+ return true;
+ } else if (provider.equals(LookupSettings.RLP_DASTELEFONBUCH)
+ && INSTANCE instanceof TelefonbuchReverseLookup) {
+ return true;
+ } else if (provider.equals(LookupSettings.RLP_AUSKUNFT)
+ && INSTANCE instanceof AuskunftReverseLookup) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Lookup image
+ *
+ * @param context The application context
+ * @param uri The image URI
+ */
+ public Bitmap lookupImage(Context context, Uri uri) {
+ return null;
+ }
+
+ /**
+ * Perform phone number lookup.
+ *
+ * @param context The application context
+ * @param normalizedNumber The normalized phone number
+ * @param formattedNumber The formatted phone number
+ * @return The phone number info object
+ */
+ public abstract ContactInfo lookupNumber(Context context,
+ String normalizedNumber, String formattedNumber) throws IOException;
+}
diff --git a/java/com/android/dialer/lookup/ReverseLookupService.java b/java/com/android/dialer/lookup/ReverseLookupService.java
new file mode 100644
index 0000000..02e873b
--- /dev/null
+++ b/java/com/android/dialer/lookup/ReverseLookupService.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Message;
+import android.telephony.PhoneNumberUtils;
+import android.telephony.TelephonyManager;
+
+import com.android.dialer.location.GeoUtil;
+import com.android.dialer.logging.ContactLookupResult;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.incallui.bindings.PhoneNumberService;
+
+import java.io.IOException;
+
+public class ReverseLookupService implements PhoneNumberService, Handler.Callback {
+ private final HandlerThread backgroundThread;
+ private final Handler backgroundHandler;
+ private final Handler handler;
+ private final Context context;
+ private final TelephonyManager telephonyManager;
+
+ private static final int MSG_LOOKUP = 1;
+ private static final int MSG_NOTIFY_NUMBER = 2;
+
+ public ReverseLookupService(Context context) {
+ this.context = context;
+ telephonyManager = context.getSystemService(TelephonyManager.class);
+
+ // TODO: stop after a while?
+ backgroundThread = new HandlerThread("ReverseLookup");
+ backgroundThread.start();
+
+ backgroundHandler = new Handler(backgroundThread.getLooper(), this);
+ handler = new Handler(this);
+ }
+
+ @Override
+ public void getPhoneNumberInfo(String phoneNumber, NumberLookupListener numberListener) {
+ if (!LookupSettings.isReverseLookupEnabled(context)) {
+ LookupCache.deleteCachedContacts(context);
+ return;
+ }
+
+ String countryIso = telephonyManager.getSimCountryIso().toUpperCase();
+ String normalizedNumber = phoneNumber != null
+ ? PhoneNumberUtils.formatNumberToE164(phoneNumber, countryIso) : null;
+
+ // Can't do reverse lookup without a number
+ if (normalizedNumber == null) {
+ return;
+ }
+
+ LookupRequest request = new LookupRequest();
+ request.normalizedNumber = normalizedNumber;
+ request.formattedNumber = PhoneNumberUtils.formatNumber(phoneNumber,
+ request.normalizedNumber, GeoUtil.getCurrentCountryIso(context));
+ request.numberListener = numberListener;
+
+ backgroundHandler.obtainMessage(MSG_LOOKUP, request).sendToTarget();
+ }
+
+ @Override
+ public boolean handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_LOOKUP: {
+ // background thread
+ LookupRequest request = (LookupRequest) msg.obj;
+ request.contactInfo = doLookup(request);
+ if (request.contactInfo != null) {
+ handler.obtainMessage(MSG_NOTIFY_NUMBER, request).sendToTarget();
+ }
+ break;
+ }
+ case MSG_NOTIFY_NUMBER: {
+ // main thread
+ LookupRequest request = (LookupRequest) msg.obj;
+ if (request.numberListener != null) {
+ LookupNumberInfo info = new LookupNumberInfo(request.contactInfo);
+ request.numberListener.onPhoneNumberInfoComplete(info);
+ }
+ break;
+ }
+ }
+
+ return true;
+ }
+
+ private ContactInfo doLookup(LookupRequest request) {
+ final String number = request.normalizedNumber;
+
+ if (LookupCache.hasCachedContact(context, number)) {
+ ContactInfo info = LookupCache.getCachedContact(context, number);
+ if (!ContactInfo.EMPTY.equals(info)) {
+ return info;
+ } else if (info != null) {
+ // If we have an empty cached contact, remove it and redo lookup
+ LookupCache.deleteCachedContact(context, number);
+ }
+ }
+
+ try {
+ ReverseLookup inst = ReverseLookup.getInstance(context);
+ ContactInfo info = inst.lookupNumber(context, number, request.formattedNumber);
+ if (info != null && !info.equals(ContactInfo.EMPTY)) {
+ LookupCache.cacheContact(context, info);
+ return info;
+ }
+ } catch (IOException e) {
+ // ignored
+ }
+
+ return null;
+ }
+
+ private Bitmap fetchImage(LookupRequest request, Uri uri) {
+ if (!LookupCache.hasCachedImage(context, request.normalizedNumber)) {
+ Bitmap bmp = ReverseLookup.getInstance(context).lookupImage(context, uri);
+ if (bmp != null) {
+ LookupCache.cacheImage(context, request.normalizedNumber, bmp);
+ }
+ }
+
+ return LookupCache.getCachedImage(context, request.normalizedNumber);
+ }
+
+ private static class LookupRequest {
+ String normalizedNumber;
+ String formattedNumber;
+ NumberLookupListener numberListener;
+ ContactInfo contactInfo;
+ }
+
+ private static class LookupNumberInfo implements PhoneNumberInfo {
+ private final ContactInfo info;
+ private LookupNumberInfo(ContactInfo info) {
+ this.info = info;
+ }
+
+ @Override
+ public String getDisplayName() {
+ return info.name;
+ }
+ @Override
+ public String getNumber() {
+ return info.number;
+ }
+ @Override
+ public int getPhoneType() {
+ return info.type;
+ }
+ @Override
+ public String getPhoneLabel() {
+ return info.label;
+ }
+ @Override
+ public String getNormalizedNumber() {
+ return info.normalizedNumber;
+ }
+ @Override
+ public String getImageUrl() {
+ return info.photoUri != null ? info.photoUri.toString() : null;
+ }
+ @Override
+ public boolean isBusiness() {
+ // FIXME
+ return false;
+ }
+ @Override
+ public String getLookupKey() {
+ return info.lookupKey;
+ }
+ @Override
+ public ContactLookupResult.Type getLookupSource() {
+ return ContactLookupResult.Type.REMOTE;
+ }
+ }
+}
diff --git a/java/com/android/dialer/lookup/auskunft/AuskunftApi.java b/java/com/android/dialer/lookup/auskunft/AuskunftApi.java
new file mode 100644
index 0000000..5b6b251
--- /dev/null
+++ b/java/com/android/dialer/lookup/auskunft/AuskunftApi.java
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2015, The CyanogenMod 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.dialer.lookup.auskunft;
+
+import android.content.Context;
+import android.net.Uri;
+import android.util.Log;
+
+import com.android.dialer.lookup.LookupUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public final class AuskunftApi {
+ private static final String TAG = AuskunftApi.class.getSimpleName();
+
+ private static final String PEOPLE_LOOKUP_URL = "https://auskunft.at/suche";
+
+ private static final String SEARCH_RESULTS_REGEX =
+ "(?i)<section[\\s]+class=[\"']?search-entry(.*?)?</section";
+ private static final String NAME_REGEX =
+ "(?i)<h1[\\s]+itemprop=[\"']?name[\"']?>(.*?)</h1";
+ private static final String NUMBER_REGEX =
+ "(?i)phone[\"'][\\s]+?href=[\"']{1}tel:(.*?)[\"']{1}";
+ private static final String ADDRESS_REGEX =
+ "(?i)<span[\\s]+itemprop=[\"']?streetAddress[\"']?>(.*?)</a";
+
+ private static final String BUSINESS_IDENTIFIER = "(Firma)";
+
+ private AuskunftApi() {
+ }
+
+ public static List<ContactInfo> query(String filter) throws IOException {
+ // build URI
+ Uri uri = Uri.parse(PEOPLE_LOOKUP_URL)
+ .buildUpon()
+ .appendQueryParameter("query", filter)
+ .build();
+
+ // get all search entry sections
+ List<String> entries = LookupUtils.allRegexResults(
+ LookupUtils.httpGet(uri.toString(), null), SEARCH_RESULTS_REGEX, true);
+
+ // abort lookup if nothing found
+ if (entries == null || entries.isEmpty()) {
+ Log.w(TAG, "nothing found");
+ return null;
+ }
+
+ // build response by iterating through the search entries and parsing their HTML data
+ List<ContactInfo> infos = new ArrayList<ContactInfo>();
+ for (String entry : entries) {
+ // parse wanted data and replace null values
+ String name = replaceNullResult(LookupUtils.firstRegexResult(entry, NAME_REGEX, true));
+ String address = replaceNullResult(LookupUtils.firstRegexResult(entry, ADDRESS_REGEX, true));
+ String number = replaceNullResult(LookupUtils.firstRegexResult(entry, NUMBER_REGEX, true));
+ // ignore entry if name or number is empty (should not occur)
+ // missing addresses won't be a problem (but do occur)
+ if (name.isEmpty() || number.isEmpty()) {
+ continue;
+ }
+
+ ContactInfo info = new ContactInfo();
+ info.name = cleanupResult(name);
+ info.number = cleanupResult(number);
+ info.address = cleanupResult(address);
+ info.url = uri.toString();
+
+ infos.add(info);
+ }
+ return infos;
+ }
+
+ private static String cleanupResult(String result) {
+ // get displayable text
+ result = LookupUtils.fromHtml(result);
+ // replace newlines with spaces
+ result = result.replaceAll("\\r|\\n", " ");
+ // replace multiple spaces with one
+ result = result.replaceAll("\\s+", " ");
+ // remove business identifier that is originally not part of the name
+ result = result.replace(BUSINESS_IDENTIFIER, "");
+ // final trimming
+ result = result.trim();
+
+ return result;
+ }
+
+ private static String replaceNullResult(String result) {
+ return (result == null) ? "" : result;
+ }
+
+ static class ContactInfo {
+ String name;
+ String number;
+ String url;
+ String address;
+ };
+}
diff --git a/java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java b/java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java
new file mode 100644
index 0000000..6feb1a5
--- /dev/null
+++ b/java/com/android/dialer/lookup/auskunft/AuskunftPeopleLookup.java
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2015, The CyanogenMod 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.dialer.lookup.auskunft;
+
+import android.content.Context;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.PeopleLookup;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+public class AuskunftPeopleLookup extends PeopleLookup {
+ private static final String TAG = AuskunftPeopleLookup.class.getSimpleName();
+
+ public AuskunftPeopleLookup(Context context) {
+ }
+
+ @Override
+ public List<ContactInfo> lookup(Context context, String filter) {
+ try {
+ List<AuskunftApi.ContactInfo> infos = AuskunftApi.query(filter);
+ if (infos != null) {
+ List<ContactInfo> result = new ArrayList<>();
+ for (AuskunftApi.ContactInfo info : infos) {
+ result.add(ContactBuilder.forPeopleLookup(info.number)
+ .setName(ContactBuilder.Name.createDisplayName(info.name))
+ .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.number))
+ .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.url))
+ .addAddress(ContactBuilder.Address.createFormattedHome(info.address))
+ .build());
+ }
+ return result;
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "People lookup failed", e);
+ }
+ return null;
+ }
+}
diff --git a/java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java b/java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java
new file mode 100644
index 0000000..6b6f415
--- /dev/null
+++ b/java/com/android/dialer/lookup/auskunft/AuskunftReverseLookup.java
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2015, The CyanogenMod 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.dialer.lookup.auskunft;
+
+import android.content.Context;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.ReverseLookup;
+
+import java.io.IOException;
+import java.util.List;
+
+public class AuskunftReverseLookup extends ReverseLookup {
+ public AuskunftReverseLookup(Context context) {
+ }
+
+ @Override
+ public ContactInfo lookupNumber(Context context, String normalizedNumber,
+ String formattedNumber) throws IOException {
+ // only Austrian numbers are supported
+ if (normalizedNumber.startsWith("+") && !normalizedNumber.startsWith("+43")) {
+ return null;
+ }
+
+ // query the API and return null if nothing found or general error
+ List<AuskunftApi.ContactInfo> infos = AuskunftApi.query(normalizedNumber);
+ AuskunftApi.ContactInfo info = infos != null && !infos.isEmpty() ? infos.get(0) : null;
+ if (info == null) {
+ return null;
+ }
+
+ return ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber)
+ .setName(ContactBuilder.Name.createDisplayName(info.name))
+ .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.number))
+ .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.url))
+ .addAddress(ContactBuilder.Address.createFormattedHome(info.address))
+ .build();
+ }
+}
diff --git a/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java
new file mode 100644
index 0000000..ab1eb40
--- /dev/null
+++ b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchApi.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2014 Danny Baumann <dannybaumann@web.de>
+ *
+ * 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.dialer.lookup.dastelefonbuch;
+
+import android.content.Context;
+import android.net.Uri;
+
+import com.android.dialer.lookup.LookupUtils;
+
+import java.io.IOException;
+
+public class TelefonbuchApi {
+ private static final String TAG = TelefonbuchApi.class.getSimpleName();
+
+ private static final String REVERSE_LOOKUP_URL =
+ "https://www.dastelefonbuch.de/?s=a20000" +
+ "&cmd=search&sort_ok=0&sp=55&vert_ok=0&aktion=23";
+
+ private static String NAME_REGEX ="<a id=\"name0.*?>\\s*\n?(.*?)\n?\\s*</a>";
+ private static String NUMBER_REGEX = "<span\\s+class=\"ico fon.*>.*<span>(.*?)</span><br/>";
+ private static String ADDRESS_REGEX = "<address.*?>\n?(.*?)</address>";
+
+ private TelefonbuchApi() {
+ }
+
+ public static ContactInfo reverseLookup(Context context, String number) throws IOException {
+ Uri uri = Uri.parse(REVERSE_LOOKUP_URL)
+ .buildUpon()
+ .appendQueryParameter("kw", number)
+ .build();
+ // Cut out everything we're not interested in (scripts etc.) to
+ // speed up the subsequent matching.
+ String output = LookupUtils.firstRegexResult(
+ LookupUtils.httpGet(uri.toString(), null), ": Treffer(.*)Ende Treffer", true);
+
+ String name = parseValue(output, NAME_REGEX, true, false);
+ if (name == null) {
+ return null;
+ }
+
+ String phoneNumber = parseValue(output, NUMBER_REGEX, false, true);
+ String address = parseValue(output, ADDRESS_REGEX, true, true);
+
+ ContactInfo info = new ContactInfo();
+ info.name = name;
+ info.address = address;
+ info.formattedNumber = phoneNumber != null ? phoneNumber : number;
+ info.website = uri.toString();
+
+ return info;
+ }
+
+ private static String parseValue(String output, String regex,
+ boolean dotall, boolean removeSpans) {
+ String result = LookupUtils.firstRegexResult(output, regex, dotall);
+ if (result != null && removeSpans) {
+ // completely remove hidden spans (including contents) ...
+ result = result.replaceAll("<span class=\"hide\".*?\\/span>", "");
+ // ... and remove span wrappers around data content
+ result = result.replaceAll("</?span.*?>", "");
+ }
+ return LookupUtils.fromHtml(result);
+ }
+
+ public static class ContactInfo {
+ String name;
+ String address;
+ String formattedNumber;
+ String website;
+ }
+}
diff --git a/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java
new file mode 100644
index 0000000..cd89499
--- /dev/null
+++ b/java/com/android/dialer/lookup/dastelefonbuch/TelefonbuchReverseLookup.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2014 Danny Baumann <dannybaumann@web.de>
+ *
+ * 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.dialer.lookup.dastelefonbuch;
+
+import android.content.Context;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.ReverseLookup;
+
+import java.io.IOException;
+
+public class TelefonbuchReverseLookup extends ReverseLookup {
+ private static final String TAG = TelefonbuchReverseLookup.class.getSimpleName();
+
+ public TelefonbuchReverseLookup(Context context) {
+ }
+
+ /**
+ * Perform phone number lookup.
+ *
+ * @param context The application context
+ * @param normalizedNumber The normalized phone number
+ * @param formattedNumber The formatted phone number
+ * @return The phone number info object
+ */
+ @Override
+ public ContactInfo lookupNumber(Context context,
+ String normalizedNumber, String formattedNumber) throws IOException {
+ if (normalizedNumber.startsWith("+") && !normalizedNumber.startsWith("+49")) {
+ // Das Telefonbuch only supports German numbers
+ return null;
+ }
+
+ TelefonbuchApi.ContactInfo info = TelefonbuchApi.reverseLookup(context, normalizedNumber);
+ if (info == null) {
+ return null;
+ }
+
+ return ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber)
+ .setName(ContactBuilder.Name.createDisplayName(info.name))
+ .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.formattedNumber))
+ .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.website))
+ .addAddress(ContactBuilder.Address.createFormattedHome(info.address))
+ .build();
+ }
+}
diff --git a/java/com/android/dialer/lookup/google/GoogleForwardLookup.java b/java/com/android/dialer/lookup/google/GoogleForwardLookup.java
new file mode 100644
index 0000000..bae11c4
--- /dev/null
+++ b/java/com/android/dialer/lookup/google/GoogleForwardLookup.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup.google;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.text.Html;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.ForwardLookup;
+import com.android.dialer.lookup.LookupUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
+
+public class GoogleForwardLookup extends ForwardLookup {
+ private static final String TAG = GoogleForwardLookup.class.getSimpleName();
+
+ private static final boolean DEBUG = false;
+
+ private static final String QUERY_FILTER = "q";
+ private static final String QUERY_LANGUAGE = "hl";
+ private static final String QUERY_LOCATION = "sll";
+ private static final String QUERY_RADIUS = "radius";
+ private static final String QUERY_RANDOM = "gs_gbg";
+
+ private static final String RESULT_ADDRESS = "a";
+ private static final String RESULT_NUMBER = "b";
+ private static final String RESULT_DISTANCE = "c";
+ private static final String RESULT_PHOTO_URI = "d";
+ private static final String RESULT_WEBSITE = "f";
+ private static final String RESULT_CITY = "g";
+
+ /** Base for the query URL */
+ private static final String LOOKUP_URL = "https://www.google.com/complete/search?gs_ri=dialer";
+
+ /** Minimum query length
+ * (default for dialer_nearby_places_min_query_len) */
+ private static final int MIN_QUERY_LEN = 2;
+
+ /** Maximum query length
+ * (default for dialer_nearby_places_max_query_len) */
+ private static final int MAX_QUERY_LEN = 50;
+
+ /** Radius (in miles)
+ * (default for dialer_nearby_places_directory_radius_meters) */
+ private static final int RADIUS = 1000;
+
+ /** User agent string */
+ private final String userAgent;
+
+ public GoogleForwardLookup(Context context) {
+ StringBuilder sb = new StringBuilder("GoogleDialer ");
+ try {
+ sb.append(context.getPackageManager().getPackageInfo(
+ context.getPackageName(), 0).versionName);
+ sb.append(" ");
+ sb.append(Build.FINGERPRINT);
+ } catch (PackageManager.NameNotFoundException e) {
+ sb.setLength(0);
+ }
+ userAgent = sb.toString();
+ }
+
+ @Override
+ public List<ContactInfo> lookup(Context context, String filter, Location lastLocation) {
+ int length = filter.length();
+
+ if (length >= MIN_QUERY_LEN) {
+ if (length > MAX_QUERY_LEN) {
+ filter = filter.substring(0, MAX_QUERY_LEN);
+ }
+
+ try {
+ Uri.Builder builder = Uri.parse(LOOKUP_URL).buildUpon();
+
+ // Query string
+ builder.appendQueryParameter(QUERY_FILTER, filter);
+
+ // Language
+ builder.appendQueryParameter(QUERY_LANGUAGE,
+ context.getResources().getConfiguration().locale.getLanguage());
+
+ // Location (latitude and longitude)
+ builder.appendQueryParameter(QUERY_LOCATION,
+ String.format("%f,%f", lastLocation.getLatitude(), lastLocation.getLongitude()));
+
+ // Radius distance
+ builder.appendQueryParameter(QUERY_RADIUS, Integer.toString(RADIUS));
+
+ // Random string (not really required)
+ builder.appendQueryParameter(QUERY_RANDOM, getRandomNoiseString());
+
+ Map<String, String> headers = new HashMap<>();
+ headers.put("User-Agent", userAgent);
+ JSONArray results = new JSONArray(
+ LookupUtils.httpGet(builder.build().toString(), headers));
+
+ if (DEBUG) Log.v(TAG, "Results: " + results);
+
+ return getEntries(results);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to execute query", e);
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON error", e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Parse JSON results and return them as an array of ContactInfo
+ *
+ * @param results The JSON results returned from the server
+ * @return Array of ContactInfo containing the result information
+ */
+ private List<ContactInfo> getEntries(JSONArray results) throws JSONException {
+ ArrayList<ContactInfo> details = new ArrayList<>();
+ JSONArray entries = results.getJSONArray(1);
+
+ for (int i = 0; i < entries.length(); i++) {
+ try {
+ JSONArray entry = entries.getJSONArray(i);
+
+ String displayName = decodeHtml(entry.getString(0));
+
+ JSONObject params = entry.getJSONObject(3);
+
+ String phoneNumber = decodeHtml(params.getString(RESULT_NUMBER));
+
+ String address = decodeHtml(params.getString(RESULT_ADDRESS));
+ String city = decodeHtml(params.getString(RESULT_CITY));
+
+ String profileUrl = params.optString(RESULT_WEBSITE, null);
+ String photoUri = params.optString(RESULT_PHOTO_URI, null);
+
+ ContactBuilder.Address a = new ContactBuilder.Address();
+ a.formattedAddress = address;
+ a.city = city;
+ a.type = StructuredPostal.TYPE_WORK;
+
+ details.add(ContactBuilder.forForwardLookup(phoneNumber)
+ .setName(ContactBuilder.Name.createDisplayName(displayName))
+ .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(phoneNumber))
+ .addWebsite(ContactBuilder.WebsiteUrl.createProfile(profileUrl))
+ .addAddress(a)
+ .setPhotoUri(photoUri != null ? photoUri : ContactBuilder.PHOTO_URI_BUSINESS)
+ .build());
+ } catch (JSONException e) {
+ Log.e(TAG, "Skipping the suggestions at index " + i, e);
+ }
+ }
+
+ return details;
+ }
+
+ /**
+ * Generate a random string of alphanumeric characters of length [4, 36)
+ *
+ * @return Random alphanumeric string
+ */
+ private String getRandomNoiseString() {
+ StringBuilder garbage = new StringBuilder();
+ int length = getRandomInteger(32) + 4;
+
+ for (int i = 0; i < length; i++) {
+ int asciiCode;
+
+ if (Math.random() >= 0.3) {
+ if (Math.random() <= 0.5) {
+ // Lowercase letters
+ asciiCode = getRandomInteger(26) + 97;
+ } else {
+ // Uppercase letters
+ asciiCode = getRandomInteger(26) + 65;
+ }
+ } else {
+ // Numbers
+ asciiCode = getRandomInteger(10) + 48;
+ }
+
+ garbage.append(Character.toString((char) asciiCode));
+ }
+
+ return garbage.toString();
+ }
+
+ /**
+ * Generate number in the range [0, max).
+ *
+ * @param max Upper limit (non-inclusive)
+ * @return Random number inside [0, max)
+ */
+ private int getRandomInteger(int max) {
+ return (int) Math.floor(Math.random() * max);
+ }
+
+ /**
+ * Convert HTML to unformatted plain text.
+ *
+ * @param s HTML content
+ * @return Unformatted plain text
+ */
+ private String decodeHtml(String s) {
+ return Html.fromHtml(s).toString();
+ }
+}
diff --git a/java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java b/java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java
new file mode 100644
index 0000000..458dbc1
--- /dev/null
+++ b/java/com/android/dialer/lookup/opencnam/OpenCnamReverseLookup.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup.opencnam;
+
+import android.content.Context;
+import android.net.Uri;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.LookupSettings;
+import com.android.dialer.lookup.LookupUtils;
+import com.android.dialer.lookup.ReverseLookup;
+
+import java.io.IOException;
+
+public class OpenCnamReverseLookup extends ReverseLookup {
+ private static final String TAG = OpenCnamReverseLookup.class.getSimpleName();
+
+ private static final boolean DEBUG = false;
+
+ private static final String LOOKUP_URL = "https://api.opencnam.com/v2/phone/";
+
+ /** Query parameters for paid accounts */
+ private static final String ACCOUNT_SID = "account_sid";
+ private static final String AUTH_TOKEN = "auth_token";
+
+ public OpenCnamReverseLookup(Context context) {
+ }
+
+ /**
+ * Perform phone number lookup.
+ *
+ * @param context The application context
+ * @param normalizedNumber The normalized phone number
+ * @param formattedNumber The formatted phone number
+ * @return The phone number info object
+ */
+ @Override
+ public ContactInfo lookupNumber(Context context,
+ String normalizedNumber, String formattedNumber) throws IOException {
+ if (normalizedNumber.startsWith("+") && !normalizedNumber.startsWith("+1")) {
+ // Any non-US number will return "We currently accept only US numbers"
+ return null;
+ }
+
+ String displayName = httpGetRequest(context, normalizedNumber);
+ if (DEBUG) Log.d(TAG, "Reverse lookup returned name: " + displayName);
+
+ // Check displayName. The free tier of the service will return the
+ // following for some numbers:
+ // "CNAM for phone "NORMALIZED" is currently unavailable for Hobbyist Tier users."
+
+ if (displayName.contains("Hobbyist Tier")) {
+ return null;
+ }
+
+ String number = formattedNumber != null ? formattedNumber : normalizedNumber;
+
+ return ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber)
+ .setName(ContactBuilder.Name.createDisplayName(displayName))
+ .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(number))
+ .setPhotoUri(ContactBuilder.PHOTO_URI_BUSINESS)
+ .build();
+ }
+
+ private String httpGetRequest(Context context, String number) throws IOException {
+ Uri.Builder builder = Uri.parse(LOOKUP_URL + number).buildUpon();
+
+ // Paid account
+ String accountSid = LookupSettings.getOpenCnamAccountSid(context);
+ String authToken = LookupSettings.getOpenCnamAuthToken(context);
+
+ if (!TextUtils.isEmpty(accountSid) && !TextUtils.isEmpty(authToken)) {
+ Log.d(TAG, "Using paid account");
+
+ builder.appendQueryParameter(ACCOUNT_SID, accountSid);
+ builder.appendQueryParameter(AUTH_TOKEN, authToken);
+ }
+
+ return LookupUtils.httpGet(builder.build().toString(), null);
+ }
+}
diff --git a/java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java b/java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java
new file mode 100644
index 0000000..4482e60
--- /dev/null
+++ b/java/com/android/dialer/lookup/openstreetmap/OpenStreetMapForwardLookup.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2014 The OmniROM Project
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.
+ */
+
+// Partially based on OmniROM's implementation
+
+package com.android.dialer.lookup.openstreetmap;
+
+import android.content.Context;
+import android.location.Location;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.provider.ContactsContract.CommonDataKinds.Website;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.ForwardLookup;
+import com.android.dialer.lookup.LookupUtils;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+public class OpenStreetMapForwardLookup extends ForwardLookup {
+ private static final String TAG = OpenStreetMapForwardLookup.class.getSimpleName();
+
+ /** Search within radius (meters) */
+ private static final int RADIUS = 30000;
+
+ /** Query URL */
+ private static final String LOOKUP_URL = "https://overpass-api.de/api/interpreter";
+ private static final String LOOKUP_QUERY =
+ "[out:json];node[name~\"%s\"][phone](around:%d,%f,%f);out body;";
+
+ private static final String RESULT_ELEMENTS = "elements";
+ private static final String RESULT_TAGS = "tags";
+ private static final String TAG_NAME = "name";
+ private static final String TAG_PHONE = "phone";
+ private static final String TAG_HOUSENUMBER = "addr:housenumber";
+ private static final String TAG_STREET = "addr:street";
+ private static final String TAG_CITY = "addr:city";
+ private static final String TAG_POSTCODE = "addr:postcode";
+ private static final String TAG_WEBSITE = "website";
+
+ public OpenStreetMapForwardLookup(Context context) {
+ }
+
+ @Override
+ public List<ContactInfo> lookup(Context context, String filter, Location lastLocation) {
+ // The OSM API doesn't support case-insentive searches, but does
+ // support regular expressions.
+ String regex = "";
+ for (int i = 0; i < filter.length(); i++) {
+ char c = filter.charAt(i);
+ regex += "[" + Character.toUpperCase(c) + Character.toLowerCase(c) + "]";
+ }
+
+ String request = String.format(Locale.ENGLISH, LOOKUP_QUERY, regex,
+ RADIUS, lastLocation.getLatitude(), lastLocation.getLongitude());
+
+ try {
+ return getEntries(new JSONObject(LookupUtils.httpPost(LOOKUP_URL, null, request)));
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to execute query", e);
+ } catch (JSONException e) {
+ Log.e(TAG, "JSON error", e);
+ }
+
+ return null;
+ }
+
+ private List<ContactInfo> getEntries(JSONObject results) throws JSONException {
+ ArrayList<ContactInfo> details = new ArrayList<>();
+ JSONArray elements = results.getJSONArray(RESULT_ELEMENTS);
+
+ for (int i = 0; i < elements.length(); i++) {
+ try {
+ JSONObject element = elements.getJSONObject(i);
+ JSONObject tags = element.getJSONObject(RESULT_TAGS);
+
+ String displayName = tags.getString(TAG_NAME);
+ String phoneNumber = tags.getString(TAG_PHONE);
+
+ // Take the first number if there are multiple
+ if (phoneNumber.contains(";")) {
+ phoneNumber = phoneNumber.split(";")[0];
+ phoneNumber = phoneNumber.trim();
+ }
+
+ // The address is split
+ String addressHouseNumber = tags.optString(TAG_HOUSENUMBER, null);
+ String addressStreet = tags.optString(TAG_STREET, null);
+ String addressCity = tags.optString(TAG_CITY, null);
+ String addressPostCode = tags.optString(TAG_POSTCODE, null);
+
+ String address = String.format("%s %s, %s %s",
+ addressHouseNumber != null ? addressHouseNumber : "",
+ addressStreet != null ? addressStreet : "",
+ addressCity != null ? addressCity : "",
+ addressPostCode != null ? addressPostCode : "");
+
+ address = address.trim().replaceAll("\\s+", " ");
+ if (address.isEmpty()) {
+ address = null;
+ }
+
+ ContactBuilder builder = ContactBuilder.forForwardLookup(phoneNumber)
+ .setName(ContactBuilder.Name.createDisplayName(displayName))
+ .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(phoneNumber))
+ .setPhotoUri(ContactBuilder.PHOTO_URI_BUSINESS);
+
+ if (address != null) {
+ ContactBuilder.Address a = new ContactBuilder.Address();
+ a.formattedAddress = address;
+ a.city = addressCity;
+ a.street = addressStreet;
+ a.postCode = addressPostCode;
+ a.type = StructuredPostal.TYPE_WORK;
+ builder.addAddress(a);
+ }
+
+ String website = tags.optString(TAG_WEBSITE, null);
+ if (website != null) {
+ ContactBuilder.WebsiteUrl w = new ContactBuilder.WebsiteUrl();
+ w.url = website;
+ w.type = Website.TYPE_HOMEPAGE;
+ builder.addWebsite(w);
+ }
+
+ details.add(builder.build());
+ } catch (JSONException e) {
+ Log.e(TAG, "Skipping the suggestions at index " + i, e);
+ }
+ }
+
+ return details;
+ }
+}
diff --git a/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png
new file mode 100644
index 0000000..f0bbe73
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_180_holo_light.png
Binary files differ
diff --git a/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png
new file mode 100644
index 0000000..f70e8e7
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-hdpi/ic_places_picture_holo_light.png
Binary files differ
diff --git a/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png
new file mode 100644
index 0000000..6409ab1
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_180_holo_light.png
Binary files differ
diff --git a/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png
new file mode 100644
index 0000000..7c92a60
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-xhdpi/ic_places_picture_holo_light.png
Binary files differ
diff --git a/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png
new file mode 100644
index 0000000..97b9822
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_180_holo_light.png
Binary files differ
diff --git a/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png
new file mode 100644
index 0000000..43029bd
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/drawable-xxhdpi/ic_places_picture_holo_light.png
Binary files differ
diff --git a/java/com/android/dialer/lookup/res/values/cm_arrays.xml b/java/com/android/dialer/lookup/res/values/cm_arrays.xml
new file mode 100644
index 0000000..a566727
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/values/cm_arrays.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013 The CyanogenMod 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string-array name="forward_lookup_providers" translatable="false">
+ <item>Google</item>
+ <item>OpenStreetMap</item>
+ </string-array>
+
+ <string-array name="forward_lookup_provider_names" translatable="false">
+ <item>Google</item>
+ <item>OpenStreetMap</item>
+ </string-array>
+
+ <string-array name="people_lookup_providers" translatable="false">
+ <item>Auskunft</item>
+ </string-array>
+
+ <string-array name="people_lookup_provider_names" translatable="false">
+ <item>Auskunft (AT)</item>
+ </string-array>
+
+ <string-array name="reverse_lookup_providers" translatable="false">
+ <item>Auskunft</item>
+ <item>DasTelefonbuch</item>
+ <item>OpenCnam</item>
+ <item>YellowPages</item>
+ <item>YellowPages_CA</item>
+ <item>ZabaSearch</item>
+ </string-array>
+
+ <string-array name="reverse_lookup_provider_names" translatable="false">
+ <item>Auskunft (AT)</item>
+ <item>Das Telefonbuch (DE)</item>
+ <item>OpenCnam (US)</item>
+ <item>YellowPages (US)</item>
+ <item>YellowPages (CA)</item>
+ <item>ZabaSearch (US)</item>
+ </string-array>
+</resources>
diff --git a/java/com/android/dialer/lookup/res/values/cm_strings.xml b/java/com/android/dialer/lookup/res/values/cm_strings.xml
new file mode 100644
index 0000000..fed7c00
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/values/cm_strings.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2013-2014 The CyanogenMod 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- Forward lookup -->
+ <string name="nearby_places">Nearby places</string>
+ <string name="people">People</string>
+
+ <!-- Number lookup -->
+ <string name="lookup_settings_label">Phone number lookup</string>
+ <string name="enable_forward_lookup_title">Forward lookup</string>
+ <string name="enable_forward_lookup_summary">Show nearby places when searching in the dialer</string>
+ <string name="enable_people_lookup_title">People lookup</string>
+ <string name="enable_people_lookup_summary">Show online results for people when searching in the dialer</string>
+ <string name="enable_reverse_lookup_title">Reverse lookup</string>
+ <string name="enable_reverse_lookup_summary">Look up information about the person or place for unknown numbers on incoming calls</string>
+ <string name="forward_lookup_provider_title">Forward lookup provider</string>
+ <string name="people_lookup_provider_title">People lookup provider</string>
+ <string name="reverse_lookup_provider_title">Reverse lookup provider</string>
+
+ <!-- Disclaimer -->
+ <string name="lookup_disclaimer">Lookups may send queries over a secure protocol (https) to remote websites to gather information. The query may include the other party\'s phone number or the search query</string>
+</resources>
diff --git a/java/com/android/dialer/lookup/res/xml/lookup_settings.xml b/java/com/android/dialer/lookup/res/xml/lookup_settings.xml
new file mode 100644
index 0000000..006345f
--- /dev/null
+++ b/java/com/android/dialer/lookup/res/xml/lookup_settings.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ ~ Copyright (C) 2014 The CyanogenMod 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
+ -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+ <SwitchPreference
+ android:key="enable_forward_lookup"
+ android:title="@string/enable_forward_lookup_title"
+ android:summary="@string/enable_forward_lookup_summary"
+ android:defaultValue="false"
+ android:persistent="false" />
+
+ <ListPreference
+ android:key="forward_lookup_provider"
+ android:title="@string/forward_lookup_provider_title"
+ android:entries="@array/forward_lookup_provider_names"
+ android:entryValues="@array/forward_lookup_providers"
+ android:dependency="enable_forward_lookup"
+ android:summary="%s"
+ android:persistent="false" />
+
+ <SwitchPreference
+ android:key="enable_people_lookup"
+ android:title="@string/enable_people_lookup_title"
+ android:summary="@string/enable_people_lookup_summary"
+ android:defaultValue="false"
+ android:persistent="false" />
+
+ <ListPreference
+ android:key="people_lookup_provider"
+ android:title="@string/people_lookup_provider_title"
+ android:entries="@array/people_lookup_provider_names"
+ android:entryValues="@array/people_lookup_providers"
+ android:summary="%s"
+ android:dependency="enable_people_lookup"
+ android:persistent="false" />
+
+ <SwitchPreference
+ android:key="enable_reverse_lookup"
+ android:title="@string/enable_reverse_lookup_title"
+ android:summary="@string/enable_reverse_lookup_summary"
+ android:defaultValue="false"
+ android:persistent="false" />
+
+ <ListPreference
+ android:key="reverse_lookup_provider"
+ android:title="@string/reverse_lookup_provider_title"
+ android:entries="@array/reverse_lookup_provider_names"
+ android:entryValues="@array/reverse_lookup_providers"
+ android:dependency="enable_reverse_lookup"
+ android:summary="%s"
+ android:persistent="false" />
+
+ <Preference android:summary="@string/lookup_disclaimer" />
+</PreferenceScreen>
+
diff --git a/java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java b/java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java
new file mode 100644
index 0000000..30d5aaf
--- /dev/null
+++ b/java/com/android/dialer/lookup/yellowpages/YellowPagesApi.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup.yellowpages;
+
+import android.content.Context;
+import android.text.TextUtils;
+
+import com.android.dialer.lookup.LookupUtils;
+
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class YellowPagesApi {
+ private static final String TAG = YellowPagesApi.class.getSimpleName();
+
+ static final String LOOKUP_URL_UNITED_STATES =
+ "https://www.yellowpages.com/phone?phone_search_terms=";
+ static final String LOOKUP_URL_CANADA =
+ "https://www.yellowpages.ca/search/si/1/";
+
+ private final String number;
+ private String output = null;
+ private ContactInfo info = null;
+ private final String lookupUrl;
+
+ public YellowPagesApi(String number, String lookupUrl) {
+ this.number = number;
+ this.lookupUrl = lookupUrl;
+ }
+
+ private void fetchPage() throws IOException {
+ output = LookupUtils.httpGet(lookupUrl + number, null);
+ }
+
+ private String getPhotoUrl(String website) throws IOException {
+ String output = LookupUtils.httpGet(website, null);
+ String galleryRef = LookupUtils.firstRegexResult(output,
+ "href=\"([^\"]+gallery\\?lid=[^\"]+)\"", true);
+ if (galleryRef == null) {
+ return null;
+ }
+
+ // Get first image
+ return LookupUtils.firstRegexResult(
+ LookupUtils.httpGet("https://www.yellowpages.com" + galleryRef, null),
+ "\"type\":\"image\",\"src\":\"([^\"]+)\"", true);
+ }
+
+ private String[] parseNameWebsiteUnitedStates() {
+ Pattern regexNameAndWebsite = Pattern.compile(
+ "<a href=\"([^>]+?)\"[^>]+?class=\"url[^>]+?>([^<]+)</a>",
+ Pattern.DOTALL);
+ String name = null;
+ String website = null;
+
+ Matcher m = regexNameAndWebsite.matcher(output);
+ if (m.find()) {
+ website = m.group(1).trim();
+ name = m.group(2).trim();
+ }
+
+ return new String[] { name, website };
+ }
+
+ private String[] parseNameWebsiteCanada() {
+ Pattern regexNameAndWebsite = Pattern.compile(
+ "class=\"ypgListingTitleLink utagLink\".*?href=\"(.*?)\">"
+ + "(<span\\s+class=\"listingTitle\">.*?</span>)",
+ Pattern.DOTALL);
+ String name = null;
+ String website = null;
+
+ Matcher m = regexNameAndWebsite.matcher(output);
+ if (m.find()) {
+ website = m.group(1).trim();
+ name = LookupUtils.fromHtml(m.group(2).trim());
+ }
+
+ if (website != null) {
+ website = "https://www.yellowpages.ca" + website;
+ }
+
+ return new String[] { name, website };
+ }
+
+ private String parseNumberUnitedStates() {
+ return LookupUtils.firstRegexResult(output,
+ "business-phone.*?>\n*([^\n<]+)\n*<", true);
+ }
+
+ private String parseNumberCanada() {
+ return LookupUtils.firstRegexResult(output,
+ "<div\\s+class=\"phoneNumber\">(.*?)</div>", true);
+ }
+
+ private String parseAddressUnitedStates() {
+ String addressStreet = LookupUtils.firstRegexResult(output,
+ "street-address.*?>\n*([^\n<]+)\n*<", true);
+ if (addressStreet != null && addressStreet.endsWith(",")) {
+ addressStreet = addressStreet.substring(0, addressStreet.length() - 1);
+ }
+
+ String addressCity = LookupUtils.firstRegexResult(output,
+ "locality.*?>\n*([^\n<]+)\n*<", true);
+ String addressState = LookupUtils.firstRegexResult(output,
+ "region.*?>\n*([^\n<]+)\n*<", true);
+ String addressZip = LookupUtils.firstRegexResult(output,
+ "postal-code.*?>\n*([^\n<]+)\n*<", true);
+
+ StringBuilder sb = new StringBuilder();
+
+ if (!TextUtils.isEmpty(addressStreet)) {
+ sb.append(addressStreet);
+ }
+ if (!TextUtils.isEmpty(addressCity)) {
+ sb.append(", ");
+ sb.append(addressCity);
+ }
+ if (!TextUtils.isEmpty(addressState)) {
+ sb.append(", ");
+ sb.append(addressState);
+ }
+ if (!TextUtils.isEmpty(addressZip)) {
+ sb.append(", ");
+ sb.append(addressZip);
+ }
+
+ String address = sb.toString();
+ return address.isEmpty() ? null : address;
+ }
+
+ private String parseAddressCanada() {
+ String address = LookupUtils.firstRegexResult(output,
+ "<div\\s+class=\"address\">(.*?)</div>", true);
+ return LookupUtils.fromHtml(address);
+ }
+
+ private void buildContactInfo() throws IOException {
+ Matcher m;
+
+ String name = null;
+ String website = null;
+ String phoneNumber = null;
+ String address = null;
+ String photoUrl = null;
+
+ if (lookupUrl.equals(LOOKUP_URL_UNITED_STATES)) {
+ String[] ret = parseNameWebsiteUnitedStates();
+ name = ret[0];
+ website = ret[1];
+ phoneNumber = parseNumberUnitedStates();
+ address = parseAddressUnitedStates();
+ if (website != null) {
+ photoUrl = getPhotoUrl(website);
+ }
+ } else {
+ String[] ret = parseNameWebsiteCanada();
+ name = ret[0];
+ website = ret[1];
+ phoneNumber = parseNumberCanada();
+ address = parseAddressCanada();
+ // AFAIK, Canada's YellowPages doesn't have photos
+ }
+
+ info = new ContactInfo();
+ info.name = name;
+ info.address = address;
+ info.formattedNumber = phoneNumber != null ? phoneNumber : number;
+ info.website = website;
+ info.photoUrl = photoUrl;
+ }
+
+ public ContactInfo getContactInfo() throws IOException {
+ if (info == null) {
+ fetchPage();
+ buildContactInfo();
+ }
+
+ return info;
+ }
+
+ public static class ContactInfo {
+ String name;
+ String address;
+ String formattedNumber;
+ String website;
+ String photoUrl;
+ }
+}
diff --git a/java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java b/java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java
new file mode 100644
index 0000000..5638df6
--- /dev/null
+++ b/java/com/android/dialer/lookup/yellowpages/YellowPagesReverseLookup.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup.yellowpages;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.net.Uri;
+import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
+import android.util.Log;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.LookupSettings;
+import com.android.dialer.lookup.LookupUtils;
+import com.android.dialer.lookup.ReverseLookup;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+public class YellowPagesReverseLookup extends ReverseLookup {
+ private static final String TAG = YellowPagesReverseLookup.class.getSimpleName();
+
+ private final String type;
+
+ public YellowPagesReverseLookup(Context context, String type) {
+ this.type = type;
+ }
+
+ /**
+ * Lookup image
+ *
+ * @param context The application context
+ * @param uri The image URI
+ */
+ @Override
+ public Bitmap lookupImage(Context context, Uri uri) {
+ if (uri == null) {
+ throw new NullPointerException("URI is null");
+ }
+
+ Log.e(TAG, "Fetching " + uri);
+
+ String scheme = uri.getScheme();
+
+ if (scheme.startsWith("http")) {
+ try {
+ byte[] response = LookupUtils.httpGetBytes(uri.toString(), null);
+ return BitmapFactory.decodeByteArray(response, 0, response.length);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to retrieve image", e);
+ }
+ } else if (scheme.equals(ContentResolver.SCHEME_CONTENT)) {
+ try {
+ ContentResolver cr = context.getContentResolver();
+ return BitmapFactory.decodeStream(cr.openInputStream(uri));
+ } catch (FileNotFoundException e) {
+ Log.e(TAG, "Failed to retrieve image", e);
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Perform phone number lookup.
+ *
+ * @param context The application context
+ * @param normalizedNumber The normalized phone number
+ * @param formattedNumber The formatted phone number
+ * @return The phone number info object
+ */
+ @Override
+ public ContactInfo lookupNumber(Context context,
+ String normalizedNumber, String formattedNumber) throws IOException {
+ String lookupUrl = type.equals(LookupSettings.RLP_YELLOWPAGES_CA)
+ ? YellowPagesApi.LOOKUP_URL_CANADA : YellowPagesApi.LOOKUP_URL_UNITED_STATES;
+ YellowPagesApi ypa = new YellowPagesApi(normalizedNumber, lookupUrl);
+ YellowPagesApi.ContactInfo info = ypa.getContactInfo();
+ if (info.name == null) {
+ return null;
+ }
+
+ ContactBuilder builder = ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber)
+ .setName(ContactBuilder.Name.createDisplayName(info.name))
+ .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.formattedNumber))
+ .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.website));
+
+ if (info.address != null) {
+ ContactBuilder.Address a = new ContactBuilder.Address();
+ a.formattedAddress = info.address;
+ a.type = StructuredPostal.TYPE_WORK;
+ builder.addAddress(a);
+ }
+
+ if (info.photoUrl != null) {
+ builder.setPhotoUri(info.photoUrl);
+ } else {
+ builder.setPhotoUri(ContactBuilder.PHOTO_URI_BUSINESS);
+ }
+
+ return builder.build();
+ }
+}
diff --git a/java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java b/java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java
new file mode 100644
index 0000000..6118740
--- /dev/null
+++ b/java/com/android/dialer/lookup/zabasearch/ZabaSearchApi.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup.zabasearch;
+
+import android.text.TextUtils;
+
+import com.android.dialer.lookup.LookupUtils;
+
+import java.io.IOException;
+
+public class ZabaSearchApi {
+ private static final String TAG = ZabaSearchApi.class.getSimpleName();
+
+ private static final String LOOKUP_URL = "https://www.zabasearch.com/phone/";
+
+ private final String number;
+ public String output = null;
+ private ContactInfo info = null;
+
+ public ZabaSearchApi(String number) {
+ this.number = number;
+ }
+
+ private void fetchPage() throws IOException {
+ output = LookupUtils.httpGet(LOOKUP_URL + number, null);
+ }
+
+ private void buildContactInfo() {
+ // Name
+ String name = LookupUtils.firstRegexResult(output,
+ "itemprop=\"?name\"?>([^<]+)<", true);
+ // Formatted phone number
+ String phoneNumber = LookupUtils.firstRegexResult(output,
+ "itemprop=\"?telephone\"?>([^<]+)<", true);
+ // Address
+ String addressStreet = LookupUtils.firstRegexResult(output,
+ "itemprop=\"?streetAddress\"?>([^<]+?)( )*<", true);
+ String addressCity = LookupUtils.firstRegexResult(output,
+ "itemprop=\"?addressLocality\"?>([^<]+)<", true);
+ String addressState = LookupUtils.firstRegexResult(output,
+ "itemprop=\"?addressRegion\"?>([^<]+)<", true);
+ String addressZip = LookupUtils.firstRegexResult(output,
+ "itemprop=\"?postalCode\"?>([^<]+)<", true);
+
+ StringBuilder sb = new StringBuilder();
+
+ if (!TextUtils.isEmpty(addressStreet)) {
+ sb.append(addressStreet);
+ }
+ if (!TextUtils.isEmpty(addressCity)) {
+ sb.append(", ");
+ sb.append(addressCity);
+ }
+ if (!TextUtils.isEmpty(addressState)) {
+ sb.append(", ");
+ sb.append(addressState);
+ }
+ if (!TextUtils.isEmpty(addressZip)) {
+ sb.append(", ");
+ sb.append(addressZip);
+ }
+
+ String address = sb.toString();
+ if (address.isEmpty()) {
+ address = null;
+ }
+
+ info = new ContactInfo();
+ info.name = name;
+ info.address = address;
+ info.formattedNumber = number;
+ info.website = LOOKUP_URL + info.formattedNumber;
+ }
+
+ public ContactInfo getContactInfo() throws IOException {
+ if (info == null) {
+ fetchPage();
+ buildContactInfo();
+ }
+
+ return info;
+ }
+
+ public static class ContactInfo {
+ String name;
+ String address;
+ String formattedNumber;
+ String website;
+ }
+}
diff --git a/java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java b/java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java
new file mode 100644
index 0000000..5c6608b
--- /dev/null
+++ b/java/com/android/dialer/lookup/zabasearch/ZabaSearchReverseLookup.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2014 Xiao-Long Chen <chillermillerlong@hotmail.com>
+ *
+ * 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.dialer.lookup.zabasearch;
+
+import android.content.Context;
+
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.lookup.ContactBuilder;
+import com.android.dialer.lookup.ReverseLookup;
+
+import java.io.IOException;
+
+public class ZabaSearchReverseLookup extends ReverseLookup {
+ private static final String TAG = ZabaSearchReverseLookup.class.getSimpleName();
+
+ public ZabaSearchReverseLookup(Context context) {
+ }
+
+ /**
+ * Perform phone number lookup.
+ *
+ * @param context The application context
+ * @param normalizedNumber The normalized phone number
+ * @param formattedNumber The formatted phone number
+ * @return The phone number info object
+ */
+ @Override
+ public ContactInfo lookupNumber(Context context,
+ String normalizedNumber, String formattedNumber) throws IOException {
+ ZabaSearchApi zsa = new ZabaSearchApi(normalizedNumber);
+ ZabaSearchApi.ContactInfo info = zsa.getContactInfo();
+ if (info.name == null) {
+ return null;
+ }
+
+ return ContactBuilder.forReverseLookup(normalizedNumber, formattedNumber)
+ .setName(ContactBuilder.Name.createDisplayName(info.name))
+ .addPhoneNumber(ContactBuilder.PhoneNumber.createMainNumber(info.formattedNumber))
+ .addWebsite(ContactBuilder.WebsiteUrl.createProfile(info.website))
+ .addAddress(ContactBuilder.Address.createFormattedHome(info.address))
+ .build();
+ }
+}
diff --git a/java/com/android/dialer/main/impl/AndroidManifest.xml b/java/com/android/dialer/main/impl/AndroidManifest.xml
index a1ed5eb..55eea5d 100644
--- a/java/com/android/dialer/main/impl/AndroidManifest.xml
+++ b/java/com/android/dialer/main/impl/AndroidManifest.xml
@@ -30,7 +30,7 @@
android:label="@string/main_activity_label"
android:launchMode="singleTask"
android:resizeableActivity="true"
- android:theme="@style/MainActivityTheme"
+ android:theme="@style/LaunchTheme"
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing">
<intent-filter>
diff --git a/java/com/android/dialer/main/impl/MainActivity.java b/java/com/android/dialer/main/impl/MainActivity.java
index 1129609..f539bdc 100644
--- a/java/com/android/dialer/main/impl/MainActivity.java
+++ b/java/com/android/dialer/main/impl/MainActivity.java
@@ -29,6 +29,7 @@
import com.android.dialer.interactions.PhoneNumberInteraction.InteractionErrorListener;
import com.android.dialer.main.MainActivityPeer;
import com.android.dialer.main.impl.bottomnav.BottomNavBar.TabIndex;
+import com.android.dialer.R;
import com.android.dialer.util.TransactionSafeActivity;
/** This is the main activity for dialer. It hosts favorites, call log, search, dialpad, etc... */
@@ -72,6 +73,7 @@
@Override
protected void onCreate(Bundle savedInstanceState) {
+ setTheme(R.style.MainActivityTheme);
super.onCreate(savedInstanceState);
LogUtil.enterBlock("MainActivity.onCreate");
// If peer was set by the super, don't reset it.
diff --git a/java/com/android/dialer/main/impl/OldMainActivityPeer.java b/java/com/android/dialer/main/impl/OldMainActivityPeer.java
index feb76ac..a1afc61 100644
--- a/java/com/android/dialer/main/impl/OldMainActivityPeer.java
+++ b/java/com/android/dialer/main/impl/OldMainActivityPeer.java
@@ -639,7 +639,7 @@
R.string.view_conversation,
v ->
activity.startActivity(
- IntentProvider.getSendSmsIntentProvider(number).getIntent(activity)))
+ IntentProvider.getSendSmsIntentProvider(number).getClickIntent(activity)))
.setActionTextColor(
ContextCompat.getColor(activity, R.color.dialer_snackbar_action_text_color))
.show();
diff --git a/java/com/android/dialer/main/impl/bottomnav/BottomNavItem.java b/java/com/android/dialer/main/impl/bottomnav/BottomNavItem.java
index 4794b88..4cba2a4 100644
--- a/java/com/android/dialer/main/impl/bottomnav/BottomNavItem.java
+++ b/java/com/android/dialer/main/impl/bottomnav/BottomNavItem.java
@@ -56,8 +56,8 @@
super.setSelected(selected);
int colorId =
selected
- ? ThemeComponent.get(getContext()).theme().getColorPrimary()
- : ThemeComponent.get(getContext()).theme().getTextColorSecondary();
+ ? getContext().getResources().getColor(R.color.nav_item_selected)
+ : getContext().getResources().getColor(R.color.nav_item);
image.setImageTintList(ColorStateList.valueOf(colorId));
text.setTextColor(colorId);
}
diff --git a/java/com/android/dialer/main/impl/res/values-night/styles.xml b/java/com/android/dialer/main/impl/res/values-night/styles.xml
new file mode 100644
index 0000000..63d4e9c
--- /dev/null
+++ b/java/com/android/dialer/main/impl/res/values-night/styles.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2017 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
+ -->
+<resources>
+
+ <style name="LaunchTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
+ <item name="android:colorPrimary">@color/dialer_theme_color</item>
+ <item name="colorPrimary">@color/dialer_theme_color</item>
+ <item name="android:colorPrimaryDark">@color/dialer_theme_color_dark</item>
+ <item name="colorPrimaryDark">@color/dialer_theme_color_dark</item>
+ <item name="android:windowLightStatusBar">false</item>
+ </style>
+
+</resources>
diff --git a/java/com/android/dialer/main/impl/res/values/styles.xml b/java/com/android/dialer/main/impl/res/values/styles.xml
index 25f247e..4b153b6 100644
--- a/java/com/android/dialer/main/impl/res/values/styles.xml
+++ b/java/com/android/dialer/main/impl/res/values/styles.xml
@@ -16,6 +16,14 @@
-->
<resources>
+ <style name="LaunchTheme" parent="Theme.AppCompat.DayNight.DarkActionBar">
+ <item name="android:colorPrimary">@color/dialer_theme_color</item>
+ <item name="colorPrimary">@color/dialer_theme_color</item>
+ <item name="android:colorPrimaryDark">@color/dialer_theme_color_dark</item>
+ <item name="colorPrimaryDark">@color/dialer_theme_color_dark</item>
+ <item name="android:windowLightStatusBar">true</item>
+ </style>
+
<!-- Activities should use this theme as their style -->
<style name="MainActivityTheme" parent="MainActivityThemeBase"/>
@@ -28,6 +36,8 @@
<item name="actionModeStyle">@style/MainActionModeStyle</item>
<item name="actionBarTheme">@style/ThemeOverlay.AppCompat.Dark.ActionBar</item>
<item name="dialpad_style">@style/Dialpad.Themed</item>
+ <item name="android:colorPrimaryDark">@color/dialer_theme_color_dark</item>
+ <item name="colorPrimaryDark">@color/dialer_theme_color_dark</item>
</style>
<style name="MainActivityThemeBase.Dark" parent="Dialer.Dark.ThemeBase.NoActionBar">
diff --git a/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners.xml b/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners.xml
index 88f5a04..2130db4 100644
--- a/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners.xml
+++ b/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners.xml
@@ -14,8 +14,15 @@
~ See the License for the specific language governing permissions and
~ limitations under the License
-->
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
- android:shape="rectangle">
- <solid android:color="?android:attr/colorBackgroundFloating"/>
- <corners android:radius="2dp"/>
-</shape>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="@color/dialer_ripple_color">
+
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <solid android:color="@android:color/white" />
+ <corners android:radius="8dp" />
+ </shape>
+ </item>
+
+ <item android:drawable="@drawable/search_bar_background_rounded_corners_shape" />
+</ripple>
diff --git a/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners_shape.xml b/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners_shape.xml
new file mode 100644
index 0000000..ee3219e
--- /dev/null
+++ b/java/com/android/dialer/main/impl/toolbar/res/drawable/search_bar_background_rounded_corners_shape.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 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
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <solid android:color="?attr/colorBackgroundSearchBar"/>
+ <corners android:radius="8dp"/>
+</shape>
diff --git a/java/com/android/dialer/main/impl/toolbar/res/layout/expanded_search_bar.xml b/java/com/android/dialer/main/impl/toolbar/res/layout/expanded_search_bar.xml
index a7b3aeb..d9051a6 100644
--- a/java/com/android/dialer/main/impl/toolbar/res/layout/expanded_search_bar.xml
+++ b/java/com/android/dialer/main/impl/toolbar/res/layout/expanded_search_bar.xml
@@ -29,7 +29,7 @@
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_menu_back_from_search"
android:src="@drawable/quantum_ic_arrow_back_vd_theme_24"
- android:tint="?android:attr/colorPrimary"/>
+ android:tint="?colorIcon"/>
<EditText
android:id="@+id/search_view"
@@ -62,4 +62,4 @@
android:src="@drawable/quantum_ic_close_vd_theme_24"
android:tint="?colorIcon"
android:visibility="gone"/>
-</RelativeLayout>
\ No newline at end of file
+</RelativeLayout>
diff --git a/java/com/android/dialer/main/impl/toolbar/res/layout/toolbar_layout.xml b/java/com/android/dialer/main/impl/toolbar/res/layout/toolbar_layout.xml
index 049cf2e..129aac9 100644
--- a/java/com/android/dialer/main/impl/toolbar/res/layout/toolbar_layout.xml
+++ b/java/com/android/dialer/main/impl/toolbar/res/layout/toolbar_layout.xml
@@ -18,7 +18,6 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="@dimen/expanded_search_bar_height"
- android:background="?android:attr/colorPrimary"
app:contentInsetEnd="0dp"
app:contentInsetStart="0dp">
<FrameLayout
@@ -38,7 +37,6 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
- android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical">
<ImageView
diff --git a/java/com/android/dialer/proguard/proguard_base.flags b/java/com/android/dialer/proguard/proguard_base.flags
index 6d5d373..3b8fe2c 100644
--- a/java/com/android/dialer/proguard/proguard_base.flags
+++ b/java/com/android/dialer/proguard/proguard_base.flags
@@ -71,3 +71,8 @@
# AOSP support library: Handle classes that use reflection.
-dontnote android.support.v4.app.NotificationCompatJellybean
+
+-keep class android.support.design.widget.AppBarLayout$ScrollingViewBehavior {
+ public <init>(android.content.Context, android.util.AttributeSet);
+ public <init>();
+}
diff --git a/java/com/android/dialer/theme/base/res/values/attr.xml b/java/com/android/dialer/theme/base/res/values/attr.xml
index 606d161..790ae70 100644
--- a/java/com/android/dialer/theme/base/res/values/attr.xml
+++ b/java/com/android/dialer/theme/base/res/values/attr.xml
@@ -15,6 +15,8 @@
~ limitations under the License
-->
<resources>
+ <!-- Used to color the search bar background -->
+ <attr name="colorBackgroundSearchBar" format="color"/>
<!-- Used to style all icons in Dialer. -->
<attr name="colorIcon" format="color"/>
<!-- Used to style some icons a little lighter in Dialer. -->
diff --git a/java/com/android/dialer/theme/base/res/values/theme_dialer_light.xml b/java/com/android/dialer/theme/base/res/values/theme_dialer_light.xml
index db06df4..61e0a1b 100644
--- a/java/com/android/dialer/theme/base/res/values/theme_dialer_light.xml
+++ b/java/com/android/dialer/theme/base/res/values/theme_dialer_light.xml
@@ -27,6 +27,9 @@
<item name="actionBarWidgetTheme">@null</item>
<item name="actionBarTheme">@style/DialerActionBarBaseTheme</item>
<item name="listChoiceBackgroundIndicator">@drawable/abc_list_selector_holo_dark</item>
+
+ <item name="android:colorPrimaryDark">@color/settings_primary_dark</item>
+ <item name="colorControlHighlight">@color/dialer_ripple_color</item>
</style>
<style name="Dialer.ThemeBase.NoActionBar">
@@ -34,13 +37,18 @@
base style once none of our activities depend on ActionBar anymore. -->
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
+
+ <item name="android:colorPrimaryDark">@color/settings_primary_dark</item>
+ <item name="colorControlHighlight">@color/dialer_ripple_color</item>
</style>
<!-- Activities and Applications should inherit from one of the themes above. -->
<style name="Dialer.ThemeBase">
+ <item name="android:forceDarkAllowed">true</item>
<!-- These values should be used to color all backgrounds. -->
<item name="android:colorBackground">@color/dialer_background_color</item>
<item name="android:colorBackgroundFloating">@android:color/white</item>
+ <item name="colorBackgroundSearchBar">@color/dialer_search_bar_color</item>
<!-- These values should be used to set text color. -->
<item name="android:textColorPrimary">@color/dialer_primary_text_color</item>
diff --git a/java/com/android/dialer/theme/common/res/values-night/colors.xml b/java/com/android/dialer/theme/common/res/values-night/colors.xml
new file mode 100644
index 0000000..aa42d8b
--- /dev/null
+++ b/java/com/android/dialer/theme/common/res/values-night/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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
+ -->
+<!-- The colors in this file aren't configured at the theme level. -->
+<resources>
+ <!-- Settings -->
+ <color name="dialer_settings_theme_color_dark">#1C3AA9</color>
+</resources>
diff --git a/java/com/android/dialer/theme/hidden/res/values-night/colors_dialer_dark.xml b/java/com/android/dialer/theme/hidden/res/values-night/colors_dialer_dark.xml
new file mode 100644
index 0000000..8d04c72
--- /dev/null
+++ b/java/com/android/dialer/theme/hidden/res/values-night/colors_dialer_dark.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2018 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
+ -->
+<resources>
+ <!-- Essential theme colors -->
+ <color name="dialer_theme_color">#5195EA</color>
+ <color name="dialer_theme_color_20pct">#335195EA</color>
+ <color name="dialer_secondary_color">#5195EA</color>
+ <color name="dialer_ripple_color">#33ffffff</color>
+ <color name="dialer_search_bar_color">@color/google_grey_900</color>
+ <color name="settings_primary_dark">#2374CE</color>
+</resources>
diff --git a/java/com/android/dialer/theme/hidden/res/values/colors_dialer_light.xml b/java/com/android/dialer/theme/hidden/res/values/colors_dialer_light.xml
index 988aad7..e4d1e29 100644
--- a/java/com/android/dialer/theme/hidden/res/values/colors_dialer_light.xml
+++ b/java/com/android/dialer/theme/hidden/res/values/colors_dialer_light.xml
@@ -17,7 +17,10 @@
<!-- Essential theme colors -->
<color name="dialer_theme_color">#2A56C6</color>
<color name="dialer_theme_color_20pct">#332A56C6</color>
- <color name="dialer_theme_color_dark">#1C3AA9</color>
+ <color name="dialer_theme_color_dark">@color/dialer_background_color</color>
<color name="dialer_secondary_color">#F50057</color>
<color name="dialer_background_color">#FAFAFA</color>
+ <color name="dialer_ripple_color">#1f000000</color>
+ <color name="dialer_search_bar_color">@color/google_grey_50</color>
+ <color name="settings_primary_dark">#1C3AA9</color>
</resources>
diff --git a/java/com/android/dialer/util/CallUtil.java b/java/com/android/dialer/util/CallUtil.java
index 89af0b9..160a40f 100644
--- a/java/com/android/dialer/util/CallUtil.java
+++ b/java/com/android/dialer/util/CallUtil.java
@@ -24,6 +24,8 @@
import android.telecom.TelecomManager;
import com.android.dialer.common.LogUtil;
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+
+import java.util.ArrayList;
import java.util.List;
/** Utilities related to calls that can be used by non system apps. */
@@ -89,6 +91,25 @@
}
/**
+ * Returns a list of phone accounts that are able to call to numbers with the supplied scheme
+ */
+ public static List<PhoneAccount> getCallCapablePhoneAccounts(Context context, String scheme) {
+ if (!PermissionsUtil.hasPermission(context, android.Manifest.permission.READ_PHONE_STATE)) {
+ return null;
+ }
+ TelecomManager tm = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+ final ArrayList<PhoneAccount> accounts = new ArrayList<>();
+
+ for (PhoneAccountHandle handle : tm.getCallCapablePhoneAccounts()) {
+ final PhoneAccount account = tm.getPhoneAccount(handle);
+ if (account != null && account.supportsUriScheme(scheme)) {
+ accounts.add(account);
+ }
+ }
+ return accounts;
+ }
+
+ /**
* Determines if one of the call capable phone accounts defined supports video calling.
*
* @param context The context.
diff --git a/java/com/android/dialer/widget/LinearColorBar.java b/java/com/android/dialer/widget/LinearColorBar.java
new file mode 100644
index 0000000..b016264
--- /dev/null
+++ b/java/com/android/dialer/widget/LinearColorBar.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ * Copyright (C) 2013 Android Open Kang 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.dialer.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.LinearGradient;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.widget.LinearLayout;
+
+import com.android.dialer.R;
+
+public class LinearColorBar extends LinearLayout {
+ private float mFirstRatio;
+ private float mSecondRatio;
+ private float mThirdRatio;
+ private float mFourthRatio;
+
+ private int mBackgroundColor;
+ private int mBlueColor;
+ private int mGreenColor;
+ private int mRedColor;
+ private int mOrangeColor;
+
+ final Rect mRect = new Rect();
+ final Paint mPaint = new Paint();
+
+ int mLastInterestingLeft, mLastInterestingRight;
+ int mLineWidth;
+
+ final Path mColorPath = new Path();
+ final Path mEdgePath = new Path();
+ final Paint mColorGradientPaint = new Paint();
+ final Paint mEdgeGradientPaint = new Paint();
+
+ public LinearColorBar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ setWillNotDraw(false);
+
+ TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.LinearColorBar, 0, 0);
+ int n = a.getIndexCount();
+
+ for (int i = 0; i < n; i++) {
+ int attr = a.getIndex(i);
+
+ switch (attr) {
+ case R.styleable.LinearColorBar_backgroundColor:
+ mBackgroundColor = a.getColor(attr, 0);
+ break;
+ case R.styleable.LinearColorBar_redColor:
+ mRedColor = a.getColor(attr, 0);
+ break;
+ case R.styleable.LinearColorBar_greenColor:
+ mGreenColor = a.getColor(attr, 0);
+ break;
+ case R.styleable.LinearColorBar_blueColor:
+ mBlueColor = a.getColor(attr, 0);
+ break;
+ case R.styleable.LinearColorBar_orangeColor:
+ mOrangeColor = a.getColor(attr, 0);
+ break;
+ }
+ }
+
+ a.recycle();
+
+ mPaint.setStyle(Paint.Style.FILL);
+ mColorGradientPaint.setStyle(Paint.Style.FILL);
+ mColorGradientPaint.setAntiAlias(true);
+ mEdgeGradientPaint.setStyle(Paint.Style.STROKE);
+ mLineWidth = getResources().getDisplayMetrics().densityDpi >= DisplayMetrics.DENSITY_HIGH
+ ? 2 : 1;
+ mEdgeGradientPaint.setStrokeWidth(mLineWidth);
+ mEdgeGradientPaint.setAntiAlias(true);
+ }
+
+ public void setRatios(float blue, float green, float red, float orange) {
+ mFirstRatio = blue;
+ mSecondRatio = green;
+ mThirdRatio = red;
+ mFourthRatio = orange;
+ invalidate();
+ }
+
+ private void updateIndicator() {
+ int off = Math.max(0, getPaddingTop() - getPaddingBottom());
+ mRect.top = off;
+ mRect.bottom = getHeight();
+
+ mColorGradientPaint.setShader(new LinearGradient(
+ 0, 0, 0, off - 2, mBackgroundColor & 0xffffff,
+ mBackgroundColor, Shader.TileMode.CLAMP));
+ mEdgeGradientPaint.setShader(new LinearGradient(
+ 0, 0, 0, off / 2, 0x00a0a0a0, 0xffa0a0a0, Shader.TileMode.CLAMP));
+ }
+
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+ updateIndicator();
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ int width = getWidth();
+
+ int left = 0;
+
+ int right = left + (int) (width * mFirstRatio);
+ int right2 = right + (int) (width * mSecondRatio);
+ int right3 = right2 + (int) (width * mThirdRatio);
+ int right4 = right3 + (int) (width * mFourthRatio);
+
+ int indicatorLeft = right4;
+ int indicatorRight = width;
+
+ if (mLastInterestingLeft != indicatorLeft || mLastInterestingRight != indicatorRight) {
+ mColorPath.reset();
+ mEdgePath.reset();
+ if (indicatorLeft < indicatorRight) {
+ final int midTopY = mRect.top;
+ final int midBottomY = 0;
+ final int xoff = 2;
+ mColorPath.moveTo(indicatorLeft, mRect.top);
+ mColorPath.cubicTo(indicatorLeft, midBottomY, -xoff, midTopY, -xoff, 0);
+ mColorPath.lineTo(width + xoff - 1, 0);
+ mColorPath.cubicTo(width + xoff - 1, midTopY,
+ indicatorRight, midBottomY, indicatorRight, mRect.top);
+ mColorPath.close();
+ final float lineOffset = mLineWidth + .5f;
+ mEdgePath.moveTo(-xoff + lineOffset, 0);
+ mEdgePath.cubicTo(-xoff + lineOffset, midTopY,
+ indicatorLeft + lineOffset, midBottomY, indicatorLeft + lineOffset, mRect.top);
+ mEdgePath.moveTo(width + xoff - 1 - lineOffset, 0);
+ mEdgePath.cubicTo(width + xoff - 1 - lineOffset, midTopY,
+ indicatorRight - lineOffset, midBottomY, indicatorRight - lineOffset, mRect.top);
+ }
+ mLastInterestingLeft = indicatorLeft;
+ mLastInterestingRight = indicatorRight;
+ }
+
+ if (!mEdgePath.isEmpty()) {
+ canvas.drawPath(mEdgePath, mEdgeGradientPaint);
+ canvas.drawPath(mColorPath, mColorGradientPaint);
+ }
+
+ if (left < right) {
+ mRect.left = left;
+ mRect.right = right;
+ mPaint.setColor(mBlueColor);
+ canvas.drawRect(mRect, mPaint);
+ width -= (right - left);
+ left = right;
+ }
+
+ right = right2;
+
+ if (left < right) {
+ mRect.left = left;
+ mRect.right = right;
+ mPaint.setColor(mGreenColor);
+ canvas.drawRect(mRect, mPaint);
+ width -= (right - left);
+ left = right;
+ }
+
+ right = right3;
+
+ if (left < right) {
+ mRect.left = left;
+ mRect.right = right;
+ mPaint.setColor(mRedColor);
+ canvas.drawRect(mRect, mPaint);
+ width -= (right - left);
+ left = right;
+ }
+
+ right = right4;
+
+ if (left < right) {
+ mRect.left = left;
+ mRect.right = right;
+ mPaint.setColor(mOrangeColor);
+ canvas.drawRect(mRect, mPaint);
+ width -= (right - left);
+ left = right;
+ }
+
+ right = left + width;
+ if (left < right) {
+ mRect.left = left;
+ mRect.right = right;
+ mPaint.setColor(mBackgroundColor);
+ canvas.drawRect(mRect, mPaint);
+ }
+ }
+}
diff --git a/java/com/android/incallui/AndroidManifest.xml b/java/com/android/incallui/AndroidManifest.xml
index 7283702..b10e044 100644
--- a/java/com/android/incallui/AndroidManifest.xml
+++ b/java/com/android/incallui/AndroidManifest.xml
@@ -46,6 +46,9 @@
<!-- Set audio selector window type TYPE_APPLICATION_OVERLAY -->
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
+ <!-- Required when the "Enable Do Not Disturb during call" setting is enabled -->
+ <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
+
<!-- Set android:taskAffinity="com.android.incallui" for all activities to ensure proper
navigation. Otherwise system could bring up MainActivity instead, e.g. when user unmerge a
call.
diff --git a/java/com/android/incallui/CallButtonPresenter.java b/java/com/android/incallui/CallButtonPresenter.java
index f2f7a40..0fa833e 100644
--- a/java/com/android/incallui/CallButtonPresenter.java
+++ b/java/com/android/incallui/CallButtonPresenter.java
@@ -16,9 +16,13 @@
package com.android.incallui;
+import android.app.AlertDialog;
import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Trace;
+import android.preference.PreferenceManager;
import android.support.v4.app.Fragment;
import android.support.v4.os.UserManagerCompat;
import android.telecom.CallAudioState;
@@ -40,6 +44,7 @@
import com.android.incallui.audiomode.AudioModeProvider;
import com.android.incallui.audiomode.AudioModeProvider.AudioModeListener;
import com.android.incallui.call.CallList;
+import com.android.incallui.call.CallRecorder;
import com.android.incallui.call.DialerCall;
import com.android.incallui.call.DialerCall.CameraDirection;
import com.android.incallui.call.DialerCallListener;
@@ -62,12 +67,33 @@
InCallButtonUiDelegate,
DialerCallListener {
+ private static final String KEY_RECORDING_WARNING_PRESENTED = "recording_warning_presented";
+
private final Context context;
private InCallButtonUi inCallButtonUi;
private DialerCall call;
private boolean isInCallButtonUiReady;
private PhoneAccountHandle otherAccount;
+ private CallRecorder.RecordingProgressListener recordingProgressListener =
+ new CallRecorder.RecordingProgressListener() {
+ @Override
+ public void onStartRecording() {
+ inCallButtonUi.setCallRecordingState(true);
+ inCallButtonUi.setCallRecordingDuration(0);
+ }
+
+ @Override
+ public void onStopRecording() {
+ inCallButtonUi.setCallRecordingState(false);
+ }
+
+ @Override
+ public void onRecordingTimeProgress(final long elapsedTimeMs) {
+ inCallButtonUi.setCallRecordingDuration(elapsedTimeMs);
+ }
+ };
+
public CallButtonPresenter(Context context) {
this.context = context.getApplicationContext();
}
@@ -86,6 +112,9 @@
inCallPresenter.addCanAddCallListener(this);
inCallPresenter.getInCallCameraManager().addCameraSelectionListener(this);
+ CallRecorder recorder = CallRecorder.getInstance();
+ recorder.addRecordingProgressListener(recordingProgressListener);
+
// Update the buttons state immediately for the current call
onStateChange(InCallState.NO_CALLS, inCallPresenter.getInCallState(), CallList.getInstance());
isInCallButtonUiReady = true;
@@ -101,6 +130,10 @@
InCallPresenter.getInstance().removeDetailsListener(this);
InCallPresenter.getInstance().getInCallCameraManager().removeCameraSelectionListener(this);
InCallPresenter.getInstance().removeCanAddCallListener(this);
+
+ CallRecorder recorder = CallRecorder.getInstance();
+ recorder.removeRecordingProgressListener(recordingProgressListener);
+
isInCallButtonUiReady = false;
if (call != null) {
@@ -301,6 +334,52 @@
}
@Override
+ public void callRecordClicked(boolean checked) {
+ CallRecorder recorder = CallRecorder.getInstance();
+ if (checked) {
+ final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean warningPresented = prefs.getBoolean(KEY_RECORDING_WARNING_PRESENTED, false);
+ if (!warningPresented) {
+ new AlertDialog.Builder(getActivity())
+ .setTitle(R.string.recording_warning_title)
+ .setMessage(R.string.recording_warning_text)
+ .setPositiveButton(R.string.onscreenCallRecordText, (dialog, which) -> {
+ prefs.edit()
+ .putBoolean(KEY_RECORDING_WARNING_PRESENTED, true)
+ .apply();
+ startCallRecordingOrAskForPermission();
+ })
+ .setNegativeButton(android.R.string.cancel, null)
+ .show();
+ } else {
+ startCallRecordingOrAskForPermission();
+ }
+ } else {
+ if (recorder.isRecording()) {
+ recorder.finishRecording();
+ }
+ }
+ }
+
+ private void startCallRecordingOrAskForPermission() {
+ if (hasAllPermissions(CallRecorder.REQUIRED_PERMISSIONS)) {
+ CallRecorder recorder = CallRecorder.getInstance();
+ recorder.startRecording(call.getNumber(), call.getCreationTimeMillis());
+ } else {
+ inCallButtonUi.requestCallRecordingPermissions(CallRecorder.REQUIRED_PERMISSIONS);
+ }
+ }
+
+ private boolean hasAllPermissions(String[] permissions) {
+ for (String p : permissions) {
+ if (context.checkSelfPermission(p) != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
public void changeToVideoClicked() {
LogUtil.enterBlock("CallButtonPresenter.changeToVideoClicked");
Logger.get(context)
@@ -486,6 +565,10 @@
&& call.getState() != DialerCallState.DIALING
&& call.getState() != DialerCallState.CONNECTING;
+ final CallRecorder recorder = CallRecorder.getInstance();
+ final boolean showCallRecordOption = recorder.canRecordInCurrentCountry()
+ && !isVideo && call.getState() == DialerCallState.ACTIVE;
+
otherAccount = TelecomUtil.getOtherAccount(getContext(), call.getAccountHandle());
boolean showSwapSim =
!call.isEmergencyCall()
@@ -519,6 +602,7 @@
}
inCallButtonUi.showButton(InCallButtonIds.BUTTON_DIALPAD, true);
inCallButtonUi.showButton(InCallButtonIds.BUTTON_MERGE, showMerge);
+ inCallButtonUi.showButton(InCallButtonIds.BUTTON_RECORD_CALL, showCallRecordOption);
inCallButtonUi.updateButtonStates();
}
diff --git a/java/com/android/incallui/InCallDndHandler.java b/java/com/android/incallui/InCallDndHandler.java
new file mode 100644
index 0000000..4ab3e84
--- /dev/null
+++ b/java/com/android/incallui/InCallDndHandler.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2018 The LineageOS 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.incallui;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.state.DialerCallState;
+import com.android.incallui.InCallPresenter.InCallState;
+
+public class InCallDndHandler implements InCallPresenter.InCallStateListener {
+
+ private static final String KEY_ENABLE_DND = "incall_enable_dnd";
+
+ private SharedPreferences prefs;
+ private DialerCall activeCall;
+ private NotificationManager notificationManager;
+ private int userSelectedDndMode;
+
+ public InCallDndHandler(Context context) {
+ prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ notificationManager = context.getSystemService(NotificationManager.class);
+
+ // Save the user's Do Not Disturb mode so that it can be restored when the call ends
+ userSelectedDndMode = notificationManager.getCurrentInterruptionFilter();
+ }
+
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ DialerCall activeCall = callList.getActiveCall();
+
+ if (activeCall != null && this.activeCall == null) {
+ Log.d(this, "Transition to active call " + activeCall);
+ handleDndState(activeCall);
+ this.activeCall = activeCall;
+ } else if (activeCall == null && this.activeCall != null) {
+ Log.d(this, "Transition from active call " + this.activeCall);
+ handleDndState(this.activeCall);
+ this.activeCall = null;
+ }
+ }
+
+ private void handleDndState(DialerCall call) {
+ if (!prefs.getBoolean(KEY_ENABLE_DND, false)) {
+ return;
+ }
+ if (DialerCallState.isConnectingOrConnected(call.getState())) {
+ Log.d(this, "Enabling Do Not Disturb mode");
+ setDoNotDisturbMode(NotificationManager.INTERRUPTION_FILTER_NONE);
+ } else {
+ Log.d(this, "Restoring previous Do Not Disturb mode");
+ setDoNotDisturbMode(userSelectedDndMode);
+ }
+ }
+
+ private void setDoNotDisturbMode(int newMode) {
+ if (notificationManager.isNotificationPolicyAccessGranted()) {
+ notificationManager.setInterruptionFilter(newMode);
+ } else {
+ Log.e(this, "Failed to set Do Not Disturb mode " + newMode + " due to lack of permissions");
+ }
+ }
+}
diff --git a/java/com/android/incallui/InCallPresenter.java b/java/com/android/incallui/InCallPresenter.java
index 17af756..8881029 100644
--- a/java/com/android/incallui/InCallPresenter.java
+++ b/java/com/android/incallui/InCallPresenter.java
@@ -127,6 +127,8 @@
private StatusBarNotifier statusBarNotifier;
private ExternalCallNotifier externalCallNotifier;
+ private InCallVibrationHandler vibrationHandler;
+ private InCallDndHandler dndHandler;
private ContactInfoCache contactInfoCache;
private Context context;
private final OnCheckBlockedListener onCheckBlockedListener =
@@ -364,6 +366,12 @@
.getEnrichedCallManager()
.registerStateChangedListener(this.statusBarNotifier);
+ vibrationHandler = new InCallVibrationHandler(context);
+ addListener(vibrationHandler);
+
+ dndHandler = new InCallDndHandler(context);
+ addListener(dndHandler);
+
this.proximitySensor = proximitySensor;
addListener(this.proximitySensor);
@@ -1653,6 +1661,16 @@
}
statusBarNotifier = null;
+ if (vibrationHandler != null) {
+ removeListener(vibrationHandler);
+ }
+ vibrationHandler = null;
+
+ if (dndHandler != null) {
+ removeListener(dndHandler);
+ }
+ dndHandler = null;
+
if (callList != null) {
callList.removeListener(this);
callList.removeListener(spamCallListListener);
diff --git a/java/com/android/incallui/InCallServiceImpl.java b/java/com/android/incallui/InCallServiceImpl.java
index b9d0ecc..b2d318f 100644
--- a/java/com/android/incallui/InCallServiceImpl.java
+++ b/java/com/android/incallui/InCallServiceImpl.java
@@ -27,6 +27,7 @@
import com.android.dialer.feedback.FeedbackComponent;
import com.android.incallui.audiomode.AudioModeProvider;
import com.android.incallui.call.CallList;
+import com.android.incallui.call.CallRecorder;
import com.android.incallui.call.ExternalCallList;
import com.android.incallui.call.TelecomAdapter;
import com.android.incallui.speakeasy.SpeakEasyCallManager;
@@ -112,6 +113,7 @@
InCallPresenter.getInstance().onServiceBind();
InCallPresenter.getInstance().maybeStartRevealAnimation(intent);
TelecomAdapter.getInstance().setInCallService(this);
+ CallRecorder.getInstance().setUp(context);
returnToCallController =
new ReturnToCallController(this, ContactInfoCache.getInstance(context));
feedbackListener = FeedbackComponent.get(context).getCallFeedbackListener();
diff --git a/java/com/android/incallui/InCallVibrationHandler.java b/java/com/android/incallui/InCallVibrationHandler.java
new file mode 100644
index 0000000..067d6bf
--- /dev/null
+++ b/java/com/android/incallui/InCallVibrationHandler.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.incallui;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Vibrator;
+import android.preference.PreferenceManager;
+import android.telecom.DisconnectCause;
+
+import com.android.incallui.call.CallList;
+import com.android.incallui.call.DialerCall;
+import com.android.incallui.call.state.DialerCallState;
+import com.android.incallui.InCallPresenter.InCallState;
+
+public class InCallVibrationHandler extends Handler implements
+ InCallPresenter.InCallStateListener {
+
+ private static final int MSG_VIBRATE_45_SEC = 1;
+
+ private static final String KEY_VIBRATE_CALL_WAITING = "incall_vibrate_call_waiting";
+ private static final String KEY_VIBRATE_OUTGOING = "incall_vibrate_outgoing";
+ private static final String KEY_VIBRATE_45SECS = "incall_vibrate_45secs";
+ private static final String KEY_VIBRATE_HANGUP = "incall_vibrate_hangup";
+
+ private SharedPreferences prefs;
+ private Vibrator vibrator;
+ private DialerCall activeCall;
+
+ public InCallVibrationHandler(Context context) {
+ prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_VIBRATE_45_SEC:
+ vibrate(70, 0, 0);
+ sendEmptyMessageDelayed(MSG_VIBRATE_45_SEC, 60000);
+ break;
+ }
+ }
+
+ @Override
+ public void onStateChange(InCallState oldState, InCallState newState, CallList callList) {
+ DialerCall activeCall = callList.getActiveCall();
+
+ if (activeCall != null && this.activeCall == null) {
+ Log.d(this, "Transition to active call " + activeCall);
+ if (activeCall.isOutgoing()) {
+ handleOutgoingCallVibration(activeCall);
+ }
+ this.activeCall = activeCall;
+ } else if (activeCall != null && callList.getIncomingCall() != null
+ && !callList.getIncomingCall().equals(activeCall)) {
+ Log.d(this, "New incoming call" + callList.getIncomingCall());
+ handleCallWaitingVibration(activeCall);
+ } else if (activeCall == null && this.activeCall != null) {
+ Log.d(this, "Transition from active call " + this.activeCall);
+ handleCallEnd(this.activeCall);
+ this.activeCall = null;
+ }
+ }
+
+ private void handleOutgoingCallVibration(DialerCall call) {
+ long durationMillis = System.currentTimeMillis() - call.getConnectTimeMillis();
+ Log.d(this, "Start outgoing call: duration = " + durationMillis);
+
+ if (prefs.getBoolean(KEY_VIBRATE_OUTGOING, false) && durationMillis < 200) {
+ vibrate(100, 200, 0);
+ }
+ if (prefs.getBoolean(KEY_VIBRATE_45SECS, false)) {
+ start45SecondVibration(durationMillis);
+ }
+ }
+
+ private void handleCallWaitingVibration(DialerCall call) {
+ Log.d(this, "Start call waiting vibration");
+ if (prefs.getBoolean(KEY_VIBRATE_CALL_WAITING, false)) {
+ vibrate(200, 300, 500);
+ }
+ }
+
+ private void handleCallEnd(DialerCall call) {
+ long durationMillis = System.currentTimeMillis() - call.getConnectTimeMillis();
+ DisconnectCause cause = call.getDisconnectCause();
+ boolean localDisconnect =
+ // Disconnection not yet processed
+ call.getState() == DialerCallState.DISCONNECTING ||
+ // Disconnection already processed
+ (cause != null && cause.getCode() == DisconnectCause.LOCAL);
+
+ Log.d(this, "Ending active call: duration = " + durationMillis
+ + ", locally disconnected = " + localDisconnect);
+
+ if (prefs.getBoolean(KEY_VIBRATE_HANGUP, false)
+ && !localDisconnect && durationMillis > 500) {
+ vibrate(50, 100, 50);
+ }
+ // Stop 45-second vibration
+ removeMessages(MSG_VIBRATE_45_SEC);
+ }
+
+ private void start45SecondVibration(long callDurationMillis) {
+ callDurationMillis = callDurationMillis % 60000;
+ Log.d(this, "vibrate start @" + callDurationMillis);
+ removeMessages(MSG_VIBRATE_45_SEC);
+
+ long timer;
+ if (callDurationMillis > 45000) {
+ // Schedule the alarm at the next minute + 45 secs
+ timer = 45000 + 60000 - callDurationMillis;
+ } else {
+ // Schedule the alarm at the first 45 second mark
+ timer = 45000 - callDurationMillis;
+ }
+ sendEmptyMessageDelayed(MSG_VIBRATE_45_SEC, timer);
+ }
+
+ private void vibrate(int v1, int p1, int v2) {
+ long[] pattern = new long[] {
+ 0, v1, p1, v2
+ };
+ vibrator.vibrate(pattern, -1);
+ }
+}
diff --git a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
index a21073d..7abea54 100644
--- a/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
+++ b/java/com/android/incallui/answer/impl/answermethod/FlingUpDownTouchHandler.java
@@ -367,21 +367,6 @@
}
private boolean isFalseTouch() {
- if (falsingManager != null && falsingManager.isEnabled()) {
- if (falsingManager.isFalseTouch()) {
- if (touchUsesFalsing) {
- LogUtil.i("FlingUpDownTouchHandler.isFalseTouch", "rejecting false touch");
- return true;
- } else {
- LogUtil.i(
- "FlingUpDownTouchHandler.isFalseTouch",
- "Suspected false touch, but not using false touch rejection for this gesture");
- return false;
- }
- } else {
- return false;
- }
- }
return !touchAboveFalsingThreshold;
}
diff --git a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
index 8aeb05f..dbb8df9 100644
--- a/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
+++ b/java/com/android/incallui/answerproximitysensor/AnswerProximitySensor.java
@@ -27,6 +27,8 @@
import com.android.incallui.call.DialerCallListener;
import com.android.incallui.call.state.DialerCallState;
+import com.android.incallui.R;
+
/**
* This class prevents users from accidentally answering calls by keeping the screen off until the
* proximity sensor is unblocked. If the screen is already on or if this is a call waiting call then
@@ -61,6 +63,11 @@
return false;
}
+ if (!context.getResources().getBoolean(R.bool.config_answer_proximity_sensor_enabled)) {
+ LogUtil.i("AnswerProximitySensor.shouldUse", "disabled by overlay");
+ return false;
+ }
+
if (!context
.getSystemService(PowerManager.class)
.isWakeLockLevelSupported(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK)) {
diff --git a/java/com/android/incallui/audiomode/AudioModeProvider.java b/java/com/android/incallui/audiomode/AudioModeProvider.java
index 47b021e..cefee0d 100644
--- a/java/com/android/incallui/audiomode/AudioModeProvider.java
+++ b/java/com/android/incallui/audiomode/AudioModeProvider.java
@@ -89,6 +89,7 @@
hasBluetooth = true;
continue;
case AudioDeviceInfo.TYPE_WIRED_HEADSET:
+ case AudioDeviceInfo.TYPE_USB_HEADSET:
hasHeadset = true;
continue;
default:
diff --git a/java/com/android/incallui/call/CallList.java b/java/com/android/incallui/call/CallList.java
index 634a302..8428c84 100644
--- a/java/com/android/incallui/call/CallList.java
+++ b/java/com/android/incallui/call/CallList.java
@@ -26,6 +26,7 @@
import android.telecom.Call;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccount;
+import android.text.TextUtils;
import android.util.ArrayMap;
import com.android.dialer.blocking.FilteredNumberAsyncQueryHandler;
import com.android.dialer.common.Assert;
@@ -535,6 +536,15 @@
return retval;
}
+ public DialerCall getCallWithStateAndNumber(int state, String number) {
+ for (DialerCall call : callById.values()) {
+ if (TextUtils.equals(call.getNumber(), number) && call.getState() == state) {
+ return call;
+ }
+ }
+ return null;
+ }
+
/**
* Return if there is any active or background call which was not a parent call (never had a child
* call)
diff --git a/java/com/android/incallui/call/CallRecorder.java b/java/com/android/incallui/call/CallRecorder.java
new file mode 100644
index 0000000..867d5a5
--- /dev/null
+++ b/java/com/android/incallui/call/CallRecorder.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (C) 2014 The CyanogenMod 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.incallui.call;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.res.XmlResourceParser;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.text.TextUtils;
+import android.util.Log;
+import android.widget.Toast;
+
+import com.android.dialer.R;
+import com.android.dialer.callrecord.CallRecordingDataStore;
+import com.android.dialer.callrecord.CallRecording;
+import com.android.dialer.callrecord.ICallRecorderService;
+import com.android.dialer.callrecord.impl.CallRecorderService;
+import com.android.dialer.location.GeoUtil;
+import com.android.incallui.call.state.DialerCallState;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Locale;
+
+/**
+ * InCall UI's interface to the call recorder
+ *
+ * Manages the call recorder service lifecycle. We bind to the service whenever an active call
+ * is established, and unbind when all calls have been disconnected.
+ */
+public class CallRecorder implements CallList.Listener {
+ public static final String TAG = "CallRecorder";
+
+ public static final String[] REQUIRED_PERMISSIONS = new String[] {
+ android.Manifest.permission.RECORD_AUDIO,
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+ };
+ private static final HashMap<String, Boolean> RECORD_ALLOWED_STATE_BY_COUNTRY = new HashMap<>();
+
+ private static CallRecorder instance = null;
+ private Context context;
+ private boolean initialized = false;
+ private ICallRecorderService service = null;
+
+ private HashSet<RecordingProgressListener> progressListeners =
+ new HashSet<RecordingProgressListener>();
+ private Handler handler = new Handler();
+
+ private ServiceConnection connection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ CallRecorder.this.service = ICallRecorderService.Stub.asInterface(service);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ CallRecorder.this.service = null;
+ }
+ };
+
+ public static CallRecorder getInstance() {
+ if (instance == null) {
+ instance = new CallRecorder();
+ }
+ return instance;
+ }
+
+ public boolean isEnabled() {
+ return CallRecorderService.isEnabled(context);
+ }
+
+ public boolean canRecordInCurrentCountry() {
+ if (!isEnabled()) {
+ return false;
+ }
+ if (RECORD_ALLOWED_STATE_BY_COUNTRY.isEmpty()) {
+ loadAllowedStates();
+ }
+
+ String currentCountryIso = GeoUtil.getCurrentCountryIso(context);
+ Boolean allowedState = RECORD_ALLOWED_STATE_BY_COUNTRY.get(currentCountryIso);
+
+ return allowedState != null && allowedState;
+ }
+
+ private CallRecorder() {
+ CallList.getInstance().addListener(this);
+ }
+
+ public void setUp(Context context) {
+ this.context = context.getApplicationContext();
+ }
+
+ private void initialize() {
+ if (isEnabled() && !initialized) {
+ Intent serviceIntent = new Intent(context, CallRecorderService.class);
+ context.bindService(serviceIntent, connection, Context.BIND_AUTO_CREATE);
+ initialized = true;
+ }
+ }
+
+ private void uninitialize() {
+ if (initialized) {
+ context.unbindService(connection);
+ initialized = false;
+ }
+ }
+
+ public boolean startRecording(final String phoneNumber, final long creationTime) {
+ if (service == null) {
+ return false;
+ }
+
+ try {
+ if (service.startRecording(phoneNumber, creationTime)) {
+ for (RecordingProgressListener l : progressListeners) {
+ l.onStartRecording();
+ }
+ updateRecordingProgressTask.run();
+ return true;
+ } else {
+ Toast.makeText(context, R.string.call_recording_failed_message, Toast.LENGTH_SHORT)
+ .show();
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to start recording " + phoneNumber + ", " + new Date(creationTime), e);
+ }
+
+ return false;
+ }
+
+ public boolean isRecording() {
+ if (service == null) {
+ return false;
+ }
+
+ try {
+ return service.isRecording();
+ } catch (RemoteException e) {
+ Log.w(TAG, "Exception checking recording status", e);
+ }
+ return false;
+ }
+
+ public CallRecording getActiveRecording() {
+ if (service == null) {
+ return null;
+ }
+
+ try {
+ return service.getActiveRecording();
+ } catch (RemoteException e) {
+ Log.w("Exception getting active recording", e);
+ }
+ return null;
+ }
+
+ public void finishRecording() {
+ if (service != null) {
+ try {
+ final CallRecording recording = service.stopRecording();
+ if (recording != null) {
+ if (!TextUtils.isEmpty(recording.phoneNumber)) {
+ new Thread(() -> {
+ CallRecordingDataStore dataStore = new CallRecordingDataStore();
+ dataStore.open(context);
+ dataStore.putRecording(recording);
+ dataStore.close();
+ }).start();
+ } else {
+ // Data store is an index by number so that we can link recordings in the
+ // call detail page. If phone number is not available (conference call or
+ // unknown number) then just display a toast.
+ String msg = context.getResources().getString(
+ R.string.call_recording_file_location, recording.fileName);
+ Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
+ }
+ }
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to stop recording", e);
+ }
+ }
+
+ for (RecordingProgressListener l : progressListeners) {
+ l.onStopRecording();
+ }
+ handler.removeCallbacks(updateRecordingProgressTask);
+ }
+
+ //
+ // Call list listener methods.
+ //
+ @Override
+ public void onIncomingCall(DialerCall call) {
+ // do nothing
+ }
+
+ @Override
+ public void onCallListChange(final CallList callList) {
+ if (!initialized && callList.getActiveCall() != null) {
+ // we'll come here if this is the first active call
+ initialize();
+ } else {
+ // we can come down this branch to resume a call that was on hold
+ CallRecording active = getActiveRecording();
+ if (active != null) {
+ DialerCall call =
+ callList.getCallWithStateAndNumber(DialerCallState.ONHOLD, active.phoneNumber);
+ if (call != null) {
+ // The call associated with the active recording has been placed
+ // on hold, so stop the recording.
+ finishRecording();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onDisconnect(final DialerCall call) {
+ CallRecording active = getActiveRecording();
+ if (active != null && TextUtils.equals(call.getNumber(), active.phoneNumber)) {
+ // finish the current recording if the call gets disconnected
+ finishRecording();
+ }
+
+ // tear down the service if there are no more active calls
+ if (CallList.getInstance().getActiveCall() == null) {
+ uninitialize();
+ }
+ }
+
+ @Override
+ public void onUpgradeToVideo(DialerCall call) {}
+
+ @Override
+ public void onSessionModificationStateChange(DialerCall call) {}
+
+ @Override
+ public void onWiFiToLteHandover(DialerCall call) {}
+
+ @Override
+ public void onHandoverToWifiFailed(DialerCall call) {}
+
+ @Override
+ public void onInternationalCallOnWifi(DialerCall call) {}
+
+ // allow clients to listen for recording progress updates
+ public interface RecordingProgressListener {
+ void onStartRecording();
+ void onStopRecording();
+ void onRecordingTimeProgress(long elapsedTimeMs);
+ }
+
+ public void addRecordingProgressListener(RecordingProgressListener listener) {
+ progressListeners.add(listener);
+ }
+
+ public void removeRecordingProgressListener(RecordingProgressListener listener) {
+ progressListeners.remove(listener);
+ }
+
+ private static final int UPDATE_INTERVAL = 500;
+
+ private Runnable updateRecordingProgressTask = new Runnable() {
+ @Override
+ public void run() {
+ CallRecording active = getActiveRecording();
+ if (active != null) {
+ long elapsed = System.currentTimeMillis() - active.startRecordingTime;
+ for (RecordingProgressListener l : progressListeners) {
+ l.onRecordingTimeProgress(elapsed);
+ }
+ }
+ handler.postDelayed(this, UPDATE_INTERVAL);
+ }
+ };
+
+ private void loadAllowedStates() {
+ XmlResourceParser parser = context.getResources().getXml(R.xml.call_record_states);
+ try {
+ // Consume all START_DOCUMENT which can appear more than once.
+ while (parser.next() == XmlPullParser.START_DOCUMENT) {}
+
+ parser.require(XmlPullParser.START_TAG, null, "call-record-allowed-flags");
+
+ while (parser.next() != XmlPullParser.END_DOCUMENT) {
+ if (parser.getEventType() != XmlPullParser.START_TAG) {
+ continue;
+ }
+ parser.require(XmlPullParser.START_TAG, null, "country");
+
+ String iso = parser.getAttributeValue(null, "iso");
+ String allowed = parser.getAttributeValue(null, "allowed");
+ if (iso != null && ("true".equals(allowed) || "false".equals(allowed))) {
+ for (String splittedIso : iso.split(",")) {
+ RECORD_ALLOWED_STATE_BY_COUNTRY.put(
+ splittedIso.toUpperCase(Locale.US), Boolean.valueOf(allowed));
+ }
+ } else {
+ throw new XmlPullParserException("Unexpected country specification", parser, null);
+ }
+ }
+ Log.d(TAG, "Loaded " + RECORD_ALLOWED_STATE_BY_COUNTRY.size() + " country records");
+ } catch (XmlPullParserException | IOException e) {
+ Log.e(TAG, "Could not parse allowed country list", e);
+ RECORD_ALLOWED_STATE_BY_COUNTRY.clear();
+ } finally {
+ parser.close();
+ }
+ }
+}
diff --git a/java/com/android/incallui/call/DialerCall.java b/java/com/android/incallui/call/DialerCall.java
index 76d3e8b..b028946 100644
--- a/java/com/android/incallui/call/DialerCall.java
+++ b/java/com/android/incallui/call/DialerCall.java
@@ -173,6 +173,7 @@
@Nullable private SpamStatus spamStatus;
private boolean isBlocked;
+ private boolean isOutgoing;
private boolean didShowCameraPermission;
private boolean didDismissVideoChargesAlertDialog;
@@ -905,6 +906,8 @@
logState.dialerConnectTimeMillisElapsedRealtime == 0
? 0
: SystemClock.elapsedRealtime() - logState.dialerConnectTimeMillisElapsedRealtime;
+ } else if (state == DialerCallState.DIALING || state == DialerCallState.CONNECTING) {
+ isOutgoing = true;
}
}
@@ -913,6 +916,10 @@
this.clock = clock;
}
+ public boolean isOutgoing() {
+ return isOutgoing;
+ }
+
public int getNumberPresentation() {
return telecomCall == null ? -1 : telecomCall.getDetails().getHandlePresentation();
}
diff --git a/java/com/android/incallui/callpending/CallPendingActivity.java b/java/com/android/incallui/callpending/CallPendingActivity.java
index 4086e14..5177783 100644
--- a/java/com/android/incallui/callpending/CallPendingActivity.java
+++ b/java/com/android/incallui/callpending/CallPendingActivity.java
@@ -285,6 +285,9 @@
public void swapSimClicked() {}
@Override
+ public void callRecordClicked(boolean checked) {}
+
+ @Override
public Context getContext() {
return CallPendingActivity.this;
}
diff --git a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
index 757d813..733dcf9 100644
--- a/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
+++ b/java/com/android/incallui/incall/impl/ButtonChooserFactory.java
@@ -117,9 +117,10 @@
mapping.put(InCallButtonIds.BUTTON_MUTE, MappingInfo.builder(0).build());
mapping.put(InCallButtonIds.BUTTON_DIALPAD, MappingInfo.builder(1).build());
mapping.put(InCallButtonIds.BUTTON_AUDIO, MappingInfo.builder(2).build());
- mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(3).setSlotOrder(5).build());
- mapping.put(InCallButtonIds.BUTTON_ADD_CALL, MappingInfo.builder(3).build());
- mapping.put(InCallButtonIds.BUTTON_SWAP_SIM, MappingInfo.builder(4).build());
+ mapping.put(InCallButtonIds.BUTTON_RECORD_CALL, MappingInfo.builder(3).build());
+ mapping.put(InCallButtonIds.BUTTON_MERGE, MappingInfo.builder(4).setSlotOrder(5).build());
+ mapping.put(InCallButtonIds.BUTTON_ADD_CALL, MappingInfo.builder(4).build());
+ mapping.put(InCallButtonIds.BUTTON_SWAP_SIM, MappingInfo.builder(5).build());
return mapping;
}
}
diff --git a/java/com/android/incallui/incall/impl/ButtonController.java b/java/com/android/incallui/incall/impl/ButtonController.java
index 328ebbe..2ad3d3e 100644
--- a/java/com/android/incallui/incall/impl/ButtonController.java
+++ b/java/com/android/incallui/incall/impl/ButtonController.java
@@ -16,6 +16,7 @@
package com.android.incallui.incall.impl;
+import android.content.res.Resources;
import android.graphics.drawable.AnimationDrawable;
import android.support.annotation.CallSuper;
import android.support.annotation.DrawableRes;
@@ -23,6 +24,7 @@
import android.support.annotation.StringRes;
import android.telecom.CallAudioState;
import android.text.TextUtils;
+import android.text.format.DateUtils;
import android.view.View;
import android.view.View.OnClickListener;
import com.android.dialer.common.Assert;
@@ -411,6 +413,95 @@
}
}
+ class CallRecordButtonController implements ButtonController, OnClickListener {
+ @NonNull private final InCallButtonUiDelegate delegate;
+ private boolean isEnabled;
+ private boolean isAllowed;
+ private boolean isChecked;
+ private long recordingSeconds;
+ private CheckableLabeledButton button;
+
+ public CallRecordButtonController(@NonNull InCallButtonUiDelegate delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return isEnabled;
+ }
+
+ @Override
+ public void setEnabled(boolean isEnabled) {
+ this.isEnabled = isEnabled;
+ if (button != null) {
+ button.setEnabled(isEnabled);
+ }
+ }
+
+ @Override
+ public boolean isAllowed() {
+ return isAllowed;
+ }
+
+ @Override
+ public void setAllowed(boolean isAllowed) {
+ this.isAllowed = isAllowed;
+ if (button != null) {
+ button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
+
+ @Override
+ public void setChecked(boolean isChecked) {
+ this.isChecked = isChecked;
+ if (button != null) {
+ button.setChecked(isChecked);
+ }
+ }
+
+ @Override
+ public int getInCallButtonId() {
+ return InCallButtonIds.BUTTON_RECORD_CALL;
+ }
+
+ @Override
+ public void setButton(CheckableLabeledButton button) {
+ this.button = button;
+ if (button != null) {
+ final Resources res = button.getContext().getResources();
+ if (isChecked) {
+ CharSequence duration = DateUtils.formatElapsedTime(recordingSeconds);
+ button.setLabelText(res.getString(R.string.onscreenCallRecordingText, duration));
+ } else {
+ button.setLabelText(R.string.onscreenCallRecordText);
+ }
+ button.setEnabled(isEnabled);
+ button.setVisibility(isAllowed ? View.VISIBLE : View.INVISIBLE);
+ button.setChecked(isChecked);
+ button.setOnClickListener(this);
+ button.setIconDrawable(R.drawable.quantum_ic_record_white_36);
+ button.setContentDescription(res.getText(
+ isChecked ? R.string.onscreenStopCallRecordText : R.string.onscreenCallRecordText));
+ button.setShouldShowMoreIndicator(false);
+ }
+ }
+
+ public void setRecordingState(boolean recording) {
+ isChecked = recording;
+ setButton(button);
+ }
+
+ public void setRecordingDuration(long durationMs) {
+ recordingSeconds = (durationMs + 500) / 1000;
+ setButton(button);
+ }
+
+ @Override
+ public void onClick(View v) {
+ delegate.callRecordClicked(!isChecked);
+ }
+ }
+
class DialpadButtonController extends SimpleCheckableButtonController {
public DialpadButtonController(@NonNull InCallButtonUiDelegate delegate) {
diff --git a/java/com/android/incallui/incall/impl/CheckableLabeledButton.java b/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
index bfc2781..ec932b9 100644
--- a/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
+++ b/java/com/android/incallui/incall/impl/CheckableLabeledButton.java
@@ -156,6 +156,10 @@
labelView.setText(stringRes);
}
+ public void setLabelText(CharSequence label) {
+ labelView.setText(label);
+ }
+
/** Shows or hides a little down arrow to indicate that the button will pop up a menu. */
public void setShouldShowMoreIndicator(boolean shouldShow) {
iconView.setBackground(shouldShow ? backgroundMore : background);
diff --git a/java/com/android/incallui/incall/impl/InCallFragment.java b/java/com/android/incallui/incall/impl/InCallFragment.java
index 3062069..336550d 100644
--- a/java/com/android/incallui/incall/impl/InCallFragment.java
+++ b/java/com/android/incallui/incall/impl/InCallFragment.java
@@ -54,6 +54,7 @@
import com.android.incallui.audioroute.AudioRouteSelectorDialogFragment.AudioRouteSelectorPresenter;
import com.android.incallui.contactgrid.ContactGridManager;
import com.android.incallui.hold.OnHoldFragment;
+import com.android.incallui.incall.impl.ButtonController.CallRecordButtonController;
import com.android.incallui.incall.impl.ButtonController.SpeakerButtonController;
import com.android.incallui.incall.impl.ButtonController.UpgradeToRttButtonController;
import com.android.incallui.incall.impl.InCallButtonGridFragment.OnButtonGridCreatedListener;
@@ -95,6 +96,8 @@
private int phoneType;
private boolean stateRestored;
+ private static final int REQUEST_CODE_CALL_RECORD_PERMISSION = 1000;
+
// Add animation to educate users. If a call has enriched calling attachments then we'll
// initially show the attachment page. After a delay seconds we'll animate to the button grid.
private final Handler handler = new Handler();
@@ -117,7 +120,8 @@
|| id == InCallButtonIds.BUTTON_MERGE
|| id == InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE
|| id == InCallButtonIds.BUTTON_SWAP_SIM
- || id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT;
+ || id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT
+ || id == InCallButtonIds.BUTTON_RECORD_CALL;
}
@Override
@@ -232,6 +236,7 @@
new ButtonController.ManageConferenceButtonController(inCallScreenDelegate));
buttonControllers.add(
new ButtonController.SwitchToSecondaryButtonController(inCallScreenDelegate));
+ buttonControllers.add(new ButtonController.CallRecordButtonController(inCallButtonUiDelegate));
inCallScreenDelegate.onInCallScreenDelegateInit(this);
inCallScreenDelegate.onInCallScreenReady();
@@ -467,6 +472,39 @@
}
@Override
+ public void setCallRecordingState(boolean isRecording) {
+ ((CallRecordButtonController) getButtonController(InCallButtonIds.BUTTON_RECORD_CALL))
+ .setRecordingState(isRecording);
+ }
+
+ @Override
+ public void setCallRecordingDuration(long durationMs) {
+ ((CallRecordButtonController) getButtonController(InCallButtonIds.BUTTON_RECORD_CALL))
+ .setRecordingDuration(durationMs);
+ }
+
+ @Override
+ public void requestCallRecordingPermissions(String[] permissions) {
+ requestPermissions(permissions, REQUEST_CODE_CALL_RECORD_PERMISSION);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode,
+ @NonNull String[] permissions, @NonNull int[] grantResults) {
+ if (requestCode == REQUEST_CODE_CALL_RECORD_PERMISSION) {
+ boolean allGranted = grantResults.length > 0;
+ for (int i = 0; i < grantResults.length; i++) {
+ allGranted &= grantResults[i] == PackageManager.PERMISSION_GRANTED;
+ }
+ if (allGranted) {
+ inCallButtonUiDelegate.callRecordClicked(true);
+ }
+ } else {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ }
+ }
+
+ @Override
public void updateButtonStates() {
// When the incall screen is ready, this method is called from #setSecondary, even though the
// incall button ui is not ready yet. This method is called again once the incall button ui is
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIds.java b/java/com/android/incallui/incall/protocol/InCallButtonIds.java
index 80ea75e..2c956c8 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonIds.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonIds.java
@@ -38,6 +38,7 @@
InCallButtonIds.BUTTON_MANAGE_VOICE_CONFERENCE,
InCallButtonIds.BUTTON_SWITCH_TO_SECONDARY,
InCallButtonIds.BUTTON_SWAP_SIM,
+ InCallButtonIds.BUTTON_RECORD_CALL,
InCallButtonIds.BUTTON_COUNT,
InCallButtonIds.BUTTON_UPGRADE_TO_RTT
})
@@ -58,6 +59,7 @@
int BUTTON_MANAGE_VOICE_CONFERENCE = 12;
int BUTTON_SWITCH_TO_SECONDARY = 13;
int BUTTON_SWAP_SIM = 14;
- int BUTTON_COUNT = 15;
+ int BUTTON_RECORD_CALL = 15;
int BUTTON_UPGRADE_TO_RTT = 16;
+ int BUTTON_COUNT = 17;
}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
index 5239d9d..ee03c3d 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonIdsExtension.java
@@ -58,6 +58,8 @@
return "SWAP_SIM";
} else if (id == InCallButtonIds.BUTTON_UPGRADE_TO_RTT) {
return "UPGRADE_TO_RTT";
+ } else if (id == InCallButtonIds.BUTTON_RECORD_CALL) {
+ return "RECORD_CALL";
} else {
return "INVALID_BUTTON: " + id;
}
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUi.java b/java/com/android/incallui/incall/protocol/InCallButtonUi.java
index 28dd84c..f22efeb 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonUi.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUi.java
@@ -37,6 +37,12 @@
void setAudioState(CallAudioState audioState);
+ void setCallRecordingState(boolean isRecording);
+
+ void setCallRecordingDuration(long durationMs);
+
+ void requestCallRecordingPermissions(String[] permissions);
+
/**
* Once showButton() has been called on each of the individual buttons in the UI, call this to
* configure the overflow menu appropriately.
diff --git a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
index 4cf40ef..4e25ff0 100644
--- a/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
+++ b/java/com/android/incallui/incall/protocol/InCallButtonUiDelegate.java
@@ -65,5 +65,7 @@
void swapSimClicked();
+ void callRecordClicked(boolean checked);
+
Context getContext();
}
diff --git a/java/com/android/incallui/res/values/cm_strings.xml b/java/com/android/incallui/res/values/cm_strings.xml
new file mode 100644
index 0000000..5e65ffc
--- /dev/null
+++ b/java/com/android/incallui/res/values/cm_strings.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (C) 2013-2014 The CyanogenMod 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.
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <string name="call_recording_failed_message">Failed to start call recording</string>
+ <string name="call_recording_file_location">Saved call recording to <xliff:g id="filename">%s</xliff:g></string>
+ <string name="onscreenCallRecordText">Record call</string>
+ <string name="onscreenCallRecordingText">Recording call - <xliff:g id="duration" example="00:10">%1$s</xliff:g></string>
+ <string name="onscreenStopCallRecordText">Stop recording</string>
+ <string name="recording_time_text">Recording</string>
+ <string name="recording_warning_title">Enable call recording?</string>
+ <string name="recording_warning_text">Notice: You are responsible for compliance with any laws, regulations and rules that apply to the use of call recording functionality and the use or distribution of those recordings.</string>
+</resources>
diff --git a/java/com/android/incallui/res/values/lineage_config.xml b/java/com/android/incallui/res/values/lineage_config.xml
new file mode 100644
index 0000000..e9a958d
--- /dev/null
+++ b/java/com/android/incallui/res/values/lineage_config.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2018 The LineageOS 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.
+-->
+<resources>
+ <!-- Whether to check that the proximity is unblocked before showing the incoming call UI. -->
+ <bool name="config_answer_proximity_sensor_enabled">true</bool>
+</resources>
diff --git a/java/com/android/incallui/rtt/impl/RttChatFragment.java b/java/com/android/incallui/rtt/impl/RttChatFragment.java
index 649e808..3e76f1f 100644
--- a/java/com/android/incallui/rtt/impl/RttChatFragment.java
+++ b/java/com/android/incallui/rtt/impl/RttChatFragment.java
@@ -595,4 +595,13 @@
@Override
public void onAudioRouteSelectorDismiss() {}
+
+ @Override
+ public void requestCallRecordingPermissions(String[] permissions) {}
+
+ @Override
+ public void setCallRecordingDuration(long duration) {}
+
+ @Override
+ public void setCallRecordingState(boolean isRecording) {}
}
diff --git a/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java b/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java
index 89db079..07965b9 100644
--- a/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/SurfaceViewVideoCallFragment.java
@@ -797,6 +797,18 @@
}
@Override
+ public void setCallRecordingState(boolean isRecording) {
+ }
+
+ @Override
+ public void setCallRecordingDuration(long durationMs) {
+ }
+
+ @Override
+ public void requestCallRecordingPermissions(String[] permissions) {
+ }
+
+ @Override
public void updateButtonStates() {
LogUtil.i("SurfaceViewVideoCallFragment.updateButtonState", null);
speakerButtonController.updateButtonState();
diff --git a/java/com/android/incallui/video/impl/VideoCallFragment.java b/java/com/android/incallui/video/impl/VideoCallFragment.java
index edfc17c..3fbce5c 100644
--- a/java/com/android/incallui/video/impl/VideoCallFragment.java
+++ b/java/com/android/incallui/video/impl/VideoCallFragment.java
@@ -909,6 +909,18 @@
}
@Override
+ public void setCallRecordingState(boolean isRecording) {
+ }
+
+ @Override
+ public void setCallRecordingDuration(long durationMs) {
+ }
+
+ @Override
+ public void requestCallRecordingPermissions(String[] permissions) {
+ }
+
+ @Override
public void updateButtonStates() {
LogUtil.i("VideoCallFragment.updateButtonState", null);
speakerButtonController.updateButtonState();
diff --git a/privapp_whitelist_com.android.dialer-ext.xml b/privapp_whitelist_com.android.dialer-ext.xml
new file mode 100644
index 0000000..09e6e0f
--- /dev/null
+++ b/privapp_whitelist_com.android.dialer-ext.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019-2020 The LineageOS 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.
+-->
+<permissions>
+ <!-- Additional permissions on top of privapp_whitelist_com.android.dialer.xml -->
+ <privapp-permissions package="com.android.dialer">
+ <permission name="android.permission.CAPTURE_AUDIO_OUTPUT"/>
+ </privapp-permissions>
+</permissions>
diff --git a/proguard.flags b/proguard.flags
index 160020b..cb80337 100644
--- a/proguard.flags
+++ b/proguard.flags
@@ -1,4 +1,8 @@
# These are referenced by com.google.common.util.concurrent and
# com.google.errorprone.annotations but don't exist on Android.
-dontwarn java.lang.ClassValue
--dontwarn javax.lang.model.element.Modifier
\ No newline at end of file
+-dontwarn javax.lang.model.element.Modifier
+
+-keep public class * extends android.app.Fragment {
+ public <init>();
+}