summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTreehugger Robot <android-test-infra-autosubmit@system.gserviceaccount.com>2023-09-13 22:51:06 +0000
committerAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>2023-09-13 22:51:06 +0000
commitbe2414684db23d31266f25adee5587cdc18f35ec (patch)
tree332f68d89179598adaaa6ae413ad970acb72b6d8
parent4c4c503b188798cdc031a958d8bda4145905d839 (diff)
parente4f6e0b3a32837a705593c1f2244c0f1ad5edc44 (diff)
downloadlibphonenumber-be2414684db23d31266f25adee5587cdc18f35ec.tar.gz
Merge "Update libphonenumber to v8.13.20" into main am: 4b3245f4b9 am: 68a9dd675a am: 43262c4b45 am: e4f6e0b3a3
Original change: https://android-review.googlesource.com/c/platform/external/libphonenumber/+/2750943 Change-Id: I836d2d3947f4e243357109180225abb1d1bbac67 Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
-rw-r--r--README.version2
-rw-r--r--carrier/pom.xml8
-rw-r--r--carrier/src/com/google/i18n/phonenumbers/carrier/data/216_enbin111 -> 115 bytes
-rw-r--r--carrier/src/com/google/i18n/phonenumbers/carrier/data/250_enbin59 -> 69 bytes
-rw-r--r--carrier/src/com/google/i18n/phonenumbers/carrier/data/46_enbin5896 -> 5902 bytes
-rw-r--r--carrier/src/com/google/i18n/phonenumbers/carrier/data/56_enbin20280 -> 20286 bytes
-rw-r--r--carrier/src/com/google/i18n/phonenumbers/carrier/data/592_enbin247 -> 222 bytes
-rw-r--r--demo/pom.xml10
-rw-r--r--demoapp/README.md51
-rw-r--r--demoapp/app/build.gradle45
-rw-r--r--demoapp/app/src/main/AndroidManifest.xml39
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/MyApplication.java16
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsPermissionManagement.java161
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsRead.java62
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsWrite.java59
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/main/CountryDropdown.java203
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/main/MainActivity.java226
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormatting.java74
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberInApp.java96
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/FormattableFragment.java161
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/FormattableRvAdapter.java157
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/NotFormattableFragment.java119
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/NotFormattableRvAdapter.java94
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/ResultActivity.java102
-rw-r--r--demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/ResultVpAdapter.java72
-rw-r--r--demoapp/app/src/main/res/drawable/ic_launcher_background.xml32
-rw-r--r--demoapp/app/src/main/res/drawable/ic_launcher_foreground.xml16
-rw-r--r--demoapp/app/src/main/res/drawable/ic_outline_home_30.xml10
-rw-r--r--demoapp/app/src/main/res/layout/activity_main.xml83
-rw-r--r--demoapp/app/src/main/res/layout/activity_result.xml26
-rw-r--r--demoapp/app/src/main/res/layout/country_dropdown.xml24
-rw-r--r--demoapp/app/src/main/res/layout/country_dropdown_item.xml9
-rw-r--r--demoapp/app/src/main/res/layout/formattable_list_item.xml66
-rw-r--r--demoapp/app/src/main/res/layout/fragment_formattable.xml30
-rw-r--r--demoapp/app/src/main/res/layout/fragment_not_formattable.xml64
-rw-r--r--demoapp/app/src/main/res/layout/not_formattable_list_item.xml57
-rw-r--r--demoapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml5
-rw-r--r--demoapp/app/src/main/res/values-night/themes.xml4
-rw-r--r--demoapp/app/src/main/res/values/dimens.xml9
-rw-r--r--demoapp/app/src/main/res/values/strings.xml48
-rw-r--r--demoapp/app/src/main/res/values/themes.xml4
-rw-r--r--demoapp/app/src/test/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormattingTest.java103
-rw-r--r--demoapp/app/src/test/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberInAppTest.java70
-rw-r--r--demoapp/build.gradle5
-rw-r--r--demoapp/gradle.properties21
-rw-r--r--demoapp/settings.gradle16
-rw-r--r--geocoder/pom.xml8
-rw-r--r--internal/prefixmapper/pom.xml6
-rw-r--r--libphonenumber/pom.xml4
-rw-r--r--libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ARbin8267 -> 8267 bytes
-rw-r--r--libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BDbin2120 -> 2123 bytes
-rw-r--r--libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GYbin486 -> 502 bytes
-rw-r--r--libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_ILbin1184 -> 1193 bytes
-rw-r--r--libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZbin1108 -> 1090 bytes
-rw-r--r--libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_OMbin541 -> 544 bytes
-rw-r--r--libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RWbin512 -> 513 bytes
-rw-r--r--libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TNbin463 -> 463 bytes
-rw-r--r--pom.xml4
-rw-r--r--repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_ARbin8267 -> 8267 bytes
-rw-r--r--repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_BDbin2120 -> 2123 bytes
-rw-r--r--repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_GYbin486 -> 502 bytes
-rw-r--r--repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_ILbin1184 -> 1193 bytes
-rw-r--r--repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZbin1108 -> 1090 bytes
-rw-r--r--repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_OMbin541 -> 544 bytes
-rw-r--r--repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_RWbin512 -> 513 bytes
-rw-r--r--repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_TNbin463 -> 463 bytes
66 files changed, 2460 insertions, 21 deletions
diff --git a/README.version b/README.version
index 127cfaa1..89d76e67 100644
--- a/README.version
+++ b/README.version
@@ -1,3 +1,3 @@
URL: https://github.com/googlei18n/libphonenumber/
-Version: 8.13.19
+Version: 8.13.20
BugComponent: 20868
diff --git a/carrier/pom.xml b/carrier/pom.xml
index 169e2838..cb198993 100644
--- a/carrier/pom.xml
+++ b/carrier/pom.xml
@@ -3,14 +3,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>carrier</artifactId>
- <version>1.203</version>
+ <version>1.204</version>
<packaging>jar</packaging>
<url>https://github.com/google/libphonenumber/</url>
<parent>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber-parent</artifactId>
- <version>8.13.19</version>
+ <version>8.13.20</version>
</parent>
<build>
@@ -79,12 +79,12 @@
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
- <version>8.13.19</version>
+ <version>8.13.20</version>
</dependency>
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>prefixmapper</artifactId>
- <version>2.213</version>
+ <version>2.214</version>
</dependency>
</dependencies>
diff --git a/carrier/src/com/google/i18n/phonenumbers/carrier/data/216_en b/carrier/src/com/google/i18n/phonenumbers/carrier/data/216_en
index aa0e8101..786bf231 100644
--- a/carrier/src/com/google/i18n/phonenumbers/carrier/data/216_en
+++ b/carrier/src/com/google/i18n/phonenumbers/carrier/data/216_en
Binary files differ
diff --git a/carrier/src/com/google/i18n/phonenumbers/carrier/data/250_en b/carrier/src/com/google/i18n/phonenumbers/carrier/data/250_en
index a2cfbf2c..cc01eb04 100644
--- a/carrier/src/com/google/i18n/phonenumbers/carrier/data/250_en
+++ b/carrier/src/com/google/i18n/phonenumbers/carrier/data/250_en
Binary files differ
diff --git a/carrier/src/com/google/i18n/phonenumbers/carrier/data/46_en b/carrier/src/com/google/i18n/phonenumbers/carrier/data/46_en
index 6723b21d..91064522 100644
--- a/carrier/src/com/google/i18n/phonenumbers/carrier/data/46_en
+++ b/carrier/src/com/google/i18n/phonenumbers/carrier/data/46_en
Binary files differ
diff --git a/carrier/src/com/google/i18n/phonenumbers/carrier/data/56_en b/carrier/src/com/google/i18n/phonenumbers/carrier/data/56_en
index a8ff5f47..d5b2f1a3 100644
--- a/carrier/src/com/google/i18n/phonenumbers/carrier/data/56_en
+++ b/carrier/src/com/google/i18n/phonenumbers/carrier/data/56_en
Binary files differ
diff --git a/carrier/src/com/google/i18n/phonenumbers/carrier/data/592_en b/carrier/src/com/google/i18n/phonenumbers/carrier/data/592_en
index ce6352d1..70e6ce7a 100644
--- a/carrier/src/com/google/i18n/phonenumbers/carrier/data/592_en
+++ b/carrier/src/com/google/i18n/phonenumbers/carrier/data/592_en
Binary files differ
diff --git a/demo/pom.xml b/demo/pom.xml
index 0a9ba529..98a7727d 100644
--- a/demo/pom.xml
+++ b/demo/pom.xml
@@ -3,13 +3,13 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>demo</artifactId>
- <version>8.13.19</version>
+ <version>8.13.20</version>
<packaging>war</packaging>
<url>https://github.com/google/libphonenumber/</url>
<parent>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber-parent</artifactId>
- <version>8.13.19</version>
+ <version>8.13.20</version>
</parent>
<properties>
@@ -68,17 +68,17 @@
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
- <version>8.13.19</version>
+ <version>8.13.20</version>
</dependency>
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>geocoder</artifactId>
- <version>2.213</version>
+ <version>2.214</version>
</dependency>
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>carrier</artifactId>
- <version>1.203</version>
+ <version>1.204</version>
</dependency>
</dependencies>
diff --git a/demoapp/README.md b/demoapp/README.md
new file mode 100644
index 00000000..94d4db0c
--- /dev/null
+++ b/demoapp/README.md
@@ -0,0 +1,51 @@
+# Demo App: E.164 Formatter
+
+## What is this?
+
+The E.164 Formatter is an Android App that reads all the phone numbers stored in
+the device's contacts and processes them using the
+[LibPhoneNumber](https://github.com/google/libphonenumber) Library.
+
+The purpose of this App is to show an example of how LPN can be used in a
+real-life situation, in this case specifically in an Android App using Java.
+
+## How can I install the app?
+
+You can use the source code to build the app yourself.
+
+## Where is the LPN code located?
+
+The code using LPN is located in
+[`PhoneNumberFormatting#formatPhoneNumberInApp(PhoneNumberInApp, String,
+boolean)`](app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormatting.java#L31)
+.
+
+## How does the app work?
+
+On the start screen, the app asks the user for a country to later use when
+trying to convert the phone numbers to E.164. After the user starts the process
+and grants permission to read and write contacts, the app shows the user two
+lists in the UI.
+
+**List 1: Formattable**
+
+Contains all the phone number that are parsable by LPN, are not short numbers,
+and are valid numbers and can be reformatted to E.164 using the country selected
+on the start screen. In other words, valid locally formatted phone numbers of
+the selected country (e.g. `044 668 18 00` if the selected country is
+Switzerland).
+
+Each list item (= one phone number in the device's contacts) has a checkbox.
+With the click of the button "Update selected" under the list, the app replaces
+the phone numbers of the checked list elements in the contacts with the
+suggested E.164 replacements.
+
+**List 2: Not formattable**
+
+Shows all the phone number that do not fit the criteria of List 1, each tagged
+with one of the following errors:
+
+* Parsing error
+* Short number (e.g. `112`)
+* Invalid number (e.g. `+41446681800123`)
+* Already E.164 (e.g. `+41446681800`)
diff --git a/demoapp/app/build.gradle b/demoapp/app/build.gradle
new file mode 100644
index 00000000..9f5344e2
--- /dev/null
+++ b/demoapp/app/build.gradle
@@ -0,0 +1,45 @@
+plugins {
+ id 'com.android.application'
+}
+
+android {
+ namespace 'com.google.phonenumbers.demoapp'
+ compileSdk 33
+
+ defaultConfig {
+ applicationId "com.google.phonenumbers.demoapp"
+ minSdk 31
+ targetSdk 33
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ debuggable false
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile(
+ 'proguard-android-optimize.txt')
+ }
+
+ debug {
+ debuggable true
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation 'androidx.appcompat:appcompat:1.6.1'
+ implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
+ implementation 'com.google.android.material:material:1.8.0'
+ implementation 'com.googlecode.libphonenumber:libphonenumber:8.13.5'
+ testImplementation 'junit:junit:4.13.2'
+}
diff --git a/demoapp/app/src/main/AndroidManifest.xml b/demoapp/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..32b365ef
--- /dev/null
+++ b/demoapp/app/src/main/AndroidManifest.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <uses-permission android:name="android.permission.READ_CONTACTS" />
+ <uses-permission android:name="android.permission.WRITE_CONTACTS" />
+
+ <application
+ android:name=".MyApplication"
+ android:allowBackup="true"
+ android:icon="@mipmap/ic_launcher"
+ android:label="@string/app_name"
+ android:supportsRtl="true"
+ android:taskAffinity=""
+ android:theme="@style/AppTheme"
+ tools:targetApi="33">
+ <activity
+ android:name=".main.MainActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+
+ <meta-data
+ android:name="android.app.lib_name"
+ android:value="" />
+ </activity>
+ <activity
+ android:name=".result.ResultActivity"
+ android:exported="false">
+ <intent-filter>
+ <action android:name="android.intent.action.DEFAULT" />
+ </intent-filter>
+ </activity>
+ </application>
+
+</manifest>
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/MyApplication.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/MyApplication.java
new file mode 100644
index 00000000..f15889f2
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/MyApplication.java
@@ -0,0 +1,16 @@
+package com.google.phonenumbers.demoapp;
+
+import android.app.Application;
+import com.google.android.material.color.DynamicColors;
+
+/**
+ * Used instead of default {@link Application} instance. Only difference is that this implementation
+ * enabled Dynamic Colors for the app.
+ */
+public class MyApplication extends Application {
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ DynamicColors.applyToActivitiesIfAvailable(this);
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsPermissionManagement.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsPermissionManagement.java
new file mode 100644
index 00000000..cc90c080
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsPermissionManagement.java
@@ -0,0 +1,161 @@
+package com.google.phonenumbers.demoapp.contacts;
+
+import static android.content.Context.MODE_PRIVATE;
+
+import android.Manifest.permission;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+/**
+ * Handles everything related to the contacts permissions ({@link permission#READ_CONTACTS} and
+ * {@link permission#WRITE_CONTACTS}) and the requesting process to grant the permissions.
+ */
+public class ContactsPermissionManagement {
+
+ public static final int CONTACTS_PERMISSION_REQUEST_CODE = 0;
+
+ private static final String SHARED_PREFS_NAME = "contacts-permission-management";
+ private static final String NUMBER_OF_CONTACTS_PERMISSION_DENIALS_KEY =
+ "NUMBER_OF_CONTACTS_PERMISSION_DENIALS";
+
+ private ContactsPermissionManagement() {}
+
+ /**
+ * Returns the current state of the permissions granting as {@link PermissionState}.
+ *
+ * @param activity Activity of the app
+ * @return {@link PermissionState} of the permissions granting
+ */
+ public static PermissionState getState(Activity activity) {
+ if (isGranted(activity.getApplicationContext())) {
+ return PermissionState.ALREADY_GRANTED;
+ }
+ if (!shouldPermissionBeRequestedInApp(activity.getApplicationContext())) {
+ return PermissionState.NEEDS_GRANT_IN_SETTINGS;
+ }
+ if (shouldShowRationale(activity)) {
+ return PermissionState.SHOW_RATIONALE;
+ }
+ return PermissionState.NEEDS_REQUEST;
+ }
+
+ /**
+ * Returns whether the contacts permissions ({@link permission#READ_CONTACTS} and {@link
+ * permission#WRITE_CONTACTS}) are granted for the param {@code context}.
+ *
+ * @param context Context of the app
+ * @return boolean whether contacts permissions are granted
+ */
+ public static boolean isGranted(Context context) {
+ if (ContextCompat.checkSelfPermission(context, permission.READ_CONTACTS)
+ == PackageManager.PERMISSION_DENIED) {
+ return false;
+ }
+ return ContextCompat.checkSelfPermission(context, permission.WRITE_CONTACTS)
+ != PackageManager.PERMISSION_DENIED;
+ }
+
+ /**
+ * Returns whether the permissions should be requested directly in the app or not. Specifically
+ * returns true if less than 2 denials happened since the app installation.
+ *
+ * @param context Context of the app
+ * @return boolean whether the permissions should be requested directly in the app
+ */
+ private static boolean shouldPermissionBeRequestedInApp(Context context) {
+ return getNumberOfDenials(context) < 2;
+ }
+
+ /**
+ * Returns the number of times the permission dialog has been denied since the app installation.
+ * Dismissing the permission dialog instead of answering is considered a denial.
+ *
+ * @param context Context of the app
+ * @return int number of times the permission has been denied
+ */
+ private static int getNumberOfDenials(Context context) {
+ SharedPreferences preferences = context.getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE);
+ return preferences.getInt(NUMBER_OF_CONTACTS_PERMISSION_DENIALS_KEY, 0);
+ }
+
+ /**
+ * Adds 1 to the number of denials since the app installation. Should be called every time the
+ * user denies the permission (in the dialog). Dismissing the permission dialog instead of
+ * answering is considered a denial.
+ *
+ * @param context Context of the app
+ */
+ public static void addOneToNumberOfDenials(Context context) {
+ SharedPreferences.Editor editor =
+ context.getSharedPreferences(SHARED_PREFS_NAME, MODE_PRIVATE).edit();
+ editor.putInt(NUMBER_OF_CONTACTS_PERMISSION_DENIALS_KEY, getNumberOfDenials(context) + 1);
+ editor.apply();
+ }
+
+ /**
+ * Returns whether a rational should be shown explaining why the app requests these permissions
+ * (before requesting them).
+ *
+ * @param activity Activity of the app
+ * @return boolean whether a rational should be shown
+ */
+ private static boolean shouldShowRationale(Activity activity) {
+ if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission.READ_CONTACTS)) {
+ return true;
+ }
+ return ActivityCompat.shouldShowRequestPermissionRationale(activity, permission.WRITE_CONTACTS);
+ }
+
+ /**
+ * Requests the contact permissions ({@link permission#READ_CONTACTS} and {@link
+ * permission#WRITE_CONTACTS}) in the param {@code activity} with the request code {@link
+ * ContactsPermissionManagement#CONTACTS_PERMISSION_REQUEST_CODE}.
+ *
+ * @param activity Activity of the app
+ */
+ public static void request(Activity activity) {
+ activity.requestPermissions(
+ new String[] {permission.READ_CONTACTS, permission.WRITE_CONTACTS},
+ CONTACTS_PERMISSION_REQUEST_CODE);
+ }
+
+ /**
+ * Opens the system settings (app details page) if the app can. Special cases that can not open
+ * the system settings are for example emulators without Play Store installed.
+ *
+ * @param activity Activity of the app
+ */
+ public static void openSystemSettings(Activity activity) {
+ Intent intent =
+ new Intent(
+ android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.parse("package:" + activity.getPackageName()));
+ activity.startActivity(intent);
+ }
+
+ /** Represents the different states the permissions granting process can be at. */
+ public enum PermissionState {
+ /** The permissions are already granted. The action requiring the permissions can be started. */
+ ALREADY_GRANTED,
+ /**
+ * The permissions are not granted, but can be requested directly (without showing a rationale).
+ */
+ NEEDS_REQUEST,
+ /**
+ * The permissions are not granted and a rationale should be shown explaining why the app
+ * requests the permissions before requesting them (directly in the app).
+ */
+ SHOW_RATIONALE,
+ /**
+ * The permissions are not granted and can not be granted directly in the app. The user has to
+ * grant permissions in the system settings instead.
+ */
+ NEEDS_GRANT_IN_SETTINGS
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsRead.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsRead.java
new file mode 100644
index 00000000..068da309
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsRead.java
@@ -0,0 +1,62 @@
+package com.google.phonenumbers.demoapp.contacts;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp;
+import java.util.ArrayList;
+import java.util.Collections;
+
+/** Handles everything related to reading the device contacts. */
+public class ContactsRead {
+
+ private ContactsRead() {}
+
+ /**
+ * Reads all phone numbers in the device's contacts and return them as a list of {@link
+ * PhoneNumberInApp}s ascending sorted by the contact name. An empty list is also returned if the
+ * app has no permission to read contacts or an error occurred while doing so
+ *
+ * @param context Context of the app
+ * @return ArrayList of all phone numbers in the device's contacts, also empty if the app has no
+ * permission to read contacts or an error occurred while doing so
+ */
+ public static ArrayList<PhoneNumberInApp> getAllPhoneNumbersSorted(Context context) {
+ ArrayList<PhoneNumberInApp> phoneNumbers = new ArrayList<>();
+
+ if (!ContactsPermissionManagement.isGranted(context)) {
+ return phoneNumbers;
+ }
+
+ ContentResolver cr = context.getContentResolver();
+ // Only query for contacts with phone number(s).
+ Cursor cursor =
+ cr.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, null, null, null);
+ // If query doesn't work as intended.
+ if (cursor == null) {
+ return phoneNumbers;
+ }
+
+ while (cursor.moveToNext()) {
+ // ID to identify the phone number entry in the contacts (can be used to update in contacts).
+ int idIndex = cursor.getColumnIndex(Phone._ID);
+ String id = idIndex != -1 ? cursor.getString(idIndex) : "";
+
+ int contactNameIndex = cursor.getColumnIndex(Phone.DISPLAY_NAME);
+ String contactName = contactNameIndex != -1 ? cursor.getString(contactNameIndex) : "";
+
+ int originalPhoneNumberIndex = cursor.getColumnIndex(Phone.NUMBER);
+ String originalPhoneNumber =
+ originalPhoneNumberIndex != -1 ? cursor.getString(originalPhoneNumberIndex) : "";
+
+ PhoneNumberInApp phoneNumberInApp =
+ new PhoneNumberInApp(id, contactName, originalPhoneNumber);
+ phoneNumbers.add(phoneNumberInApp);
+ }
+ cursor.close();
+ Collections.sort(phoneNumbers);
+ return phoneNumbers;
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsWrite.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsWrite.java
new file mode 100644
index 00000000..434ba55a
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsWrite.java
@@ -0,0 +1,59 @@
+package com.google.phonenumbers.demoapp.contacts;
+
+import android.content.ContentProviderOperation;
+import android.content.Context;
+import android.content.OperationApplicationException;
+import android.os.RemoteException;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp;
+import java.util.ArrayList;
+
+/** Handles everything related to writing the device contacts. */
+public class ContactsWrite {
+
+ private ContactsWrite() {}
+
+ /**
+ * Attempts to update all phone numbers in param {@code phoneNumbers} in the device's contacts.
+ * {@link PhoneNumberInApp#shouldContactBeUpdated()} is not called in this method and should be
+ * checked while creating the param {@code phoneNumbers}.
+ *
+ * @param phoneNumbers ArrayList of all phone numbers to update
+ * @param context Context of the app
+ * @return boolean whether operation was successful
+ */
+ public static boolean updatePhoneNumbers(
+ ArrayList<PhoneNumberInApp> phoneNumbers, Context context) {
+ if (!ContactsPermissionManagement.isGranted(context)) {
+ return false;
+ }
+
+ // Create a list of operations to only have to apply one batch.
+ ArrayList<ContentProviderOperation> contentProviderOperations = new ArrayList<>();
+
+ for (PhoneNumberInApp phoneNumber : phoneNumbers) {
+ // Identify the exact phone number entry to update.
+ String whereConditionBase = Phone._ID + " = ? AND " + ContactsContract.Data.MIMETYPE + " = ?";
+ String[] whereConditionParams =
+ new String[] {
+ phoneNumber.getId(), ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE
+ };
+
+ contentProviderOperations.add(
+ ContentProviderOperation.newUpdate(ContactsContract.Data.CONTENT_URI)
+ .withSelection(whereConditionBase, whereConditionParams)
+ .withValue(Phone.NUMBER, phoneNumber.getFormattedPhoneNumber())
+ .build());
+ }
+
+ try {
+ context
+ .getContentResolver()
+ .applyBatch(ContactsContract.AUTHORITY, contentProviderOperations);
+ } catch (OperationApplicationException | RemoteException e) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/main/CountryDropdown.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/main/CountryDropdown.java
new file mode 100644
index 00000000..5b36e3c4
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/main/CountryDropdown.java
@@ -0,0 +1,203 @@
+package com.google.phonenumbers.demoapp.main;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.widget.ArrayAdapter;
+import android.widget.AutoCompleteTextView;
+import android.widget.LinearLayout;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import com.google.android.material.textfield.TextInputLayout;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.phonenumbers.demoapp.R;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A component containing a searchable dropdown input populated with all regions {@link
+ * PhoneNumberUtil} supports. Dropdown items are of format {@code [countryName] ([nameCode]) -
+ * +[callingCode]} (e.g. {@code Switzerland (CH) - +41}). Method provides access to the name code
+ * (e.g. {@code CH}) of the current input. Name code: <a
+ * href="https://www.iso.org/glossary-for-iso-3166.html">ISO 3166-1 alpha-2 country code</a> (e.g.
+ * {@code CH}). Calling code: <a
+ * href="https://www.itu.int/dms_pub/itu-t/opb/sp/T-SP-E.164D-2016-PDF-E.pdf">ITU-T E.164 assigned
+ * country code</a> (e.g. {@code 41}).
+ */
+public class CountryDropdown extends LinearLayout {
+
+ /**
+ * Map containing keys of format {@code [countryName] ([nameCode]) - +[callingCode]} (e.g. {@code
+ * Switzerland (CH) - +41}), and name codes (e.g. {@code CH}) as values.
+ */
+ private static final Map<String, String> countryLabelMapNameCode = new HashMap<>();
+ /** Ascending sorted list of the keys in {@link CountryDropdown#countryLabelMapNameCode}. */
+ private static final List<String> countryLabelSorted = new ArrayList<>();
+
+ private final TextInputLayout input;
+ private final AutoCompleteTextView inputEditText;
+
+ /** The name code of the current input. */
+ private String nameCode;
+
+ public CountryDropdown(@NonNull Context context, @Nullable AttributeSet attrs) {
+ super(context, attrs);
+ inflate(getContext(), R.layout.country_dropdown, this);
+ input = findViewById(R.id.country_dropdown_input);
+ inputEditText = findViewById(R.id.country_dropdown_input_edit_text);
+
+ inputEditText.setOnKeyListener(
+ (v, keyCode, event) -> {
+ // If the DEL key is used and the input was a valid dropdown option, clear the input
+ // completely
+ if (keyCode == KeyEvent.KEYCODE_DEL && setNameCodeForInput()) {
+ inputEditText.setText("");
+ }
+ // Disable the error state when editing the input after the validation revealed an error
+ if (input.isErrorEnabled()) {
+ disableInputError();
+ }
+ return false;
+ });
+
+ populateCountryLabelMapNameCode();
+ setAdapter();
+ }
+
+ /**
+ * Populates {@link CountryDropdown#countryLabelMapNameCode} with all regions {@link
+ * PhoneNumberUtil} supports if not populated yet.
+ */
+ private void populateCountryLabelMapNameCode() {
+ if (!countryLabelMapNameCode.isEmpty()) {
+ return;
+ }
+
+ Set<String> supportedNameCodes = PhoneNumberUtil.getInstance().getSupportedRegions();
+ for (String nameCode : supportedNameCodes) {
+ String countryLabel = getCountryLabelForNameCode(nameCode);
+ countryLabelMapNameCode.put(countryLabel, nameCode);
+ }
+ }
+
+ /**
+ * Returns the label of format {@code [countryName] ([nameCode]) - +[callingCode]} (e.g. {@code
+ * Switzerland (CH) - +41}) for the param {@code nameCode}.
+ *
+ * @param nameCode String in format of a name code (e.g. {@code CH})
+ * @return String label of format {@code [countryName] ([nameCode]) - +[callingCode]} (e.g. {@code
+ * Switzerland (CH) - +41})
+ */
+ private String getCountryLabelForNameCode(String nameCode) {
+ Locale locale = new Locale("en", nameCode);
+ String countryName = locale.getDisplayCountry();
+ int callingCode =
+ PhoneNumberUtil.getInstance().getCountryCodeForRegion(nameCode.toUpperCase(Locale.ROOT));
+
+ return countryName + " (" + nameCode.toUpperCase(Locale.ROOT) + ") - +" + callingCode;
+ }
+
+ /**
+ * Populates {@link CountryDropdown#countryLabelSorted} with the ascending sorted keys of {@link
+ * CountryDropdown#countryLabelMapNameCode} if not populated yet. Then sets an {@link
+ * ArrayAdapter} with {@link CountryDropdown#countryLabelSorted} for the dropdown to show the
+ * list.
+ */
+ private void setAdapter() {
+ if (countryLabelSorted.isEmpty()) {
+ countryLabelSorted.addAll(countryLabelMapNameCode.keySet());
+ Collections.sort(countryLabelSorted);
+ }
+
+ ArrayAdapter<String> arrayAdapter =
+ new ArrayAdapter<>(getContext(), R.layout.country_dropdown_item, countryLabelSorted);
+ inputEditText.setAdapter(arrayAdapter);
+ }
+
+ /**
+ * Returns whether the current input is a valid dropdown option. Also updates the input error
+ * accordingly.
+ *
+ * @return boolean whether the current input is a valid dropdown option
+ */
+ public boolean validateInput() {
+ if (!setNameCodeForInput()) {
+ enableInputError();
+ return false;
+ }
+
+ disableInputError();
+ return true;
+ }
+
+ /**
+ * Sets the {@link CountryDropdown#nameCode} to the name code of the current input if that's a
+ * valid dropdown option. Else set's it to an empty String.
+ *
+ * @return boolean whether the current input is a valid dropdown option
+ */
+ private boolean setNameCodeForInput() {
+ String nameCodeForInput = countryLabelMapNameCode.get(getInput());
+ if (nameCodeForInput == null) {
+ nameCode = "";
+ return false;
+ }
+
+ nameCode = nameCodeForInput;
+ return true;
+ }
+
+ /** Shows the error message on the input component. */
+ private void enableInputError() {
+ input.setErrorEnabled(true);
+ input.setError(getResources().getString(R.string.main_activity_country_dropdown_error));
+ }
+
+ /** Hides the error message on the input component. */
+ private void disableInputError() {
+ input.setError(null);
+ input.setErrorEnabled(false);
+ }
+
+ private String getInput() {
+ return inputEditText.getText().toString();
+ }
+
+ /**
+ * Returns the name code of the current input if it's a valid dropdown option, else returns an
+ * empty String.
+ *
+ * @return String name code of the current input if it's a valid dropdown option, else returns an
+ * empty String
+ */
+ public String getNameCodeForInput() {
+ setNameCodeForInput();
+ return nameCode;
+ }
+
+ /**
+ * Sets the label of the country with the name code param {@code nameCode} on the input if it's
+ * valid. Else the input is not changed.
+ *
+ * @param nameCode String in format of a name code (e.g. {@code CH})
+ */
+ public void setInputForNameCode(String nameCode) {
+ String countryLabel = getCountryLabelForNameCode(nameCode);
+ if (!countryLabelSorted.contains(countryLabel)) {
+ return;
+ }
+
+ inputEditText.setText(countryLabel);
+ validateInput();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ input.setEnabled(enabled);
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/main/MainActivity.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/main/MainActivity.java
new file mode 100644
index 00000000..5df73e3b
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/main/MainActivity.java
@@ -0,0 +1,226 @@
+package com.google.phonenumbers.demoapp.main;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.telephony.TelephonyManager;
+import android.view.View;
+import android.widget.Button;
+import android.widget.CheckBox;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import com.google.android.material.snackbar.Snackbar;
+import com.google.phonenumbers.demoapp.R;
+import com.google.phonenumbers.demoapp.contacts.ContactsPermissionManagement;
+import com.google.phonenumbers.demoapp.contacts.ContactsRead;
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberFormatting;
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp;
+import com.google.phonenumbers.demoapp.result.ResultActivity;
+import java.util.ArrayList;
+
+/** Used to handle and process interactions from/with the main page UI of the app. */
+public class MainActivity extends AppCompatActivity {
+
+ private CountryDropdown countryDropdown;
+ private Button btnCountryDropdownReset;
+ private CheckBox cbIgnoreWhitespace;
+ private TextView tvError;
+ private Button btnError;
+ private Button btnStart;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(R.string.app_name_long);
+ }
+
+ countryDropdown = findViewById(R.id.country_dropdown);
+ btnCountryDropdownReset = findViewById(R.id.btn_country_dropdown_reset);
+ cbIgnoreWhitespace = findViewById(R.id.cb_ignore_whitespace);
+ tvError = findViewById(R.id.tv_error);
+ btnError = findViewById(R.id.btn_error);
+ btnStart = findViewById(R.id.btn_start);
+
+ btnCountryDropdownReset.setOnClickListener(v -> setSimCountryOnCountryDropdown());
+ btnStart.setOnClickListener(v -> btnStartClicked());
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ // Reset all UI elements to default state
+ updateUiState(UiState.SELECT_COUNTRY_CODE);
+ setSimCountryOnCountryDropdown();
+ cbIgnoreWhitespace.setChecked(true);
+ }
+
+ @Override
+ public void onRequestPermissionsResult(
+ int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ // Return of the permission result is not about the requested contacts permission
+ if (requestCode != ContactsPermissionManagement.CONTACTS_PERMISSION_REQUEST_CODE) {
+ return;
+ }
+
+ if (grantResults.length == 2
+ && grantResults[0] == PackageManager.PERMISSION_GRANTED
+ && grantResults[1] == PackageManager.PERMISSION_GRANTED) {
+ updateUiState(UiState.PROCESSING);
+ startProcess();
+ } else {
+ ContactsPermissionManagement.addOneToNumberOfDenials(this);
+ switch (ContactsPermissionManagement.getState(this)) {
+ // NEED_REQUEST is specifically to handle the case where the user dismisses the first
+ // permission dialog shown since the app's installation.
+ case NEEDS_REQUEST:
+ case SHOW_RATIONALE:
+ updateUiState(UiState.PERMISSION_ERROR_GRANT_IN_APP);
+ break;
+ case NEEDS_GRANT_IN_SETTINGS:
+ default:
+ updateUiState(UiState.PERMISSION_ERROR_GRANT_IN_SETTINGS);
+ break;
+ }
+ }
+ }
+
+ /**
+ * Updates the UI to represent the param {@code uiState}.
+ *
+ * @param uiState State the UI should be changed to
+ */
+ private void updateUiState(UiState uiState) {
+ // Specifically: countryDropdown, btnCountryDropdownReset, cbIgnoreWhitespace, and btnStart
+ boolean mainInteractionsEnabled = false;
+ // Specifically: tvError, and btnError
+ boolean showError = false;
+
+ switch (uiState) {
+ case SELECT_COUNTRY_CODE:
+ default:
+ mainInteractionsEnabled = true;
+ btnStart.setText(getText(R.string.main_activity_start_text_default));
+ break;
+ case PROCESSING:
+ btnStart.setText(getText(R.string.main_activity_start_text_processing));
+ break;
+ case PERMISSION_ERROR_GRANT_IN_APP:
+ showError = true;
+ tvError.setText(getText(R.string.main_activity_error_text_grant_in_app));
+ btnError.setText(getText(R.string.main_activity_error_cta_grant_in_app));
+ btnError.setOnClickListener(v -> ContactsPermissionManagement.request(this));
+ btnStart.setText(getText(R.string.main_activity_start_text_processing));
+ break;
+ case PERMISSION_ERROR_GRANT_IN_SETTINGS:
+ showError = true;
+ tvError.setText(getText(R.string.main_activity_error_text_grant_in_settings));
+ btnError.setText(getText(R.string.main_activity_error_cta_grant_in_settings));
+ btnError.setOnClickListener(v -> ContactsPermissionManagement.openSystemSettings(this));
+ btnStart.setText(getText(R.string.main_activity_start_text_default));
+ break;
+ }
+
+ countryDropdown.setEnabled(mainInteractionsEnabled);
+ btnCountryDropdownReset.setEnabled(mainInteractionsEnabled);
+ cbIgnoreWhitespace.setEnabled(mainInteractionsEnabled);
+ tvError.setVisibility(showError ? View.VISIBLE : View.INVISIBLE);
+ btnError.setVisibility(showError ? View.VISIBLE : View.INVISIBLE);
+ btnStart.setEnabled(mainInteractionsEnabled);
+ }
+
+ /** Sets the SIM's country as selected item in the country dropdown. */
+ private void setSimCountryOnCountryDropdown() {
+ TelephonyManager telephonyManager =
+ (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
+ countryDropdown.setInputForNameCode(telephonyManager.getSimCountryIso());
+ }
+
+ /**
+ * Called when the start button is clicked. If contacts permissions are granted, starts reading
+ * the contacts. If permissions are not granted, handle that appropriately based on the current
+ * state in the process.
+ */
+ private void btnStartClicked() {
+ updateUiState(UiState.PROCESSING);
+
+ if (!countryDropdown.validateInput()) {
+ updateUiState(UiState.SELECT_COUNTRY_CODE);
+ return;
+ }
+
+ switch (ContactsPermissionManagement.getState(this)) {
+ case ALREADY_GRANTED:
+ startProcess();
+ break;
+ case NEEDS_REQUEST:
+ ContactsPermissionManagement.request(this);
+ break;
+ case SHOW_RATIONALE:
+ updateUiState(UiState.PERMISSION_ERROR_GRANT_IN_APP);
+ break;
+ case NEEDS_GRANT_IN_SETTINGS:
+ default:
+ updateUiState(UiState.PERMISSION_ERROR_GRANT_IN_SETTINGS);
+ break;
+ }
+ }
+
+ /**
+ * Starts the process of reading the contacts, formatting the numbers and starting a {@link
+ * ResultActivity} to show the results.
+ */
+ private void startProcess() {
+ ArrayList<PhoneNumberInApp> phoneNumbersSorted = ContactsRead.getAllPhoneNumbersSorted(this);
+
+ if (phoneNumbersSorted.isEmpty()) {
+ showNoContactsExistSnackbar();
+ updateUiState(UiState.SELECT_COUNTRY_CODE);
+ return;
+ }
+
+ // Format each phone number.
+ for (PhoneNumberInApp phoneNumber : phoneNumbersSorted) {
+ PhoneNumberFormatting.formatPhoneNumberInApp(
+ phoneNumber, countryDropdown.getNameCodeForInput(), cbIgnoreWhitespace.isChecked());
+ }
+
+ // Start new activity to show results.
+ Intent intent = new Intent(this, ResultActivity.class);
+ intent.putExtra(ResultActivity.PHONE_NUMBERS_SORTED_SERIALIZABLE_EXTRA_KEY, phoneNumbersSorted);
+ startActivity(intent);
+ }
+
+ /** Shows a Snackbar informing that no contacts exist. */
+ private void showNoContactsExistSnackbar() {
+ Snackbar.make(
+ countryDropdown, R.string.main_activity_no_contacts_exist_text, Snackbar.LENGTH_LONG)
+ .show();
+ }
+
+ /** Represents the different states the UI of this activity can become. */
+ enum UiState {
+ /** The user should select a country from the dropdown. */
+ SELECT_COUNTRY_CODE,
+ /** Used when loading or processing. The UI is disabled for the user during this time. */
+ PROCESSING,
+ /**
+ * Shows a text explaining that the app needs contacts permission to work, and a button to grant
+ * the permission directly in the app.
+ */
+ PERMISSION_ERROR_GRANT_IN_APP,
+ /**
+ * Shows a text explaining that the app does not have contacts permission, and a button to go to
+ * the system settings to grant the permission.
+ */
+ PERMISSION_ERROR_GRANT_IN_SETTINGS
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormatting.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormatting.java
new file mode 100644
index 00000000..a2198c14
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormatting.java
@@ -0,0 +1,74 @@
+package com.google.phonenumbers.demoapp.phonenumbers;
+
+import com.google.i18n.phonenumbers.NumberParseException;
+import com.google.i18n.phonenumbers.PhoneNumberUtil;
+import com.google.i18n.phonenumbers.PhoneNumberUtil.PhoneNumberFormat;
+import com.google.i18n.phonenumbers.Phonenumber.PhoneNumber;
+import com.google.i18n.phonenumbers.ShortNumberInfo;
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp.FormattingState;
+
+/**
+ * Handles everything related to the formatting {@link PhoneNumberInApp}s to E.164 format (e.g.
+ * {@code +41446681800}) using LibPhoneNumber ({@link PhoneNumberUtil}).
+ */
+public class PhoneNumberFormatting {
+
+ private static final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance();
+ private static final ShortNumberInfo shortNumberInfo = ShortNumberInfo.getInstance();
+
+ private PhoneNumberFormatting() {}
+
+ /**
+ * Attempts to format the param {@code phoneNumberInApp} in E.164 format (e.g. {@code
+ * +41446681800}) using the country from param {@code nameCodeToUse} (e.g. {@code CH}).
+ *
+ * @param phoneNumberInApp PhoneNumberInApp to format to E.164 format
+ * @param nameCodeToUse String in format of a name code (e.g. {@code CH})
+ * @param ignoreWhitespace boolean whether a phone number should be treated as {@link
+ * FormattingState#NUMBER_IS_ALREADY_IN_E164} instead of suggesting to remove whitespace if
+ * that whitespace is the only difference
+ */
+ public static void formatPhoneNumberInApp(
+ PhoneNumberInApp phoneNumberInApp, String nameCodeToUse, boolean ignoreWhitespace) {
+ PhoneNumber originalPhoneNumberParsed;
+
+ // Check PARSING_ERROR
+ try {
+ originalPhoneNumberParsed =
+ phoneNumberUtil.parse(phoneNumberInApp.getOriginalPhoneNumber(), nameCodeToUse);
+ } catch (NumberParseException e) {
+ phoneNumberInApp.setFormattingState(FormattingState.PARSING_ERROR);
+ return;
+ }
+
+ // Check NUMBER_IS_SHORT_NUMBER
+ if (shortNumberInfo.isValidShortNumber(originalPhoneNumberParsed)) {
+ phoneNumberInApp.setFormattingState(FormattingState.NUMBER_IS_SHORT_NUMBER);
+ return;
+ }
+
+ // Check NUMBER_IS_NOT_VALID
+ if (!phoneNumberUtil.isValidNumber(originalPhoneNumberParsed)) {
+ phoneNumberInApp.setFormattingState(FormattingState.NUMBER_IS_NOT_VALID);
+ return;
+ }
+
+ String formattedPhoneNumber =
+ phoneNumberUtil.format(originalPhoneNumberParsed, PhoneNumberFormat.E164);
+
+ // Check NUMBER_IS_ALREADY_IN_E164
+ if (ignoreWhitespace
+ ? phoneNumberInApp
+ .getOriginalPhoneNumber()
+ .replaceAll("\\s+", "")
+ .equals(formattedPhoneNumber)
+ : phoneNumberInApp.getOriginalPhoneNumber().equals(formattedPhoneNumber)) {
+ phoneNumberInApp.setFormattingState(FormattingState.NUMBER_IS_ALREADY_IN_E164);
+ return;
+ }
+
+ phoneNumberInApp.setFormattedPhoneNumber(formattedPhoneNumber);
+ phoneNumberInApp.setFormattingState(FormattingState.COMPLETED);
+ phoneNumberInApp.setShouldContactBeUpdated(true);
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberInApp.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberInApp.java
new file mode 100644
index 00000000..6e35d982
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberInApp.java
@@ -0,0 +1,96 @@
+package com.google.phonenumbers.demoapp.phonenumbers;
+
+import java.io.Serializable;
+
+/**
+ * Represents a phone number and the conversion of it in the app (between reading from and writing
+ * to contacts).
+ */
+public class PhoneNumberInApp implements Serializable, Comparable<PhoneNumberInApp> {
+
+ /** ID to identify the phone number in the device's contacts. */
+ private final String id;
+ /** Display name of the contact the phone number belongs to. */
+ private final String contactName;
+
+ /** Phone number as originally in contacts. */
+ private final String originalPhoneNumber;
+ /**
+ * The in E.164 formatted {@link PhoneNumberInApp#originalPhoneNumber} (e.g. {@code +41446681800})
+ * if formattable, else {@code null}.
+ */
+ private String formattedPhoneNumber = null;
+
+ private FormattingState formattingState = FormattingState.PENDING;
+
+ /**
+ * Equal to the value of the checkbox in the UI. Only if {@code true} the phone number should be
+ * updated in the contacts.
+ */
+ private boolean shouldContactBeUpdated = false;
+
+ public PhoneNumberInApp(String id, String contactName, String originalPhoneNumber) {
+ this.id = id;
+ this.contactName = contactName;
+ this.originalPhoneNumber = originalPhoneNumber;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getContactName() {
+ return contactName;
+ }
+
+ public String getOriginalPhoneNumber() {
+ return originalPhoneNumber;
+ }
+
+ public String getFormattedPhoneNumber() {
+ return formattedPhoneNumber;
+ }
+
+ public void setFormattedPhoneNumber(String formattedPhoneNumber) {
+ this.formattedPhoneNumber = formattedPhoneNumber;
+ }
+
+ public FormattingState getFormattingState() {
+ return formattingState;
+ }
+
+ public void setFormattingState(FormattingState formattingState) {
+ this.formattingState = formattingState;
+ }
+
+ public boolean shouldContactBeUpdated() {
+ return shouldContactBeUpdated;
+ }
+
+ public void setShouldContactBeUpdated(boolean shouldContactBeUpdated) {
+ this.shouldContactBeUpdated = shouldContactBeUpdated;
+ }
+
+ @Override
+ public int compareTo(PhoneNumberInApp o) {
+ return getContactName().compareTo(o.getContactName());
+ }
+
+ /**
+ * Represents the state the formatting of {@link PhoneNumberInApp#originalPhoneNumber} can be at.
+ */
+ public enum FormattingState {
+ /** Used before the formatting is tried/done. */
+ PENDING,
+ /** Formatting completed to {@link PhoneNumberInApp#formattedPhoneNumber} without errors. */
+ COMPLETED,
+ /** Error while parsing the {@link PhoneNumberInApp#originalPhoneNumber}. */
+ PARSING_ERROR,
+ /** {@link PhoneNumberInApp#originalPhoneNumber} is a short number. */
+ NUMBER_IS_SHORT_NUMBER,
+ /** {@link PhoneNumberInApp#originalPhoneNumber} is not a valid number. */
+ NUMBER_IS_NOT_VALID,
+ /** {@link PhoneNumberInApp#originalPhoneNumber} is already in E.164 format. */
+ NUMBER_IS_ALREADY_IN_E164
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/FormattableFragment.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/FormattableFragment.java
new file mode 100644
index 00000000..f73fc8f3
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/FormattableFragment.java
@@ -0,0 +1,161 @@
+package com.google.phonenumbers.demoapp.result;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import com.google.android.material.snackbar.Snackbar;
+import com.google.phonenumbers.demoapp.R;
+import com.google.phonenumbers.demoapp.contacts.ContactsWrite;
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp;
+import java.util.ArrayList;
+
+/**
+ * Used to handle and process interactions from/with the "Formattable" results section in the result
+ * page UI of the app.
+ */
+public class FormattableFragment extends Fragment {
+
+ /** The fragment root view. */
+ private View root;
+ /** The RecyclerView containing the list. */
+ private RecyclerView recyclerView;
+
+ private Button btnUpdateSelected;
+
+ /** The sorted phone numbers the list currently contains. */
+ private ArrayList<PhoneNumberInApp> phoneNumbers;
+
+ public FormattableFragment(ArrayList<PhoneNumberInApp> phoneNumbers) {
+ this.phoneNumbers = phoneNumbers;
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ root = inflater.inflate(R.layout.fragment_formattable, container, false);
+ recyclerView = root.findViewById(R.id.recycler_view);
+ recyclerView.setLayoutManager(new LinearLayoutManager(root.getContext()));
+
+ btnUpdateSelected = root.findViewById(R.id.btn_update_selected);
+ btnUpdateSelected.setOnClickListener(v -> btnUpdateSelectedClicked());
+
+ reloadList();
+ return root;
+ }
+
+ /**
+ * Attempts to update the selected contacts and shows success or error based on the outcome.
+ * Called when the update selected button is clicked.
+ */
+ private void btnUpdateSelectedClicked() {
+ updateUiState(UiState.PROCESSING);
+
+ // Get the most up to date list of phone numbers from the RecyclerView adapter.
+ if (recyclerView.getAdapter() == null) {
+ showErrorSnackbar();
+ updateUiState(UiState.SELECT_PHONE_NUMBERS);
+ return;
+ }
+ phoneNumbers = ((FormattableRvAdapter) recyclerView.getAdapter()).getAllPhoneNumbers();
+
+ // Create a sublist with all phone numbers that have the checkbox checked.
+ ArrayList<PhoneNumberInApp> phoneNumbersToUpdate = new ArrayList<>();
+ for (PhoneNumberInApp phoneNumber : phoneNumbers) {
+ if (phoneNumber.shouldContactBeUpdated()) {
+ phoneNumbersToUpdate.add(phoneNumber);
+ }
+ }
+
+ if (phoneNumbersToUpdate.isEmpty()) {
+ showNoNumbersSelectedSnackbar();
+ updateUiState(UiState.SELECT_PHONE_NUMBERS);
+ return;
+ }
+
+ boolean errorWhileUpdatingPhoneNumbers =
+ !ContactsWrite.updatePhoneNumbers(phoneNumbersToUpdate, root.getContext());
+ if (errorWhileUpdatingPhoneNumbers) {
+ showErrorSnackbar();
+ updateUiState(UiState.SELECT_PHONE_NUMBERS);
+ } else {
+ showContactsWriteSuccessSnackbar();
+ phoneNumbers.removeAll(phoneNumbersToUpdate);
+ reloadList();
+ }
+ }
+
+ /** Shows a Snackbar informing that no numbers are selected. */
+ private void showNoNumbersSelectedSnackbar() {
+ Snackbar.make(root, R.string.formattable_no_numbers_selected_text, Snackbar.LENGTH_LONG).show();
+ }
+
+ /** Shows a Snackbar informing that the selected contacts were successfully written. */
+ private void showContactsWriteSuccessSnackbar() {
+ Snackbar.make(root, R.string.formattable_contacts_write_success_text, Snackbar.LENGTH_LONG)
+ .show();
+ }
+
+ /** Shows a Snackbar informing that there was an error (and the user should try again). */
+ private void showErrorSnackbar() {
+ Snackbar.make(root, R.string.formattable_error_text, Snackbar.LENGTH_LONG).show();
+ }
+
+ /**
+ * Reloads the UI so the list contains the phone numbers currently in {@link
+ * FormattableFragment#phoneNumbers}.
+ */
+ private void reloadList() {
+ FormattableRvAdapter adapter = new FormattableRvAdapter(phoneNumbers, root.getContext());
+ recyclerView.setAdapter(adapter);
+ updateUiState(
+ phoneNumbers.isEmpty() ? UiState.NO_PHONE_NUMBERS_IN_LIST : UiState.SELECT_PHONE_NUMBERS);
+ }
+
+ /**
+ * Updates the UI to represent the param {@code uiState}.
+ *
+ * @param uiState State the UI should be changed to
+ */
+ private void updateUiState(UiState uiState) {
+ // Specifically: btnUpdateSelected, and all CheckBoxes (of the list items)
+ boolean mainInteractionsEnabled = false;
+
+ switch (uiState) {
+ case SELECT_PHONE_NUMBERS:
+ default:
+ mainInteractionsEnabled = true;
+ btnUpdateSelected.setText(R.string.formattable_update_selected_text_default);
+ break;
+ case PROCESSING:
+ btnUpdateSelected.setText(R.string.formattable_update_selected_text_processing);
+ break;
+ case NO_PHONE_NUMBERS_IN_LIST:
+ btnUpdateSelected.setText(R.string.formattable_update_selected_text_default);
+ break;
+ }
+
+ btnUpdateSelected.setEnabled(mainInteractionsEnabled);
+
+ if (recyclerView.getAdapter() != null) {
+ ((FormattableRvAdapter) recyclerView.getAdapter()).setAllEnabled(mainInteractionsEnabled);
+ }
+ }
+
+ /** Represents the different states the UI of this fragment can become. */
+ enum UiState {
+ /** The user should select the phone numbers to update. */
+ SELECT_PHONE_NUMBERS,
+ /** Used when loading or processing. The UI is disabled for the user during this time. */
+ PROCESSING,
+ /**
+ * There are no phone number sin the list (the list is empty). Therefore the update selected
+ * button is disabled.
+ */
+ NO_PHONE_NUMBERS_IN_LIST
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/FormattableRvAdapter.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/FormattableRvAdapter.java
new file mode 100644
index 00000000..a0edd107
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/FormattableRvAdapter.java
@@ -0,0 +1,157 @@
+package com.google.phonenumbers.demoapp.result;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CheckBox;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.constraintlayout.widget.ConstraintLayout;
+import androidx.recyclerview.widget.RecyclerView;
+import com.google.phonenumbers.demoapp.R;
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp;
+import java.util.ArrayList;
+
+/** Adapter for the {@link RecyclerView} used in {@link FormattableFragment}. */
+public class FormattableRvAdapter extends RecyclerView.Adapter<FormattableRvAdapter.ViewHolder> {
+
+ private final LayoutInflater layoutInflater;
+
+ /** List of the original version of {@link PhoneNumberInApp}s at the time of object creation. */
+ private final ArrayList<PhoneNumberInApp> originalPhoneNumbers;
+
+ /** List of all created {@link ViewHolder}s. */
+ private final ArrayList<ViewHolder> viewHolders = new ArrayList<>();
+
+ public FormattableRvAdapter(ArrayList<PhoneNumberInApp> phoneNumbers, Context context) {
+ this.originalPhoneNumbers = phoneNumbers;
+ this.layoutInflater = LayoutInflater.from(context);
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = layoutInflater.inflate(R.layout.formattable_list_item, parent, false);
+ ViewHolder viewHolder = new ViewHolder(view);
+ viewHolders.add(viewHolder);
+ return viewHolder;
+ }
+
+ @Override
+ public void onViewRecycled(@NonNull ViewHolder holder) {
+ super.onViewRecycled(holder);
+ viewHolders.remove(holder);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
+ if (position >= 0 && position < getItemCount()) {
+ viewHolder.setFromPhoneNumberInAppRepresentation(originalPhoneNumbers.get(position));
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return originalPhoneNumbers.size();
+ }
+
+ /**
+ * Sets the enabled state for the checkbox of all list items.
+ *
+ * @param enabled boolean enable state to set
+ */
+ public void setAllEnabled(boolean enabled) {
+ for (ViewHolder viewHolder : viewHolders) {
+ viewHolder.setEnabled(enabled);
+ }
+ }
+
+ /**
+ * Returns a list of all list items as {@link PhoneNumberInApp}s in the current state of the UI.
+ *
+ * @return ArrayList of all list items as {@link PhoneNumberInApp}s in the current state of the UI
+ */
+ public ArrayList<PhoneNumberInApp> getAllPhoneNumbers() {
+ ArrayList<PhoneNumberInApp> phoneNumbers = new ArrayList<>();
+ for (ViewHolder viewHolder : viewHolders) {
+ phoneNumbers.add(viewHolder.getPhoneNumberInAppRepresentation());
+ }
+ return phoneNumbers;
+ }
+
+ /** {@link RecyclerView.ViewHolder} specifically for a list item of a formattable phone number. */
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+
+ /** Representation of the UI as a {@link PhoneNumberInApp}. */
+ private PhoneNumberInApp phoneNumberInAppRepresentation;
+
+ private final TextView tvContactName;
+ private final TextView tvOriginalPhoneNumber;
+ private final TextView tvArrow;
+ private final TextView tvFormattedPhoneNumber;
+
+ private final CheckBox checkBox;
+
+ public ViewHolder(View view) {
+ super(view);
+ ConstraintLayout clListItem = view.findViewById(R.id.cl_list_item);
+ clListItem.setOnClickListener(v -> toggleChecked());
+
+ tvContactName = view.findViewById(R.id.tv_contact_name);
+ tvOriginalPhoneNumber = view.findViewById(R.id.tv_original_phone_number);
+ tvArrow = view.findViewById(R.id.tv_arrow);
+ tvFormattedPhoneNumber = view.findViewById(R.id.tv_formatted_phone_number);
+ checkBox = view.findViewById(R.id.check_box);
+
+ checkBox.setOnCheckedChangeListener((buttonView, isChecked) -> updateUiToMatchCheckBox());
+ }
+
+ /**
+ * Sets the content of the view to the information of param {@code
+ * phoneNumberInAppRepresentation}.
+ *
+ * @param phoneNumberInAppRepresentation PhoneNumberInApp to set content of the view from
+ */
+ public void setFromPhoneNumberInAppRepresentation(
+ PhoneNumberInApp phoneNumberInAppRepresentation) {
+ this.phoneNumberInAppRepresentation = phoneNumberInAppRepresentation;
+ tvContactName.setText(phoneNumberInAppRepresentation.getContactName());
+ tvOriginalPhoneNumber.setText(phoneNumberInAppRepresentation.getOriginalPhoneNumber());
+ String formattedPhoneNumber = phoneNumberInAppRepresentation.getFormattedPhoneNumber();
+ tvFormattedPhoneNumber.setText(formattedPhoneNumber != null ? formattedPhoneNumber : "");
+ checkBox.setChecked(phoneNumberInAppRepresentation.shouldContactBeUpdated());
+ }
+
+ /** Toggles the checked state of the {@link ViewHolder#checkBox} if it is enabled. */
+ private void toggleChecked() {
+ if (checkBox.isEnabled()) {
+ checkBox.toggle();
+ phoneNumberInAppRepresentation.setShouldContactBeUpdated(checkBox.isChecked());
+ }
+ }
+
+ /**
+ * Update the rest of the UI elements to represent the checked state of {@link
+ * ViewHolder#checkBox} correctly.
+ */
+ private void updateUiToMatchCheckBox() {
+ boolean isChecked = checkBox.isChecked();
+ tvArrow.setEnabled(isChecked);
+ tvFormattedPhoneNumber.setEnabled(isChecked);
+ }
+
+ /**
+ * Sets the enabled state of the {@link ViewHolder#checkBox}.
+ *
+ * @param enabled boolean whether the {@link ViewHolder#checkBox} should be enabled
+ */
+ public void setEnabled(boolean enabled) {
+ checkBox.setEnabled(enabled);
+ }
+
+ public PhoneNumberInApp getPhoneNumberInAppRepresentation() {
+ return phoneNumberInAppRepresentation;
+ }
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/NotFormattableFragment.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/NotFormattableFragment.java
new file mode 100644
index 00000000..52061472
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/NotFormattableFragment.java
@@ -0,0 +1,119 @@
+package com.google.phonenumbers.demoapp.result;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import com.google.android.material.chip.Chip;
+import com.google.android.material.snackbar.Snackbar;
+import com.google.phonenumbers.demoapp.R;
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp;
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp.FormattingState;
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * Used to handle and process interactions from/with the "Not formattable" results section in the
+ * result page UI of the app.
+ */
+public class NotFormattableFragment extends Fragment {
+
+ /** The fragment root view. */
+ private View root;
+ /** The RecyclerView containing the list. */
+ private RecyclerView recyclerView;
+
+ /**
+ * The sorted phone numbers the list contains (some might not be visible in the UI due to the
+ * {@link NotFormattableFragment#appliedFilters}).
+ */
+ private final ArrayList<PhoneNumberInApp> phoneNumbers;
+
+ /** The filters that are currently applied to the list. */
+ private final ArrayList<FormattingState> appliedFilters = new ArrayList<>();
+
+ public NotFormattableFragment(ArrayList<PhoneNumberInApp> phoneNumbers) {
+ this.phoneNumbers = phoneNumbers;
+ }
+
+ @Override
+ public View onCreateView(
+ LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ root = inflater.inflate(R.layout.fragment_not_formattable, container, false);
+ recyclerView = root.findViewById(R.id.recycler_view);
+ recyclerView.setLayoutManager(new LinearLayoutManager(root.getContext()));
+
+ Chip chipParsingError = root.findViewById(R.id.chip_parsing_error);
+ connectChipToFormattingState(chipParsingError, FormattingState.PARSING_ERROR);
+ Chip chipShortNumber = root.findViewById(R.id.chip_short_number);
+ connectChipToFormattingState(chipShortNumber, FormattingState.NUMBER_IS_SHORT_NUMBER);
+ Chip chipAlreadyE164 = root.findViewById(R.id.chip_already_e164);
+ connectChipToFormattingState(chipAlreadyE164, FormattingState.NUMBER_IS_ALREADY_IN_E164);
+ Chip chipInvalidNumber = root.findViewById(R.id.chip_invalid_number);
+ connectChipToFormattingState(chipInvalidNumber, FormattingState.NUMBER_IS_NOT_VALID);
+
+ // Add add filters as they are all preselected in the UI
+ appliedFilters.addAll(
+ Arrays.asList(
+ FormattingState.PARSING_ERROR,
+ FormattingState.NUMBER_IS_SHORT_NUMBER,
+ FormattingState.NUMBER_IS_ALREADY_IN_E164,
+ FormattingState.NUMBER_IS_NOT_VALID));
+ // List only needs to be loaded if there are phone numbers.
+ if (!phoneNumbers.isEmpty()) {
+ reloadListWithFilters();
+ }
+ return root;
+ }
+
+ /**
+ * Sets up the param {@code chip} to add/remove the param {@code formattingState} from the {@link
+ * NotFormattableFragment#appliedFilters} list when it is checked/unchecked, and then reloads the
+ * phone number list.
+ *
+ * @param chip Chip of which to handle check/uncheck action
+ * @param formattingState FormattingState the param {@code chip} represents
+ */
+ private void connectChipToFormattingState(Chip chip, FormattingState formattingState) {
+ chip.setOnCheckedChangeListener(
+ (buttonView, isChecked) -> {
+ if (isChecked) {
+ appliedFilters.add(formattingState);
+ } else {
+ appliedFilters.remove(formattingState);
+ }
+ reloadListWithFilters();
+ });
+ }
+
+ /**
+ * Reloads the UI so the list contains the phone numbers matching the currently {@link
+ * NotFormattableFragment#appliedFilters}.
+ */
+ private void reloadListWithFilters() {
+ ArrayList<PhoneNumberInApp> phoneNumbersToShow = new ArrayList<>();
+ for (PhoneNumberInApp phoneNumber : phoneNumbers) {
+ if (appliedFilters.contains(phoneNumber.getFormattingState())) {
+ phoneNumbersToShow.add(phoneNumber);
+ }
+ }
+
+ if (phoneNumbersToShow.isEmpty()) {
+ showNoNumbersMatchFiltersSnackbar();
+ }
+
+ NotFormattableRvAdapter adapter =
+ new NotFormattableRvAdapter(phoneNumbersToShow, root.getContext());
+ recyclerView.setAdapter(adapter);
+ }
+
+ /** Shows a Snackbar informing that no numbers match the selected filters. */
+ private void showNoNumbersMatchFiltersSnackbar() {
+ Snackbar.make(
+ root, R.string.not_formattable_no_numbers_match_filters_text, Snackbar.LENGTH_LONG)
+ .show();
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/NotFormattableRvAdapter.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/NotFormattableRvAdapter.java
new file mode 100644
index 00000000..3297d035
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/NotFormattableRvAdapter.java
@@ -0,0 +1,94 @@
+package com.google.phonenumbers.demoapp.result;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.RecyclerView;
+import com.google.phonenumbers.demoapp.R;
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp;
+import java.util.ArrayList;
+
+/** Adapter for the {@link RecyclerView} used in {@link NotFormattableFragment}. */
+public class NotFormattableRvAdapter
+ extends RecyclerView.Adapter<NotFormattableRvAdapter.ViewHolder> {
+
+ private final LayoutInflater layoutInflater;
+
+ /** List of the original version of {@link PhoneNumberInApp}s at the time of object creation. */
+ private final ArrayList<PhoneNumberInApp> originalPhoneNumbers;
+
+ public NotFormattableRvAdapter(ArrayList<PhoneNumberInApp> phoneNumbers, Context context) {
+ this.originalPhoneNumbers = phoneNumbers;
+ this.layoutInflater = LayoutInflater.from(context);
+ }
+
+ @NonNull
+ @Override
+ public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ View view = layoutInflater.inflate(R.layout.not_formattable_list_item, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull ViewHolder viewHolder, int position) {
+ if (position >= 0 && position < getItemCount()) {
+ viewHolder.setFromPhoneNumberInAppRepresentation(originalPhoneNumbers.get(position));
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return originalPhoneNumbers.size();
+ }
+
+ /**
+ * {@link RecyclerView.ViewHolder} specifically for a list item of a not formattable phone number.
+ */
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+
+ private final TextView tvContactName;
+ private final TextView tvReason;
+ private final TextView tvOriginalPhoneNumber;
+
+ public ViewHolder(View view) {
+ super(view);
+ tvContactName = view.findViewById(R.id.tv_contact_name);
+ tvReason = view.findViewById(R.id.tv_reason);
+ tvOriginalPhoneNumber = view.findViewById(R.id.tv_original_phone_number);
+ }
+
+ /**
+ * Sets the content of the view to the information of param {@code
+ * phoneNumberInAppRepresentation}.
+ *
+ * @param phoneNumberInAppRepresentation PhoneNumberInApp to set content of the view from
+ */
+ public void setFromPhoneNumberInAppRepresentation(
+ PhoneNumberInApp phoneNumberInAppRepresentation) {
+ tvContactName.setText(phoneNumberInAppRepresentation.getContactName());
+
+ switch (phoneNumberInAppRepresentation.getFormattingState()) {
+ case PARSING_ERROR:
+ tvReason.setText(R.string.not_formattable_parsing_error_text);
+ break;
+ case NUMBER_IS_SHORT_NUMBER:
+ tvReason.setText(R.string.not_formattable_short_number_text);
+ break;
+ case NUMBER_IS_ALREADY_IN_E164:
+ tvReason.setText(R.string.not_formattable_already_e164_text);
+ break;
+ case NUMBER_IS_NOT_VALID:
+ tvReason.setText(R.string.not_formattable_invalid_number_text);
+ break;
+ default:
+ tvReason.setText(R.string.not_formattable_unknown_error_text);
+ break;
+ }
+
+ tvOriginalPhoneNumber.setText(phoneNumberInAppRepresentation.getOriginalPhoneNumber());
+ }
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/ResultActivity.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/ResultActivity.java
new file mode 100644
index 00000000..86ac974d
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/ResultActivity.java
@@ -0,0 +1,102 @@
+package com.google.phonenumbers.demoapp.result;
+
+import android.os.Bundle;
+import android.view.MenuItem;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.Fragment;
+import androidx.viewpager2.widget.ViewPager2;
+import com.google.android.material.tabs.TabLayout;
+import com.google.android.material.tabs.TabLayoutMediator;
+import com.google.phonenumbers.demoapp.R;
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp;
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp.FormattingState;
+import java.util.ArrayList;
+
+/** Used to handle and process interactions from/with the result page UI of the app. */
+public class ResultActivity extends AppCompatActivity {
+
+ public static final String PHONE_NUMBERS_SORTED_SERIALIZABLE_EXTRA_KEY =
+ "PHONE_NUMBERS_SORTED_SERIALIZABLE_EXTRA";
+
+ @Override
+ @SuppressWarnings("unchecked")
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_result);
+
+ // Setup ActionBar (title, and home button).
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setTitle(R.string.app_name_long);
+ actionBar.setHomeAsUpIndicator(R.drawable.ic_outline_home_30);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ ArrayList<PhoneNumberInApp> phoneNumbersFormattableSorted = new ArrayList<>();
+ ArrayList<PhoneNumberInApp> phoneNumbersNotFormattableSorted = new ArrayList<>();
+ try {
+ ArrayList<PhoneNumberInApp> phoneNumbersSorted =
+ (ArrayList<PhoneNumberInApp>)
+ getIntent().getSerializableExtra(PHONE_NUMBERS_SORTED_SERIALIZABLE_EXTRA_KEY);
+ // Split phoneNumbersSorted into two separate lists.
+ for (PhoneNumberInApp phoneNumber : phoneNumbersSorted) {
+ if (phoneNumber.getFormattingState() == FormattingState.COMPLETED) {
+ phoneNumbersFormattableSorted.add(phoneNumber);
+ } else if (phoneNumber.getFormattingState() != FormattingState.PENDING) {
+ phoneNumbersNotFormattableSorted.add(phoneNumber);
+ }
+ }
+ } catch (ClassCastException exception) {
+ this.finish();
+ }
+
+ // Create two Fragments with each one of the split lists.
+ FormattableFragment formattableFragment =
+ new FormattableFragment(phoneNumbersFormattableSorted);
+ NotFormattableFragment notFormattableFragment =
+ new NotFormattableFragment(phoneNumbersNotFormattableSorted);
+ setUpTapLayout(formattableFragment, notFormattableFragment);
+ }
+
+ /**
+ * Sets up the {@link TabLayout} with the two param fragments.
+ *
+ * @param formattableFragment FormattableFragment for first tap
+ * @param notFormattableFragment NotFormattableFragment for second tab
+ */
+ private void setUpTapLayout(
+ FormattableFragment formattableFragment, NotFormattableFragment notFormattableFragment) {
+ // The Fragments for the taps in correct order.
+ ArrayList<Fragment> fragments = new ArrayList<>();
+ // The titles for the tabs (respectively for the Fragment at the same position in fragments).
+ ArrayList<String> fragmentTitles = new ArrayList<>();
+ fragments.add(formattableFragment);
+ fragmentTitles.add(getString(R.string.formattable_formattable_text));
+ fragments.add(notFormattableFragment);
+ fragmentTitles.add(getString(R.string.not_formattable_not_formattable_text));
+
+ ResultVpAdapter vpAdapter =
+ new ResultVpAdapter(getSupportFragmentManager(), getLifecycle(), fragments, fragmentTitles);
+ ViewPager2 viewPager = findViewById(R.id.view_pager);
+ viewPager.setAdapter(vpAdapter);
+
+ new TabLayoutMediator(
+ findViewById(R.id.tab_layout),
+ viewPager,
+ (tab, position) -> tab.setText(vpAdapter.getTitle(position)))
+ .attach();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ // If home button (house icon) in the ActionBar
+ if (item.getItemId() == android.R.id.home) {
+ this.finish();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+}
diff --git a/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/ResultVpAdapter.java b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/ResultVpAdapter.java
new file mode 100644
index 00000000..812cec72
--- /dev/null
+++ b/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/ResultVpAdapter.java
@@ -0,0 +1,72 @@
+package com.google.phonenumbers.demoapp.result;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.lifecycle.Lifecycle;
+import androidx.viewpager2.adapter.FragmentStateAdapter;
+import androidx.viewpager2.widget.ViewPager2;
+import java.util.ArrayList;
+
+/** Adapter for the {@link androidx.viewpager2.widget.ViewPager2} used in {@link ResultActivity}. */
+class ResultVpAdapter extends FragmentStateAdapter {
+
+ private final ArrayList<Fragment> fragments;
+ private final ArrayList<String> titles;
+
+ /**
+ * Constructor to set predefined Fragments and their titles.
+ *
+ * @param fragmentManager of {@link ViewPager2}'s host
+ * @param lifecycle of {@link ViewPager2}'s host
+ * @param fragments ArrayList of predefined Fragments (in correct order)
+ * @param titles ArrayList of titles of the predefined Fragments in param {@code fragments}
+ * (respectively for the Fragment at the same position in param {@code fragments}
+ */
+ public ResultVpAdapter(
+ @NonNull FragmentManager fragmentManager,
+ @NonNull Lifecycle lifecycle,
+ ArrayList<Fragment> fragments,
+ ArrayList<String> titles) {
+ super(fragmentManager, lifecycle);
+ this.fragments = fragments;
+ this.titles = titles;
+ }
+
+ /**
+ * Returns the predefined Fragment (set with constructor) at position param {@code position}.
+ * Returns a new Fragment if no predefined Fragment exists at position.
+ *
+ * @param position int position of the predefined Fragment
+ * @return Fragment at position param {@code position} or new Fragment if no predefined Fragment
+ * exists at position
+ */
+ @NonNull
+ @Override
+ public Fragment createFragment(int position) {
+ if (position >= 0 && position < getItemCount()) {
+ return fragments.get(position);
+ }
+ return new Fragment();
+ }
+
+ @Override
+ public int getItemCount() {
+ return fragments.size();
+ }
+
+ /**
+ * Returns the predefined title (set with constructor) at position param {@code position}. Returns
+ * an empty String if no predefined Fragment exists at position.
+ *
+ * @param position int position of the predefined title
+ * @return String title at position param {@code position} or empty String if no predefined title
+ * exists at position
+ */
+ public String getTitle(int position) {
+ if (position >= 0 && position < titles.size()) {
+ return titles.get(position);
+ }
+ return "";
+ }
+}
diff --git a/demoapp/app/src/main/res/drawable/ic_launcher_background.xml b/demoapp/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 00000000..6f91cc79
--- /dev/null
+++ b/demoapp/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,32 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportHeight="108"
+ android:viewportWidth="108">
+ <path android:pathData="M0,0h108v108h-108z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:centerX="0"
+ android:centerY="0"
+ android:gradientRadius="108"
+ android:type="radial">
+ <item
+ android:color="#FF7EB0E3"
+ android:offset="0" />
+ <item
+ android:color="#FF669DE1"
+ android:offset="0.2" />
+ <item
+ android:color="#FF5488E0"
+ android:offset="0.4" />
+ <item
+ android:color="#FF4C71DF"
+ android:offset="0.6" />
+ <item
+ android:color="#FF5C2DDF"
+ android:offset="1" />
+ </gradient>
+ </aapt:attr>
+ </path>
+</vector>
diff --git a/demoapp/app/src/main/res/drawable/ic_launcher_foreground.xml b/demoapp/app/src/main/res/drawable/ic_launcher_foreground.xml
new file mode 100644
index 00000000..d4f2aa4f
--- /dev/null
+++ b/demoapp/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -0,0 +1,16 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="108dp"
+ android:height="108dp"
+ android:tint="#FFFFFF"
+ android:viewportHeight="108"
+ android:viewportWidth="108">
+ <group
+ android:scaleX="1.9575"
+ android:scaleY="1.9575"
+ android:translateX="30.51"
+ android:translateY="30.51">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M22,3L2,3C0.9,3 0,3.9 0,5v14c0,1.1 0.9,2 2,2h20c1.1,0 1.99,-0.9 1.99,-2L24,5c0,-1.1 -0.9,-2 -2,-2zM8,6c1.66,0 3,1.34 3,3s-1.34,3 -3,3 -3,-1.34 -3,-3 1.34,-3 3,-3zM14,18L2,18v-1c0,-2 4,-3.1 6,-3.1s6,1.1 6,3.1v1zM17.85,14h1.64L21,16l-1.99,1.99c-1.31,-0.98 -2.28,-2.38 -2.73,-3.99 -0.18,-0.64 -0.28,-1.31 -0.28,-2s0.1,-1.36 0.28,-2c0.45,-1.62 1.42,-3.01 2.73,-3.99L21,8l-1.51,2h-1.64c-0.22,0.63 -0.35,1.3 -0.35,2s0.13,1.37 0.35,2z" />
+ </group>
+</vector>
diff --git a/demoapp/app/src/main/res/drawable/ic_outline_home_30.xml b/demoapp/app/src/main/res/drawable/ic_outline_home_30.xml
new file mode 100644
index 00000000..5d774e12
--- /dev/null
+++ b/demoapp/app/src/main/res/drawable/ic_outline_home_30.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="30dp"
+ android:height="30dp"
+ android:tint="?colorOnBackground"
+ android:viewportHeight="24"
+ android:viewportWidth="24">
+ <path
+ android:fillColor="?colorOnBackground"
+ android:pathData="M12,5.69l5,4.5V18h-2v-6H9v6H7v-7.81l5,-4.5M12,3L2,12h3v8h6v-6h2v6h6v-8h3L12,3z" />
+</vector>
diff --git a/demoapp/app/src/main/res/layout/activity_main.xml b/demoapp/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..cd5f8a58
--- /dev/null
+++ b/demoapp/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".main.MainActivity">
+
+ <TextView
+ android:id="@+id/tv_country_dropdown_label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="150dp"
+ android:text="@string/main_activity_country_dropdown_label_text"
+ android:textAppearance="?attr/textAppearanceBodyLarge"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <com.google.phonenumbers.demoapp.main.CountryDropdown
+ android:id="@+id/country_dropdown"
+ android:layout_width="@dimen/main_activity_default_width_item"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/app_default_spacing_between_items"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/tv_country_dropdown_label" />
+
+ <Button
+ android:id="@+id/btn_country_dropdown_reset"
+ style="@style/Widget.Material3.Button.TextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/main_activity_country_dropdown_reset_text"
+ app:layout_constraintStart_toStartOf="@id/country_dropdown"
+ app:layout_constraintTop_toBottomOf="@id/country_dropdown" />
+
+ <CheckBox
+ android:id="@+id/cb_ignore_whitespace"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/app_default_spacing_between_items"
+ android:checked="true"
+ android:text="@string/main_activity_ignore_whitespace_text"
+ android:textAppearance="?attr/textAppearanceBodyMedium"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/btn_country_dropdown_reset" />
+
+ <TextView
+ android:id="@+id/tv_error"
+ android:layout_width="@dimen/main_activity_default_width_item"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:text=""
+ android:textAppearance="?attr/textAppearanceBodyLarge"
+ android:textColor="?colorError"
+ app:layout_constraintBottom_toTopOf="@id/btn_error"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <Button
+ android:id="@+id/btn_error"
+ style="@style/Widget.Material3.Button.TextButton"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/app_default_spacing_between_items"
+ android:text=""
+ app:layout_constraintBottom_toTopOf="@id/btn_start"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+ <Button
+ android:id="@+id/btn_start"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/app_default_spacing_between_items"
+ android:text="@string/main_activity_start_text_default"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/demoapp/app/src/main/res/layout/activity_result.xml b/demoapp/app/src/main/res/layout/activity_result.xml
new file mode 100644
index 00000000..2a018884
--- /dev/null
+++ b/demoapp/app/src/main/res/layout/activity_result.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".result.ResultActivity">
+
+ <com.google.android.material.tabs.TabLayout
+ android:id="@+id/tab_layout"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.viewpager2.widget.ViewPager2
+ android:id="@+id/view_pager"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/tab_layout" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/demoapp/app/src/main/res/layout/country_dropdown.xml b/demoapp/app/src/main/res/layout/country_dropdown.xml
new file mode 100644
index 00000000..a6b5dab1
--- /dev/null
+++ b/demoapp/app/src/main/res/layout/country_dropdown.xml
@@ -0,0 +1,24 @@
+<?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="match_parent"
+ android:orientation="vertical">
+
+ <com.google.android.material.textfield.TextInputLayout
+ style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
+ android:id="@+id/country_dropdown_input"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:hint="@string/main_activity_country_dropdown_hint">
+
+ <AutoCompleteTextView
+ android:id="@+id/country_dropdown_input_edit_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:hint="@string/main_activity_country_dropdown_hint"
+ android:imeOptions="actionDone"
+ android:inputType="text" />
+
+ </com.google.android.material.textfield.TextInputLayout>
+
+</LinearLayout>
diff --git a/demoapp/app/src/main/res/layout/country_dropdown_item.xml b/demoapp/app/src/main/res/layout/country_dropdown_item.xml
new file mode 100644
index 00000000..e97b58ea
--- /dev/null
+++ b/demoapp/app/src/main/res/layout/country_dropdown_item.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:padding="@dimen/app_default_spacing_from_layout_outline"
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:textAppearance="?attr/textAppearanceBodyLarge" />
diff --git a/demoapp/app/src/main/res/layout/formattable_list_item.xml b/demoapp/app/src/main/res/layout/formattable_list_item.xml
new file mode 100644
index 00000000..d5f63f13
--- /dev/null
+++ b/demoapp/app/src/main/res/layout/formattable_list_item.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/cl_list_item"
+ android:layout_width="match_parent"
+ android:layout_height="?listPreferredItemHeight"
+ android:background="?selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ android:paddingEnd="@dimen/app_default_spacing_from_layout_outline"
+ android:paddingStart="@dimen/app_default_spacing_from_layout_outline">
+
+ <TextView
+ android:id="@+id/tv_contact_name"
+ style="?textAppearanceLabelMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:textStyle="bold"
+ app:layout_constraintBottom_toTopOf="@id/ll_phone_number"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <LinearLayout
+ android:id="@+id/ll_phone_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/tv_contact_name">
+
+ <TextView
+ android:id="@+id/tv_original_phone_number"
+ style="?textAppearanceBodyMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="" />
+
+ <TextView
+ android:id="@+id/tv_arrow"
+ style="?textAppearanceBodyMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/formattable_arrow_text" />
+
+ <TextView
+ android:id="@+id/tv_formatted_phone_number"
+ style="?textAppearanceBodyMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:textStyle="bold" />
+
+ </LinearLayout>
+
+ <CheckBox
+ android:id="@+id/check_box"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="true"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/demoapp/app/src/main/res/layout/fragment_formattable.xml b/demoapp/app/src/main/res/layout/fragment_formattable.xml
new file mode 100644
index 00000000..82aee3cc
--- /dev/null
+++ b/demoapp/app/src/main/res/layout/fragment_formattable.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ tools:context=".result.FormattableFragment">
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:layout_marginBottom="5dp"
+ android:scrollbars="vertical"
+ app:layout_constraintBottom_toTopOf="@id/btn_update_selected"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <Button
+ android:id="@+id/btn_update_selected"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/app_default_spacing_between_items"
+ android:text="@string/formattable_update_selected_text_default"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/demoapp/app/src/main/res/layout/fragment_not_formattable.xml b/demoapp/app/src/main/res/layout/fragment_not_formattable.xml
new file mode 100644
index 00000000..1127986d
--- /dev/null
+++ b/demoapp/app/src/main/res/layout/fragment_not_formattable.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ tools:context=".result.NotFormattableFragment">
+
+ <com.google.android.material.chip.ChipGroup
+ android:id="@+id/cg_filters"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingEnd="@dimen/app_default_spacing_from_layout_outline"
+ android:paddingStart="@dimen/app_default_spacing_from_layout_outline"
+ app:chipSpacingVertical="0dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_parsing_error"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="true"
+ android:text="@string/not_formattable_parsing_error_text" />
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_short_number"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="true"
+ android:text="@string/not_formattable_short_number_text" />
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_already_e164"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="true"
+ android:text="@string/not_formattable_already_e164_text" />
+
+ <com.google.android.material.chip.Chip
+ android:id="@+id/chip_invalid_number"
+ style="@style/Widget.Material3.Chip.Filter"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:checked="true"
+ android:text="@string/not_formattable_invalid_number_text" />
+
+ </com.google.android.material.chip.ChipGroup>
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:id="@+id/recycler_view"
+ android:layout_width="match_parent"
+ android:layout_height="0dp"
+ android:scrollbars="vertical"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/cg_filters" />
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/demoapp/app/src/main/res/layout/not_formattable_list_item.xml b/demoapp/app/src/main/res/layout/not_formattable_list_item.xml
new file mode 100644
index 00000000..c1ad1460
--- /dev/null
+++ b/demoapp/app/src/main/res/layout/not_formattable_list_item.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/cl_list_item"
+ android:layout_width="match_parent"
+ android:layout_height="?listPreferredItemHeight"
+ android:background="?selectableItemBackground"
+ android:clickable="true"
+ android:focusable="true"
+ android:paddingEnd="@dimen/app_default_spacing_from_layout_outline"
+ android:paddingStart="@dimen/app_default_spacing_from_layout_outline">
+
+ <TextView
+ android:id="@+id/tv_contact_name"
+ style="?textAppearanceLabelMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:textStyle="bold"
+ app:layout_constraintBottom_toTopOf="@id/ll_phone_number"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <LinearLayout
+ android:id="@+id/ll_phone_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/tv_contact_name">
+
+ <TextView
+ android:id="@+id/tv_reason"
+ style="?textAppearanceBodyMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text=""
+ android:textStyle="italic" />
+
+ <TextView
+ android:id="@+id/tv_colon"
+ style="?textAppearanceBodyMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="@string/not_formattable_colon_text" />
+
+ <TextView
+ android:id="@+id/tv_original_phone_number"
+ style="?textAppearanceBodyMedium"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="" />
+
+ </LinearLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/demoapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/demoapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 00000000..ea737cd4
--- /dev/null
+++ b/demoapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@drawable/ic_launcher_background" />
+ <foreground android:drawable="@drawable/ic_launcher_foreground" />
+</adaptive-icon>
diff --git a/demoapp/app/src/main/res/values-night/themes.xml b/demoapp/app/src/main/res/values-night/themes.xml
new file mode 100644
index 00000000..f352489e
--- /dev/null
+++ b/demoapp/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="AppTheme" parent="Theme.Material3.Dark" />
+</resources>
diff --git a/demoapp/app/src/main/res/values/dimens.xml b/demoapp/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..02371b02
--- /dev/null
+++ b/demoapp/app/src/main/res/values/dimens.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- App (app_) -->
+ <dimen name="app_default_spacing_from_layout_outline">15dp</dimen>
+ <dimen name="app_default_spacing_between_items">30dp</dimen>
+
+ <!-- Main activity (main_activity_) -->
+ <dimen name="main_activity_default_width_item">300dp</dimen>
+</resources>
diff --git a/demoapp/app/src/main/res/values/strings.xml b/demoapp/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..fbe98697
--- /dev/null
+++ b/demoapp/app/src/main/res/values/strings.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <!-- App (app_) -->
+ <string name="app_name" translatable="false">E.164</string>
+ <string name="app_name_long" translatable="false">E.164 Formatter</string>
+
+ <!-- Main activity (main_activity_) -->
+ <string name="main_activity_country_dropdown_label_text">Select the country to use</string>
+ <string name="main_activity_country_dropdown_hint">Country</string>
+ <string name="main_activity_country_dropdown_error">Choose dropdown item</string>
+ <string name="main_activity_country_dropdown_reset_text">Reset to SIM country</string>
+ <string name="main_activity_ignore_whitespace_text">Treat as E.164 if only difference is whitespace</string>
+
+ <string name="main_activity_error_text_grant_in_app">To use this functionality, the app needs access to Contacts.</string>
+ <string name="main_activity_error_cta_grant_in_app">Allow access</string>
+ <string name="main_activity_error_text_grant_in_settings">The app does not have permission to access Contacts. In Settings, tap Permissions and enable Contacts.</string>
+ <string name="main_activity_error_cta_grant_in_settings">Go to Settings</string>
+
+ <string name="main_activity_start_text_default">Start</string>
+ <string name="main_activity_start_text_processing">Process started…</string>
+
+ <string name="main_activity_no_contacts_exist_text">No contacts exist</string>
+
+ <!-- Result activity: Formattable (formattable_) -->
+ <string name="formattable_formattable_text">Formattable</string>
+
+ <string name="formattable_arrow_text" translatable="false">&#160;—>&#160;</string>
+
+ <string name="formattable_update_selected_text_default">Update selected</string>
+ <string name="formattable_update_selected_text_processing">Updating selected…</string>
+
+ <string name="formattable_no_numbers_selected_text">No numbers selected</string>
+ <string name="formattable_contacts_write_success_text">Successfully updated selected</string>
+ <string name="formattable_error_text">An error occurred. Please try again</string>
+
+ <!-- Result activity: Not formattable (not_formattable_) -->
+ <string name="not_formattable_not_formattable_text">Not formattable</string>
+
+ <string name="not_formattable_colon_text" translatable="false">:&#160;</string>
+
+ <string name="not_formattable_parsing_error_text">Parsing error</string>
+ <string name="not_formattable_short_number_text">Short number</string>
+ <string name="not_formattable_already_e164_text">Already E.164</string>
+ <string name="not_formattable_invalid_number_text">Invalid number</string>
+ <string name="not_formattable_unknown_error_text">Unknown error</string>
+
+ <string name="not_formattable_no_numbers_match_filters_text">No numbers match the selected filters</string>
+</resources>
diff --git a/demoapp/app/src/main/res/values/themes.xml b/demoapp/app/src/main/res/values/themes.xml
new file mode 100644
index 00000000..c262da82
--- /dev/null
+++ b/demoapp/app/src/main/res/values/themes.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+ <style name="AppTheme" parent="Theme.Material3.Light" />
+</resources>
diff --git a/demoapp/app/src/test/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormattingTest.java b/demoapp/app/src/test/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormattingTest.java
new file mode 100644
index 00000000..185e03b4
--- /dev/null
+++ b/demoapp/app/src/test/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormattingTest.java
@@ -0,0 +1,103 @@
+package com.google.phonenumbers.demoapp.phonenumbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp.FormattingState;
+import org.junit.Test;
+
+/** JUnit Tests for class {@link PhoneNumberFormatting}. */
+public class PhoneNumberFormattingTest {
+
+ @Test
+ public void formatPhoneNumberInApp_parsingError() {
+ PhoneNumberInApp phoneNumberInApp = new PhoneNumberInApp("19735", "Izabelle Goodwin", "#");
+
+ PhoneNumberFormatting.formatPhoneNumberInApp(phoneNumberInApp, "CH", false);
+
+ assertNull(phoneNumberInApp.getFormattedPhoneNumber());
+ assertEquals(FormattingState.PARSING_ERROR, phoneNumberInApp.getFormattingState());
+ assertFalse(phoneNumberInApp.shouldContactBeUpdated());
+ }
+
+ @Test
+ public void formatPhoneNumberInApp_numberIsShortNumber() {
+ PhoneNumberInApp phoneNumberInApp = new PhoneNumberInApp("2", "Beatrice Bradley", "144");
+
+ PhoneNumberFormatting.formatPhoneNumberInApp(phoneNumberInApp, "CH", false);
+
+ assertNull(phoneNumberInApp.getFormattedPhoneNumber());
+ assertEquals(FormattingState.NUMBER_IS_SHORT_NUMBER, phoneNumberInApp.getFormattingState());
+ assertFalse(phoneNumberInApp.shouldContactBeUpdated());
+ }
+
+ @Test
+ public void formatPhoneNumberInApp_invalidNumber() {
+ PhoneNumberInApp phoneNumberInApp =
+ new PhoneNumberInApp("1283", "Donte Salinas", "04466818029999");
+
+ PhoneNumberFormatting.formatPhoneNumberInApp(phoneNumberInApp, "CH", false);
+
+ assertNull(phoneNumberInApp.getFormattedPhoneNumber());
+ assertEquals(FormattingState.NUMBER_IS_NOT_VALID, phoneNumberInApp.getFormattingState());
+ assertFalse(phoneNumberInApp.shouldContactBeUpdated());
+ }
+
+ @Test
+ public void formatPhoneNumberInApp_numberIsAlreadyInE164() {
+ PhoneNumberInApp phoneNumberInApp =
+ new PhoneNumberInApp("345", "Kassandra Coffey", "+41446681804");
+
+ PhoneNumberFormatting.formatPhoneNumberInApp(phoneNumberInApp, "CH", false);
+
+ assertNull(phoneNumberInApp.getFormattedPhoneNumber());
+ assertEquals(FormattingState.NUMBER_IS_ALREADY_IN_E164, phoneNumberInApp.getFormattingState());
+ assertFalse(phoneNumberInApp.shouldContactBeUpdated());
+ }
+
+ @Test
+ public void
+ formatPhoneNumberInApp_originalWithWhitespace_ignoreWhitespaceTrue_numberIsAlreadyInE164() {
+ PhoneNumberInApp phoneNumberInApp =
+
+ new PhoneNumberInApp("443221", "Nayeli Martinez", "+41 446 68 18 07");
+ PhoneNumberFormatting.formatPhoneNumberInApp(phoneNumberInApp, "CH", true);
+
+ assertNull(phoneNumberInApp.getFormattedPhoneNumber());
+ assertEquals(FormattingState.NUMBER_IS_ALREADY_IN_E164, phoneNumberInApp.getFormattingState());
+ assertFalse(phoneNumberInApp.shouldContactBeUpdated());
+ }
+
+ @Test
+ public void formatPhoneNumberInApp_originalWithWhitespace_ignoreWhitespaceFalse_completed() {
+ PhoneNumberInApp phoneNumberInApp =
+ new PhoneNumberInApp("22", "Mariyah Johnston", "+41 446 68 18 05");
+
+ PhoneNumberFormatting.formatPhoneNumberInApp(phoneNumberInApp, "CH", false);
+
+ assertEquals("+41446681805", phoneNumberInApp.getFormattedPhoneNumber());
+ assertEquals(FormattingState.COMPLETED, phoneNumberInApp.getFormattingState());
+ assertTrue(phoneNumberInApp.shouldContactBeUpdated());
+ }
+
+ @Test
+ public void formatPhoneNumberInApp_completed() {
+ PhoneNumberInApp phoneNumberInAppCh = new PhoneNumberInApp("45", "Alena Potts", "0446681800");
+ PhoneNumberInApp phoneNumberInAppUs =
+ new PhoneNumberInApp("3829", "Rebecca Haimo", "9495550102");
+
+ PhoneNumberFormatting.formatPhoneNumberInApp(phoneNumberInAppCh, "CH", false);
+ PhoneNumberFormatting.formatPhoneNumberInApp(phoneNumberInAppUs, "US", false);
+
+ String expectedFormattedPhoneNumberCh = "+41446681800";
+ assertEquals(expectedFormattedPhoneNumberCh, phoneNumberInAppCh.getFormattedPhoneNumber());
+ assertEquals(FormattingState.COMPLETED, phoneNumberInAppCh.getFormattingState());
+ assertTrue(phoneNumberInAppCh.shouldContactBeUpdated());
+ String expectedFormattedPhoneNumberUs = "+19495550102";
+ assertEquals(expectedFormattedPhoneNumberUs, phoneNumberInAppUs.getFormattedPhoneNumber());
+ assertEquals(FormattingState.COMPLETED, phoneNumberInAppUs.getFormattingState());
+ assertTrue(phoneNumberInAppUs.shouldContactBeUpdated());
+ }
+}
diff --git a/demoapp/app/src/test/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberInAppTest.java b/demoapp/app/src/test/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberInAppTest.java
new file mode 100644
index 00000000..031deb89
--- /dev/null
+++ b/demoapp/app/src/test/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberInAppTest.java
@@ -0,0 +1,70 @@
+package com.google.phonenumbers.demoapp.phonenumbers;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.phonenumbers.demoapp.phonenumbers.PhoneNumberInApp.FormattingState;
+import org.junit.Test;
+
+/** JUnit Tests for class {@link PhoneNumberInApp}. */
+public class PhoneNumberInAppTest {
+
+ @Test
+ public void constructor() {
+ String id = "45";
+ String contactName = "Alena Potts";
+ String originalPhoneNumber = "0446681800";
+
+ PhoneNumberInApp phoneNumberInApp = new PhoneNumberInApp(id, contactName, originalPhoneNumber);
+
+ assertEquals(id, phoneNumberInApp.getId());
+ assertEquals(contactName, phoneNumberInApp.getContactName());
+ assertEquals(originalPhoneNumber, phoneNumberInApp.getOriginalPhoneNumber());
+ assertNull(phoneNumberInApp.getFormattedPhoneNumber());
+ assertEquals(PhoneNumberInApp.FormattingState.PENDING, phoneNumberInApp.getFormattingState());
+ assertFalse(phoneNumberInApp.shouldContactBeUpdated());
+ }
+
+ @Test
+ public void setFormattedPhoneNumber() {
+ PhoneNumberInApp phoneNumberInApp = new PhoneNumberInApp("2", "Beatrice Bradley", "0446681801");
+ String formattedPhoneNumber = "+41446681801";
+
+ phoneNumberInApp.setFormattedPhoneNumber(formattedPhoneNumber);
+
+ assertEquals(formattedPhoneNumber, phoneNumberInApp.getFormattedPhoneNumber());
+ }
+
+ @Test
+ public void setFormattingState() {
+ PhoneNumberInApp phoneNumberInApp = new PhoneNumberInApp("1283", "Donte Salinas", "0446681802");
+ FormattingState formattingState = FormattingState.NUMBER_IS_ALREADY_IN_E164;
+
+ phoneNumberInApp.setFormattingState(formattingState);
+
+ assertEquals(formattingState, phoneNumberInApp.getFormattingState());
+ }
+
+ @Test
+ public void setShouldContactBeUpdated() {
+ PhoneNumberInApp phoneNumberInApp =
+ new PhoneNumberInApp("19735", "Izabelle Goodwin", "0446681803");
+
+ phoneNumberInApp.setShouldContactBeUpdated(true);
+
+ assertTrue(phoneNumberInApp.shouldContactBeUpdated());
+ }
+
+ @Test
+ public void compareTo() {
+ PhoneNumberInApp phoneNumberInApp1 =
+ new PhoneNumberInApp("345", "Kassandra Coffey", "0446681804");
+ PhoneNumberInApp phoneNumberInApp2 =
+ new PhoneNumberInApp("22", "Mariyah Johnston", "0446681805");
+
+ assertTrue(phoneNumberInApp1.compareTo(phoneNumberInApp2) < 0);
+ assertTrue(phoneNumberInApp2.compareTo(phoneNumberInApp1) > 0);
+ }
+}
diff --git a/demoapp/build.gradle b/demoapp/build.gradle
new file mode 100644
index 00000000..9d4048d4
--- /dev/null
+++ b/demoapp/build.gradle
@@ -0,0 +1,5 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+plugins {
+ id 'com.android.application' version '7.3.1' apply false
+ id 'com.android.library' version '7.3.1' apply false
+}
diff --git a/demoapp/gradle.properties b/demoapp/gradle.properties
new file mode 100644
index 00000000..a03b3548
--- /dev/null
+++ b/demoapp/gradle.properties
@@ -0,0 +1,21 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Enables namespacing of each library's R class so that its R class includes only the
+# resources declared in the library itself and none from the library's dependencies,
+# thereby reducing the size of the R class for that library
+android.nonTransitiveRClass=true
diff --git a/demoapp/settings.gradle b/demoapp/settings.gradle
new file mode 100644
index 00000000..406d3819
--- /dev/null
+++ b/demoapp/settings.gradle
@@ -0,0 +1,16 @@
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ google()
+ mavenCentral()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+rootProject.name = "E.164 Formatter"
+include ':app'
diff --git a/geocoder/pom.xml b/geocoder/pom.xml
index 54fd69b4..9bff68b7 100644
--- a/geocoder/pom.xml
+++ b/geocoder/pom.xml
@@ -3,14 +3,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>geocoder</artifactId>
- <version>2.213</version>
+ <version>2.214</version>
<packaging>jar</packaging>
<url>https://github.com/google/libphonenumber/</url>
<parent>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber-parent</artifactId>
- <version>8.13.19</version>
+ <version>8.13.20</version>
</parent>
<build>
@@ -87,12 +87,12 @@
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
- <version>8.13.19</version>
+ <version>8.13.20</version>
</dependency>
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>prefixmapper</artifactId>
- <version>2.213</version>
+ <version>2.214</version>
</dependency>
</dependencies>
diff --git a/internal/prefixmapper/pom.xml b/internal/prefixmapper/pom.xml
index 92d6f62d..88c24d75 100644
--- a/internal/prefixmapper/pom.xml
+++ b/internal/prefixmapper/pom.xml
@@ -3,14 +3,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>prefixmapper</artifactId>
- <version>2.213</version>
+ <version>2.214</version>
<packaging>jar</packaging>
<url>https://github.com/google/libphonenumber/</url>
<parent>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber-parent</artifactId>
- <version>8.13.19</version>
+ <version>8.13.20</version>
<relativePath>../../pom.xml</relativePath>
</parent>
@@ -75,7 +75,7 @@
<dependency>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
- <version>8.13.19</version>
+ <version>8.13.20</version>
</dependency>
</dependencies>
diff --git a/libphonenumber/pom.xml b/libphonenumber/pom.xml
index 41d27502..df393a71 100644
--- a/libphonenumber/pom.xml
+++ b/libphonenumber/pom.xml
@@ -3,14 +3,14 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber</artifactId>
- <version>8.13.19</version>
+ <version>8.13.20</version>
<packaging>jar</packaging>
<url>https://github.com/google/libphonenumber/</url>
<parent>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber-parent</artifactId>
- <version>8.13.19</version>
+ <version>8.13.20</version>
</parent>
<build>
diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR
index ec3e1383..b9e21d9b 100644
--- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR
+++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR
Binary files differ
diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD
index fd80abf4..16852052 100644
--- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD
+++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD
Binary files differ
diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY
index 82853a71..e379eb17 100644
--- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY
+++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY
Binary files differ
diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL
index f48c7363..1054d0d5 100644
--- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL
+++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL
Binary files differ
diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ
index 2dfb2008..39f9504e 100644
--- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ
+++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ
Binary files differ
diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM
index 632c8f55..24c2d62c 100644
--- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM
+++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM
Binary files differ
diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW
index d38a76d0..9538d1a6 100644
--- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW
+++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW
Binary files differ
diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN
index cf187dda..926d1040 100644
--- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN
+++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN
Binary files differ
diff --git a/pom.xml b/pom.xml
index d6314652..6bcf1fe5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.googlecode.libphonenumber</groupId>
<artifactId>libphonenumber-parent</artifactId>
- <version>8.13.19</version>
+ <version>8.13.20</version>
<packaging>pom</packaging>
<url>https://github.com/google/libphonenumber/</url>
@@ -34,7 +34,7 @@
<connection>scm:git:https://github.com/google/libphonenumber.git</connection>
<developerConnection>scm:git:git@github.com:googlei18n/libphonenumber.git</developerConnection>
<url>https://github.com/google/libphonenumber/</url>
- <tag>v8.13.19</tag>
+ <tag>v8.13.20</tag>
</scm>
<properties>
diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR
index ec3e1383..b9e21d9b 100644
--- a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR
+++ b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR
Binary files differ
diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD
index fd80abf4..16852052 100644
--- a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD
+++ b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD
Binary files differ
diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY
index 82853a71..e379eb17 100644
--- a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY
+++ b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY
Binary files differ
diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL
index f48c7363..1054d0d5 100644
--- a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL
+++ b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL
Binary files differ
diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ
index 2dfb2008..39f9504e 100644
--- a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ
+++ b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ
Binary files differ
diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM
index 632c8f55..24c2d62c 100644
--- a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM
+++ b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM
Binary files differ
diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW
index d38a76d0..9538d1a6 100644
--- a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW
+++ b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW
Binary files differ
diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN
index cf187dda..926d1040 100644
--- a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN
+++ b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN
Binary files differ