| @ -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`) | |||||
| @ -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' | |||||
| } | |||||
| @ -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> | |||||
| @ -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); | |||||
| } | |||||
| } | |||||
| @ -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 | |||||
| } | |||||
| } | |||||
| @ -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; | |||||
| } | |||||
| } | |||||
| @ -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; | |||||
| } | |||||
| } | |||||
| @ -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); | |||||
| } | |||||
| } | |||||
| @ -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 | |||||
| } | |||||
| } | |||||
| @ -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); | |||||
| } | |||||
| } | |||||
| @ -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 | |||||
| } | |||||
| } | |||||
| @ -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 | |||||
| } | |||||
| } | |||||
| @ -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; | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -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(); | |||||
| } | |||||
| } | |||||
| @ -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()); | |||||
| } | |||||
| } | |||||
| } | |||||
| @ -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); | |||||
| } | |||||
| } | |||||
| @ -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 ""; | |||||
| } | |||||
| } | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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" /> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -0,0 +1,4 @@ | |||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <resources> | |||||
| <style name="AppTheme" parent="Theme.Material3.Dark" /> | |||||
| </resources> | |||||
| @ -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> | |||||
| @ -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> | |||||
| @ -0,0 +1,4 @@ | |||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <resources> | |||||
| <style name="AppTheme" parent="Theme.Material3.Light" /> | |||||
| </resources> | |||||
| @ -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()); | |||||
| } | |||||
| } | |||||
| @ -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); | |||||
| } | |||||
| } | |||||
| @ -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 | |||||
| } | |||||
| @ -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 | |||||
| @ -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' | |||||