Browse Source

Add a demo Android App that uses LPN in Java. (#3135)

pull/3182/head
Fabrice Berchtold 2 years ago
committed by GitHub
parent
commit
b7c90e201c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 2446 additions and 0 deletions
  1. +7
    -0
      README.md
  2. +51
    -0
      java/demoapp/README.md
  3. +45
    -0
      java/demoapp/app/build.gradle
  4. +39
    -0
      java/demoapp/app/src/main/AndroidManifest.xml
  5. +16
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/MyApplication.java
  6. +161
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsPermissionManagement.java
  7. +62
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsRead.java
  8. +59
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsWrite.java
  9. +203
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/main/CountryDropdown.java
  10. +226
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/main/MainActivity.java
  11. +74
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormatting.java
  12. +96
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberInApp.java
  13. +161
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/FormattableFragment.java
  14. +157
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/FormattableRvAdapter.java
  15. +119
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/NotFormattableFragment.java
  16. +94
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/NotFormattableRvAdapter.java
  17. +102
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/ResultActivity.java
  18. +72
    -0
      java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/ResultVpAdapter.java
  19. +32
    -0
      java/demoapp/app/src/main/res/drawable/ic_launcher_background.xml
  20. +16
    -0
      java/demoapp/app/src/main/res/drawable/ic_launcher_foreground.xml
  21. +10
    -0
      java/demoapp/app/src/main/res/drawable/ic_outline_home_30.xml
  22. +83
    -0
      java/demoapp/app/src/main/res/layout/activity_main.xml
  23. +26
    -0
      java/demoapp/app/src/main/res/layout/activity_result.xml
  24. +24
    -0
      java/demoapp/app/src/main/res/layout/country_dropdown.xml
  25. +9
    -0
      java/demoapp/app/src/main/res/layout/country_dropdown_item.xml
  26. +66
    -0
      java/demoapp/app/src/main/res/layout/formattable_list_item.xml
  27. +30
    -0
      java/demoapp/app/src/main/res/layout/fragment_formattable.xml
  28. +64
    -0
      java/demoapp/app/src/main/res/layout/fragment_not_formattable.xml
  29. +57
    -0
      java/demoapp/app/src/main/res/layout/not_formattable_list_item.xml
  30. +5
    -0
      java/demoapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  31. +4
    -0
      java/demoapp/app/src/main/res/values-night/themes.xml
  32. +9
    -0
      java/demoapp/app/src/main/res/values/dimens.xml
  33. +48
    -0
      java/demoapp/app/src/main/res/values/strings.xml
  34. +4
    -0
      java/demoapp/app/src/main/res/values/themes.xml
  35. +103
    -0
      java/demoapp/app/src/test/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormattingTest.java
  36. +70
    -0
      java/demoapp/app/src/test/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberInAppTest.java
  37. +5
    -0
      java/demoapp/build.gradle
  38. +21
    -0
      java/demoapp/gradle.properties
  39. +16
    -0
      java/demoapp/settings.gradle

+ 7
- 0
README.md View File

@ -66,6 +66,13 @@ If this number is lower than the [latest release's version
number](https://github.com/google/libphonenumber/releases), we are between
releases and the demo may be at either version.
### Demo App
There is a demo Android App called [E.164 Formatter](java/demoapp) in this
repository. The purpose of this App is to show an example of how the library can
be used in a real-life situation, in this case specifically in an Android App
using Java.
## JavaScript
The [JavaScript


+ 51
- 0
java/demoapp/README.md View File

@ -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`)

+ 45
- 0
java/demoapp/app/build.gradle View File

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

+ 39
- 0
java/demoapp/app/src/main/AndroidManifest.xml View File

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

+ 16
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/MyApplication.java View File

@ -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);
}
}

+ 161
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsPermissionManagement.java View File

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

+ 62
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsRead.java View File

@ -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;
}
}

+ 59
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/contacts/ContactsWrite.java View File

@ -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;
}
}

+ 203
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/main/CountryDropdown.java View File

@ -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);
}
}

+ 226
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/main/MainActivity.java View File

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

+ 74
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormatting.java View File

@ -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);
}
}

+ 96
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberInApp.java View File

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

+ 161
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/FormattableFragment.java View File

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

+ 157
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/FormattableRvAdapter.java View File

@ -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;
}
}
}

+ 119
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/NotFormattableFragment.java View File

@ -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();
}
}

+ 94
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/NotFormattableRvAdapter.java View File

@ -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());
}
}
}

+ 102
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/ResultActivity.java View File

@ -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);
}
}

+ 72
- 0
java/demoapp/app/src/main/java/com/google/phonenumbers/demoapp/result/ResultVpAdapter.java View File

@ -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 "";
}
}

+ 32
- 0
java/demoapp/app/src/main/res/drawable/ic_launcher_background.xml View File

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

+ 16
- 0
java/demoapp/app/src/main/res/drawable/ic_launcher_foreground.xml View File

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

+ 10
- 0
java/demoapp/app/src/main/res/drawable/ic_outline_home_30.xml View File

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

+ 83
- 0
java/demoapp/app/src/main/res/layout/activity_main.xml View File

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

+ 26
- 0
java/demoapp/app/src/main/res/layout/activity_result.xml View File

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

+ 24
- 0
java/demoapp/app/src/main/res/layout/country_dropdown.xml View File

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

+ 9
- 0
java/demoapp/app/src/main/res/layout/country_dropdown_item.xml View File

@ -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" />

+ 66
- 0
java/demoapp/app/src/main/res/layout/formattable_list_item.xml View File

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

+ 30
- 0
java/demoapp/app/src/main/res/layout/fragment_formattable.xml View File

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

+ 64
- 0
java/demoapp/app/src/main/res/layout/fragment_not_formattable.xml View File

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

+ 57
- 0
java/demoapp/app/src/main/res/layout/not_formattable_list_item.xml View File

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

+ 5
- 0
java/demoapp/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml View File

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

+ 4
- 0
java/demoapp/app/src/main/res/values-night/themes.xml View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.Material3.Dark" />
</resources>

+ 9
- 0
java/demoapp/app/src/main/res/values/dimens.xml View File

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

+ 48
- 0
java/demoapp/app/src/main/res/values/strings.xml View File

@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- App (app_) -->
<string name="app_name" translatable="false">E.164</string>
<string name="app_name_long" translatable="false">E.164 Formatter</string>
<!-- Main activity (main_activity_) -->
<string name="main_activity_country_dropdown_label_text">Select the country to use</string>
<string name="main_activity_country_dropdown_hint">Country</string>
<string name="main_activity_country_dropdown_error">Choose dropdown item</string>
<string name="main_activity_country_dropdown_reset_text">Reset to SIM country</string>
<string name="main_activity_ignore_whitespace_text">Treat as E.164 if only difference is whitespace</string>
<string name="main_activity_error_text_grant_in_app">To use this functionality, the app needs access to Contacts.</string>
<string name="main_activity_error_cta_grant_in_app">Allow access</string>
<string name="main_activity_error_text_grant_in_settings">The app does not have permission to access Contacts. In Settings, tap Permissions and enable Contacts.</string>
<string name="main_activity_error_cta_grant_in_settings">Go to Settings</string>
<string name="main_activity_start_text_default">Start</string>
<string name="main_activity_start_text_processing">Process started…</string>
<string name="main_activity_no_contacts_exist_text">No contacts exist</string>
<!-- Result activity: Formattable (formattable_) -->
<string name="formattable_formattable_text">Formattable</string>
<string name="formattable_arrow_text" translatable="false">&#160;—>&#160;</string>
<string name="formattable_update_selected_text_default">Update selected</string>
<string name="formattable_update_selected_text_processing">Updating selected…</string>
<string name="formattable_no_numbers_selected_text">No numbers selected</string>
<string name="formattable_contacts_write_success_text">Successfully updated selected</string>
<string name="formattable_error_text">An error occurred. Please try again</string>
<!-- Result activity: Not formattable (not_formattable_) -->
<string name="not_formattable_not_formattable_text">Not formattable</string>
<string name="not_formattable_colon_text" translatable="false">:&#160;</string>
<string name="not_formattable_parsing_error_text">Parsing error</string>
<string name="not_formattable_short_number_text">Short number</string>
<string name="not_formattable_already_e164_text">Already E.164</string>
<string name="not_formattable_invalid_number_text">Invalid number</string>
<string name="not_formattable_unknown_error_text">Unknown error</string>
<string name="not_formattable_no_numbers_match_filters_text">No numbers match the selected filters</string>
</resources>

+ 4
- 0
java/demoapp/app/src/main/res/values/themes.xml View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="Theme.Material3.Light" />
</resources>

+ 103
- 0
java/demoapp/app/src/test/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberFormattingTest.java View File

@ -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());
}
}

+ 70
- 0
java/demoapp/app/src/test/java/com/google/phonenumbers/demoapp/phonenumbers/PhoneNumberInAppTest.java View File

@ -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);
}
}

+ 5
- 0
java/demoapp/build.gradle View File

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

+ 21
- 0
java/demoapp/gradle.properties View File

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

+ 16
- 0
java/demoapp/settings.gradle View File

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

Loading…
Cancel
Save