diff options
author | Treehugger Robot <android-test-infra-autosubmit@system.gserviceaccount.com> | 2023-09-13 22:51:06 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2023-09-13 22:51:06 +0000 |
commit | be2414684db23d31266f25adee5587cdc18f35ec (patch) | |
tree | 332f68d89179598adaaa6ae413ad970acb72b6d8 | |
parent | 4c4c503b188798cdc031a958d8bda4145905d839 (diff) | |
parent | e4f6e0b3a32837a705593c1f2244c0f1ad5edc44 (diff) | |
download | libphonenumber-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>
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 Binary files differindex 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 diff --git a/carrier/src/com/google/i18n/phonenumbers/carrier/data/250_en b/carrier/src/com/google/i18n/phonenumbers/carrier/data/250_en Binary files differindex 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 diff --git a/carrier/src/com/google/i18n/phonenumbers/carrier/data/46_en b/carrier/src/com/google/i18n/phonenumbers/carrier/data/46_en Binary files differindex 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 diff --git a/carrier/src/com/google/i18n/phonenumbers/carrier/data/56_en b/carrier/src/com/google/i18n/phonenumbers/carrier/data/56_en Binary files differindex 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 diff --git a/carrier/src/com/google/i18n/phonenumbers/carrier/data/592_en b/carrier/src/com/google/i18n/phonenumbers/carrier/data/592_en Binary files differindex 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 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"> —> </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">: </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 Binary files differindex ec3e1383..b9e21d9b 100644 --- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR +++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_AR diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD Binary files differindex fd80abf4..16852052 100644 --- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD +++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY Binary files differindex 82853a71..e379eb17 100644 --- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY +++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL Binary files differindex f48c7363..1054d0d5 100644 --- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL +++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ Binary files differindex 2dfb2008..39f9504e 100644 --- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ +++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM Binary files differindex 632c8f55..24c2d62c 100644 --- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM +++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW Binary files differindex d38a76d0..9538d1a6 100644 --- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW +++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW diff --git a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN Binary files differindex cf187dda..926d1040 100644 --- a/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN +++ b/libphonenumber/src/com/google/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN @@ -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 Binary files differindex 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 diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_BD Binary files differindex 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 diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_GY Binary files differindex 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 diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_IL Binary files differindex 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 diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_NZ Binary files differindex 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 diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_OM Binary files differindex 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 diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_RW Binary files differindex 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 diff --git a/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN b/repackaged/libphonenumber/src/com/android/i18n/phonenumbers/data/PhoneNumberMetadataProto_TN Binary files differindex 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 |