| @ -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' | |||