diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dd84ea7..3205926 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -12,6 +12,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -24,15 +25,17 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] + +- OS: [e.g. iOS] +- Browser [e.g. chrome, safari] +- Version [e.g. 22] **Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser [e.g. stock browser, safari] +- Version [e.g. 22] **Additional context** Add any other context about the problem here. diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index d536a1e..195d3a9 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -12,15 +12,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - name: set up JDK 17 - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'temurin' - cache: gradle + - uses: actions/checkout@v3 + - name: set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - name: Build with Gradle - run: ./gradlew build + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Build with Gradle + run: ./gradlew build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5bc87cf..c6eae34 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,11 +6,13 @@ You may contribute to OpenDialer in many ways: ## Creating issues -Before you create a new issue, please do a search in open issues to see if the issue or feature request has already been filed. +Before you create a new issue, please do a search in open issues to see if the issue or feature +request has already been filed. ## Reporting bugs -Please open one issue per bug report. Be as detailed as you can in order to increase chances that the bug gets fixed. +Please open one issue per bug report. Be as detailed as you can in order to increase chances that +the bug gets fixed. ## Confirming bugs @@ -20,7 +22,7 @@ Please comment only if you have valuable info that can help debugging. ## Requesting features -Please open one issue per feature request. +Please open one issue per feature request. Describe as best as you can the feature you'd like to see implemented. @@ -38,4 +40,5 @@ Feel free to give feedback on someone else's PR. ## Translating -If you are interested in contributing to translations, you can do so in the [Crowdin project](https://crowdin.com/project/opendialer). +If you are interested in contributing to translations, you can do so in +the [Crowdin project](https://crowdin.com/project/opendialer). diff --git a/README.md b/README.md index 85fd9e1..8ed3985 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,15 @@ An open, clean and modern dialer for Android. ## Build and Run -**OpenDialer** uses the Gradle build system and can be imported directly into Android Studio (make sure you are using the latest stable version available [here](https://developer.android.com/studio)). +**OpenDialer** uses the Gradle build system and can be imported directly into Android Studio (make +sure you are using the latest stable version +available [here](https://developer.android.com/studio)). Change the run configuration to `app`. ![image](docs/images/android_studio_build.png) -The app contains the usual `debug` and `release` build variants which can be built and run. +The app contains the usual `debug` and `release` build variants which can be built and run. ![image](docs/images/android_studio_build_variant.png) @@ -59,7 +61,9 @@ together to create a complete app. ## Modularization -The **OpenDialer** app has been fully modularized based on the [official recommendations](https://developer.android.com/topic/modularization/patterns) and you can find the +The **OpenDialer** app has been fully modularized based on +the [official recommendations](https://developer.android.com/topic/modularization/patterns) and you +can find the description of the modularization strategy used in [modularization learning journey](./docs/ModularizationLearningJourney.md). diff --git a/app/build.gradle b/app/build.gradle index b7cff6d..8c62d1e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,8 @@ plugins { id 'kotlin-kapt' id 'androidx.navigation.safeargs.kotlin' id 'com.google.dagger.hilt.android' + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) } def keystorePropertiesFile = rootProject.file("keystore.properties") @@ -50,6 +52,7 @@ android { } buildFeatures { viewBinding true + compose true } namespace 'dev.alenajam.opendialer' lint { @@ -58,6 +61,9 @@ android { compileOptions { coreLibraryDesugaringEnabled true } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.2" + } } dependencies { @@ -96,7 +102,7 @@ dependencies { implementation 'com.google.dagger:hilt-android:2.48.1' kapt 'com.google.dagger:hilt-compiler:2.48.1' - androidTestImplementation 'com.google.dagger:hilt-android-testing:2.48.1' + androidTestImplementation 'com.google.dagger:hilt-android-testing:2.48.1' kaptAndroidTest 'com.google.dagger:hilt-compiler:2.48.1' testImplementation 'com.google.dagger:hilt-android-testing:2.48.1' kaptTest 'com.google.dagger:hilt-compiler:2.48.1' @@ -105,6 +111,19 @@ dependencies { testImplementation 'junit:junit:4.13.2' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.4' + + def composeBom = platform('androidx.compose:compose-bom:2024.09.03') + implementation composeBom + androidTestImplementation composeBom + implementation(libs.androidx.material3) + implementation 'androidx.compose.ui:ui-tooling-preview' + debugImplementation 'androidx.compose.ui:ui-tooling' + androidTestImplementation 'androidx.compose.ui:ui-test-junit4' + debugImplementation 'androidx.compose.ui:ui-test-manifest' + implementation 'androidx.compose.material:material-icons-extended' + implementation "androidx.compose.ui:ui-viewbinding" + implementation(libs.navigation.compose) + implementation(libs.kotlinx.serialization) } repositories { diff --git a/app/src/androidTest/java/dev/alenajam/opendialer/ExampleInstrumentedTest.java b/app/src/androidTest/java/dev/alenajam/opendialer/ExampleInstrumentedTest.java index 9a1ec86..7517106 100644 --- a/app/src/androidTest/java/dev/alenajam/opendialer/ExampleInstrumentedTest.java +++ b/app/src/androidTest/java/dev/alenajam/opendialer/ExampleInstrumentedTest.java @@ -1,5 +1,7 @@ package dev.alenajam.opendialer; +import static org.junit.Assert.assertEquals; + import android.content.Context; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -8,8 +10,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import static org.junit.Assert.assertEquals; - /** * Instrumented test, which will execute on an Android device. * @@ -17,11 +17,11 @@ */ @RunWith(AndroidJUnit4.class) public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); - assertEquals("dev.alenajam.opendialer", appContext.getPackageName()); - } + assertEquals("dev.alenajam.opendialer", appContext.getPackageName()); + } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ace97fc..3a9f112 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,7 +23,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher" android:supportsRtl="true" - android:theme="@style/AppTheme"> + android:theme="@style/Theme.OpenDialer"> - - - @@ -66,17 +56,6 @@ - - - - - { - Fragment currentFragment = navHostFragment.getChildFragmentManager().getPrimaryNavigationFragment(); - if (currentFragment instanceof SearchOpenChangeListener) { - ((SearchOpenChangeListener) currentFragment).onOpenChange(isOpen); - } - }); - - searchView.setTextListener((editText, text) -> { - Fragment currentFragment = navHostFragment.getChildFragmentManager().getPrimaryNavigationFragment(); - if (currentFragment instanceof BackPressedListener) { - if (currentFragment instanceof SearchListener) { - ((SearchListener) currentFragment).onSearch(text); - } - } - }); - - handleIntent(getIntent()); - } - - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - - handleIntent(intent); - } - - private void handleIntent(Intent intent) { - if (intent == null) return; - - String action = intent.getAction(); - Uri data = intent.getData(); - boolean hasPhoneData = data != null && PhoneAccount.SCHEME_TEL.equals(data.getScheme()); - - if (Intent.ACTION_DIAL.equals(action)) { - if (hasPhoneData || intent.getBooleanExtra(EXTRA_KEY_ADD_CALL, false)) { - handleIntentNumber(data); - } - } - - if (Intent.ACTION_VIEW.equals(action)) { - if (hasPhoneData) { - handleIntentNumber(data); - } - } - } - - private void handleIntentNumber(Uri data) { - String prefilled = ""; - if (data != null && PhoneAccount.SCHEME_TEL.equals(data.getScheme())) { - String number = data.getSchemeSpecificPart(); - prefilled = PhoneNumberUtils.convertKeypadLettersToDigits( - PhoneNumberUtils.replaceUnicodeDigits(number) - ); - } - - navHostFragment - .getNavController() - .popBackStack(R.id.homeFragment, false); - - navHostFragment - .getNavController() - .navigate( - MainFragmentDirections.Companion.actionHomeFragmentToSearchContactsFragment( - SearchContactsFragment.InitiationType.DIALPAD, prefilled - ) - ); - } - - @Override - public void onBackPressed() { - Fragment currentFragment = navHostFragment.getChildFragmentManager().getPrimaryNavigationFragment(); - if (currentFragment instanceof BackPressedListener) { - if (!((BackPressedListener) currentFragment).onBackPressed()) { - return; - } - } - - super.onBackPressed(); - } - - @Override - public void hideToolbar(boolean animate) { - searchView.setVisibility(View.GONE); - } - - @Override - public void showToolbar(boolean animate) { - searchView.setVisibility(View.VISIBLE); - } - - @Override - public void openSearch() { - searchView.open(); - } - - @Override - public void closeSearch() { - searchView.close(); - } - - @Override - public void closeSearchKeyboard() { - searchView.closeKeyboard(); - } - - @Override - public void onColorChange(int color) { - getWindow().setStatusBarColor(color); - CommonUtilsKt.updateStatusBarLightMode(getWindow(), color); - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/alenajam/opendialer/features/main/MainFragment.kt b/app/src/main/java/dev/alenajam/opendialer/features/main/MainFragment.kt deleted file mode 100644 index d1fef9a..0000000 --- a/app/src/main/java/dev/alenajam/opendialer/features/main/MainFragment.kt +++ /dev/null @@ -1,163 +0,0 @@ -package dev.alenajam.opendialer.features.main - -import android.content.Context -import android.os.Bundle -import android.view.LayoutInflater -import android.view.MenuItem -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.Fragment -import androidx.navigation.fragment.findNavController -import androidx.viewpager2.adapter.FragmentStateAdapter -import androidx.viewpager2.widget.ViewPager2 -import com.google.android.material.bottomnavigation.BottomNavigationView -import dev.alenajam.opendialer.R -import dev.alenajam.opendialer.core.common.OnStatusBarColorChange -import dev.alenajam.opendialer.core.common.SearchOpenChangeListener -import dev.alenajam.opendialer.core.common.ToolbarListener -import dev.alenajam.opendialer.core.common.functional.safeNavigate -import dev.alenajam.opendialer.databinding.FragmentHomeBinding -import dev.alenajam.opendialer.feature.calls.RecentsFragment -import dev.alenajam.opendialer.feature.contacts.ContactsFragment -import dev.alenajam.opendialer.feature.contactsSearch.SearchContactsFragment -import dev.alenajam.opendialer.feature.settings.ProfileFragment -import dev.alenajam.opendialer.features.main.MainFragmentDirections.Companion.actionHomeFragmentToSearchContactsFragment - -class MainFragment : - Fragment(), - View.OnClickListener, - BottomNavigationView.OnNavigationItemSelectedListener, - SearchOpenChangeListener { - companion object { - fun newInstance() = MainFragment() - } - - private lateinit var viewPagerAdapter: FragmentStateAdapterImpl - private var toolbarListener: ToolbarListener? = null - private var onStatusBarColorChange: OnStatusBarColorChange? = null - private var _binding: FragmentHomeBinding? = null - private val binding get() = _binding!! - - override fun onAttach(context: Context) { - super.onAttach(context) - if (context is ToolbarListener) { - toolbarListener = context - } - if (context is OnStatusBarColorChange) { - onStatusBarColorChange = context - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentHomeBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - onStatusBarColorChange?.onColorChange(view.context.getColor(R.color.windowBackground)) - - binding.bottomNavigation.setOnNavigationItemSelectedListener(this) - binding.bottomNavigation.itemIconTintList = null - - binding.fab.setOnClickListener(this) - - viewPagerAdapter = FragmentStateAdapterImpl(this) - binding.viewPager.apply { - adapter = viewPagerAdapter - isUserInputEnabled = false - registerOnPageChangeCallback(OnPageChange()) - } - } - - private fun setPage(fragment: Tab) { - binding.viewPager.setCurrentItem(fragment.ordinal, false) - } - - override fun onClick(v: View?) { - if (v?.id == binding.fab.id) { - val action = - actionHomeFragmentToSearchContactsFragment(SearchContactsFragment.InitiationType.DIALPAD) - findNavController().safeNavigate(action) - } - } - - override fun onNavigationItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.recents -> setPage(Tab.RECENTS) - R.id.contacts -> setPage(Tab.CONTACTS) - R.id.profile -> setPage(Tab.PROFILE) - else -> return false - } - - return true - } - - override fun onOpenChange(isOpen: Boolean) { - if (isOpen) { - findNavController().safeNavigate( - actionHomeFragmentToSearchContactsFragment( - SearchContactsFragment.InitiationType.REGULAR - ) - ) - } - } - - private inner class OnPageChange : ViewPager2.OnPageChangeCallback() { - override fun onPageSelected(position: Int) { - /** Handle toolbar visibility */ - when (position) { - Tab.RECENTS.ordinal, - Tab.CONTACTS.ordinal -> toolbarListener?.showToolbar(false) - - else -> toolbarListener?.hideToolbar(false) - } - - /** Handle fab visibility */ - binding.fab.visibility = when (position) { - Tab.RECENTS.ordinal, - Tab.CONTACTS.ordinal -> View.VISIBLE - - else -> View.GONE - } - - context?.getColor(R.color.windowBackground)?.let { - onStatusBarColorChange?.onColorChange(it) - } - } - } - - class FragmentStateAdapterImpl(fragment: MainFragment) : FragmentStateAdapter(fragment) { - private val fragments: List = listOf( - Tab.RECENTS, - Tab.CONTACTS, - Tab.PROFILE - ) - - override fun createFragment(position: Int): Fragment { - return when (position) { - Tab.RECENTS.ordinal -> RecentsFragment() - Tab.PROFILE.ordinal -> ProfileFragment() - else -> ContactsFragment() - } - } - - override fun getItemCount(): Int = fragments.size - } - - enum class Tab { - RECENTS, - CONTACTS, - PROFILE - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/alenajam/opendialer/helper/NotificationHelper.java b/app/src/main/java/dev/alenajam/opendialer/helper/NotificationHelper.java index 4ed42b4..3e9ea50 100644 --- a/app/src/main/java/dev/alenajam/opendialer/helper/NotificationHelper.java +++ b/app/src/main/java/dev/alenajam/opendialer/helper/NotificationHelper.java @@ -7,32 +7,32 @@ import dev.alenajam.opendialer.R; public abstract class NotificationHelper { - private static final String CHANNEL_ID_INCOMING_CALLS = "dev.alenajam.opendialer.notification_channel.incoming_calls"; - private static final String CHANNEL_ID_ONGOING_CALLS = "dev.alenajam.opendialer.notification_channel.ongoing_calls"; - private static final String CHANNEL_ID_OUTGOING_CALLS = "dev.alenajam.opendialer.notification_channel.outgoing_calls"; - private static final int NOTIFICATION_ID_CALL = 1; - private static final String INTENT_ACTION_CALL_BUTTON_CLICK_ACCEPT = "dev.alenajam.opendialer.CALL_ACCEPT"; - private static final String INTENT_ACTION_CALL_BUTTON_CLICK_DECLINE = "dev.alenajam.opendialer.CALL_DECLINE"; - - - public static void setupNotificationChannels(Context context) { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - createCallChannel(context, CHANNEL_ID_INCOMING_CALLS, context.getString(R.string.channel_incoming_calls), NotificationManager.IMPORTANCE_HIGH); - createCallChannel(context, CHANNEL_ID_ONGOING_CALLS, context.getString(R.string.channel_ongoing_calls), NotificationManager.IMPORTANCE_DEFAULT); - createCallChannel(context, CHANNEL_ID_OUTGOING_CALLS, context.getString(R.string.channel_outgoing_calls), NotificationManager.IMPORTANCE_DEFAULT); + private static final String CHANNEL_ID_INCOMING_CALLS = "dev.alenajam.opendialer.notification_channel.incoming_calls"; + private static final String CHANNEL_ID_ONGOING_CALLS = "dev.alenajam.opendialer.notification_channel.ongoing_calls"; + private static final String CHANNEL_ID_OUTGOING_CALLS = "dev.alenajam.opendialer.notification_channel.outgoing_calls"; + private static final int NOTIFICATION_ID_CALL = 1; + private static final String INTENT_ACTION_CALL_BUTTON_CLICK_ACCEPT = "dev.alenajam.opendialer.CALL_ACCEPT"; + private static final String INTENT_ACTION_CALL_BUTTON_CLICK_DECLINE = "dev.alenajam.opendialer.CALL_DECLINE"; + + + public static void setupNotificationChannels(Context context) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + createCallChannel(context, CHANNEL_ID_INCOMING_CALLS, context.getString(R.string.channel_incoming_calls), NotificationManager.IMPORTANCE_HIGH); + createCallChannel(context, CHANNEL_ID_ONGOING_CALLS, context.getString(R.string.channel_ongoing_calls), NotificationManager.IMPORTANCE_DEFAULT); + createCallChannel(context, CHANNEL_ID_OUTGOING_CALLS, context.getString(R.string.channel_outgoing_calls), NotificationManager.IMPORTANCE_DEFAULT); + } } - } - private static void createCallChannel(Context context, String channelId, String channelName, int channelImportance) { - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - NotificationChannel notificationChannel = new NotificationChannel(channelId, channelName, channelImportance); + private static void createCallChannel(Context context, String channelId, String channelName, int channelImportance) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + NotificationChannel notificationChannel = new NotificationChannel(channelId, channelName, channelImportance); - notificationChannel.setSound(null, null); + notificationChannel.setSound(null, null); - NotificationManager notificationManager = context.getSystemService(NotificationManager.class); - if (notificationManager != null) { - notificationManager.createNotificationChannel(notificationChannel); - } + NotificationManager notificationManager = context.getSystemService(NotificationManager.class); + if (notificationManager != null) { + notificationManager.createNotificationChannel(notificationChannel); + } + } } - } } \ No newline at end of file diff --git a/app/src/main/java/dev/alenajam/opendialer/helper/SharedPreferenceHelper.java b/app/src/main/java/dev/alenajam/opendialer/helper/SharedPreferenceHelper.java index 2009554..226a886 100644 --- a/app/src/main/java/dev/alenajam/opendialer/helper/SharedPreferenceHelper.java +++ b/app/src/main/java/dev/alenajam/opendialer/helper/SharedPreferenceHelper.java @@ -17,28 +17,28 @@ import dev.alenajam.opendialer.util.CommonUtils; public abstract class SharedPreferenceHelper { - public static final String SP_QUICK_RESPONSES = "SP_QUICK_RESPONSES"; - public static final String KEY_SETTING_THEME = "theme"; - - public static SharedPreferences getSharedPreferences(Context context) { - return PreferenceManager.getDefaultSharedPreferences(context); - } - - public static void init(Context context) { - SharedPreferences sharedPreferences = getSharedPreferences(context); - - String theme = sharedPreferences.getString(SharedPreferenceHelper.KEY_SETTING_THEME, null); - try { - CommonUtils.setTheme(theme == null ? AppCompatDelegate.MODE_NIGHT_NO : Integer.parseInt(theme)); - } catch (NumberFormatException e) { - if (e.getLocalizedMessage() != null) - Log.d(App.class.getSimpleName(), e.getLocalizedMessage()); + public static final String SP_QUICK_RESPONSES = "SP_QUICK_RESPONSES"; + public static final String KEY_SETTING_THEME = "theme"; + + public static SharedPreferences getSharedPreferences(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context); } - if (!sharedPreferences.contains(SharedPreferenceHelper.SP_QUICK_RESPONSES)) { - String[] quickResponses = context.getResources().getStringArray(R.array.array_quick_responses); - ArrayList quickResponseList = new ArrayList<>(Arrays.asList(quickResponses)); - sharedPreferences.edit().putString(SharedPreferenceHelper.SP_QUICK_RESPONSES, new Gson().toJson(quickResponseList)).apply(); + public static void init(Context context) { + SharedPreferences sharedPreferences = getSharedPreferences(context); + + String theme = sharedPreferences.getString(SharedPreferenceHelper.KEY_SETTING_THEME, null); + try { + CommonUtils.setTheme(theme == null ? AppCompatDelegate.MODE_NIGHT_NO : Integer.parseInt(theme)); + } catch (NumberFormatException e) { + if (e.getLocalizedMessage() != null) + Log.d(App.class.getSimpleName(), e.getLocalizedMessage()); + } + + if (!sharedPreferences.contains(SharedPreferenceHelper.SP_QUICK_RESPONSES)) { + String[] quickResponses = context.getResources().getStringArray(R.array.array_quick_responses); + ArrayList quickResponseList = new ArrayList<>(Arrays.asList(quickResponses)); + sharedPreferences.edit().putString(SharedPreferenceHelper.SP_QUICK_RESPONSES, new Gson().toJson(quickResponseList)).apply(); + } } - } } diff --git a/app/src/main/java/dev/alenajam/opendialer/ui/AppComposable.kt b/app/src/main/java/dev/alenajam/opendialer/ui/AppComposable.kt new file mode 100644 index 0000000..86a5c28 --- /dev/null +++ b/app/src/main/java/dev/alenajam/opendialer/ui/AppComposable.kt @@ -0,0 +1,93 @@ +package dev.alenajam.opendialer.ui + +import android.content.Intent +import android.telecom.PhoneAccount +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.platform.LocalContext +import androidx.core.util.Consumer +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import dev.alenajam.opendialer.core.common.MAIN_ACTIVITY_INTENT_DIAL_EXTRA_ADD_CALL +import dev.alenajam.opendialer.core.common.getActivity +import dev.alenajam.opendialer.feature.callDetail.CallDetailRoute +import dev.alenajam.opendialer.feature.callDetail.CallDetailScreen +import dev.alenajam.opendialer.feature.contactsSearch.ContactsSearchRoute +import dev.alenajam.opendialer.feature.contactsSearch.ContactsSearchScreen +import dev.alenajam.opendialer.feature.settings.SettingsRoute +import dev.alenajam.opendialer.feature.settings.SettingsScreen + +@Composable +internal fun AppComposable() { + val navController = rememberNavController() + + HandleIntent( + onOpenContactsSearch = { navController.navigate(ContactsSearchRoute(it)) } + ) + + AppTheme { + NavHost(navController = navController, startDestination = HomeRoute) { + composable { + HomeScreen( + onOpenDialpad = { + navController.navigate(ContactsSearchRoute()) + }, + onOpenHistory = { + navController.navigate(CallDetailRoute(callIds = it)) + }, + onOpenSettings = { + navController.navigate(SettingsRoute) + } + ) + } + composable { + ContactsSearchScreen() + } + composable { + CallDetailScreen( + onNavigateBack = { navController.popBackStack() } + ) + } + composable { + SettingsScreen( + onNavigateBack = { navController.popBackStack() } + ) + } + } + } +} + +@Composable +fun HandleIntent( + onOpenContactsSearch: (prefilledNumber: String) -> Unit +) { + val activity = LocalContext.current.getActivity() + + fun handleIntent(intent: Intent?) { + if (intent == null) return + + if ( + intent.action == Intent.ACTION_DIAL + && intent.getBooleanExtra(MAIN_ACTIVITY_INTENT_DIAL_EXTRA_ADD_CALL, false) + ) { + onOpenContactsSearch("") + } else if ( + arrayOf(Intent.ACTION_DIAL, Intent.ACTION_VIEW).contains(intent.action) + && intent.data?.scheme == PhoneAccount.SCHEME_TEL + ) { + onOpenContactsSearch(intent.data!!.schemeSpecificPart) + } + } + + LaunchedEffect(Unit) { + handleIntent(activity?.intent) + } + + DisposableEffect(Unit) { + val listener = Consumer(::handleIntent) + activity?.addOnNewIntentListener(listener) + onDispose { activity?.removeOnNewIntentListener(listener) } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/alenajam/opendialer/ui/Color.kt b/app/src/main/java/dev/alenajam/opendialer/ui/Color.kt new file mode 100644 index 0000000..2233cc2 --- /dev/null +++ b/app/src/main/java/dev/alenajam/opendialer/ui/Color.kt @@ -0,0 +1,225 @@ +package dev.alenajam.opendialer.ui +import androidx.compose.ui.graphics.Color + +val primaryLight = Color(0xFF8E4954) +val onPrimaryLight = Color(0xFFFFFFFF) +val primaryContainerLight = Color(0xFFFFD9DD) +val onPrimaryContainerLight = Color(0xFF3B0714) +val secondaryLight = Color(0xFF76565A) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFFFFD9DD) +val onSecondaryContainerLight = Color(0xFF2C1518) +val tertiaryLight = Color(0xFF785831) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFFFFDDB8) +val onTertiaryContainerLight = Color(0xFF2A1700) +val errorLight = Color(0xFFBA1A1A) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFFFDAD6) +val onErrorContainerLight = Color(0xFF410002) +val backgroundLight = Color(0xFFFFF8F7) +val onBackgroundLight = Color(0xFF22191A) +val surfaceLight = Color(0xFFFFF8F7) +val onSurfaceLight = Color(0xFF22191A) +val surfaceVariantLight = Color(0xFFF4DDDF) +val onSurfaceVariantLight = Color(0xFF524344) +val outlineLight = Color(0xFF847374) +val outlineVariantLight = Color(0xFFD7C1C3) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF382E2F) +val inverseOnSurfaceLight = Color(0xFFFEEDED) +val inversePrimaryLight = Color(0xFFFFB2BB) +val surfaceDimLight = Color(0xFFE7D6D7) +val surfaceBrightLight = Color(0xFFFFF8F7) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFFFF0F1) +val surfaceContainerLight = Color(0xFFFBEAEB) +val surfaceContainerHighLight = Color(0xFFF6E4E5) +val surfaceContainerHighestLight = Color(0xFFF0DEDF) + +val primaryLightMediumContrast = Color(0xFF6D2F3A) +val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) +val primaryContainerLightMediumContrast = Color(0xFFA95F6A) +val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val secondaryLightMediumContrast = Color(0xFF583B3F) +val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) +val secondaryContainerLightMediumContrast = Color(0xFF8E6C70) +val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryLightMediumContrast = Color(0xFF5A3D18) +val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightMediumContrast = Color(0xFF916E44) +val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val errorLightMediumContrast = Color(0xFF8C0009) +val onErrorLightMediumContrast = Color(0xFFFFFFFF) +val errorContainerLightMediumContrast = Color(0xFFDA342E) +val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) +val backgroundLightMediumContrast = Color(0xFFFFF8F7) +val onBackgroundLightMediumContrast = Color(0xFF22191A) +val surfaceLightMediumContrast = Color(0xFFFFF8F7) +val onSurfaceLightMediumContrast = Color(0xFF22191A) +val surfaceVariantLightMediumContrast = Color(0xFFF4DDDF) +val onSurfaceVariantLightMediumContrast = Color(0xFF4E3F41) +val outlineLightMediumContrast = Color(0xFF6B5B5C) +val outlineVariantLightMediumContrast = Color(0xFF887678) +val scrimLightMediumContrast = Color(0xFF000000) +val inverseSurfaceLightMediumContrast = Color(0xFF382E2F) +val inverseOnSurfaceLightMediumContrast = Color(0xFFFEEDED) +val inversePrimaryLightMediumContrast = Color(0xFFFFB2BB) +val surfaceDimLightMediumContrast = Color(0xFFE7D6D7) +val surfaceBrightLightMediumContrast = Color(0xFFFFF8F7) +val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightMediumContrast = Color(0xFFFFF0F1) +val surfaceContainerLightMediumContrast = Color(0xFFFBEAEB) +val surfaceContainerHighLightMediumContrast = Color(0xFFF6E4E5) +val surfaceContainerHighestLightMediumContrast = Color(0xFFF0DEDF) + +val primaryLightHighContrast = Color(0xFF440E1A) +val onPrimaryLightHighContrast = Color(0xFFFFFFFF) +val primaryContainerLightHighContrast = Color(0xFF6D2F3A) +val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) +val secondaryLightHighContrast = Color(0xFF331B1F) +val onSecondaryLightHighContrast = Color(0xFFFFFFFF) +val secondaryContainerLightHighContrast = Color(0xFF583B3F) +val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) +val tertiaryLightHighContrast = Color(0xFF341D00) +val onTertiaryLightHighContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightHighContrast = Color(0xFF5A3D18) +val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) +val errorLightHighContrast = Color(0xFF4E0002) +val onErrorLightHighContrast = Color(0xFFFFFFFF) +val errorContainerLightHighContrast = Color(0xFF8C0009) +val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) +val backgroundLightHighContrast = Color(0xFFFFF8F7) +val onBackgroundLightHighContrast = Color(0xFF22191A) +val surfaceLightHighContrast = Color(0xFFFFF8F7) +val onSurfaceLightHighContrast = Color(0xFF000000) +val surfaceVariantLightHighContrast = Color(0xFFF4DDDF) +val onSurfaceVariantLightHighContrast = Color(0xFF2D2122) +val outlineLightHighContrast = Color(0xFF4E3F41) +val outlineVariantLightHighContrast = Color(0xFF4E3F41) +val scrimLightHighContrast = Color(0xFF000000) +val inverseSurfaceLightHighContrast = Color(0xFF382E2F) +val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) +val inversePrimaryLightHighContrast = Color(0xFFFFE6E8) +val surfaceDimLightHighContrast = Color(0xFFE7D6D7) +val surfaceBrightLightHighContrast = Color(0xFFFFF8F7) +val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightHighContrast = Color(0xFFFFF0F1) +val surfaceContainerLightHighContrast = Color(0xFFFBEAEB) +val surfaceContainerHighLightHighContrast = Color(0xFFF6E4E5) +val surfaceContainerHighestLightHighContrast = Color(0xFFF0DEDF) + +val primaryDark = Color(0xFFFFB2BB) +val onPrimaryDark = Color(0xFF561D28) +val primaryContainerDark = Color(0xFF72333D) +val onPrimaryContainerDark = Color(0xFFFFD9DD) +val secondaryDark = Color(0xFFE5BDC0) +val onSecondaryDark = Color(0xFF43292D) +val secondaryContainerDark = Color(0xFF5C3F43) +val onSecondaryContainerDark = Color(0xFFFFD9DD) +val tertiaryDark = Color(0xFFE9BF8F) +val onTertiaryDark = Color(0xFF452B07) +val tertiaryContainerDark = Color(0xFF5E411C) +val onTertiaryContainerDark = Color(0xFFFFDDB8) +val errorDark = Color(0xFFFFB4AB) +val onErrorDark = Color(0xFF690005) +val errorContainerDark = Color(0xFF93000A) +val onErrorContainerDark = Color(0xFFFFDAD6) +val backgroundDark = Color(0xFF1A1112) +val onBackgroundDark = Color(0xFFF0DEDF) +val surfaceDark = Color(0xFF1A1112) +val onSurfaceDark = Color(0xFFF0DEDF) +val surfaceVariantDark = Color(0xFF524344) +val onSurfaceVariantDark = Color(0xFFD7C1C3) +val outlineDark = Color(0xFF9F8C8E) +val outlineVariantDark = Color(0xFF524344) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFF0DEDF) +val inverseOnSurfaceDark = Color(0xFF382E2F) +val inversePrimaryDark = Color(0xFF8E4954) +val surfaceDimDark = Color(0xFF1A1112) +val surfaceBrightDark = Color(0xFF413737) +val surfaceContainerLowestDark = Color(0xFF140C0D) +val surfaceContainerLowDark = Color(0xFF22191A) +val surfaceContainerDark = Color(0xFF261D1E) +val surfaceContainerHighDark = Color(0xFF312828) +val surfaceContainerHighestDark = Color(0xFF3D3233) + +val primaryDarkMediumContrast = Color(0xFFFFB8C1) +val onPrimaryDarkMediumContrast = Color(0xFF33030F) +val primaryContainerDarkMediumContrast = Color(0xFFC97A85) +val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) +val secondaryDarkMediumContrast = Color(0xFFE9C1C5) +val onSecondaryDarkMediumContrast = Color(0xFF261013) +val secondaryContainerDarkMediumContrast = Color(0xFFAC888B) +val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) +val tertiaryDarkMediumContrast = Color(0xFFEEC393) +val onTertiaryDarkMediumContrast = Color(0xFF231300) +val tertiaryContainerDarkMediumContrast = Color(0xFFAF8A5E) +val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) +val errorDarkMediumContrast = Color(0xFFFFBAB1) +val onErrorDarkMediumContrast = Color(0xFF370001) +val errorContainerDarkMediumContrast = Color(0xFFFF5449) +val onErrorContainerDarkMediumContrast = Color(0xFF000000) +val backgroundDarkMediumContrast = Color(0xFF1A1112) +val onBackgroundDarkMediumContrast = Color(0xFFF0DEDF) +val surfaceDarkMediumContrast = Color(0xFF1A1112) +val onSurfaceDarkMediumContrast = Color(0xFFFFF9F9) +val surfaceVariantDarkMediumContrast = Color(0xFF524344) +val onSurfaceVariantDarkMediumContrast = Color(0xFFDBC6C7) +val outlineDarkMediumContrast = Color(0xFFB29EA0) +val outlineVariantDarkMediumContrast = Color(0xFF917F80) +val scrimDarkMediumContrast = Color(0xFF000000) +val inverseSurfaceDarkMediumContrast = Color(0xFFF0DEDF) +val inverseOnSurfaceDarkMediumContrast = Color(0xFF312828) +val inversePrimaryDarkMediumContrast = Color(0xFF73343F) +val surfaceDimDarkMediumContrast = Color(0xFF1A1112) +val surfaceBrightDarkMediumContrast = Color(0xFF413737) +val surfaceContainerLowestDarkMediumContrast = Color(0xFF140C0D) +val surfaceContainerLowDarkMediumContrast = Color(0xFF22191A) +val surfaceContainerDarkMediumContrast = Color(0xFF261D1E) +val surfaceContainerHighDarkMediumContrast = Color(0xFF312828) +val surfaceContainerHighestDarkMediumContrast = Color(0xFF3D3233) + +val primaryDarkHighContrast = Color(0xFFFFF9F9) +val onPrimaryDarkHighContrast = Color(0xFF000000) +val primaryContainerDarkHighContrast = Color(0xFFFFB8C1) +val onPrimaryContainerDarkHighContrast = Color(0xFF000000) +val secondaryDarkHighContrast = Color(0xFFFFF9F9) +val onSecondaryDarkHighContrast = Color(0xFF000000) +val secondaryContainerDarkHighContrast = Color(0xFFE9C1C5) +val onSecondaryContainerDarkHighContrast = Color(0xFF000000) +val tertiaryDarkHighContrast = Color(0xFFFFFAF7) +val onTertiaryDarkHighContrast = Color(0xFF000000) +val tertiaryContainerDarkHighContrast = Color(0xFFEEC393) +val onTertiaryContainerDarkHighContrast = Color(0xFF000000) +val errorDarkHighContrast = Color(0xFFFFF9F9) +val onErrorDarkHighContrast = Color(0xFF000000) +val errorContainerDarkHighContrast = Color(0xFFFFBAB1) +val onErrorContainerDarkHighContrast = Color(0xFF000000) +val backgroundDarkHighContrast = Color(0xFF1A1112) +val onBackgroundDarkHighContrast = Color(0xFFF0DEDF) +val surfaceDarkHighContrast = Color(0xFF1A1112) +val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkHighContrast = Color(0xFF524344) +val onSurfaceVariantDarkHighContrast = Color(0xFFFFF9F9) +val outlineDarkHighContrast = Color(0xFFDBC6C7) +val outlineVariantDarkHighContrast = Color(0xFFDBC6C7) +val scrimDarkHighContrast = Color(0xFF000000) +val inverseSurfaceDarkHighContrast = Color(0xFFF0DEDF) +val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) +val inversePrimaryDarkHighContrast = Color(0xFF4E1622) +val surfaceDimDarkHighContrast = Color(0xFF1A1112) +val surfaceBrightDarkHighContrast = Color(0xFF413737) +val surfaceContainerLowestDarkHighContrast = Color(0xFF140C0D) +val surfaceContainerLowDarkHighContrast = Color(0xFF22191A) +val surfaceContainerDarkHighContrast = Color(0xFF261D1E) +val surfaceContainerHighDarkHighContrast = Color(0xFF312828) +val surfaceContainerHighestDarkHighContrast = Color(0xFF3D3233) + + + + + + + diff --git a/app/src/main/java/dev/alenajam/opendialer/ui/HomeScreen.kt b/app/src/main/java/dev/alenajam/opendialer/ui/HomeScreen.kt new file mode 100644 index 0000000..40128e5 --- /dev/null +++ b/app/src/main/java/dev/alenajam/opendialer/ui/HomeScreen.kt @@ -0,0 +1,166 @@ +package dev.alenajam.opendialer.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccessTimeFilled +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.People +import androidx.compose.material.icons.outlined.AccessTime +import androidx.compose.material.icons.outlined.Dialpad +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material.icons.outlined.People +import androidx.compose.material.icons.outlined.Search +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SearchBar +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import dev.alenajam.opendialer.R +import dev.alenajam.opendialer.feature.calls.CallsScreen +import dev.alenajam.opendialer.feature.contacts.ContactsScreen + +private enum class Route { + CALLS, + CONTACTS +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun HomeScreen( + onOpenDialpad: () -> Unit, + onOpenHistory: (ids: List) -> Unit, + onOpenSettings: () -> Unit, +) { + var currentRoute by remember { mutableStateOf(Route.CALLS) } + + Scaffold( + topBar = { + SearchBar( + inputField = @Composable { + SearchBarDefaults.InputField( + query = "", + onQueryChange = {}, + onSearch = {}, + expanded = false, + onExpandedChange = {}, + enabled = false, + placeholder = { Text(text = stringResource(id = R.string.coming_soon)) }, + leadingIcon = { + IconButton( + onClick = {} + ) { + Icon( + imageVector = Icons.Outlined.Search, + contentDescription = null + ) + } + }, + trailingIcon = { + IconButton( + onClick = {} + ) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = null + ) + + Box( + modifier = Modifier + .fillMaxSize() + .wrapContentSize(Alignment.TopStart) + ) { + var expanded by remember { mutableStateOf(false) } + + IconButton(onClick = { expanded = true }) { + Icon( + Icons.Default.MoreVert, + contentDescription = null + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }) { + DropdownMenuItem( + text = { Text(text = stringResource(R.string.home_menu_settings_label)) }, + onClick = onOpenSettings, + ) + } + } + } + } + ) + }, + expanded = false, + onExpandedChange = {}, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 8.dp) + ) {} + }, + bottomBar = { + NavigationBar { + val isSelected = { item: Route -> item == currentRoute } + + NavigationBarItem( + selected = isSelected(Route.CALLS), + icon = { + Icon( + imageVector = if (isSelected(Route.CALLS)) Icons.Filled.AccessTimeFilled else Icons.Outlined.AccessTime, + contentDescription = null + ) + }, + label = { Text(text = stringResource(R.string.recents)) }, + onClick = { currentRoute = Route.CALLS }, + ) + + NavigationBarItem( + selected = isSelected(Route.CONTACTS), + icon = { + Icon( + imageVector = if (isSelected(Route.CONTACTS)) Icons.Filled.People else Icons.Outlined.People, + contentDescription = null + ) + }, + label = { Text(text = stringResource(R.string.contacts)) }, + onClick = { currentRoute = Route.CONTACTS }, + ) + } + }, + floatingActionButton = { + FloatingActionButton(onClick = onOpenDialpad) { + Icon(imageVector = Icons.Outlined.Dialpad, contentDescription = null) + } + } + ) { innerPadding -> + Surface(modifier = Modifier.padding(innerPadding)) { + when (currentRoute) { + Route.CALLS -> CallsScreen( + onOpenHistory = onOpenHistory + ) + + Route.CONTACTS -> ContactsScreen() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/alenajam/opendialer/ui/MainActivity.kt b/app/src/main/java/dev/alenajam/opendialer/ui/MainActivity.kt new file mode 100644 index 0000000..9fd487e --- /dev/null +++ b/app/src/main/java/dev/alenajam/opendialer/ui/MainActivity.kt @@ -0,0 +1,18 @@ +package dev.alenajam.opendialer.ui + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + AppComposable() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/alenajam/opendialer/ui/Routes.kt b/app/src/main/java/dev/alenajam/opendialer/ui/Routes.kt new file mode 100644 index 0000000..a1e70ac --- /dev/null +++ b/app/src/main/java/dev/alenajam/opendialer/ui/Routes.kt @@ -0,0 +1,6 @@ +package dev.alenajam.opendialer.ui + +import kotlinx.serialization.Serializable + +@Serializable +data object HomeRoute diff --git a/app/src/main/java/dev/alenajam/opendialer/ui/Theme.kt b/app/src/main/java/dev/alenajam/opendialer/ui/Theme.kt new file mode 100644 index 0000000..d0a7ef8 --- /dev/null +++ b/app/src/main/java/dev/alenajam/opendialer/ui/Theme.kt @@ -0,0 +1,276 @@ +package dev.alenajam.opendialer.ui +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext + +private val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, +) + +private val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, +) + +private val mediumContrastLightColorScheme = lightColorScheme( + primary = primaryLightMediumContrast, + onPrimary = onPrimaryLightMediumContrast, + primaryContainer = primaryContainerLightMediumContrast, + onPrimaryContainer = onPrimaryContainerLightMediumContrast, + secondary = secondaryLightMediumContrast, + onSecondary = onSecondaryLightMediumContrast, + secondaryContainer = secondaryContainerLightMediumContrast, + onSecondaryContainer = onSecondaryContainerLightMediumContrast, + tertiary = tertiaryLightMediumContrast, + onTertiary = onTertiaryLightMediumContrast, + tertiaryContainer = tertiaryContainerLightMediumContrast, + onTertiaryContainer = onTertiaryContainerLightMediumContrast, + error = errorLightMediumContrast, + onError = onErrorLightMediumContrast, + errorContainer = errorContainerLightMediumContrast, + onErrorContainer = onErrorContainerLightMediumContrast, + background = backgroundLightMediumContrast, + onBackground = onBackgroundLightMediumContrast, + surface = surfaceLightMediumContrast, + onSurface = onSurfaceLightMediumContrast, + surfaceVariant = surfaceVariantLightMediumContrast, + onSurfaceVariant = onSurfaceVariantLightMediumContrast, + outline = outlineLightMediumContrast, + outlineVariant = outlineVariantLightMediumContrast, + scrim = scrimLightMediumContrast, + inverseSurface = inverseSurfaceLightMediumContrast, + inverseOnSurface = inverseOnSurfaceLightMediumContrast, + inversePrimary = inversePrimaryLightMediumContrast, + surfaceDim = surfaceDimLightMediumContrast, + surfaceBright = surfaceBrightLightMediumContrast, + surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, + surfaceContainerLow = surfaceContainerLowLightMediumContrast, + surfaceContainer = surfaceContainerLightMediumContrast, + surfaceContainerHigh = surfaceContainerHighLightMediumContrast, + surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, +) + +private val highContrastLightColorScheme = lightColorScheme( + primary = primaryLightHighContrast, + onPrimary = onPrimaryLightHighContrast, + primaryContainer = primaryContainerLightHighContrast, + onPrimaryContainer = onPrimaryContainerLightHighContrast, + secondary = secondaryLightHighContrast, + onSecondary = onSecondaryLightHighContrast, + secondaryContainer = secondaryContainerLightHighContrast, + onSecondaryContainer = onSecondaryContainerLightHighContrast, + tertiary = tertiaryLightHighContrast, + onTertiary = onTertiaryLightHighContrast, + tertiaryContainer = tertiaryContainerLightHighContrast, + onTertiaryContainer = onTertiaryContainerLightHighContrast, + error = errorLightHighContrast, + onError = onErrorLightHighContrast, + errorContainer = errorContainerLightHighContrast, + onErrorContainer = onErrorContainerLightHighContrast, + background = backgroundLightHighContrast, + onBackground = onBackgroundLightHighContrast, + surface = surfaceLightHighContrast, + onSurface = onSurfaceLightHighContrast, + surfaceVariant = surfaceVariantLightHighContrast, + onSurfaceVariant = onSurfaceVariantLightHighContrast, + outline = outlineLightHighContrast, + outlineVariant = outlineVariantLightHighContrast, + scrim = scrimLightHighContrast, + inverseSurface = inverseSurfaceLightHighContrast, + inverseOnSurface = inverseOnSurfaceLightHighContrast, + inversePrimary = inversePrimaryLightHighContrast, + surfaceDim = surfaceDimLightHighContrast, + surfaceBright = surfaceBrightLightHighContrast, + surfaceContainerLowest = surfaceContainerLowestLightHighContrast, + surfaceContainerLow = surfaceContainerLowLightHighContrast, + surfaceContainer = surfaceContainerLightHighContrast, + surfaceContainerHigh = surfaceContainerHighLightHighContrast, + surfaceContainerHighest = surfaceContainerHighestLightHighContrast, +) + +private val mediumContrastDarkColorScheme = darkColorScheme( + primary = primaryDarkMediumContrast, + onPrimary = onPrimaryDarkMediumContrast, + primaryContainer = primaryContainerDarkMediumContrast, + onPrimaryContainer = onPrimaryContainerDarkMediumContrast, + secondary = secondaryDarkMediumContrast, + onSecondary = onSecondaryDarkMediumContrast, + secondaryContainer = secondaryContainerDarkMediumContrast, + onSecondaryContainer = onSecondaryContainerDarkMediumContrast, + tertiary = tertiaryDarkMediumContrast, + onTertiary = onTertiaryDarkMediumContrast, + tertiaryContainer = tertiaryContainerDarkMediumContrast, + onTertiaryContainer = onTertiaryContainerDarkMediumContrast, + error = errorDarkMediumContrast, + onError = onErrorDarkMediumContrast, + errorContainer = errorContainerDarkMediumContrast, + onErrorContainer = onErrorContainerDarkMediumContrast, + background = backgroundDarkMediumContrast, + onBackground = onBackgroundDarkMediumContrast, + surface = surfaceDarkMediumContrast, + onSurface = onSurfaceDarkMediumContrast, + surfaceVariant = surfaceVariantDarkMediumContrast, + onSurfaceVariant = onSurfaceVariantDarkMediumContrast, + outline = outlineDarkMediumContrast, + outlineVariant = outlineVariantDarkMediumContrast, + scrim = scrimDarkMediumContrast, + inverseSurface = inverseSurfaceDarkMediumContrast, + inverseOnSurface = inverseOnSurfaceDarkMediumContrast, + inversePrimary = inversePrimaryDarkMediumContrast, + surfaceDim = surfaceDimDarkMediumContrast, + surfaceBright = surfaceBrightDarkMediumContrast, + surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, + surfaceContainerLow = surfaceContainerLowDarkMediumContrast, + surfaceContainer = surfaceContainerDarkMediumContrast, + surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, + surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, +) + +private val highContrastDarkColorScheme = darkColorScheme( + primary = primaryDarkHighContrast, + onPrimary = onPrimaryDarkHighContrast, + primaryContainer = primaryContainerDarkHighContrast, + onPrimaryContainer = onPrimaryContainerDarkHighContrast, + secondary = secondaryDarkHighContrast, + onSecondary = onSecondaryDarkHighContrast, + secondaryContainer = secondaryContainerDarkHighContrast, + onSecondaryContainer = onSecondaryContainerDarkHighContrast, + tertiary = tertiaryDarkHighContrast, + onTertiary = onTertiaryDarkHighContrast, + tertiaryContainer = tertiaryContainerDarkHighContrast, + onTertiaryContainer = onTertiaryContainerDarkHighContrast, + error = errorDarkHighContrast, + onError = onErrorDarkHighContrast, + errorContainer = errorContainerDarkHighContrast, + onErrorContainer = onErrorContainerDarkHighContrast, + background = backgroundDarkHighContrast, + onBackground = onBackgroundDarkHighContrast, + surface = surfaceDarkHighContrast, + onSurface = onSurfaceDarkHighContrast, + surfaceVariant = surfaceVariantDarkHighContrast, + onSurfaceVariant = onSurfaceVariantDarkHighContrast, + outline = outlineDarkHighContrast, + outlineVariant = outlineVariantDarkHighContrast, + scrim = scrimDarkHighContrast, + inverseSurface = inverseSurfaceDarkHighContrast, + inverseOnSurface = inverseOnSurfaceDarkHighContrast, + inversePrimary = inversePrimaryDarkHighContrast, + surfaceDim = surfaceDimDarkHighContrast, + surfaceBright = surfaceBrightDarkHighContrast, + surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, + surfaceContainerLow = surfaceContainerLowDarkHighContrast, + surfaceContainer = surfaceContainerDarkHighContrast, + surfaceContainerHigh = surfaceContainerHighDarkHighContrast, + surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, +) + +@Immutable +data class ColorFamily( + val color: Color, + val onColor: Color, + val colorContainer: Color, + val onColorContainer: Color +) + +val unspecified_scheme = ColorFamily( + Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified +) + +@Composable +fun AppTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable() () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> darkScheme + else -> lightScheme + } + + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} + diff --git a/app/src/main/java/dev/alenajam/opendialer/util/CommonUtils.java b/app/src/main/java/dev/alenajam/opendialer/util/CommonUtils.java index fbc1fe0..ee878f3 100644 --- a/app/src/main/java/dev/alenajam/opendialer/util/CommonUtils.java +++ b/app/src/main/java/dev/alenajam/opendialer/util/CommonUtils.java @@ -3,7 +3,7 @@ import androidx.appcompat.app.AppCompatDelegate; public abstract class CommonUtils { - public static void setTheme(int mode) { - AppCompatDelegate.setDefaultNightMode(mode); - } + public static void setTheme(int mode) { + AppCompatDelegate.setDefaultNightMode(mode); + } } \ No newline at end of file diff --git a/app/src/main/java/dev/alenajam/opendialer/util/CommonUtils.kt b/app/src/main/java/dev/alenajam/opendialer/util/CommonUtils.kt index bbc5a7a..7d2f5ec 100644 --- a/app/src/main/java/dev/alenajam/opendialer/util/CommonUtils.kt +++ b/app/src/main/java/dev/alenajam/opendialer/util/CommonUtils.kt @@ -10,54 +10,54 @@ import android.view.inputmethod.InputMethodManager import androidx.annotation.ColorInt fun showInputMethod(view: View) { - val manager: InputMethodManager = - view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - manager?.let { manager.showSoftInput(view, 0) } + val manager: InputMethodManager = + view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + manager?.let { manager.showSoftInput(view, 0) } } fun hideInputMethod(view: View) { - val manager: InputMethodManager = - view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - manager?.let { manager.hideSoftInputFromWindow(view.windowToken, 0) } + val manager: InputMethodManager = + view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + manager?.let { manager.hideSoftInputFromWindow(view.windowToken, 0) } } private fun setStatusBarLightMode(window: Window, light: Boolean) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - if (light) { - window.insetsController?.setSystemBarsAppearance( - WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, - WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS - ) - } else { - window.insetsController?.setSystemBarsAppearance( - 0, - WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS - ) - } - } else { - @Suppress("DEPRECATION") - if (light) { - window.decorView.systemUiVisibility = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + if (light) { + window.insetsController?.setSystemBarsAppearance( + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + ) } else { - View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + window.insetsController?.setSystemBarsAppearance( + 0, + WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS + ) } } else { - window.decorView.systemUiVisibility = 0 + @Suppress("DEPRECATION") + if (light) { + window.decorView.systemUiVisibility = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + } else { + View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + } + } else { + window.decorView.systemUiVisibility = 0 + } } - } } @ColorInt fun getContrastColor(@ColorInt color: Int): Int { - // Counting the perceptive luminance - human eye favors green color... - val a = - 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255 - return if (a < 0.5) Color.BLACK else Color.WHITE + // Counting the perceptive luminance - human eye favors green color... + val a = + 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255 + return if (a < 0.5) Color.BLACK else Color.WHITE } fun updateStatusBarLightMode(window: Window, @ColorInt bgColor: Int) { - val contrast = getContrastColor(bgColor) - setStatusBarLightMode(window, contrast == Color.BLACK) + val contrast = getContrastColor(bgColor) + setStatusBarLightMode(window, contrast == Color.BLACK) } \ No newline at end of file diff --git a/app/src/main/java/dev/alenajam/opendialer/view/BottomNavigationBadged.kt b/app/src/main/java/dev/alenajam/opendialer/view/BottomNavigationBadged.kt deleted file mode 100644 index 4e65ba3..0000000 --- a/app/src/main/java/dev/alenajam/opendialer/view/BottomNavigationBadged.kt +++ /dev/null @@ -1,39 +0,0 @@ -package dev.alenajam.opendialer.view - -import android.content.Context -import android.util.AttributeSet -import android.view.View -import dev.alenajam.opendialer.R -import com.google.android.material.bottomnavigation.BottomNavigationItemView -import com.google.android.material.bottomnavigation.BottomNavigationMenuView -import com.google.android.material.bottomnavigation.BottomNavigationView - -class BottomNavigationBadged(context: Context, attributeSet: AttributeSet) : - BottomNavigationView(context, attributeSet) { - - private val badges = mutableMapOf() - - /*fun showItemBadge(position: Int, show: Boolean) { - val item = getItem(position) - - if (show) { - if (badges.contains(position)) return - - val badgeView = inflate(context, R.layout.bottom_nav_badge, null) - item.addView(badgeView) - - badges[position] = badgeView - } else { - if (badges.contains(position)) { - val badgeView = badges[position] - item.removeView(badgeView) - badges.remove(position) - } - } - }*/ - - /*private fun getItem(position: Int): BottomNavigationItemView { - val menuView = getChildAt(0) as BottomNavigationMenuView - return menuView.getChildAt(position) as BottomNavigationItemView - }*/ -} \ No newline at end of file diff --git a/app/src/main/java/dev/alenajam/opendialer/view/NonSwipeableViewPager.java b/app/src/main/java/dev/alenajam/opendialer/view/NonSwipeableViewPager.java deleted file mode 100644 index cc60a64..0000000 --- a/app/src/main/java/dev/alenajam/opendialer/view/NonSwipeableViewPager.java +++ /dev/null @@ -1,60 +0,0 @@ -package dev.alenajam.opendialer.view; - -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.animation.DecelerateInterpolator; -import android.widget.Scroller; - -import androidx.viewpager.widget.ViewPager; - -import java.lang.reflect.Field; - -public class NonSwipeableViewPager extends ViewPager { - - public NonSwipeableViewPager(Context context) { - super(context); - setMyScroller(); - } - - public NonSwipeableViewPager(Context context, AttributeSet attrs) { - super(context, attrs); - setMyScroller(); - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent event) { - // Never allow swiping to switch between pages - return false; - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - // Never allow swiping to switch between pages - return false; - } - - //down one is added for smooth scrolling - - private void setMyScroller() { - try { - Class viewpager = ViewPager.class; - Field scroller = viewpager.getDeclaredField("mScroller"); - scroller.setAccessible(true); - scroller.set(this, new MyScroller(getContext())); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public class MyScroller extends Scroller { - public MyScroller(Context context) { - super(context, new DecelerateInterpolator()); - } - - @Override - public void startScroll(int startX, int startY, int dx, int dy, int duration) { - super.startScroll(startX, startY, dx, dy, 350 /*1 secs*/); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/dev/alenajam/opendialer/view/SearchView.java b/app/src/main/java/dev/alenajam/opendialer/view/SearchView.java deleted file mode 100644 index b6fe79f..0000000 --- a/app/src/main/java/dev/alenajam/opendialer/view/SearchView.java +++ /dev/null @@ -1,175 +0,0 @@ -package dev.alenajam.opendialer.view; - -import android.content.Context; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.RelativeLayout; - -import dev.alenajam.opendialer.R; -import dev.alenajam.opendialer.util.CommonUtilsKt; - -public class SearchView extends LinearLayout { - private Context context; - private EditTextFocusChangeListener focusListener; - private EditTextChangeListener textListener; - private SearchViewOpenListener openListener; - private ImageView icon; - private boolean hasFocus = false; - private boolean isOpen = false; - private EditText editText; - - public SearchView(Context context) { - super(context); - init(context); - } - - public SearchView(Context context, AttributeSet attrs) { - super(context, attrs); - init(context); - } - - private void init(Context context) { - this.context = context; - LayoutInflater mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - assert mInflater != null; - View rootView = mInflater.inflate(R.layout.search_view, this, true); - - editText = findViewById(R.id.searchEditText); - icon = findViewById(R.id.searchIcon); - - editText.setOnFocusChangeListener((v, hasFocus) -> { - if (focusListener != null) focusListener.onFocusChange(editText, hasFocus); - setOpen(hasFocus); - }); - - editText.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - if (textListener != null) { - textListener.onTextChange(editText, s.toString()); - } - } - - @Override - public void afterTextChanged(Editable s) { - - } - }); - - icon.setOnClickListener(v -> { - setOpen(!this.isOpen); - }); - - RelativeLayout layout = findViewById(R.id.layout); - layout.setOnClickListener(v -> { - setOpen(true); - }); - } - - public boolean isFocused() { - return hasFocus; - } - - public void setFocus(boolean focus) { - if (focus) { - editText.requestFocus(); - } else { - editText.clearFocus(); - } - } - - public void onOpenChange(boolean isOpen) { - if (openListener != null) { - openListener.onOpen(isOpen); - } - - if (isOpen) { - icon.setImageDrawable(context.getDrawable(R.drawable.ic_arrow_back)); - setFocus(true); - } else { - icon.setImageDrawable(context.getDrawable(R.drawable.icon_12)); - setFocus(false); - clear(); - } - } - - private void updateKeyboard() { - if (isOpen) { - CommonUtilsKt.showInputMethod(editText); - } else { - closeKeyboard(); - } - } - - public void closeKeyboard() { - CommonUtilsKt.hideInputMethod(editText); - } - - public void open() { - setOpen(true); - } - - public void close() { - setOpen(false); - } - - public boolean isOpen() { - return isOpen; - } - - private void setOpen(boolean isOpen) { - if (this.isOpen != isOpen) { - this.isOpen = isOpen; - onOpenChange(this.isOpen); - } - - updateKeyboard(); - } - - public void clear() { - editText.setText(null); - } - - public String getText() { - return editText.getText().toString(); - } - - public void setText(String text) { - editText.setText(text); - } - - public void setFocusChangeListener(EditTextFocusChangeListener listener) { - this.focusListener = listener; - } - - public void setOpenListener(SearchViewOpenListener openListener) { - this.openListener = openListener; - } - - public void setTextListener(EditTextChangeListener textListener) { - this.textListener = textListener; - } - - public interface EditTextFocusChangeListener { - void onFocusChange(EditText editText, boolean hasFocus); - } - - public interface EditTextChangeListener { - void onTextChange(EditText editText, String text); - } - - public interface SearchViewOpenListener { - void onOpen(boolean isOpen); - } -} diff --git a/app/src/main/res/color/bottom_nav_text_color.xml b/app/src/main/res/color/bottom_nav_text_color.xml deleted file mode 100644 index 771f66d..0000000 --- a/app/src/main/res/color/bottom_nav_text_color.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_back.png b/app/src/main/res/drawable-hdpi/ic_arrow_back.png deleted file mode 100644 index 0f7b2c7..0000000 Binary files a/app/src/main/res/drawable-hdpi/ic_arrow_back.png and /dev/null differ diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_back.png b/app/src/main/res/drawable-mdpi/ic_arrow_back.png deleted file mode 100644 index 1e4b7c3..0000000 Binary files a/app/src/main/res/drawable-mdpi/ic_arrow_back.png and /dev/null differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_back.png b/app/src/main/res/drawable-xhdpi/ic_arrow_back.png deleted file mode 100644 index bdfd67e..0000000 Binary files a/app/src/main/res/drawable-xhdpi/ic_arrow_back.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_back.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_back.png deleted file mode 100644 index a1f4d26..0000000 Binary files a/app/src/main/res/drawable-xxhdpi/ic_arrow_back.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_arrow_back.png b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back.png deleted file mode 100644 index aa86273..0000000 Binary files a/app/src/main/res/drawable-xxxhdpi/ic_arrow_back.png and /dev/null differ diff --git a/app/src/main/res/drawable/bottom_nav_badge.xml b/app/src/main/res/drawable/bottom_nav_badge.xml deleted file mode 100644 index efd46c9..0000000 --- a/app/src/main/res/drawable/bottom_nav_badge.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml index ca3826a..5cc10bf 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -1,74 +1,170 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index da1dcd9..6107c77 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -4,8 +4,7 @@ android:height="108dp" android:viewportWidth="108" android:viewportHeight="108"> - + + android:strokeColor="#00000000" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/icon_00.png b/app/src/main/res/drawable/icon_00.png deleted file mode 100644 index 513a881..0000000 Binary files a/app/src/main/res/drawable/icon_00.png and /dev/null differ diff --git a/app/src/main/res/drawable/icon_01.png b/app/src/main/res/drawable/icon_01.png deleted file mode 100644 index fda3b5e..0000000 Binary files a/app/src/main/res/drawable/icon_01.png and /dev/null differ diff --git a/app/src/main/res/drawable/icon_03.png b/app/src/main/res/drawable/icon_03.png deleted file mode 100644 index f9ad290..0000000 Binary files a/app/src/main/res/drawable/icon_03.png and /dev/null differ diff --git a/app/src/main/res/drawable/icon_12.png b/app/src/main/res/drawable/icon_12.png deleted file mode 100644 index 16aba31..0000000 Binary files a/app/src/main/res/drawable/icon_12.png and /dev/null differ diff --git a/app/src/main/res/drawable/icon_15_light.png b/app/src/main/res/drawable/icon_15_light.png deleted file mode 100644 index 0be4bb2..0000000 Binary files a/app/src/main/res/drawable/icon_15_light.png and /dev/null differ diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml deleted file mode 100644 index 4f44926..0000000 --- a/app/src/main/res/layout/activity_main.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/bottom_nav_badge.xml b/app/src/main/res/layout/bottom_nav_badge.xml deleted file mode 100644 index cf4f766..0000000 --- a/app/src/main/res/layout/bottom_nav_badge.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml deleted file mode 100644 index aa2374b..0000000 --- a/app/src/main/res/layout/fragment_home.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/progress_bar.xml b/app/src/main/res/layout/progress_bar.xml deleted file mode 100644 index ba1c36f..0000000 --- a/app/src/main/res/layout/progress_bar.xml +++ /dev/null @@ -1,14 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/layout/search_view.xml b/app/src/main/res/layout/search_view.xml deleted file mode 100644 index 18e434f..0000000 --- a/app/src/main/res/layout/search_view.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/main_navigation.xml b/app/src/main/res/menu/main_navigation.xml deleted file mode 100644 index 0cb918d..0000000 --- a/app/src/main/res/menu/main_navigation.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index bbd3e02..eca70cf 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/navigation/main.xml b/app/src/main/res/navigation/main.xml deleted file mode 100644 index 3b86ba2..0000000 --- a/app/src/main/res/navigation/main.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml deleted file mode 100644 index dd245db..0000000 --- a/app/src/main/res/values-night/colors.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - #303030 - @color/white - #28AFAFAF - \ No newline at end of file diff --git a/app/src/main/res/values-night/styles.xml b/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 0beef36..0000000 --- a/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..e6d69a0 --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,4 @@ + + + - - - - - - - - diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..047977a --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + + - - - \ No newline at end of file diff --git a/core/common/src/test/java/dev/alenajam/opendialer/core/common/ExampleUnitTest.kt b/core/common/src/test/java/dev/alenajam/opendialer/core/common/ExampleUnitTest.kt index 504e13c..5723474 100644 --- a/core/common/src/test/java/dev/alenajam/opendialer/core/common/ExampleUnitTest.kt +++ b/core/common/src/test/java/dev/alenajam/opendialer/core/common/ExampleUnitTest.kt @@ -1,8 +1,7 @@ package dev.alenajam.opendialer.core.common -import org.junit.Test - import org.junit.Assert.* +import org.junit.Test /** * Example local unit test, which will execute on the development machine (host). @@ -10,8 +9,8 @@ import org.junit.Assert.* * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } } \ No newline at end of file diff --git a/data/calls/build.gradle.kts b/data/calls/build.gradle.kts index 9f5a60f..d2c8845 100644 --- a/data/calls/build.gradle.kts +++ b/data/calls/build.gradle.kts @@ -1,45 +1,48 @@ plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") - id("kotlin-kapt") - id("com.google.dagger.hilt.android") + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("kotlin-kapt") + id("com.google.dagger.hilt.android") } android { - namespace = "dev.alenajam.opendialer.data.calls" - compileSdk = 34 + namespace = "dev.alenajam.opendialer.data.calls" + compileSdk = 34 - defaultConfig { - minSdk = 24 + defaultConfig { + minSdk = 24 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } } - } } dependencies { - implementation(project(":core:common")) - - implementation("androidx.core:core-ktx:1.12.0") - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - - implementation("com.google.dagger:hilt-android:2.48.1") - kapt("com.google.dagger:hilt-compiler:2.48.1") - androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1") - testImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptTest("com.google.dagger:hilt-compiler:2.48.1") + implementation(project(":core:common")) + + implementation("androidx.core:core-ktx:1.12.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + implementation("com.google.dagger:hilt-android:2.48.1") + kapt("com.google.dagger:hilt-compiler:2.48.1") + androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1") + testImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptTest("com.google.dagger:hilt-compiler:2.48.1") } kotlin { - jvmToolchain(17) + jvmToolchain(17) } \ No newline at end of file diff --git a/data/calls/src/androidTest/java/dev/alenajam/opendialer/data/calls/ExampleInstrumentedTest.kt b/data/calls/src/androidTest/java/dev/alenajam/opendialer/data/calls/ExampleInstrumentedTest.kt index 2827fe0..7c7e66d 100644 --- a/data/calls/src/androidTest/java/dev/alenajam/opendialer/data/calls/ExampleInstrumentedTest.kt +++ b/data/calls/src/androidTest/java/dev/alenajam/opendialer/data/calls/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package dev.alenajam.opendialer.data.calls -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -15,10 +13,10 @@ import org.junit.Assert.* */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("dev.alenajam.opendialer.data.calls.test", appContext.packageName) - } + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("dev.alenajam.opendialer.data.calls.test", appContext.packageName) + } } \ No newline at end of file diff --git a/data/calls/src/main/AndroidManifest.xml b/data/calls/src/main/AndroidManifest.xml index a5918e6..44008a4 100644 --- a/data/calls/src/main/AndroidManifest.xml +++ b/data/calls/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallDetailData.kt b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallDetailData.kt index 115aff3..41c1674 100644 --- a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallDetailData.kt +++ b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallDetailData.kt @@ -7,39 +7,39 @@ import android.provider.CallLog.Calls import android.util.Log abstract class CallDetailData { - companion object { - private val TAG = CallDetailData::class.simpleName - val URI: Uri = Calls.CONTENT_URI - private const val LIMIT = 1000 + companion object { + private val TAG = CallDetailData::class.simpleName + val URI: Uri = Calls.CONTENT_URI + private const val LIMIT = 1000 - private val projection = arrayOf( - Calls._ID, - Calls.NUMBER, - Calls.CACHED_NORMALIZED_NUMBER, - Calls.CACHED_NAME, - Calls.DATE, - Calls.DURATION, - Calls.TYPE, - Calls.NEW, - Calls.CACHED_PHOTO_URI, - Calls.COUNTRY_ISO, - Calls.CACHED_NUMBER_LABEL, - Calls.CACHED_PHOTO_ID, - Calls.GEOCODED_LOCATION, - Calls.CACHED_FORMATTED_NUMBER, - Calls.CACHED_NORMALIZED_NUMBER, - Calls.CACHED_LOOKUP_URI, - Calls.POST_DIAL_DIGITS, - Calls.CACHED_MATCHED_NUMBER, - Calls.CACHED_NUMBER_TYPE - ) + private val projection = arrayOf( + Calls._ID, + Calls.NUMBER, + Calls.CACHED_NORMALIZED_NUMBER, + Calls.CACHED_NAME, + Calls.DATE, + Calls.DURATION, + Calls.TYPE, + Calls.NEW, + Calls.CACHED_PHOTO_URI, + Calls.COUNTRY_ISO, + Calls.CACHED_NUMBER_LABEL, + Calls.CACHED_PHOTO_ID, + Calls.GEOCODED_LOCATION, + Calls.CACHED_FORMATTED_NUMBER, + Calls.CACHED_NORMALIZED_NUMBER, + Calls.CACHED_LOOKUP_URI, + Calls.POST_DIAL_DIGITS, + Calls.CACHED_MATCHED_NUMBER, + Calls.CACHED_NUMBER_TYPE + ) - /** Filter out: - * - Blocked calls - * - Non-video Duo calls - */ - private val where = { ids: List -> - """ + /** Filter out: + * - Blocked calls + * - Non-video Duo calls + */ + private val where = { ids: List -> + """ ${Calls.TYPE} != ${Calls.BLOCKED_TYPE} AND ( ${Calls.PHONE_ACCOUNT_COMPONENT_NAME} IS NULL @@ -48,54 +48,55 @@ abstract class CallDetailData { ) AND ${Calls._ID} in (${ids.joinToString(",")}) """ - } + } - fun getCursor(contentResolver: ContentResolver, ids: List): Cursor? = contentResolver.query( - URI - .buildUpon() - .appendQueryParameter(Calls.LIMIT_PARAM_KEY, LIMIT.toString()) - .build(), - projection, - where(ids), - null, - Calls.DEFAULT_SORT_ORDER - ) + fun getCursor(contentResolver: ContentResolver, ids: List): Cursor? = + contentResolver.query( + URI + .buildUpon() + .appendQueryParameter(Calls.LIMIT_PARAM_KEY, LIMIT.toString()) + .build(), + projection, + where(ids), + null, + Calls.DEFAULT_SORT_ORDER + ) - fun getData(cursor: Cursor): List { - val start = System.currentTimeMillis() - val list = mutableListOf() + fun getData(cursor: Cursor): List { + val start = System.currentTimeMillis() + val list = mutableListOf() - if (cursor.moveToFirst()) { - do { - list.add( - DialerCallEntity( - id = cursor.getInt(cursor.getColumnIndexOrThrow(Calls._ID)), - number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER)), - name = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_NAME)), - date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE)), - duration = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DURATION)), - type = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.TYPE)), - isNew = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NEW)), - photoUri = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_PHOTO_URI)) - ?.takeIf { it.isNotBlank() }, - countryIso = cursor.getString(cursor.getColumnIndexOrThrow(Calls.COUNTRY_ISO)), - label = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_LABEL)), - photoId = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.CACHED_PHOTO_ID)), - geoDescription = cursor.getString(cursor.getColumnIndexOrThrow(Calls.GEOCODED_LOCATION)), - formattedNumber = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_FORMATTED_NUMBER)), - normalizedNumber = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_NORMALIZED_NUMBER)), - lookupUri = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_LOOKUP_URI)), - postDialDigits = cursor.getString(cursor.getColumnIndexOrThrow(Calls.POST_DIAL_DIGITS)), - matchedNumber = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_MATCHED_NUMBER)), - numberType = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_TYPE)) - ) - ) - } while (cursor.moveToNext()) - } + if (cursor.moveToFirst()) { + do { + list.add( + DialerCallEntity( + id = cursor.getInt(cursor.getColumnIndexOrThrow(Calls._ID)), + number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER)), + name = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_NAME)), + date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE)), + duration = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DURATION)), + type = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.TYPE)), + isNew = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NEW)), + photoUri = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_PHOTO_URI)) + ?.takeIf { it.isNotBlank() }, + countryIso = cursor.getString(cursor.getColumnIndexOrThrow(Calls.COUNTRY_ISO)), + label = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_LABEL)), + photoId = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.CACHED_PHOTO_ID)), + geoDescription = cursor.getString(cursor.getColumnIndexOrThrow(Calls.GEOCODED_LOCATION)), + formattedNumber = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_FORMATTED_NUMBER)), + normalizedNumber = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_NORMALIZED_NUMBER)), + lookupUri = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_LOOKUP_URI)), + postDialDigits = cursor.getString(cursor.getColumnIndexOrThrow(Calls.POST_DIAL_DIGITS)), + matchedNumber = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_MATCHED_NUMBER)), + numberType = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_TYPE)) + ) + ) + } while (cursor.moveToNext()) + } - val time = (System.currentTimeMillis() - start) / 1000f - Log.d(TAG, "Call log query time: $time seconds") - return list + val time = (System.currentTimeMillis() - start) / 1000f + Log.d(TAG, "Call log query time: $time seconds") + return list + } } - } } \ No newline at end of file diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallOption.java b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallOption.java index d16cce1..a031d79 100644 --- a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallOption.java +++ b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallOption.java @@ -5,45 +5,45 @@ import java.io.Serializable; public class CallOption implements Serializable { - public static final int ID_COPY_NUMBER = 0; - public static final int ID_EDIT_BEFORE_CALL = 1; - public static final int ID_DELETE = 2; - public static final int ID_SEND_MESSAGE = 3; - public static final int ID_ADD_EXISTING = 4; - public static final int ID_CREATE_CONTACT = 5; - public static final int ID_CALL_DETAILS = 6; - public static final int ID_BLOCK_CALLER = 7; - public static final int ID_UNBLOCK_CALLER = 8; - - @DrawableRes - private int id; - private int icon; - private String text; - - public CallOption(int id, int icon, String text) { - this.id = id; - this.icon = icon; - this.text = text; - } - - public CallOption(int id, int icon) { - this.id = id; - this.icon = icon; - } - - public int getIcon() { - return icon; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - - public int getId() { - return id; - } + public static final int ID_COPY_NUMBER = 0; + public static final int ID_EDIT_BEFORE_CALL = 1; + public static final int ID_DELETE = 2; + public static final int ID_SEND_MESSAGE = 3; + public static final int ID_ADD_EXISTING = 4; + public static final int ID_CREATE_CONTACT = 5; + public static final int ID_CALL_DETAILS = 6; + public static final int ID_BLOCK_CALLER = 7; + public static final int ID_UNBLOCK_CALLER = 8; + + @DrawableRes + private int id; + private int icon; + private String text; + + public CallOption(int id, int icon, String text) { + this.id = id; + this.icon = icon; + this.text = text; + } + + public CallOption(int id, int icon) { + this.id = id; + this.icon = icon; + } + + public int getIcon() { + return icon; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public int getId() { + return id; + } } diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallType.kt b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallType.kt index e7c6944..4c53e9c 100644 --- a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallType.kt +++ b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallType.kt @@ -1,27 +1,27 @@ package dev.alenajam.opendialer.data.calls enum class CallType(val value: Int) { - INCOMING(1), + INCOMING(1), - /** Call log type for outgoing calls. */ - OUTGOING(2), + /** Call log type for outgoing calls. */ + OUTGOING(2), - /** Call log type for missed calls. */ - MISSED(3), + /** Call log type for missed calls. */ + MISSED(3), - /** Call log type for voicemails. */ - VOICEMAIL(4), + /** Call log type for voicemails. */ + VOICEMAIL(4), - /** Call log type for calls rejected by direct user action. */ - REJECTED(5), + /** Call log type for calls rejected by direct user action. */ + REJECTED(5), - /** Call log type for calls blocked automatically. */ - BLOCKED(6), + /** Call log type for calls blocked automatically. */ + BLOCKED(6), - /** - * Call log type for a call which was answered on another device. Used in situations where - * a call rings on multiple devices simultaneously and it ended up being answered on a - * device other than the current one. - */ - ANSWERED_EXTERNALLY(7) + /** + * Call log type for a call which was answered on another device. Used in situations where + * a call rings on multiple devices simultaneously and it ended up being answered on a + * device other than the current one. + */ + ANSWERED_EXTERNALLY(7) } \ No newline at end of file diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallsData.kt b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallsData.kt index d53009d..798eadc 100644 --- a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallsData.kt +++ b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallsData.kt @@ -4,42 +4,41 @@ import android.content.ContentResolver import android.database.Cursor import android.net.Uri import android.provider.CallLog.Calls -import android.telephony.PhoneNumberUtils import android.util.Log abstract class CallsData { - companion object { - private val TAG = CallsData::class.simpleName - val URI: Uri = Calls.CONTENT_URI - private const val LIMIT = 1000 + companion object { + private val TAG = CallsData::class.simpleName + val URI: Uri = Calls.CONTENT_URI + private const val LIMIT = 1000 - private val projection = arrayOf( - Calls._ID, - Calls.NUMBER, - Calls.CACHED_NORMALIZED_NUMBER, - Calls.CACHED_NAME, - Calls.DATE, - Calls.DURATION, - Calls.TYPE, - Calls.NEW, - Calls.CACHED_PHOTO_URI, - Calls.COUNTRY_ISO, - Calls.CACHED_NUMBER_LABEL, - Calls.CACHED_PHOTO_ID, - Calls.GEOCODED_LOCATION, - Calls.CACHED_FORMATTED_NUMBER, - Calls.CACHED_NORMALIZED_NUMBER, - Calls.CACHED_LOOKUP_URI, - Calls.POST_DIAL_DIGITS, - Calls.CACHED_MATCHED_NUMBER, - Calls.CACHED_NUMBER_TYPE - ) + private val projection = arrayOf( + Calls._ID, + Calls.NUMBER, + Calls.CACHED_NORMALIZED_NUMBER, + Calls.CACHED_NAME, + Calls.DATE, + Calls.DURATION, + Calls.TYPE, + Calls.NEW, + Calls.CACHED_PHOTO_URI, + Calls.COUNTRY_ISO, + Calls.CACHED_NUMBER_LABEL, + Calls.CACHED_PHOTO_ID, + Calls.GEOCODED_LOCATION, + Calls.CACHED_FORMATTED_NUMBER, + Calls.CACHED_NORMALIZED_NUMBER, + Calls.CACHED_LOOKUP_URI, + Calls.POST_DIAL_DIGITS, + Calls.CACHED_MATCHED_NUMBER, + Calls.CACHED_NUMBER_TYPE + ) - /** Filter out: - * - Blocked calls - * - Non-video Duo calls - */ - private const val where = """ + /** Filter out: + * - Blocked calls + * - Non-video Duo calls + */ + private const val where = """ ${Calls.TYPE} != ${Calls.BLOCKED_TYPE} AND ( ${Calls.PHONE_ACCOUNT_COMPONENT_NAME} IS NULL @@ -48,52 +47,52 @@ abstract class CallsData { ) """ - fun getCursor(contentResolver: ContentResolver): Cursor? = contentResolver.query( - URI - .buildUpon() - .appendQueryParameter(Calls.LIMIT_PARAM_KEY, LIMIT.toString()) - .build(), - projection, - where, - null, - Calls.DEFAULT_SORT_ORDER - ) + fun getCursor(contentResolver: ContentResolver): Cursor? = contentResolver.query( + URI + .buildUpon() + .appendQueryParameter(Calls.LIMIT_PARAM_KEY, LIMIT.toString()) + .build(), + projection, + where, + null, + Calls.DEFAULT_SORT_ORDER + ) - fun getData(cursor: Cursor): List { - val start = System.currentTimeMillis() + fun getData(cursor: Cursor): List { + val start = System.currentTimeMillis() - val list = mutableListOf() - if (cursor.moveToFirst()) { - do { - list.add( - DialerCallEntity( - id = cursor.getInt(cursor.getColumnIndexOrThrow(Calls._ID)), - number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER)), - name = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_NAME)), - date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE)), - duration = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DURATION)), - type = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.TYPE)), - isNew = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NEW)), - photoUri = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_PHOTO_URI)) - ?.takeIf { it.isNotBlank() }, - countryIso = cursor.getString(cursor.getColumnIndexOrThrow(Calls.COUNTRY_ISO)), - label = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_LABEL)), - photoId = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.CACHED_PHOTO_ID)), - geoDescription = cursor.getString(cursor.getColumnIndexOrThrow(Calls.GEOCODED_LOCATION)), - formattedNumber = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_FORMATTED_NUMBER)), - normalizedNumber = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_NORMALIZED_NUMBER)), - lookupUri = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_LOOKUP_URI)), - postDialDigits = cursor.getString(cursor.getColumnIndexOrThrow(Calls.POST_DIAL_DIGITS)), - matchedNumber = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_MATCHED_NUMBER)), - numberType = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_TYPE)) - ) - ) - } while (cursor.moveToNext()) - } + val list = mutableListOf() + if (cursor.moveToFirst()) { + do { + list.add( + DialerCallEntity( + id = cursor.getInt(cursor.getColumnIndexOrThrow(Calls._ID)), + number = cursor.getString(cursor.getColumnIndexOrThrow(Calls.NUMBER)), + name = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_NAME)), + date = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DATE)), + duration = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.DURATION)), + type = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.TYPE)), + isNew = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.NEW)), + photoUri = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_PHOTO_URI)) + ?.takeIf { it.isNotBlank() }, + countryIso = cursor.getString(cursor.getColumnIndexOrThrow(Calls.COUNTRY_ISO)), + label = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_LABEL)), + photoId = cursor.getLong(cursor.getColumnIndexOrThrow(Calls.CACHED_PHOTO_ID)), + geoDescription = cursor.getString(cursor.getColumnIndexOrThrow(Calls.GEOCODED_LOCATION)), + formattedNumber = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_FORMATTED_NUMBER)), + normalizedNumber = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_NORMALIZED_NUMBER)), + lookupUri = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_LOOKUP_URI)), + postDialDigits = cursor.getString(cursor.getColumnIndexOrThrow(Calls.POST_DIAL_DIGITS)), + matchedNumber = cursor.getString(cursor.getColumnIndexOrThrow(Calls.CACHED_MATCHED_NUMBER)), + numberType = cursor.getInt(cursor.getColumnIndexOrThrow(Calls.CACHED_NUMBER_TYPE)) + ) + ) + } while (cursor.moveToNext()) + } - val time = (System.currentTimeMillis() - start) / 1000f - Log.d(TAG, "Call log query time: $time seconds") - return list + val time = (System.currentTimeMillis() - start) / 1000f + Log.d(TAG, "Call log query time: $time seconds") + return list + } } - } } \ No newline at end of file diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallsRepository.kt b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallsRepository.kt new file mode 100644 index 0000000..b40128e --- /dev/null +++ b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallsRepository.kt @@ -0,0 +1,19 @@ +package dev.alenajam.opendialer.data.calls + +import android.content.ContentResolver +import dev.alenajam.opendialer.core.common.exception.Failure +import dev.alenajam.opendialer.core.common.functional.Either +import kotlinx.coroutines.flow.Flow + +interface CallsRepository { + fun getCalls(): Flow> + suspend fun getCallByIds( + contentResolver: ContentResolver, + ids: List + ): Either> + + suspend fun getDetailOptions(call: DialerCall): Either> + suspend fun deleteCalls(calls: List): Either + suspend fun blockCaller(number: String): Either + suspend fun unblockCaller(number: String): Either +} \ No newline at end of file diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallsRepositoryImpl.kt b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallsRepositoryImpl.kt new file mode 100644 index 0000000..952f5c4 --- /dev/null +++ b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/CallsRepositoryImpl.kt @@ -0,0 +1,130 @@ +package dev.alenajam.opendialer.data.calls + +import android.app.Application +import android.content.ContentResolver +import android.content.ContentValues +import android.database.ContentObserver +import android.net.Uri +import android.provider.BlockedNumberContract +import android.provider.CallLog +import dev.alenajam.opendialer.core.common.DefaultPhoneUtils +import dev.alenajam.opendialer.core.common.PermissionUtils +import dev.alenajam.opendialer.core.common.exception.Failure +import dev.alenajam.opendialer.core.common.functional.Either +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import javax.inject.Inject + +class CallsRepositoryImpl +@Inject constructor(private val app: Application) : CallsRepository { + override fun getCalls(): Flow> = + callbackFlow { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + CallsData.getCursor(app.contentResolver)?.let { + trySend(CallsData.getData(it)) + it.close() + } + } + } + + app.contentResolver.registerContentObserver(CallsData.URI, true, observer) + + CallsData.getCursor(app.contentResolver)?.let { + trySend(CallsData.getData(it)) + it.close() + } + + awaitClose { + app.contentResolver.unregisterContentObserver(observer) + } + } + + override suspend fun getCallByIds( + contentResolver: ContentResolver, + ids: List + ): Either> { + val cursor = + CallDetailData.getCursor(contentResolver, ids) ?: return Either.Left(Failure.NoData) + val data = CallDetailData.getData(cursor) + + return if (data.isEmpty()) { + Either.Left(Failure.NoData) + } else { + Either.Right(data) + } + } + + override suspend fun getDetailOptions(call: DialerCall): Either> { + return with(app) { + val options = mutableListOf() + + if (!call.isAnonymous()) { + options.addAll( + listOf( + CallOption(CallOption.ID_COPY_NUMBER, 0), + CallOption( + CallOption.ID_EDIT_BEFORE_CALL, + 0 + ) + ) + ) + } + + options.add( + CallOption( + CallOption.ID_DELETE, + 0 + ) + ) + + if (!call.isAnonymous()) { + val hasDefault = DefaultPhoneUtils.hasDefault(this) + val canUserBlockNumbers = BlockedNumberContract.canCurrentUserBlockNumbers(this) + if (hasDefault && canUserBlockNumbers) { + val isBlocked = + BlockedNumberContract.isBlocked(this, call.contactInfo.number) + val blockOption = CallOption( + if (isBlocked) CallOption.ID_UNBLOCK_CALLER else CallOption.ID_BLOCK_CALLER, + 0 + ) + options.add(blockOption) + } + } + + Either.Right(options) + } + } + + override suspend fun deleteCalls(calls: List): Either { + if (!PermissionUtils.hasRecentsPermission(app)) { + return Either.Left(Failure.NotPermitted) + } + + var deleted = 0 + calls.forEach { + deleted += app.contentResolver.delete( + CallLog.Calls.CONTENT_URI, + "${CallLog.Calls._ID} = ${it.id}", + null + ) + } + + return if (deleted > 0) Either.Right(Unit) else Either.Left(Failure.LocalFailure) + } + + override suspend fun blockCaller(number: String): Either { + val values = ContentValues().apply { + put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, number) + } + val uri: Uri? = + app.contentResolver.insert(BlockedNumberContract.BlockedNumbers.CONTENT_URI, values) + return if (uri == null) Either.Left(Failure.LocalFailure) else Either.Right(Unit) + } + + override suspend fun unblockCaller(number: String): Either { + val blocked = BlockedNumberContract.unblock(app, number) + return if (blocked < 1) Either.Left(Failure.LocalFailure) else Either.Right(Unit) + } +} \ No newline at end of file diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/ContactInfo.kt b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/ContactInfo.kt index 9721368..d6b8ad9 100644 --- a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/ContactInfo.kt +++ b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/ContactInfo.kt @@ -4,76 +4,76 @@ import android.text.TextUtils import java.io.Serializable class ContactInfo( - val name: String? = null, - val number: String? = null, - val photoUri: String? = null, - var type: Int? = 0, - val label: String? = null, - val lookupUri: String? = null, - val normalizedNumber: String? = null, - val formattedNumber: String? = null, - val geoDescription: String? = null, - var photoId: Long? = 0 + val name: String? = null, + val number: String? = null, + val photoUri: String? = null, + var type: Int? = 0, + val label: String? = null, + val lookupUri: String? = null, + val normalizedNumber: String? = null, + val formattedNumber: String? = null, + val geoDescription: String? = null, + var photoId: Long? = 0 ) : Serializable { - companion object { - val EMPTY = ContactInfo() - } - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true + companion object { + val EMPTY = ContactInfo() } - if (other === null) { - return false - } + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } - if (other !is ContactInfo) { - return false - } + if (other === null) { + return false + } - if (!TextUtils.equals(lookupUri, other.lookupUri)) { - return false - } - if (!TextUtils.equals(name, other.name)) { - return false - } - if (type != other.type) { - return false - } - if (!TextUtils.equals(label, other.label)) { - return false - } - if (!TextUtils.equals(number, other.number)) { - return false - } - if (!TextUtils.equals(formattedNumber, other.formattedNumber)) { - return false - } - if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) { - return false - } - if (photoId != other.photoId) { - return false - } - if (!TextUtils.equals(photoUri, other.photoUri)) { - return false - } - if (!TextUtils.equals(geoDescription, other.geoDescription)) { - return false - } + if (other !is ContactInfo) { + return false + } + + if (!TextUtils.equals(lookupUri, other.lookupUri)) { + return false + } + if (!TextUtils.equals(name, other.name)) { + return false + } + if (type != other.type) { + return false + } + if (!TextUtils.equals(label, other.label)) { + return false + } + if (!TextUtils.equals(number, other.number)) { + return false + } + if (!TextUtils.equals(formattedNumber, other.formattedNumber)) { + return false + } + if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) { + return false + } + if (photoId != other.photoId) { + return false + } + if (!TextUtils.equals(photoUri, other.photoUri)) { + return false + } + if (!TextUtils.equals(geoDescription, other.geoDescription)) { + return false + } - return true - } + return true + } - override fun hashCode(): Int { - // Uses only name and contactUri to determine hashcode. - // This should be sufficient to have a reasonable distribution of hash codes. - // Moreover, there should be no two people with the same lookupUri. - val prime = 31 - var result = 1 - result = prime * result + (lookupUri?.hashCode() ?: 0) - result = prime * result + (name?.hashCode() ?: 0) - return result - } + override fun hashCode(): Int { + // Uses only name and contactUri to determine hashcode. + // This should be sufficient to have a reasonable distribution of hash codes. + // Moreover, there should be no two people with the same lookupUri. + val prime = 31 + var result = 1 + result = prime * result + (lookupUri?.hashCode() ?: 0) + result = prime * result + (name?.hashCode() ?: 0) + return result + } } \ No newline at end of file diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DetailCall.kt b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DetailCall.kt index fba4281..c7da853 100644 --- a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DetailCall.kt +++ b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DetailCall.kt @@ -4,24 +4,24 @@ import java.io.Serializable import java.util.Date class DetailCall( - val id: Int, - val type: CallType, - val date: Date, - val duration: Long + val id: Int, + val type: CallType, + val date: Date, + val duration: Long ) : Serializable { - companion object { - fun map(call: DialerCallEntity): DetailCall? { - val type = CallType.values().find { t -> t.value == call.type } + companion object { + fun map(call: DialerCallEntity): DetailCall? { + val type = CallType.values().find { t -> t.value == call.type } - type?.let { - return DetailCall( - id = call.id, - type = it, - date = Date(call.date), - duration = call.duration - ) - } - return null + type?.let { + return DetailCall( + id = call.id, + type = it, + date = Date(call.date), + duration = call.duration + ) + } + return null + } } - } } \ No newline at end of file diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerCall.kt b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerCall.kt index b8c18c0..03bc916 100644 --- a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerCall.kt +++ b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerCall.kt @@ -6,109 +6,110 @@ import java.io.Serializable import java.util.Date @Keep -class DialerCall( - val id: Int, - val number: String?, - val date: Date, - val type: CallType, - val options: List, - var childCalls: List, - val countryIso: String? = null, - val contactInfo: ContactInfo +data class DialerCall( + val id: Int, + val number: String?, + val date: Date, + val type: CallType, + val options: List, + var childCalls: List, + val countryIso: String? = null, + val contactInfo: ContactInfo ) : Serializable { - companion object { - fun mapList(list: List): List { - val calls = mutableListOf() - list.forEach { - if ( - // Never group anonymous calls - !it.number.isNullOrBlank() - && calls.isNotEmpty() - ) { - val last = calls.last() - if (equalNumbers(last.contactInfo.number, it.number)) { - DetailCall.map(it)?.let { call -> - last.childCalls = last.childCalls.plus(call) + companion object { + fun mapList(list: List): List { + val calls = mutableListOf() + list.forEach { + if ( + // Never group anonymous calls + !it.number.isNullOrBlank() + && calls.isNotEmpty() + ) { + val last = calls.last() + if (equalNumbers(last.contactInfo.number, it.number)) { + DetailCall.map(it)?.let { call -> + last.childCalls = last.childCalls.plus(call) + } + return@forEach + } + } + + map(it)?.let { call -> calls.add(call) } } - return@forEach - } + return calls } - map(it)?.let { call -> calls.add(call) } - } - return calls - } + fun map(call: DialerCallEntity): DialerCall? { + val type = CallType.values().find { t -> t.value == call.type } - fun map(call: DialerCallEntity): DialerCall? { - val type = CallType.values().find { t -> t.value == call.type } + type?.let { t -> + val options = mutableListOf() + // Not anonymous + if (!call.number.isNullOrBlank()) { + // Not contact + if (call.name.isNullOrBlank()) { + options.addAll( + listOf( + CallOption( + CallOption.ID_CREATE_CONTACT, + 0 + ), + CallOption( + CallOption.ID_ADD_EXISTING, + 0 + ) + ) + ) + } - type?.let { t -> - val options = mutableListOf() - // Not anonymous - if (!call.number.isNullOrBlank()) { - // Not contact - if (call.name.isNullOrBlank()) { - options.addAll( - listOf( - CallOption( - CallOption.ID_CREATE_CONTACT, - 0 - ), - CallOption( - CallOption.ID_ADD_EXISTING, - 0 - ) - ) - ) - } - - options.add( - CallOption( - CallOption.ID_SEND_MESSAGE, - 0 - ) - ) - } + options.add( + CallOption( + CallOption.ID_SEND_MESSAGE, + 0 + ) + ) + } - // Always - options.add( - CallOption( - CallOption.ID_CALL_DETAILS, - 0 - ) - ) + // Always + options.add( + CallOption( + CallOption.ID_CALL_DETAILS, + 0 + ) + ) - val contactNumber = call.matchedNumber ?: (call.number + call.postDialDigits) + val contactNumber = call.matchedNumber ?: (call.number + call.postDialDigits) - return DialerCall( - id = call.id, - number = call.number, - date = Date(call.date), - type = t, - options = options, - childCalls = listOfNotNull(DetailCall.map(call)), - countryIso = call.countryIso, - contactInfo = ContactInfo( - name = call.name, - number = contactNumber, - photoUri = call.photoUri, - type = call.numberType, - label = call.label, - lookupUri = call.lookupUri, - normalizedNumber = call.normalizedNumber, - formattedNumber = call.formattedNumber, - geoDescription = call.geoDescription, - photoId = call.photoId - ) - ) - } - return null + return DialerCall( + id = call.id, + number = call.number, + date = Date(call.date), + type = t, + options = options, + childCalls = listOfNotNull(DetailCall.map(call)), + countryIso = call.countryIso, + contactInfo = ContactInfo( + name = call.name, + number = contactNumber, + photoUri = call.photoUri, + type = call.numberType, + label = call.label, + lookupUri = call.lookupUri, + normalizedNumber = call.normalizedNumber, + formattedNumber = call.formattedNumber, + geoDescription = call.geoDescription, + photoId = call.photoId + ) + ) + } + return null + } } - } - fun isAnonymous(): Boolean = contactInfo.number.isNullOrBlank() + fun isAnonymous(): Boolean = contactInfo.number.isNullOrBlank() + fun isContactSaved(): Boolean = !contactInfo.name.isNullOrBlank() } fun equalNumbers(number1: String?, number2: String?): Boolean { - return PhoneNumberUtils.compare(number1, number2) + return PhoneNumberUtils.compare(number1, number2) } \ No newline at end of file diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerCallEntity.kt b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerCallEntity.kt index 5b97f52..b7807e3 100644 --- a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerCallEntity.kt +++ b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerCallEntity.kt @@ -1,22 +1,22 @@ package dev.alenajam.opendialer.data.calls class DialerCallEntity( - val id: Int, - val number: String?, - val name: String?, - val date: Long, - val duration: Long, - val type: Int, - val isNew: Int, - val photoUri: String?, - val countryIso: String? = null, - val label: String?, - val lookupUri: String?, - val normalizedNumber: String? = null, - val formattedNumber: String? = null, - val geoDescription: String? = null, - val photoId: Long? = null, - val postDialDigits: String? = null, - val matchedNumber: String? = null, - val numberType: Int? = null + val id: Int, + val number: String?, + val name: String?, + val date: Long, + val duration: Long, + val type: Int, + val isNew: Int, + val photoUri: String?, + val countryIso: String? = null, + val label: String?, + val lookupUri: String?, + val normalizedNumber: String? = null, + val formattedNumber: String? = null, + val geoDescription: String? = null, + val photoId: Long? = null, + val postDialDigits: String? = null, + val matchedNumber: String? = null, + val numberType: Int? = null ) \ No newline at end of file diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerRepository.kt b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerRepository.kt deleted file mode 100644 index cb383ab..0000000 --- a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerRepository.kt +++ /dev/null @@ -1,15 +0,0 @@ -package dev.alenajam.opendialer.data.calls - -import android.content.ContentResolver -import dev.alenajam.opendialer.core.common.exception.Failure -import dev.alenajam.opendialer.core.common.functional.Either -import kotlinx.coroutines.flow.Flow - -interface DialerRepository { - fun getCalls(contentResolver: ContentResolver): Flow> - suspend fun getCallByIds(contentResolver: ContentResolver, ids: List): Either> - suspend fun getDetailOptions(call: DialerCall): Either> - suspend fun deleteCalls(calls: List): Either - suspend fun blockCaller(number: String): Either - suspend fun unblockCaller(number: String): Either -} \ No newline at end of file diff --git a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerRepositoryImpl.kt b/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerRepositoryImpl.kt deleted file mode 100644 index 3eb01a3..0000000 --- a/data/calls/src/main/java/dev/alenajam/opendialer/data/calls/DialerRepositoryImpl.kt +++ /dev/null @@ -1,129 +0,0 @@ -package dev.alenajam.opendialer.data.calls - -import android.app.Application -import android.content.ContentResolver -import android.content.ContentValues -import android.database.ContentObserver -import android.net.Uri -import android.provider.BlockedNumberContract -import android.provider.CallLog -import dev.alenajam.opendialer.core.common.DefaultPhoneUtils -import dev.alenajam.opendialer.core.common.PermissionUtils -import dev.alenajam.opendialer.core.common.exception.Failure -import dev.alenajam.opendialer.core.common.functional.Either -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import javax.inject.Inject - -class DialerRepositoryImpl -@Inject constructor(private val app: Application) : DialerRepository { - override fun getCalls(contentResolver: ContentResolver): Flow> = - callbackFlow { - val observer = object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) { - CallsData.getCursor(contentResolver)?.let { - trySend(CallsData.getData(it)) - it.close() - } - } - } - - contentResolver.registerContentObserver(CallsData.URI, true, observer) - - CallsData.getCursor(contentResolver)?.let { - trySend(CallsData.getData(it)) - it.close() - } - - awaitClose { - contentResolver.unregisterContentObserver(observer) - } - } - - override suspend fun getCallByIds( - contentResolver: ContentResolver, - ids: List - ): Either> { - val cursor = CallDetailData.getCursor(contentResolver, ids) ?: return Either.Left(Failure.NoData) - val data = CallDetailData.getData(cursor) - - return if (data.isEmpty()) { - Either.Left(Failure.NoData) - } else { - Either.Right(data) - } - } - - override suspend fun getDetailOptions(call: DialerCall): Either> { - return with(app) { - val options = mutableListOf() - - if (!call.isAnonymous()) { - options.addAll( - listOf( - CallOption(CallOption.ID_COPY_NUMBER, 0), - CallOption( - CallOption.ID_EDIT_BEFORE_CALL, - 0 - ) - ) - ) - } - - options.add( - CallOption( - CallOption.ID_DELETE, - 0 - ) - ) - - if (!call.isAnonymous()) { - val hasDefault = DefaultPhoneUtils.hasDefault(this) - val canUserBlockNumbers = BlockedNumberContract.canCurrentUserBlockNumbers(this) - if (hasDefault && canUserBlockNumbers) { - val isBlocked = - BlockedNumberContract.isBlocked(this, call.contactInfo.number) - val blockOption = CallOption( - if (isBlocked) CallOption.ID_UNBLOCK_CALLER else CallOption.ID_BLOCK_CALLER, - 0 - ) - options.add(blockOption) - } - } - - Either.Right(options) - } - } - - override suspend fun deleteCalls(calls: List): Either { - if (!PermissionUtils.hasRecentsPermission(app)) { - return Either.Left(Failure.NotPermitted) - } - - var deleted = 0 - calls.forEach { - deleted += app.contentResolver.delete( - CallLog.Calls.CONTENT_URI, - "${CallLog.Calls._ID} = ${it.id}", - null - ) - } - - return if (deleted > 0) Either.Right(Unit) else Either.Left(Failure.LocalFailure) - } - - override suspend fun blockCaller(number: String): Either { - val values = ContentValues().apply { - put(BlockedNumberContract.BlockedNumbers.COLUMN_ORIGINAL_NUMBER, number) - } - val uri: Uri? = - app.contentResolver.insert(BlockedNumberContract.BlockedNumbers.CONTENT_URI, values) - return if (uri == null) Either.Left(Failure.LocalFailure) else Either.Right(Unit) - } - - override suspend fun unblockCaller(number: String): Either { - val blocked = BlockedNumberContract.unblock(app, number) - return if (blocked < 1) Either.Left(Failure.LocalFailure) else Either.Right(Unit) - } -} \ No newline at end of file diff --git a/data/calls/src/test/java/dev/alenajam/opendialer/data/calls/ExampleUnitTest.kt b/data/calls/src/test/java/dev/alenajam/opendialer/data/calls/ExampleUnitTest.kt index 0327caf..fd1dcd2 100644 --- a/data/calls/src/test/java/dev/alenajam/opendialer/data/calls/ExampleUnitTest.kt +++ b/data/calls/src/test/java/dev/alenajam/opendialer/data/calls/ExampleUnitTest.kt @@ -1,8 +1,7 @@ package dev.alenajam.opendialer.data.calls -import org.junit.Test - import org.junit.Assert.* +import org.junit.Test /** * Example local unit test, which will execute on the development machine (host). @@ -10,8 +9,8 @@ import org.junit.Assert.* * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } } \ No newline at end of file diff --git a/data/callsCache/build.gradle.kts b/data/callsCache/build.gradle.kts index 16956d7..59f3423 100644 --- a/data/callsCache/build.gradle.kts +++ b/data/callsCache/build.gradle.kts @@ -1,46 +1,49 @@ plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") - id("kotlin-kapt") - id("com.google.dagger.hilt.android") + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("kotlin-kapt") + id("com.google.dagger.hilt.android") } android { - namespace = "com.example.callscache" - compileSdk = 34 + namespace = "com.example.callscache" + compileSdk = 34 - defaultConfig { - minSdk = 24 + defaultConfig { + minSdk = 24 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } } - } } dependencies { - implementation(project(":core:common")) - implementation(project(":core:aosp")) - - implementation("androidx.core:core-ktx:1.12.0") - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - - implementation("com.google.dagger:hilt-android:2.48.1") - kapt("com.google.dagger:hilt-compiler:2.48.1") - androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1") - testImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptTest("com.google.dagger:hilt-compiler:2.48.1") + implementation(project(":core:common")) + implementation(project(":core:aosp")) + + implementation("androidx.core:core-ktx:1.12.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + implementation("com.google.dagger:hilt-android:2.48.1") + kapt("com.google.dagger:hilt-compiler:2.48.1") + androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1") + testImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptTest("com.google.dagger:hilt-compiler:2.48.1") } kotlin { - jvmToolchain(17) + jvmToolchain(17) } \ No newline at end of file diff --git a/data/callsCache/src/androidTest/java/com/example/callscache/ExampleInstrumentedTest.kt b/data/callsCache/src/androidTest/java/com/example/callscache/ExampleInstrumentedTest.kt index 7d07f40..9b15a3a 100644 --- a/data/callsCache/src/androidTest/java/com/example/callscache/ExampleInstrumentedTest.kt +++ b/data/callsCache/src/androidTest/java/com/example/callscache/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package com.example.callscache -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -15,10 +13,10 @@ import org.junit.Assert.* */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.example.callscache.test", appContext.packageName) - } + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.callscache.test", appContext.packageName) + } } \ No newline at end of file diff --git a/data/callsCache/src/main/AndroidManifest.xml b/data/callsCache/src/main/AndroidManifest.xml index a5918e6..44008a4 100644 --- a/data/callsCache/src/main/AndroidManifest.xml +++ b/data/callsCache/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/CacheRepository.kt b/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/CacheRepository.kt index 39d0682..cdcc497 100644 --- a/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/CacheRepository.kt +++ b/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/CacheRepository.kt @@ -5,14 +5,14 @@ import dev.alenajam.opendialer.core.common.functional.Either import kotlinx.coroutines.CoroutineScope interface CacheRepository { - suspend fun start(): Either - fun stop() - fun requestUpdateContactInfo( - coroutineScope: CoroutineScope, - number: String?, - countryIso: String?, - callLogInfo: ContactInfo - ): Either + suspend fun start(): Either + fun stop() + fun requestUpdateContactInfo( + coroutineScope: CoroutineScope, + number: String?, + countryIso: String?, + callLogInfo: ContactInfo + ): Either - fun invalidate() + fun invalidate() } \ No newline at end of file diff --git a/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/CacheRepositoryImpl.kt b/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/CacheRepositoryImpl.kt index 0e84d6a..aecc59a 100644 --- a/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/CacheRepositoryImpl.kt +++ b/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/CacheRepositoryImpl.kt @@ -16,153 +16,153 @@ import javax.inject.Singleton @Singleton class CacheRepositoryImpl @Inject constructor(private val app: Application) : CacheRepository { - companion object { - private val TAG = CacheRepositoryImpl::class.simpleName - } - - private var channel: Channel? = null - private val updatedNumbers = mutableSetOf() - - override suspend fun start(): Either { - channel = Channel() - channel?.let { - for (request in it) { - attemptUpdateContactInfo(request) - } + companion object { + private val TAG = CacheRepositoryImpl::class.simpleName } - return Either.Right(Unit) - } - override fun stop() { - channel?.cancel() - channel = null - } - - private fun attemptUpdateContactInfo(request: ContactInfoRequest) { - if (request.number === null) { - return + private var channel: Channel? = null + private val updatedNumbers = mutableSetOf() + + override suspend fun start(): Either { + channel = Channel() + channel?.let { + for (request in it) { + attemptUpdateContactInfo(request) + } + } + return Either.Right(Unit) } - /** Fetch new contact info */ - val info = getContactInfoByNumber( - app, - request.number, - request.countryIso - ) - - if (info === ContactInfo.EMPTY) { - return + override fun stop() { + channel?.cancel() + channel = null } - /** Update call log */ - ContactInfoHelper(app).updateCallLogContactInfo( - request.number, - request.countryIso, - info, - request.callLogInfo - ) - } - - override fun requestUpdateContactInfo( - coroutineScope: CoroutineScope, - number: String?, - countryIso: String?, - callLogInfo: ContactInfo - ): Either { - val numberWithCountryIso = NumberWithCountryIso(number, countryIso) - - if (!updatedNumbers.contains(numberWithCountryIso)) { - updatedNumbers.add(numberWithCountryIso) - val request = ContactInfoRequest( - numberWithCountryIso.number, - numberWithCountryIso.countryIso, - callLogInfo - ) - - coroutineScope.launch { channel?.send(request) } + private fun attemptUpdateContactInfo(request: ContactInfoRequest) { + if (request.number === null) { + return + } + + /** Fetch new contact info */ + val info = getContactInfoByNumber( + app, + request.number, + request.countryIso + ) + + if (info === ContactInfo.EMPTY) { + return + } + + /** Update call log */ + ContactInfoHelper(app).updateCallLogContactInfo( + request.number, + request.countryIso, + info, + request.callLogInfo + ) } - return Either.Right(Unit) - } + override fun requestUpdateContactInfo( + coroutineScope: CoroutineScope, + number: String?, + countryIso: String?, + callLogInfo: ContactInfo + ): Either { + val numberWithCountryIso = NumberWithCountryIso(number, countryIso) + + if (!updatedNumbers.contains(numberWithCountryIso)) { + updatedNumbers.add(numberWithCountryIso) + val request = ContactInfoRequest( + numberWithCountryIso.number, + numberWithCountryIso.countryIso, + callLogInfo + ) + + coroutineScope.launch { channel?.send(request) } + } + + return Either.Right(Unit) + } - override fun invalidate() { - updatedNumbers.clear() - } + override fun invalidate() { + updatedNumbers.clear() + } } fun getContactInfoByNumber( - context: Context, - number: String, - countryIso: String? + context: Context, + number: String, + countryIso: String? ): ContactInfo { - val uri = ContactsContract.PhoneLookup.CONTENT_FILTER_URI - - val projection = arrayOf( - ContactsContract.PhoneLookup.CONTACT_ID, - ContactsContract.PhoneLookup.DISPLAY_NAME, - ContactsContract.PhoneLookup.TYPE, - ContactsContract.PhoneLookup.LABEL, - ContactsContract.PhoneLookup.NUMBER, - ContactsContract.PhoneLookup.NORMALIZED_NUMBER, - ContactsContract.PhoneLookup.PHOTO_ID, - ContactsContract.PhoneLookup.LOOKUP_KEY, - ContactsContract.PhoneLookup.PHOTO_URI - ) - - context.contentResolver.query( - uri - .buildUpon() - .appendPath(number) - .build(), - projection, - null, - null, - null - )?.let { - if (it.moveToFirst()) { - val lookupKey = - it.getString(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.LOOKUP_KEY)) - val contactId = - it.getLong(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.CONTACT_ID)) - return ContactInfo( - name = it.getString(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.DISPLAY_NAME)), - number = it.getString(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.NUMBER)), - photoUri = it.getString(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.PHOTO_URI)) - ?.takeIf { uri -> uri.isNotBlank() }, - type = it.getInt(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.TYPE)), - label = it.getString(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.LABEL)), - lookupUri = UriUtils.uriToString( - ContactsContract.Contacts.getLookupUri( - contactId, - lookupKey - ) - ), - normalizedNumber = it.getString(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.NORMALIZED_NUMBER)), - photoId = it.getLong(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.PHOTO_ID)) - ) + val uri = ContactsContract.PhoneLookup.CONTENT_FILTER_URI + + val projection = arrayOf( + ContactsContract.PhoneLookup.CONTACT_ID, + ContactsContract.PhoneLookup.DISPLAY_NAME, + ContactsContract.PhoneLookup.TYPE, + ContactsContract.PhoneLookup.LABEL, + ContactsContract.PhoneLookup.NUMBER, + ContactsContract.PhoneLookup.NORMALIZED_NUMBER, + ContactsContract.PhoneLookup.PHOTO_ID, + ContactsContract.PhoneLookup.LOOKUP_KEY, + ContactsContract.PhoneLookup.PHOTO_URI + ) + + context.contentResolver.query( + uri + .buildUpon() + .appendPath(number) + .build(), + projection, + null, + null, + null + )?.let { + if (it.moveToFirst()) { + val lookupKey = + it.getString(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.LOOKUP_KEY)) + val contactId = + it.getLong(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.CONTACT_ID)) + return ContactInfo( + name = it.getString(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.DISPLAY_NAME)), + number = it.getString(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.NUMBER)), + photoUri = it.getString(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.PHOTO_URI)) + ?.takeIf { uri -> uri.isNotBlank() }, + type = it.getInt(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.TYPE)), + label = it.getString(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.LABEL)), + lookupUri = UriUtils.uriToString( + ContactsContract.Contacts.getLookupUri( + contactId, + lookupKey + ) + ), + normalizedNumber = it.getString(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.NORMALIZED_NUMBER)), + photoId = it.getLong(it.getColumnIndexOrThrow(ContactsContract.PhoneLookup.PHOTO_ID)) + ) + } + it.close() } - it.close() - } - return createEmptyContactInfoForNumber(context, number, countryIso) + return createEmptyContactInfoForNumber(context, number, countryIso) } private fun createEmptyContactInfoForNumber( - context: Context, - number: String, - countryIso: String? + context: Context, + number: String, + countryIso: String? ): ContactInfo { - val formattedNumber: String = - ContactInfoHelper(context).formatPhoneNumber(number, null, countryIso) - val normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso) - return ContactInfo( - number = number, - lookupUri = UriUtils.uriToString( - ContactInfoHelper.createTemporaryContactUri( - formattedNumber - ) - ), - normalizedNumber = normalizedNumber, - formattedNumber = formattedNumber - ) + val formattedNumber: String = + ContactInfoHelper(context).formatPhoneNumber(number, null, countryIso) + val normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso) + return ContactInfo( + number = number, + lookupUri = UriUtils.uriToString( + ContactInfoHelper.createTemporaryContactUri( + formattedNumber + ) + ), + normalizedNumber = normalizedNumber, + formattedNumber = formattedNumber + ) } \ No newline at end of file diff --git a/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/ContactInfo.kt b/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/ContactInfo.kt index 29ee8d4..57c5492 100644 --- a/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/ContactInfo.kt +++ b/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/ContactInfo.kt @@ -4,76 +4,76 @@ import android.text.TextUtils import java.io.Serializable class ContactInfo( - val name: String? = null, - val number: String? = null, - val photoUri: String? = null, - var type: Int? = 0, - val label: String? = null, - val lookupUri: String? = null, - val normalizedNumber: String? = null, - val formattedNumber: String? = null, - val geoDescription: String? = null, - var photoId: Long? = 0 + val name: String? = null, + val number: String? = null, + val photoUri: String? = null, + var type: Int? = 0, + val label: String? = null, + val lookupUri: String? = null, + val normalizedNumber: String? = null, + val formattedNumber: String? = null, + val geoDescription: String? = null, + var photoId: Long? = 0 ) : Serializable { - companion object { - val EMPTY = ContactInfo() - } - - override fun equals(other: Any?): Boolean { - if (this === other) { - return true + companion object { + val EMPTY = ContactInfo() } - if (other === null) { - return false - } + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } - if (other !is ContactInfo) { - return false - } + if (other === null) { + return false + } - if (!TextUtils.equals(lookupUri, other.lookupUri)) { - return false - } - if (!TextUtils.equals(name, other.name)) { - return false - } - if (type != other.type) { - return false - } - if (!TextUtils.equals(label, other.label)) { - return false - } - if (!TextUtils.equals(number, other.number)) { - return false - } - if (!TextUtils.equals(formattedNumber, other.formattedNumber)) { - return false - } - if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) { - return false - } - if (photoId != other.photoId) { - return false - } - if (!TextUtils.equals(photoUri, other.photoUri)) { - return false - } - if (!TextUtils.equals(geoDescription, other.geoDescription)) { - return false - } + if (other !is ContactInfo) { + return false + } + + if (!TextUtils.equals(lookupUri, other.lookupUri)) { + return false + } + if (!TextUtils.equals(name, other.name)) { + return false + } + if (type != other.type) { + return false + } + if (!TextUtils.equals(label, other.label)) { + return false + } + if (!TextUtils.equals(number, other.number)) { + return false + } + if (!TextUtils.equals(formattedNumber, other.formattedNumber)) { + return false + } + if (!TextUtils.equals(normalizedNumber, other.normalizedNumber)) { + return false + } + if (photoId != other.photoId) { + return false + } + if (!TextUtils.equals(photoUri, other.photoUri)) { + return false + } + if (!TextUtils.equals(geoDescription, other.geoDescription)) { + return false + } - return true - } + return true + } - override fun hashCode(): Int { - // Uses only name and contactUri to determine hashcode. - // This should be sufficient to have a reasonable distribution of hash codes. - // Moreover, there should be no two people with the same lookupUri. - val prime = 31 - var result = 1 - result = prime * result + (lookupUri?.hashCode() ?: 0) - result = prime * result + (name?.hashCode() ?: 0) - return result - } + override fun hashCode(): Int { + // Uses only name and contactUri to determine hashcode. + // This should be sufficient to have a reasonable distribution of hash codes. + // Moreover, there should be no two people with the same lookupUri. + val prime = 31 + var result = 1 + result = prime * result + (lookupUri?.hashCode() ?: 0) + result = prime * result + (name?.hashCode() ?: 0) + return result + } } \ No newline at end of file diff --git a/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/ContactInfoHelper.java b/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/ContactInfoHelper.java index b8e2128..eb9dca3 100644 --- a/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/ContactInfoHelper.java +++ b/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/ContactInfoHelper.java @@ -29,196 +29,196 @@ import java.util.Objects; -import dev.alenajam.opendialer.core.common.PermissionUtils; import dev.alenajam.opendialer.core.aosp.PhoneNumberHelper; import dev.alenajam.opendialer.core.aosp.UriUtils; +import dev.alenajam.opendialer.core.common.PermissionUtils; /** * Utility class to look up the contact information for a given number. */ public class ContactInfoHelper { - private final Context context; - - public ContactInfoHelper(Context context) { - this.context = context; - } - - /** - * Creates a JSON-encoded lookup uri for a unknown number without an associated contact - * - * @param number - Unknown phone number - * @return JSON-encoded URI that can be used to perform a lookup when clicking on the quick - * contact card. - */ - public static Uri createTemporaryContactUri(String number) { - try { - final JSONObject contactRows = - new JSONObject() - .put( - ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, - new JSONObject() - .put(ContactsContract.CommonDataKinds.Phone.NUMBER, number) - .put(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM)); - - final String jsonString = - new JSONObject() - .put(ContactsContract.Contacts.DISPLAY_NAME, number) - .put(ContactsContract.Contacts.DISPLAY_NAME_SOURCE, ContactsContract.DisplayNameSources.PHONE) - .put(ContactsContract.Contacts.CONTENT_ITEM_TYPE, contactRows) - .toString(); - - return ContactsContract.Contacts.CONTENT_LOOKUP_URI - .buildUpon() - .appendPath("encoded") - .appendQueryParameter( - ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Long.MAX_VALUE)) - .encodedFragment(jsonString) - .build(); - } catch (JSONException e) { - return null; - } - } - - /** - * Stores differences between the updated contact info and the current call log contact info. - * - * @param number The number of the contact. - * @param countryIso The country associated with this number. - * @param updatedInfo The updated contact info. - * @param callLogInfo The call log entry's current contact info. - */ - public void updateCallLogContactInfo( - String number, String countryIso, ContactInfo updatedInfo, ContactInfo callLogInfo) { - if (!PermissionUtils.hasRecentsPermission(context)) { - return; - } + private final Context context; - final ContentValues values = new ContentValues(); - boolean needsUpdate = false; - - if (callLogInfo != null) { - if (!TextUtils.equals(updatedInfo.getName(), callLogInfo.getName())) { - values.put(Calls.CACHED_NAME, updatedInfo.getName()); - needsUpdate = true; - } - - if (!Objects.equals(updatedInfo.getType(), callLogInfo.getType())) { - values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.getType()); - needsUpdate = true; - } - - if (!TextUtils.equals(updatedInfo.getLabel(), callLogInfo.getLabel())) { - values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.getLabel()); - needsUpdate = true; - } - - // Only replace the normalized number if the new updated normalized number isn't empty. - if (!TextUtils.isEmpty(updatedInfo.getNormalizedNumber()) - && !TextUtils.equals(updatedInfo.getNormalizedNumber(), callLogInfo.getNormalizedNumber())) { - values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.getNormalizedNumber()); - needsUpdate = true; - } - - if (!TextUtils.equals(updatedInfo.getNumber(), callLogInfo.getNumber())) { - values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.getNumber()); - needsUpdate = true; - } - - if (!Objects.equals(updatedInfo.getPhotoId(), callLogInfo.getPhotoId())) { - values.put(Calls.CACHED_PHOTO_ID, updatedInfo.getPhotoId()); - needsUpdate = true; - } - - final Uri updatedPhotoUriContactsOnly = UriUtils.nullForNonContactsUri(UriUtils.parseUriOrNull(updatedInfo.getPhotoUri())); - if (!UriUtils.areEqual(updatedPhotoUriContactsOnly, UriUtils.parseUriOrNull(callLogInfo.getPhotoUri()))) { - values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(updatedPhotoUriContactsOnly)); - needsUpdate = true; - } - - if (!TextUtils.equals(updatedInfo.getGeoDescription(), callLogInfo.getGeoDescription())) { - values.put(Calls.GEOCODED_LOCATION, updatedInfo.getGeoDescription()); - needsUpdate = true; - } - } else { - // No previous values, store all of them. - values.put(Calls.CACHED_NAME, updatedInfo.getName()); - values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.getType()); - values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.getLabel()); - values.put(Calls.CACHED_LOOKUP_URI, updatedInfo.getLookupUri()); - values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.getNumber()); - values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.getNormalizedNumber()); - values.put(Calls.CACHED_PHOTO_ID, updatedInfo.getPhotoId()); - values.put(Calls.CACHED_PHOTO_URI, updatedInfo.getPhotoUri()); - values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.getFormattedNumber()); - values.put(Calls.GEOCODED_LOCATION, updatedInfo.getGeoDescription()); - needsUpdate = true; + public ContactInfoHelper(Context context) { + this.context = context; } - if (!needsUpdate) { - return; + /** + * Creates a JSON-encoded lookup uri for a unknown number without an associated contact + * + * @param number - Unknown phone number + * @return JSON-encoded URI that can be used to perform a lookup when clicking on the quick + * contact card. + */ + public static Uri createTemporaryContactUri(String number) { + try { + final JSONObject contactRows = + new JSONObject() + .put( + ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE, + new JSONObject() + .put(ContactsContract.CommonDataKinds.Phone.NUMBER, number) + .put(ContactsContract.CommonDataKinds.Phone.TYPE, ContactsContract.CommonDataKinds.Phone.TYPE_CUSTOM)); + + final String jsonString = + new JSONObject() + .put(ContactsContract.Contacts.DISPLAY_NAME, number) + .put(ContactsContract.Contacts.DISPLAY_NAME_SOURCE, ContactsContract.DisplayNameSources.PHONE) + .put(ContactsContract.Contacts.CONTENT_ITEM_TYPE, contactRows) + .toString(); + + return ContactsContract.Contacts.CONTENT_LOOKUP_URI + .buildUpon() + .appendPath("encoded") + .appendQueryParameter( + ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Long.MAX_VALUE)) + .encodedFragment(jsonString) + .build(); + } catch (JSONException e) { + return null; + } } - try { - if (countryIso == null) { - context - .getContentResolver() - .update( - Calls.CONTENT_URI, - values, - Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL", - new String[]{number}); - } else { - int updated = context - .getContentResolver() - .update( - Calls.CONTENT_URI, - values, - Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?", - new String[]{number, countryIso}); - } - } catch (SQLiteFullException e) { - Log.e(ContactInfoHelper.class.getSimpleName(), "Unable to update contact info in call log db", e); + /** + * Stores differences between the updated contact info and the current call log contact info. + * + * @param number The number of the contact. + * @param countryIso The country associated with this number. + * @param updatedInfo The updated contact info. + * @param callLogInfo The call log entry's current contact info. + */ + public void updateCallLogContactInfo( + String number, String countryIso, ContactInfo updatedInfo, ContactInfo callLogInfo) { + if (!PermissionUtils.hasRecentsPermission(context)) { + return; + } + + final ContentValues values = new ContentValues(); + boolean needsUpdate = false; + + if (callLogInfo != null) { + if (!TextUtils.equals(updatedInfo.getName(), callLogInfo.getName())) { + values.put(Calls.CACHED_NAME, updatedInfo.getName()); + needsUpdate = true; + } + + if (!Objects.equals(updatedInfo.getType(), callLogInfo.getType())) { + values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.getType()); + needsUpdate = true; + } + + if (!TextUtils.equals(updatedInfo.getLabel(), callLogInfo.getLabel())) { + values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.getLabel()); + needsUpdate = true; + } + + // Only replace the normalized number if the new updated normalized number isn't empty. + if (!TextUtils.isEmpty(updatedInfo.getNormalizedNumber()) + && !TextUtils.equals(updatedInfo.getNormalizedNumber(), callLogInfo.getNormalizedNumber())) { + values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.getNormalizedNumber()); + needsUpdate = true; + } + + if (!TextUtils.equals(updatedInfo.getNumber(), callLogInfo.getNumber())) { + values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.getNumber()); + needsUpdate = true; + } + + if (!Objects.equals(updatedInfo.getPhotoId(), callLogInfo.getPhotoId())) { + values.put(Calls.CACHED_PHOTO_ID, updatedInfo.getPhotoId()); + needsUpdate = true; + } + + final Uri updatedPhotoUriContactsOnly = UriUtils.nullForNonContactsUri(UriUtils.parseUriOrNull(updatedInfo.getPhotoUri())); + if (!UriUtils.areEqual(updatedPhotoUriContactsOnly, UriUtils.parseUriOrNull(callLogInfo.getPhotoUri()))) { + values.put(Calls.CACHED_PHOTO_URI, UriUtils.uriToString(updatedPhotoUriContactsOnly)); + needsUpdate = true; + } + + if (!TextUtils.equals(updatedInfo.getGeoDescription(), callLogInfo.getGeoDescription())) { + values.put(Calls.GEOCODED_LOCATION, updatedInfo.getGeoDescription()); + needsUpdate = true; + } + } else { + // No previous values, store all of them. + values.put(Calls.CACHED_NAME, updatedInfo.getName()); + values.put(Calls.CACHED_NUMBER_TYPE, updatedInfo.getType()); + values.put(Calls.CACHED_NUMBER_LABEL, updatedInfo.getLabel()); + values.put(Calls.CACHED_LOOKUP_URI, updatedInfo.getLookupUri()); + values.put(Calls.CACHED_MATCHED_NUMBER, updatedInfo.getNumber()); + values.put(Calls.CACHED_NORMALIZED_NUMBER, updatedInfo.getNormalizedNumber()); + values.put(Calls.CACHED_PHOTO_ID, updatedInfo.getPhotoId()); + values.put(Calls.CACHED_PHOTO_URI, updatedInfo.getPhotoUri()); + values.put(Calls.CACHED_FORMATTED_NUMBER, updatedInfo.getFormattedNumber()); + values.put(Calls.GEOCODED_LOCATION, updatedInfo.getGeoDescription()); + needsUpdate = true; + } + + if (!needsUpdate) { + return; + } + + try { + if (countryIso == null) { + context + .getContentResolver() + .update( + Calls.CONTENT_URI, + values, + Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " IS NULL", + new String[]{number}); + } else { + int updated = context + .getContentResolver() + .update( + Calls.CONTENT_URI, + values, + Calls.NUMBER + " = ? AND " + Calls.COUNTRY_ISO + " = ?", + new String[]{number, countryIso}); + } + } catch (SQLiteFullException e) { + Log.e(ContactInfoHelper.class.getSimpleName(), "Unable to update contact info in call log db", e); + } } - } - - public ContactInfo createEmptyContactInfoForNumber(String number, String countryIso) { - String formattedNumber = formatPhoneNumber(number, null, countryIso); - String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso); - return new ContactInfo( - null, - number, - null, - null, - null, - UriUtils.uriToString(createTemporaryContactUri(formattedNumber)), - normalizedNumber, - formattedNumber, - null, - null - ); - } - - /** - * Format the given phone number - * - * @param number the number to be formatted. - * @param normalizedNumber the normalized number of the given number. - * @param countryIso the ISO 3166-1 two letters country code, the country's convention will be - * used to format the number if the normalized phone is null. - * @return the formatted number, or the given number if it was formatted. - */ - public String formatPhoneNumber(String number, String normalizedNumber, String countryIso) { - if (TextUtils.isEmpty(number)) { - return ""; - } - // If "number" is really a SIP address, don't try to do any formatting at all. - if (PhoneNumberHelper.isUriNumber(number)) { - return number; + + public ContactInfo createEmptyContactInfoForNumber(String number, String countryIso) { + String formattedNumber = formatPhoneNumber(number, null, countryIso); + String normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso); + return new ContactInfo( + null, + number, + null, + null, + null, + UriUtils.uriToString(createTemporaryContactUri(formattedNumber)), + normalizedNumber, + formattedNumber, + null, + null + ); } - if (TextUtils.isEmpty(countryIso)) { - return number; + + /** + * Format the given phone number + * + * @param number the number to be formatted. + * @param normalizedNumber the normalized number of the given number. + * @param countryIso the ISO 3166-1 two letters country code, the country's convention will be + * used to format the number if the normalized phone is null. + * @return the formatted number, or the given number if it was formatted. + */ + public String formatPhoneNumber(String number, String normalizedNumber, String countryIso) { + if (TextUtils.isEmpty(number)) { + return ""; + } + // If "number" is really a SIP address, don't try to do any formatting at all. + if (PhoneNumberHelper.isUriNumber(number)) { + return number; + } + if (TextUtils.isEmpty(countryIso)) { + return number; + } + return PhoneNumberHelper.formatNumber(context, number, normalizedNumber, countryIso); } - return PhoneNumberHelper.formatNumber(context, number, normalizedNumber, countryIso); - } } diff --git a/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/ContactInfoRequest.kt b/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/ContactInfoRequest.kt index 1cee7a8..2bde3c3 100644 --- a/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/ContactInfoRequest.kt +++ b/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/ContactInfoRequest.kt @@ -1,7 +1,7 @@ package dev.alenajam.opendialer.data.callsCache class ContactInfoRequest( - number: String?, - countryIso: String?, - val callLogInfo: ContactInfo + number: String?, + countryIso: String?, + val callLogInfo: ContactInfo ) : NumberWithCountryIso(number, countryIso) \ No newline at end of file diff --git a/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/NumberWithCountryIso.kt b/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/NumberWithCountryIso.kt index 57a33b7..a63e2fe 100644 --- a/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/NumberWithCountryIso.kt +++ b/data/callsCache/src/main/java/dev/alenajam/opendialer/data/callsCache/NumberWithCountryIso.kt @@ -1,20 +1,20 @@ package dev.alenajam.opendialer.data.callsCache open class NumberWithCountryIso( - val number: String?, - val countryIso: String? + val number: String?, + val countryIso: String? ) { - override fun equals(other: Any?): Boolean { - if (other == null) return false + override fun equals(other: Any?): Boolean { + if (other == null) return false - if (other !is NumberWithCountryIso) return false + if (other !is NumberWithCountryIso) return false - return number == other.number && countryIso == other.countryIso - } + return number == other.number && countryIso == other.countryIso + } - override fun hashCode(): Int { - val numberHashCode = number?.hashCode() ?: 0 - val countryIsoHashCode = countryIso?.hashCode() ?: 0 - return numberHashCode.xor(countryIsoHashCode) - } + override fun hashCode(): Int { + val numberHashCode = number?.hashCode() ?: 0 + val countryIsoHashCode = countryIso?.hashCode() ?: 0 + return numberHashCode.xor(countryIsoHashCode) + } } \ No newline at end of file diff --git a/data/callsCache/src/test/java/com/example/callscache/ExampleUnitTest.kt b/data/callsCache/src/test/java/com/example/callscache/ExampleUnitTest.kt index fda3d4e..99f2d72 100644 --- a/data/callsCache/src/test/java/com/example/callscache/ExampleUnitTest.kt +++ b/data/callsCache/src/test/java/com/example/callscache/ExampleUnitTest.kt @@ -1,8 +1,7 @@ package com.example.callscache -import org.junit.Test - import org.junit.Assert.* +import org.junit.Test /** * Example local unit test, which will execute on the development machine (host). @@ -10,8 +9,8 @@ import org.junit.Assert.* * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } } \ No newline at end of file diff --git a/data/contacts/build.gradle.kts b/data/contacts/build.gradle.kts index 4229216..8507bcc 100644 --- a/data/contacts/build.gradle.kts +++ b/data/contacts/build.gradle.kts @@ -1,43 +1,46 @@ plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") - id("kotlin-kapt") - id("com.google.dagger.hilt.android") + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("kotlin-kapt") + id("com.google.dagger.hilt.android") } android { - namespace = "dev.alenajam.opendialer.data.contacts" - compileSdk = 34 + namespace = "dev.alenajam.opendialer.data.contacts" + compileSdk = 34 - defaultConfig { - minSdk = 24 + defaultConfig { + minSdk = 24 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } } - } } dependencies { - implementation("androidx.core:core-ktx:1.12.0") - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation("androidx.core:core-ktx:1.12.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - implementation("com.google.dagger:hilt-android:2.48.1") - kapt("com.google.dagger:hilt-compiler:2.48.1") - androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1") - testImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptTest("com.google.dagger:hilt-compiler:2.48.1") + implementation("com.google.dagger:hilt-android:2.48.1") + kapt("com.google.dagger:hilt-compiler:2.48.1") + androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1") + testImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptTest("com.google.dagger:hilt-compiler:2.48.1") } kotlin { - jvmToolchain(17) + jvmToolchain(17) } \ No newline at end of file diff --git a/data/contacts/src/androidTest/java/dev/alenajam/opendialer/data/contacts/ExampleInstrumentedTest.kt b/data/contacts/src/androidTest/java/dev/alenajam/opendialer/data/contacts/ExampleInstrumentedTest.kt index e606e74..d02d5a7 100644 --- a/data/contacts/src/androidTest/java/dev/alenajam/opendialer/data/contacts/ExampleInstrumentedTest.kt +++ b/data/contacts/src/androidTest/java/dev/alenajam/opendialer/data/contacts/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package dev.alenajam.opendialer.data.contacts -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -15,10 +13,10 @@ import org.junit.Assert.* */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("dev.alenajam.opendialer.data.contacts.test", appContext.packageName) - } + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("dev.alenajam.opendialer.data.contacts.test", appContext.packageName) + } } \ No newline at end of file diff --git a/data/contacts/src/main/AndroidManifest.xml b/data/contacts/src/main/AndroidManifest.xml index a5918e6..44008a4 100644 --- a/data/contacts/src/main/AndroidManifest.xml +++ b/data/contacts/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/ContactsData.kt b/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/ContactsData.kt index 99250bb..7d9727c 100644 --- a/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/ContactsData.kt +++ b/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/ContactsData.kt @@ -6,46 +6,50 @@ import android.net.Uri import android.provider.ContactsContract abstract class ContactsData { - companion object { - val URI: Uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI + companion object { + val URI: Uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI - private val projection = arrayOf( - ContactsContract.CommonDataKinds.Phone.CONTACT_ID, - ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY, - ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, - ContactsContract.CommonDataKinds.Phone.STARRED, - ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI - ) + private val projection = arrayOf( + ContactsContract.CommonDataKinds.Phone.CONTACT_ID, + ContactsContract.CommonDataKinds.Phone.LOOKUP_KEY, + ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, + ContactsContract.CommonDataKinds.Phone.STARRED, + ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI + ) - private const val where = - "${ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME} IS NOT NULL" - private const val sort = - "${ContactsContract.CommonDataKinds.Phone.STARRED} DESC, ${ContactsContract.CommonDataKinds.Phone.SORT_KEY_PRIMARY}" + private const val where = + "${ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME} IS NOT NULL" + private const val sort = + "${ContactsContract.CommonDataKinds.Phone.STARRED} DESC, ${ContactsContract.CommonDataKinds.Phone.SORT_KEY_PRIMARY}" - fun getCursor(contentResolver: ContentResolver): Cursor? = contentResolver.query( - URI, - projection, - where, - null, - sort - ) + fun getCursor(contentResolver: ContentResolver): Cursor? = contentResolver.query( + URI, + projection, + where, + null, + sort + ) - fun getData(cursor: Cursor): List { - val list = mutableListOf() - if (cursor.moveToFirst()) { - do { - list.add( - DialerContactEntity( - id = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)), - name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)), - photoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI)) - ?.takeIf { it.isNotBlank() }, - starred = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.STARRED)) - ) - ) - } while (cursor.moveToNext()) - } - return list + fun getData(cursor: Cursor): List { + val list = mutableListOf() + if (cursor.moveToFirst()) { + do { + list.add( + DialerContactEntity( + id = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)), + name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)), + photoUri = cursor.getString( + cursor.getColumnIndexOrThrow( + ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI + ) + ) + ?.takeIf { it.isNotBlank() }, + starred = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.STARRED)) + ) + ) + } while (cursor.moveToNext()) + } + return list + } } - } } \ No newline at end of file diff --git a/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/ContactsRepository.kt b/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/ContactsRepository.kt new file mode 100644 index 0000000..0da5c11 --- /dev/null +++ b/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/ContactsRepository.kt @@ -0,0 +1,7 @@ +package dev.alenajam.opendialer.data.contacts + +import kotlinx.coroutines.flow.Flow + +interface ContactsRepository { + fun getContacts(): Flow> +} \ No newline at end of file diff --git a/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/ContactsRepositoryImpl.kt b/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/ContactsRepositoryImpl.kt new file mode 100644 index 0000000..990059a --- /dev/null +++ b/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/ContactsRepositoryImpl.kt @@ -0,0 +1,34 @@ +package dev.alenajam.opendialer.data.contacts + +import android.app.Application +import android.database.ContentObserver +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import javax.inject.Inject + +class ContactsRepositoryImpl +@Inject constructor(private val app: Application) : ContactsRepository { + override fun getContacts(): Flow> = + callbackFlow { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + ContactsData.getCursor(app.contentResolver)?.let { + trySend(ContactsData.getData(it)) + it.close() + } + } + } + + app.contentResolver.registerContentObserver(ContactsData.URI, true, observer) + + ContactsData.getCursor(app.contentResolver)?.let { + trySend(ContactsData.getData(it)) + it.close() + } + + awaitClose { + app.contentResolver.unregisterContentObserver(observer) + } + } +} \ No newline at end of file diff --git a/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerContact.kt b/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerContact.kt index 20fbfc6..88c6d70 100644 --- a/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerContact.kt +++ b/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerContact.kt @@ -1,23 +1,23 @@ package dev.alenajam.opendialer.data.contacts class DialerContact( - val id: Int, - val name: String, - val starred: Boolean, - val image: String? + val id: Int, + val name: String, + val starred: Boolean, + val image: String? ) { - companion object { - fun mapList(list: List): List { - return list.map { map(it) } - } + companion object { + fun mapList(list: List): List { + return list.map { map(it) } + } - fun map(contact: DialerContactEntity): DialerContact { - return DialerContact( - id = contact.id, - name = contact.name, - image = contact.photoUri, - starred = contact.starred == 1 - ) + fun map(contact: DialerContactEntity): DialerContact { + return DialerContact( + id = contact.id, + name = contact.name, + image = contact.photoUri, + starred = contact.starred == 1 + ) + } } - } } \ No newline at end of file diff --git a/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerContactEntity.kt b/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerContactEntity.kt index b0ecf34..5de7360 100644 --- a/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerContactEntity.kt +++ b/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerContactEntity.kt @@ -1,8 +1,8 @@ package dev.alenajam.opendialer.data.contacts class DialerContactEntity( - val id: Int, - val name: String, - val starred: Int, - val photoUri: String? + val id: Int, + val name: String, + val starred: Int, + val photoUri: String? ) \ No newline at end of file diff --git a/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerRepository.kt b/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerRepository.kt deleted file mode 100644 index 60713a1..0000000 --- a/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerRepository.kt +++ /dev/null @@ -1,8 +0,0 @@ -package dev.alenajam.opendialer.data.contacts - -import android.content.ContentResolver -import kotlinx.coroutines.flow.Flow - -interface DialerRepository { - fun getContacts(contentResolver: ContentResolver): Flow> -} \ No newline at end of file diff --git a/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerRepositoryImpl.kt b/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerRepositoryImpl.kt deleted file mode 100644 index 94e3fc8..0000000 --- a/data/contacts/src/main/java/dev/alenajam/opendialer/data/contacts/DialerRepositoryImpl.kt +++ /dev/null @@ -1,34 +0,0 @@ -package dev.alenajam.opendialer.data.contacts - -import android.content.ContentResolver -import android.database.ContentObserver -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import javax.inject.Inject - -class DialerRepositoryImpl -@Inject constructor() : DialerRepository { - override fun getContacts(contentResolver: ContentResolver): Flow> = - callbackFlow { - val observer = object : ContentObserver(null) { - override fun onChange(selfChange: Boolean) { - ContactsData.getCursor(contentResolver)?.let { - trySend(ContactsData.getData(it)) - it.close() - } - } - } - - contentResolver.registerContentObserver(ContactsData.URI, true, observer) - - ContactsData.getCursor(contentResolver)?.let { - trySend(ContactsData.getData(it)) - it.close() - } - - awaitClose { - contentResolver.unregisterContentObserver(observer) - } - } -} \ No newline at end of file diff --git a/data/contacts/src/test/java/dev/alenajam/opendialer/data/contacts/ExampleUnitTest.kt b/data/contacts/src/test/java/dev/alenajam/opendialer/data/contacts/ExampleUnitTest.kt index 044abd6..5973dbe 100644 --- a/data/contacts/src/test/java/dev/alenajam/opendialer/data/contacts/ExampleUnitTest.kt +++ b/data/contacts/src/test/java/dev/alenajam/opendialer/data/contacts/ExampleUnitTest.kt @@ -1,8 +1,7 @@ package dev.alenajam.opendialer.data.contacts -import org.junit.Test - import org.junit.Assert.* +import org.junit.Test /** * Example local unit test, which will execute on the development machine (host). @@ -10,8 +9,8 @@ import org.junit.Assert.* * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } } \ No newline at end of file diff --git a/data/contactsSearch/build.gradle.kts b/data/contactsSearch/build.gradle.kts index 4928a68..9bdae78 100644 --- a/data/contactsSearch/build.gradle.kts +++ b/data/contactsSearch/build.gradle.kts @@ -1,46 +1,49 @@ plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") - id("kotlin-kapt") - id("com.google.dagger.hilt.android") + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("kotlin-kapt") + id("com.google.dagger.hilt.android") } android { - namespace = "dev.alenajam.opendialer.data.contactsSearch" - compileSdk = 34 + namespace = "dev.alenajam.opendialer.data.contactsSearch" + compileSdk = 34 - defaultConfig { - minSdk = 24 + defaultConfig { + minSdk = 24 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } } - } } dependencies { - implementation(project(":core:common")) - implementation(project(":core:aosp")) - - implementation("androidx.core:core-ktx:1.12.0") - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - - implementation("com.google.dagger:hilt-android:2.48.1") - kapt("com.google.dagger:hilt-compiler:2.48.1") - androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1") - testImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptTest("com.google.dagger:hilt-compiler:2.48.1") + implementation(project(":core:common")) + implementation(project(":core:aosp")) + + implementation("androidx.core:core-ktx:1.12.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + implementation("com.google.dagger:hilt-android:2.48.1") + kapt("com.google.dagger:hilt-compiler:2.48.1") + androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1") + testImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptTest("com.google.dagger:hilt-compiler:2.48.1") } kotlin { - jvmToolchain(17) + jvmToolchain(17) } \ No newline at end of file diff --git a/data/contactsSearch/src/androidTest/java/dev/alenajam/opendialer/data/contactsSearch/ExampleInstrumentedTest.kt b/data/contactsSearch/src/androidTest/java/dev/alenajam/opendialer/data/contactsSearch/ExampleInstrumentedTest.kt index 256944c..c69c294 100644 --- a/data/contactsSearch/src/androidTest/java/dev/alenajam/opendialer/data/contactsSearch/ExampleInstrumentedTest.kt +++ b/data/contactsSearch/src/androidTest/java/dev/alenajam/opendialer/data/contactsSearch/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package dev.alenajam.opendialer.data.contactsSearch -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -15,10 +13,10 @@ import org.junit.Assert.* */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("dev.alenajam.opendialer.data.contactsSearch.test", appContext.packageName) - } + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("dev.alenajam.opendialer.data.contactsSearch.test", appContext.packageName) + } } \ No newline at end of file diff --git a/data/contactsSearch/src/main/AndroidManifest.xml b/data/contactsSearch/src/main/AndroidManifest.xml index a5918e6..44008a4 100644 --- a/data/contactsSearch/src/main/AndroidManifest.xml +++ b/data/contactsSearch/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerRepository.kt b/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerRepository.kt index c3d23b0..765766a 100644 --- a/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerRepository.kt +++ b/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerRepository.kt @@ -5,13 +5,13 @@ import dev.alenajam.opendialer.core.common.exception.Failure import dev.alenajam.opendialer.core.common.functional.Either interface DialerRepository { - suspend fun searchContacts( - contentResolver: ContentResolver, - query: String - ): Either> + suspend fun searchContacts( + contentResolver: ContentResolver, + query: String + ): Either> - suspend fun searchContactsDialpad( - contentResolver: ContentResolver, - query: String - ): Either> + suspend fun searchContactsDialpad( + contentResolver: ContentResolver, + query: String + ): Either> } \ No newline at end of file diff --git a/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerRepositoryImpl.kt b/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerRepositoryImpl.kt index 91912c1..ec44076 100644 --- a/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerRepositoryImpl.kt +++ b/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerRepositoryImpl.kt @@ -8,29 +8,29 @@ import javax.inject.Inject class DialerRepositoryImpl @Inject constructor(private val app: Application) : DialerRepository { - override suspend fun searchContacts( - contentResolver: ContentResolver, - query: String - ): Either> { - SearchContactsData.getCursor(contentResolver, query)?.let { - val data = SearchContactsData.getData(it) - it.close() - return Either.Right(data) + override suspend fun searchContacts( + contentResolver: ContentResolver, + query: String + ): Either> { + SearchContactsData.getCursor(contentResolver, query)?.let { + val data = SearchContactsData.getData(it) + it.close() + return Either.Right(data) + } + + return Either.Left(Failure.NoData) } - return Either.Left(Failure.NoData) - } + override suspend fun searchContactsDialpad( + contentResolver: ContentResolver, + query: String + ): Either> { + SearchContactsDialpadData.getCursor(contentResolver)?.let { + val data = SearchContactsDialpadData.getData(app, it, query) + it.close() + return Either.Right(data) + } - override suspend fun searchContactsDialpad( - contentResolver: ContentResolver, - query: String - ): Either> { - SearchContactsDialpadData.getCursor(contentResolver)?.let { - val data = SearchContactsDialpadData.getData(app, it, query) - it.close() - return Either.Right(data) + return Either.Left(Failure.NoData) } - - return Either.Left(Failure.NoData) - } } \ No newline at end of file diff --git a/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerSearchContact.kt b/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerSearchContact.kt index ce13d96..2c999a4 100644 --- a/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerSearchContact.kt +++ b/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerSearchContact.kt @@ -1,27 +1,27 @@ package dev.alenajam.opendialer.data.contactsSearch class DialerSearchContact( - val id: Int, - val name: String, - val label: String?, - val contactId: Int, - val number: String, - val image: String? + val id: Int, + val name: String, + val label: String?, + val contactId: Int, + val number: String, + val image: String? ) { - companion object { - fun mapList(list: List): List { - return list.map { map(it) } - } + companion object { + fun mapList(list: List): List { + return list.map { map(it) } + } - fun map(contact: DialerSearchContactEntity): DialerSearchContact { - return DialerSearchContact( - id = contact.id, - name = contact.name, - image = contact.photoUri, - number = contact.number, - contactId = contact.contactId, - label = contact.label - ) + fun map(contact: DialerSearchContactEntity): DialerSearchContact { + return DialerSearchContact( + id = contact.id, + name = contact.name, + image = contact.photoUri, + number = contact.number, + contactId = contact.contactId, + label = contact.label + ) + } } - } } \ No newline at end of file diff --git a/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerSearchContactEntity.kt b/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerSearchContactEntity.kt index 92cca5d..5291fc8 100644 --- a/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerSearchContactEntity.kt +++ b/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/DialerSearchContactEntity.kt @@ -1,10 +1,10 @@ package dev.alenajam.opendialer.data.contactsSearch class DialerSearchContactEntity( - val id: Int, - val name: String, - val label: String?, - val contactId: Int, - val number: String, - val photoUri: String? + val id: Int, + val name: String, + val label: String?, + val contactId: Int, + val number: String, + val photoUri: String? ) \ No newline at end of file diff --git a/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/SearchContactsData.kt b/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/SearchContactsData.kt index 71e1fb9..553c4bb 100644 --- a/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/SearchContactsData.kt +++ b/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/SearchContactsData.kt @@ -6,48 +6,52 @@ import android.net.Uri import android.provider.ContactsContract abstract class SearchContactsData { - companion object { - private val URI: Uri = ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI + companion object { + private val URI: Uri = ContactsContract.CommonDataKinds.Phone.CONTENT_FILTER_URI - private val projection = arrayOf( - ContactsContract.CommonDataKinds.Phone._ID, - ContactsContract.CommonDataKinds.Phone.LABEL, - ContactsContract.CommonDataKinds.Phone.NUMBER, - ContactsContract.CommonDataKinds.Phone.CONTACT_ID, - ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, - ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI - ) + private val projection = arrayOf( + ContactsContract.CommonDataKinds.Phone._ID, + ContactsContract.CommonDataKinds.Phone.LABEL, + ContactsContract.CommonDataKinds.Phone.NUMBER, + ContactsContract.CommonDataKinds.Phone.CONTACT_ID, + ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME, + ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI + ) - private const val where = - "${ContactsContract.CommonDataKinds.Phone.NUMBER} IS NOT NULL AND ${ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME} IS NOT NULL" + private const val where = + "${ContactsContract.CommonDataKinds.Phone.NUMBER} IS NOT NULL AND ${ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME} IS NOT NULL" - fun getCursor(contentResolver: ContentResolver, query: String): Cursor? = - contentResolver.query( - URI.buildUpon().appendPath(query).build(), - projection, - where, - null, - ContactsContract.CommonDataKinds.Phone.SORT_KEY_PRIMARY - ) - - fun getData(cursor: Cursor): List { - val list = mutableListOf() - if (cursor.moveToFirst()) { - do { - list.add( - DialerSearchContactEntity( - id = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)), - name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)), - photoUri = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI)) - ?.takeIf { it.isNotBlank() }, - label = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL)), - contactId = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)), - number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)) + fun getCursor(contentResolver: ContentResolver, query: String): Cursor? = + contentResolver.query( + URI.buildUpon().appendPath(query).build(), + projection, + where, + null, + ContactsContract.CommonDataKinds.Phone.SORT_KEY_PRIMARY ) - ) - } while (cursor.moveToNext()) - } - return list + + fun getData(cursor: Cursor): List { + val list = mutableListOf() + if (cursor.moveToFirst()) { + do { + list.add( + DialerSearchContactEntity( + id = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)), + name = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)), + photoUri = cursor.getString( + cursor.getColumnIndexOrThrow( + ContactsContract.CommonDataKinds.Phone.PHOTO_THUMBNAIL_URI + ) + ) + ?.takeIf { it.isNotBlank() }, + label = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.LABEL)), + contactId = cursor.getInt(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.CONTACT_ID)), + number = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.NUMBER)) + ) + ) + } while (cursor.moveToNext()) + } + return list + } } - } } \ No newline at end of file diff --git a/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/SearchContactsDialpadData.kt b/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/SearchContactsDialpadData.kt index ca33bc3..0d9dd9b 100644 --- a/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/SearchContactsDialpadData.kt +++ b/data/contactsSearch/src/main/java/dev/alenajam/opendialer/data/contactsSearch/SearchContactsDialpadData.kt @@ -10,81 +10,81 @@ import dev.alenajam.opendialer.core.aosp.ContactMatch import dev.alenajam.opendialer.core.aosp.SmartDialNameMatcher abstract class SearchContactsDialpadData { - companion object { - private val URI: Uri = Phone.CONTENT_URI - private const val MAX_ENTRIES = 20 + companion object { + private val URI: Uri = Phone.CONTENT_URI + private const val MAX_ENTRIES = 20 - private val projection = arrayOf( - Phone._ID, - Phone.LABEL, - Phone.NUMBER, - Phone.CONTACT_ID, - Phone.DISPLAY_NAME, - Phone.PHOTO_THUMBNAIL_URI, - Phone.LOOKUP_KEY, - Phone.IS_PRIMARY - ) + private val projection = arrayOf( + Phone._ID, + Phone.LABEL, + Phone.NUMBER, + Phone.CONTACT_ID, + Phone.DISPLAY_NAME, + Phone.PHOTO_THUMBNAIL_URI, + Phone.LOOKUP_KEY, + Phone.IS_PRIMARY + ) - private const val where = - "${Phone.NUMBER} IS NOT NULL AND ${Phone.DISPLAY_NAME} IS NOT NULL" - private const val sort = """ + private const val where = + "${Phone.NUMBER} IS NOT NULL AND ${Phone.DISPLAY_NAME} IS NOT NULL" + private const val sort = """ ${Phone.STARRED} DESC, ${Phone.IS_SUPER_PRIMARY} DESC, ${Phone.DISPLAY_NAME_PRIMARY}, ${Phone.IS_PRIMARY} DESC """ - fun getCursor(contentResolver: ContentResolver): Cursor? = contentResolver.query( - URI - .buildUpon() - .appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true") - .build(), - projection, - where, - null, - sort - ) + fun getCursor(contentResolver: ContentResolver): Cursor? = contentResolver.query( + URI + .buildUpon() + .appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true") + .build(), + projection, + where, + null, + sort + ) - fun getData( - context: Context, - cursor: Cursor, - query: String - ): List { - val nameMatcher = SmartDialNameMatcher(query) - val duplicates = HashSet() - val list = mutableListOf() - if (cursor.moveToFirst()) { - do { - val name = cursor.getString(cursor.getColumnIndexOrThrow(Phone.DISPLAY_NAME)) - val number = cursor.getString(cursor.getColumnIndexOrThrow(Phone.NUMBER)) - val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(Phone.LOOKUP_KEY)) - val contactId = cursor.getLong(cursor.getColumnIndexOrThrow(Phone.CONTACT_ID)) + fun getData( + context: Context, + cursor: Cursor, + query: String + ): List { + val nameMatcher = SmartDialNameMatcher(query) + val duplicates = HashSet() + val list = mutableListOf() + if (cursor.moveToFirst()) { + do { + val name = cursor.getString(cursor.getColumnIndexOrThrow(Phone.DISPLAY_NAME)) + val number = cursor.getString(cursor.getColumnIndexOrThrow(Phone.NUMBER)) + val lookupKey = cursor.getString(cursor.getColumnIndexOrThrow(Phone.LOOKUP_KEY)) + val contactId = cursor.getLong(cursor.getColumnIndexOrThrow(Phone.CONTACT_ID)) - val contactMatch = ContactMatch(lookupKey, contactId) - if (duplicates.contains(contactMatch)) { - continue - } + val contactMatch = ContactMatch(lookupKey, contactId) + if (duplicates.contains(contactMatch)) { + continue + } - val nameMatches = nameMatcher.matches(context, name) - val numberMatches = nameMatcher.matchesNumber(context, number, query) != null + val nameMatches = nameMatcher.matches(context, name) + val numberMatches = nameMatcher.matchesNumber(context, number, query) != null - if (nameMatches || numberMatches) { - duplicates.add(contactMatch) - list.add( - DialerSearchContactEntity( - id = cursor.getInt(cursor.getColumnIndexOrThrow(Phone.CONTACT_ID)), - name = name, - photoUri = cursor.getString(cursor.getColumnIndexOrThrow(Phone.PHOTO_THUMBNAIL_URI)) - ?.takeIf { it.isNotBlank() }, - label = cursor.getString(cursor.getColumnIndexOrThrow(Phone.LABEL)), - contactId = contactId.toInt(), - number = number - ) - ) - } - } while (cursor.moveToNext() && list.size < MAX_ENTRIES) - } - return list + if (nameMatches || numberMatches) { + duplicates.add(contactMatch) + list.add( + DialerSearchContactEntity( + id = cursor.getInt(cursor.getColumnIndexOrThrow(Phone.CONTACT_ID)), + name = name, + photoUri = cursor.getString(cursor.getColumnIndexOrThrow(Phone.PHOTO_THUMBNAIL_URI)) + ?.takeIf { it.isNotBlank() }, + label = cursor.getString(cursor.getColumnIndexOrThrow(Phone.LABEL)), + contactId = contactId.toInt(), + number = number + ) + ) + } + } while (cursor.moveToNext() && list.size < MAX_ENTRIES) + } + return list + } } - } } \ No newline at end of file diff --git a/data/contactsSearch/src/test/java/dev/alenajam/opendialer/data/contactsSearch/ExampleUnitTest.kt b/data/contactsSearch/src/test/java/dev/alenajam/opendialer/data/contactsSearch/ExampleUnitTest.kt index c3d858e..2c1f3b7 100644 --- a/data/contactsSearch/src/test/java/dev/alenajam/opendialer/data/contactsSearch/ExampleUnitTest.kt +++ b/data/contactsSearch/src/test/java/dev/alenajam/opendialer/data/contactsSearch/ExampleUnitTest.kt @@ -1,8 +1,7 @@ package dev.alenajam.opendialer.data.contactsSearch -import org.junit.Test - import org.junit.Assert.* +import org.junit.Test /** * Example local unit test, which will execute on the development machine (host). @@ -10,8 +9,8 @@ import org.junit.Assert.* * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } } \ No newline at end of file diff --git a/docs/ModularizationLearningJourney.md b/docs/ModularizationLearningJourney.md index 72b15eb..450bdd1 100644 --- a/docs/ModularizationLearningJourney.md +++ b/docs/ModularizationLearningJourney.md @@ -5,13 +5,11 @@ This learning journey was inspired by [Now in Android](https://github.com/androi In this learning journey you will learn about modularization, and the modularization strategy used to create the modules in the OpenDialer app. - ## Overview Modularization is the practice of breaking the concept of a monolithic, one-module codebase into loosely coupled, self contained modules. - ### Benefits of modularization This offers many benefits, including: @@ -38,7 +36,6 @@ allows certain features of your app to be delivered conditionally or downloaded **Reusability** - Proper modularization enables opportunities for code sharing and building multiple apps, across different platforms, from the same foundation. - ### Modularization pitfalls However, modularization is a pattern that can be misused, and there are some gotchas to be aware of @@ -60,7 +57,6 @@ your project. A dominating factor is the size and relative complexity of the cod project is not expected to grow beyond a certain threshold, the scalability and build time gains won’t apply. - ## Modularization strategy It’s important to note that there is no single modularization strategy that fits all projects. @@ -79,7 +75,6 @@ how you can organize your project. In general, you should strive for low couplin * **High cohesion** - A module should comprise a collection of code that acts as a system. It should have clearly defined responsibilities and stay within boundaries of certain domain knowledge. - ## Types of modules in OpenDialer ![Diagram showing types of modules and their dependencies in OpenDialer](./images/app_modularization.png "Diagram showing types of modules and their dependencies in OpenDialer") @@ -90,7 +85,8 @@ visualizing dependencies between modules. The OpenDialer app contains the following types of modules: * The `app` module - contains app level and scaffolding classes that bind the rest of the codebase, - such as `MainActivity`, `MainFragment`, `App` and app-level controlled navigation. A good example of this is + such as `MainActivity`, `MainFragment`, `App` and app-level controlled navigation. A good example + of this is the navigation setup through `main.xml` and the bottom navigation bar setup through `MainFragment`. The `app` module depends on all `feature` modules and required `core` modules. @@ -115,7 +111,6 @@ The OpenDialer app contains the following types of modules: * Miscellaneous modules - these are yet to be created. - ## Modules Using the above modularization strategy, the OpenDialer app has the following modules: diff --git a/feature/callDetail/build.gradle.kts b/feature/callDetail/build.gradle.kts index e54d78b..6c13792 100644 --- a/feature/callDetail/build.gradle.kts +++ b/feature/callDetail/build.gradle.kts @@ -1,72 +1,98 @@ plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") - id("kotlin-kapt") - id("com.google.dagger.hilt.android") + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("kotlin-kapt") + id("com.google.dagger.hilt.android") + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) } android { - namespace = "dev.alenajam.opendialer.feature.callDetail" - compileSdk = 34 + namespace = "dev.alenajam.opendialer.feature.callDetail" + compileSdk = 34 - defaultConfig { - minSdk = 24 + defaultConfig { + minSdk = 24 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + buildFeatures { + viewBinding = true + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.2" } - } - buildFeatures { - viewBinding = true - } } dependencies { - implementation(project(":data:calls")) - implementation(project(":data:callsCache")) - implementation(project(":data:contacts")) - implementation(project(":core:common")) + implementation(project(":data:calls")) + implementation(project(":data:callsCache")) + implementation(project(":data:contacts")) + implementation(project(":core:common")) + + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.10.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + implementation("com.google.dagger:hilt-android:2.48.1") + kapt("com.google.dagger:hilt-compiler:2.48.1") + androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1") + testImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptTest("com.google.dagger:hilt-compiler:2.48.1") - implementation("androidx.core:core-ktx:1.12.0") - implementation("androidx.appcompat:appcompat:1.6.1") - implementation("com.google.android.material:material:1.10.0") - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.6.2") - implementation("com.google.dagger:hilt-android:2.48.1") - kapt("com.google.dagger:hilt-compiler:2.48.1") - androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1") - testImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptTest("com.google.dagger:hilt-compiler:2.48.1") + implementation("androidx.fragment:fragment-ktx:1.6.2") + implementation("androidx.legacy:legacy-support-v4:1.0.0") + implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.6.2") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.5") + implementation("androidx.navigation:navigation-ui-ktx:2.7.5") + implementation("androidx.preference:preference-ktx:1.2.1") + implementation("androidx.recyclerview:recyclerview:1.3.2") - implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") - implementation("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.6.2") + implementation("com.squareup.picasso:picasso:2.71828") + implementation("org.ocpsoft.prettytime:prettytime:4.0.1.Final") - implementation("androidx.fragment:fragment-ktx:1.6.2") - implementation("androidx.legacy:legacy-support-v4:1.0.0") - implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") - implementation("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.6.2") - implementation("androidx.navigation:navigation-fragment-ktx:2.7.5") - implementation("androidx.navigation:navigation-ui-ktx:2.7.5") - implementation("androidx.preference:preference-ktx:1.2.1") - implementation("androidx.recyclerview:recyclerview:1.3.2") + val composeBom = platform("androidx.compose:compose-bom:2024.09.03") + implementation(composeBom) + androidTestImplementation(composeBom) + implementation(libs.androidx.material3) + implementation("androidx.compose.ui:ui-tooling-preview") + debugImplementation("androidx.compose.ui:ui-tooling") + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-test-manifest") + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") + implementation("androidx.compose.runtime:runtime-livedata") - implementation("com.squareup.picasso:picasso:2.71828") - implementation("org.ocpsoft.prettytime:prettytime:4.0.1.Final") + implementation("io.coil-kt:coil-compose:2.5.0") + implementation(libs.hilt.navigation.compose) + implementation(libs.kotlinx.serialization) + implementation(libs.navigation.compose) } kotlin { - jvmToolchain(17) + jvmToolchain(17) } \ No newline at end of file diff --git a/feature/callDetail/src/androidTest/java/dev/alenajam/opendialer/feature/callDetail/ExampleInstrumentedTest.kt b/feature/callDetail/src/androidTest/java/dev/alenajam/opendialer/feature/callDetail/ExampleInstrumentedTest.kt index 109a559..143e7c4 100644 --- a/feature/callDetail/src/androidTest/java/dev/alenajam/opendialer/feature/callDetail/ExampleInstrumentedTest.kt +++ b/feature/callDetail/src/androidTest/java/dev/alenajam/opendialer/feature/callDetail/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package dev.alenajam.opendialer.feature.callDetail -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -15,10 +13,10 @@ import org.junit.Assert.* */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("dev.alenajam.opendialer.feature.callDetail.test", appContext.packageName) - } + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("dev.alenajam.opendialer.feature.callDetail.test", appContext.packageName) + } } \ No newline at end of file diff --git a/feature/callDetail/src/main/AndroidManifest.xml b/feature/callDetail/src/main/AndroidManifest.xml index a5918e6..44008a4 100644 --- a/feature/callDetail/src/main/AndroidManifest.xml +++ b/feature/callDetail/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/BlockCaller.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/BlockCaller.kt index 6d097ea..cbd9451 100644 --- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/BlockCaller.kt +++ b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/BlockCaller.kt @@ -3,13 +3,13 @@ package dev.alenajam.opendialer.feature.callDetail import dev.alenajam.opendialer.core.common.exception.Failure import dev.alenajam.opendialer.core.common.functional.Either import dev.alenajam.opendialer.core.common.interactor.UseCase -import dev.alenajam.opendialer.data.calls.DialerRepositoryImpl +import dev.alenajam.opendialer.data.calls.CallsRepositoryImpl import javax.inject.Inject class BlockCaller -@Inject constructor(private val dialerRepositoryImpl: DialerRepositoryImpl) : - UseCase() { - override suspend fun run(params: String): Either = - dialerRepositoryImpl.blockCaller(params) +@Inject constructor(private val callsRepositoryImpl: CallsRepositoryImpl) : + UseCase() { + override suspend fun run(params: String): Either = + callsRepositoryImpl.blockCaller(params) } \ No newline at end of file diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailFragment.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailFragment.kt deleted file mode 100644 index bafe99c..0000000 --- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailFragment.kt +++ /dev/null @@ -1,221 +0,0 @@ -package dev.alenajam.opendialer.feature.callDetail - -import android.content.Context -import android.graphics.Color -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.activity.result.contract.ActivityResultContracts -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import com.squareup.picasso.Picasso -import com.squareup.picasso.Transformation -import dagger.hilt.android.AndroidEntryPoint -import dev.alenajam.opendialer.core.common.CALL_DETAIL_PARAM_CALL_IDS -import dev.alenajam.opendialer.core.common.CircleTransform -import dev.alenajam.opendialer.core.common.OnStatusBarColorChange -import dev.alenajam.opendialer.core.common.PermissionUtils -import dev.alenajam.opendialer.core.common.SharedPreferenceHelper -import dev.alenajam.opendialer.core.common.ToolbarListener -import dev.alenajam.opendialer.data.calls.CallOption -import dev.alenajam.opendialer.data.calls.DialerCall -import dev.alenajam.opendialer.feature.callDetail.databinding.FragmentCallDetailBinding -import kotlinx.coroutines.ExperimentalCoroutinesApi - -private val circleTransform: Transformation = CircleTransform() -private val colorList = listOf( - Color.parseColor("#4FAF44"), - Color.parseColor("#F6D145"), - Color.parseColor("#FF9526"), - Color.parseColor("#EF4423"), - Color.parseColor("#328AF0") -) - -@AndroidEntryPoint -class CallDetailFragment : Fragment(), View.OnClickListener { - private val viewModel: DialerViewModel by viewModels() - lateinit var adapter: RecentsAdapter - private lateinit var callIds: List - private lateinit var call: DialerCall - private var optionsAdapter: CallOptionsAdapter? = null - private var toolbarListener: ToolbarListener? = null - private var onStatusBarColorChange: OnStatusBarColorChange? = null - private var _binding: FragmentCallDetailBinding? = null - private val binding get() = _binding!! - private val requestMakeCallPermissions = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { data -> - /** Ensure that all permissions were allowed */ - if (!data.containsValue(false)) { - /** Retry call */ - makeCall() - } - } - - override fun onAttach(context: Context) { - super.onAttach(context) - if (context is ToolbarListener) { - toolbarListener = context - } - if (context is OnStatusBarColorChange) { - onStatusBarColorChange = context - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val pref = SharedPreferenceHelper.getSharedPreferences(context) - val idsStr = pref.getString(CALL_DETAIL_PARAM_CALL_IDS, null) - - idsStr?.let { - pref.edit().remove(CALL_DETAIL_PARAM_CALL_IDS).apply() - callIds = it.split(',').map { id -> id.toInt() } - } - - if (callIds.isEmpty()) { - throw IllegalArgumentException("No valid call IDs were set in SharedPreferences before ${CallDetailFragment::class.java.simpleName} onCreate") - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentCallDetailBinding.inflate( - inflater, - container, - false - ) - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - @ExperimentalCoroutinesApi - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - onStatusBarColorChange?.onColorChange(view.context.getColor(R.color.colorPrimaryDark)) - toolbarListener?.hideToolbar(false) - - binding.toolbarLayout.toolbar.setNavigationOnClickListener { goBack() } - context?.let { binding.toolbarLayout.toolbar.setTitle(R.string.call_details) } - - viewModel.call.observe(viewLifecycleOwner) { - this.call = it - renderCall() - } - viewModel.detailOptions.observe(viewLifecycleOwner) { handleOptions(it) } - viewModel.deletedDetailCalls.observe(viewLifecycleOwner, - dev.alenajam.opendialer.core.common.functional.EventObserver { goBack() }) - viewModel.blockedCaller.observe( - viewLifecycleOwner, - dev.alenajam.opendialer.core.common.functional.EventObserver { handleBlockedCaller(true) }) - viewModel.unblockedCaller.observe( - viewLifecycleOwner, - dev.alenajam.opendialer.core.common.functional.EventObserver { handleBlockedCaller(false) }) - - binding.callButton.setOnClickListener(this) - binding.contactIcon.setOnClickListener(this) - - viewModel.getCallByIds(callIds) - } - - private fun renderCall() { - context?.let { context -> - viewModel.getDetailOptions(call) - - Picasso.get() - .load(call.contactInfo.photoUri) - .transform(circleTransform) - .into(binding.contactIcon) - } - - when { - call.isAnonymous() -> { - binding.title.text = context?.getString(R.string.anonymous) - binding.subtitle.visibility = View.GONE - binding.callButton.visibility = View.GONE - } - - call.contactInfo.name.isNullOrBlank() -> { - binding.title.text = call.contactInfo.number - binding.subtitle.visibility = View.GONE - } - - else -> { - binding.title.text = call.contactInfo.name - binding.subtitle.text = call.contactInfo.number - } - } - - binding.recyclerViewCallDetails.layoutManager = LinearLayoutManager(context) - binding.recyclerViewCallDetails.adapter = CallDetailsAdapter(call.childCalls, context) - - binding.recyclerViewCallDetailsOptions.layoutManager = LinearLayoutManager(context) - optionsAdapter = CallOptionsAdapter { option -> - handleOptionClick(option) - } - binding.recyclerViewCallDetailsOptions.adapter = optionsAdapter - } - - private fun handleOptions(options: List) { - optionsAdapter?.setData(options) - } - - private fun handleOptionClick(option: CallOption) { - when (option.id) { - CallOption.ID_COPY_NUMBER -> viewModel.copyNumber(call) - CallOption.ID_EDIT_BEFORE_CALL -> activity?.let { - viewModel.editNumberBeforeCall( - it, - call - ) - } - - CallOption.ID_DELETE -> viewModel.deleteCalls(call) - CallOption.ID_BLOCK_CALLER -> viewModel.blockCaller(call) - CallOption.ID_UNBLOCK_CALLER -> viewModel.unblockCaller(call) - else -> Unit - } - } - - private fun handleBlockedCaller(blocked: Boolean) { - viewModel.getDetailOptions(call) - Toast.makeText( - context, - getString( - if (blocked) R.string.numberBlocked else R.string.numberUnblocked, - call.contactInfo.number - ), - Toast.LENGTH_SHORT - ).show() - } - - private fun makeCall() { - if (PermissionUtils.hasMakeCallPermission(context)) { - activity?.let { call.contactInfo.number?.let { num -> viewModel.makeCall(it, num) } } - } else { - requestMakeCallPermissions.launch(PermissionUtils.makeCallPermissions) - } - } - - override fun onClick(v: View?) { - if (v?.id == binding.callButton.id) { - activity?.let { makeCall() } - } else if (v?.id == binding.contactIcon.id) { - activity?.let { viewModel.openContact(it, call) } - } - } - - private fun goBack() { - findNavController().popBackStack() - } -} \ No newline at end of file diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailScreen.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailScreen.kt new file mode 100644 index 0000000..3e9d26f --- /dev/null +++ b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailScreen.kt @@ -0,0 +1,285 @@ +package dev.alenajam.opendialer.feature.callDetail + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.outlined.Block +import androidx.compose.material.icons.outlined.Call +import androidx.compose.material.icons.outlined.CallMade +import androidx.compose.material.icons.outlined.CallMissed +import androidx.compose.material.icons.outlined.CallReceived +import androidx.compose.material.icons.outlined.ContentCopy +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.Edit +import androidx.compose.material.icons.outlined.Message +import androidx.compose.material.icons.outlined.Voicemail +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.BottomAppBarDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.LocalLifecycleOwner +import coil.compose.AsyncImage +import dev.alenajam.opendialer.core.common.CommonUtils +import dev.alenajam.opendialer.core.common.forwardingPainter +import dev.alenajam.opendialer.core.common.functional.EventObserver +import dev.alenajam.opendialer.data.calls.CallType +import dev.alenajam.opendialer.data.calls.ContactInfo +import dev.alenajam.opendialer.data.calls.DetailCall +import dev.alenajam.opendialer.data.calls.DialerCall +import org.ocpsoft.prettytime.PrettyTime +import java.util.Date + +@Composable +fun CallDetailScreen( + viewModel: DialerViewModel = hiltViewModel(), + onNavigateBack: () -> Unit +) { + val call = viewModel.call.observeAsState() + val isAnon = call.value?.isAnonymous() == true + val childCalls = call.value?.childCalls ?: emptyList() + + viewModel.deletedDetailCalls.observe( + LocalLifecycleOwner.current, + EventObserver { onNavigateBack() }) + + Scaffold( + topBar = { + TopBar( + call = call.value, + onNavigateBack = onNavigateBack + ) + }, + bottomBar = { + BottomBar( + isAnon = isAnon, + makeCall = { viewModel.makeCall(call.value!!.number!!) }, + sendMessage = viewModel::sendMessage, + copyNumber = { viewModel.copyNumber(call.value!!) }, + dialNumber = { viewModel.editNumberBeforeCall(call.value!!) }, + deleteCalls = { viewModel.deleteCalls(call.value!!) } + ) + } + ) { innerPadding -> + Column( + verticalArrangement = Arrangement.Bottom, + modifier = Modifier + .fillMaxHeight() + .padding(innerPadding) + ) { + LazyColumn { + items(childCalls) { call -> + CallRow(call = call) + } + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun TopBar( + call: DialerCall?, + onNavigateBack: () -> Unit +) { + TopAppBar( + title = { + if (call != null) { + Row( + verticalAlignment = Alignment.CenterVertically + ) { + val placeholder = forwardingPainter( + painter = rememberVectorPainter(Icons.Filled.AccountCircle), + colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.primary) + ) + AsyncImage( + model = call.contactInfo.photoUri, + contentDescription = null, + modifier = Modifier.size(50.dp), + placeholder = placeholder, + error = placeholder, + fallback = placeholder + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = if (call.isAnonymous()) stringResource(id = R.string.anonymous) + else if (!call.contactInfo.name.isNullOrBlank()) call.contactInfo.name!! + else call.contactInfo.number!!, + ) + } + } + }, + navigationIcon = { + IconButton( + onClick = onNavigateBack + ) { + Icon(imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = null) + } + } + ) +} + +@Composable +private fun BottomBar( + isAnon: Boolean, + makeCall: () -> Unit, + sendMessage: () -> Unit, + copyNumber: () -> Unit, + dialNumber: () -> Unit, + deleteCalls: () -> Unit, +) { + BottomAppBar( + actions = { + if (!isAnon) { + IconButton(onClick = sendMessage) { + Icon(Icons.Outlined.Message, contentDescription = "Localized description") + } + IconButton(onClick = copyNumber) { + Icon(Icons.Outlined.ContentCopy, contentDescription = "Localized description") + } + IconButton(onClick = dialNumber) { + Icon(Icons.Outlined.Edit, contentDescription = "Localized description") + } + } + IconButton(onClick = deleteCalls) { + Icon(Icons.Outlined.Delete, contentDescription = "Localized description") + } + }, + floatingActionButton = { + if (!isAnon) { + FloatingActionButton( + onClick = makeCall, + containerColor = BottomAppBarDefaults.bottomAppBarFabColor, + elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation() + ) { + Icon(Icons.Outlined.Call, "Localized description") + } + } + } + ) +} + +@Composable +private fun CallRow( + call: DetailCall +) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp), + ) { + Icon( + imageVector = when (call.type) { + CallType.INCOMING, CallType.ANSWERED_EXTERNALLY -> Icons.Outlined.CallReceived + CallType.OUTGOING -> Icons.Outlined.CallMade + CallType.MISSED, CallType.REJECTED -> Icons.Outlined.CallMissed + CallType.VOICEMAIL -> Icons.Outlined.Voicemail + CallType.BLOCKED -> Icons.Outlined.Block + }, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = when (call.type) { + CallType.OUTGOING -> stringResource(id = R.string.outgoing_call) + CallType.INCOMING, CallType.ANSWERED_EXTERNALLY -> stringResource(id = R.string.incoming_call) + CallType.MISSED -> stringResource(id = R.string.missed_call) + CallType.VOICEMAIL -> stringResource(id = R.string.voicemail_call) + CallType.REJECTED -> stringResource(id = R.string.rejected_call) + CallType.BLOCKED -> stringResource(id = R.string.blocked_call) + }, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = PrettyTime().format(call.date), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Text( + text = CommonUtils.getDurationTimeStringMinimal(call.duration * 1000), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +private val incomingDetailCallMock = DetailCall( + id = 1, + date = Date(), + type = CallType.INCOMING, + duration = 500L, +) + +private val callMock = DialerCall( + id = 1, + number = "333123456", + date = Date(), + type = CallType.OUTGOING, + options = emptyList(), + childCalls = listOf( + incomingDetailCallMock + ), + contactInfo = ContactInfo( + name = "John Doe", + number = "3331234567", + photoUri = null + ) +) + +@Preview(showBackground = true) +@Composable +private fun TopBarPreview() { + TopBar(call = callMock) {} +} + +@Preview(showBackground = true) +@Composable +private fun BottomBarPreview() { + BottomBar( + isAnon = false, + makeCall = {}, + sendMessage = {}, + copyNumber = {}, + dialNumber = {}, + deleteCalls = {}, + ) +} + +@Preview(showBackground = true) +@Composable +private fun CallRowPreview() { + CallRow(call = incomingDetailCallMock) +} \ No newline at end of file diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailsAdapter.java b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailsAdapter.java deleted file mode 100644 index ac4c26e..0000000 --- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallDetailsAdapter.java +++ /dev/null @@ -1,117 +0,0 @@ -package dev.alenajam.opendialer.feature.callDetail; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import java.text.DateFormat; -import java.util.List; -import java.util.Locale; - -import dev.alenajam.opendialer.core.common.CommonUtils; -import dev.alenajam.opendialer.data.calls.DetailCall; - -public class CallDetailsAdapter extends RecyclerView.Adapter { - private DateFormat callDateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.SHORT, Locale.getDefault()); - - private List calls; - private Context context; - - public CallDetailsAdapter(List calls, Context context) { - this.calls = calls; - this.context = context; - } - - @NonNull - @Override - public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - Context context = parent.getContext(); - LayoutInflater inflater = LayoutInflater.from(context); - - // Inflate the custom layout - View contactView = inflater.inflate(R.layout.item_call_details, parent, false); - - // Return a new holder instance - ViewHolder viewHolder = new ViewHolder(contactView); - return viewHolder; - } - - @Override - public void onBindViewHolder(@NonNull ViewHolder holder, final int position) { - DetailCall currentCall = calls.get(position); - - holder.subtitle.setText(callDateFormat.format(currentCall.getDate())); - if (currentCall.getDuration() == 0) { - holder.duration.setVisibility(View.GONE); - } else { - holder.duration.setText(CommonUtils.getDurationTimeStringMinimal(currentCall.getDuration() * 1000)); - } - - // TODO refactor - int drawableRes = -1, text = -1; - switch (currentCall.getType()) { - case OUTGOING: - drawableRes = R.drawable.icon_16; - text = R.string.outgoing_call; - break; - case INCOMING: - case ANSWERED_EXTERNALLY: - drawableRes = R.drawable.icon_21; - text = R.string.incoming_call; - break; - case MISSED: - drawableRes = R.drawable.icon_22; - text = R.string.missed_call; - break; - case REJECTED: - drawableRes = R.drawable.icon_22; - text = R.string.rejected_call; - break; - case BLOCKED: - drawableRes = R.drawable.icon_18; - text = R.string.blocked_call; - break; - case VOICEMAIL: - drawableRes = R.drawable.icon_09; - text = R.string.voicemail_call; - break; - } - - if (drawableRes != -1) { - holder.title.setText(context.getString(text)); - holder.icon.setImageDrawable(context.getDrawable(drawableRes)); - } - } - - @Override - public int getItemCount() { - return calls.size(); - } - - // Provide a direct reference to each of the views within a data item - // Used to cache the views within the item layout for fast access - protected class ViewHolder extends RecyclerView.ViewHolder { - // Your holder should contain a member variable - // for any view that will be set as you render a row - private TextView title, subtitle, duration; - private ImageView icon; - - // We also create a constructor that accepts the entire item row - // and does the view lookups to find each subview - private ViewHolder(View itemView) { - // Stores the itemView in a public final member variable that can be used - // to access the context from any ViewHolder instance. - super(itemView); - title = itemView.findViewById(R.id.title); - subtitle = itemView.findViewById(R.id.subtitle); - duration = itemView.findViewById(R.id.duration); - icon = itemView.findViewById(R.id.icon); - } - } -} \ No newline at end of file diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallOptionsAdapter.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallOptionsAdapter.kt deleted file mode 100644 index 5a6a853..0000000 --- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/CallOptionsAdapter.kt +++ /dev/null @@ -1,58 +0,0 @@ -package dev.alenajam.opendialer.feature.callDetail - -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.RecyclerView -import dev.alenajam.opendialer.data.calls.CallOption - -class CallOptionsAdapter( - private var options: List = emptyList(), - private val onClick: (option: CallOption) -> Unit -) : RecyclerView.Adapter() { - inner class ViewHolder(inflater: LayoutInflater, private val parent: ViewGroup) : - RecyclerView.ViewHolder(inflater.inflate(R.layout.item_call_log_child, parent, false)) { - private val layout = itemView.findViewById(R.id.layout) - private val icon = itemView.findViewById(R.id.icon) - private val text = itemView.findViewById(R.id.text) - - fun bind(option: CallOption) { - //icon.setImageDrawable(ContextCompat.getDrawable(parent.context, option.icon)) - - val resId = when (option.id) { - CallOption.ID_CREATE_CONTACT -> R.string.create_new_contact - CallOption.ID_ADD_EXISTING -> R.string.add_to_a_contact - CallOption.ID_SEND_MESSAGE -> R.string.send_message - CallOption.ID_CALL_DETAILS -> R.string.call_details - CallOption.ID_COPY_NUMBER -> R.string.copy_number - CallOption.ID_EDIT_BEFORE_CALL -> R.string.edit_number_before_call - CallOption.ID_BLOCK_CALLER -> R.string.blockThisCaller - CallOption.ID_UNBLOCK_CALLER -> R.string.unblockThisCaller - CallOption.ID_DELETE -> R.string.delete - else -> null - } - - resId?.let { - text.text = parent.context.getString(resId) - } - - layout.setOnClickListener { onClick(option) } - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = - ViewHolder(LayoutInflater.from(parent.context), parent) - - override fun getItemCount(): Int = options.size - - override fun onBindViewHolder(holder: ViewHolder, position: Int) = - holder.bind(options[position]) - - fun setData(options: List) { - this.options = options - notifyDataSetChanged() - } -} diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/DeleteCalls.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/DeleteCalls.kt index fc2c738..3fb55a9 100644 --- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/DeleteCalls.kt +++ b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/DeleteCalls.kt @@ -3,14 +3,14 @@ package dev.alenajam.opendialer.feature.callDetail import dev.alenajam.opendialer.core.common.exception.Failure import dev.alenajam.opendialer.core.common.functional.Either import dev.alenajam.opendialer.core.common.interactor.UseCase +import dev.alenajam.opendialer.data.calls.CallsRepositoryImpl import dev.alenajam.opendialer.data.calls.DetailCall -import dev.alenajam.opendialer.data.calls.DialerRepositoryImpl import javax.inject.Inject class DeleteCalls -@Inject constructor(private val dialerRepositoryImpl: DialerRepositoryImpl) : - UseCase, Unit>() { - override suspend fun run(params: List): Either = - dialerRepositoryImpl.deleteCalls(params) +@Inject constructor(private val callsRepositoryImpl: CallsRepositoryImpl) : + UseCase, Unit>() { + override suspend fun run(params: List): Either = + callsRepositoryImpl.deleteCalls(params) } \ No newline at end of file diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/DialerViewModel.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/DialerViewModel.kt index a2f0d7f..2b9d973 100644 --- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/DialerViewModel.kt +++ b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/DialerViewModel.kt @@ -7,88 +7,111 @@ import android.net.Uri import android.telecom.PhoneAccount import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.navigation.toRoute import dagger.hilt.android.lifecycle.HiltViewModel import dev.alenajam.opendialer.core.common.CommonUtils import dev.alenajam.opendialer.core.common.ContactsHelper import dev.alenajam.opendialer.core.common.functional.Event import dev.alenajam.opendialer.data.calls.CallOption +import dev.alenajam.opendialer.data.calls.CallsRepositoryImpl import dev.alenajam.opendialer.data.calls.DialerCall -import dev.alenajam.opendialer.data.calls.DialerRepositoryImpl import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class DialerViewModel @Inject constructor( - private val app: Application, - private val getDetailOptions: GetDetailOptions, - private val deleteCallsUseCase: DeleteCalls, - private val blockCallerUseCase: BlockCaller, - private val unblockCallerUseCase: UnblockCaller, - private val dialerRepositoryImpl: DialerRepositoryImpl + savedStateHandle: SavedStateHandle, + private val app: Application, + private val getDetailOptions: GetDetailOptions, + private val deleteCallsUseCase: DeleteCalls, + private val blockCallerUseCase: BlockCaller, + private val unblockCallerUseCase: UnblockCaller, + private val callsRepositoryImpl: CallsRepositoryImpl ) : ViewModel() { - private val _call: MutableLiveData = MutableLiveData() - val call: LiveData = _call - val detailOptions: MutableLiveData> = - MutableLiveData() - val deletedDetailCalls: MutableLiveData> = MutableLiveData() - val blockedCaller: MutableLiveData> = MutableLiveData() - val unblockedCaller: MutableLiveData> = MutableLiveData() - - fun getCallByIds(ids: List) { - viewModelScope.launch { - dialerRepositoryImpl.getCallByIds(app.contentResolver, ids).fold( - { /* TODO handle failure */ }, { call -> _call.postValue(DialerCall.mapList(call).first()) } - ) + private val _call: MutableLiveData = MutableLiveData() + val call: LiveData = _call + val detailOptions: MutableLiveData> = + MutableLiveData() + val deletedDetailCalls: MutableLiveData> = MutableLiveData() + val blockedCaller: MutableLiveData> = MutableLiveData() + val unblockedCaller: MutableLiveData> = MutableLiveData() + private val callDetail = savedStateHandle.toRoute() + + init { + getCallByIds(callDetail.callIds) + } + + fun getCallByIds(ids: List) { + viewModelScope.launch { + callsRepositoryImpl.getCallByIds(app.contentResolver, ids).fold( + { /* TODO handle failure */ }, + { call -> _call.postValue(DialerCall.mapList(call).first()) } + ) + } + } + + fun getDetailOptions(call: DialerCall) = + getDetailOptions(viewModelScope, call) { it.fold({}, ::handleDetailOptions) } + + fun makeCall(activity: Activity, number: String) = CommonUtils.makeCall(activity, number) + fun makeCall(number: String) = CommonUtils.makeCall(app, number) + + fun copyNumber(call: DialerCall) = CommonUtils.copyToClipobard(app, call.contactInfo.number) + fun openContact(activity: Activity, call: DialerCall) { + ContactsHelper.getContactByPhoneNumber(activity, call.contactInfo.number)?.let { + CommonUtils.showContactDetail(activity, it.id) + } + } + + fun sendMessage() { + call.value?.number?.let { CommonUtils.makeSms(app, it) } + } + + fun editNumberBeforeCall(activity: Activity, call: DialerCall) { + val intent = Intent(Intent.ACTION_DIAL).apply { + data = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.number, null) + } + activity.startActivity(intent) + } + + fun editNumberBeforeCall(call: DialerCall) { + val intent = Intent(Intent.ACTION_DIAL).apply { + data = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.number, null) + } + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + app.startActivity(intent) } - } - fun getDetailOptions(call: DialerCall) = - getDetailOptions(viewModelScope, call) { it.fold({}, ::handleDetailOptions) } - fun makeCall(activity: Activity, number: String) = CommonUtils.makeCall(activity, number) + fun deleteCalls(call: DialerCall) = deleteCallsUseCase(viewModelScope, call.childCalls) { + it.fold( + {}, + ::handleDeletedDetailCalls + ) + } - fun copyNumber(call: DialerCall) = CommonUtils.copyToClipobard(app, call.contactInfo.number) - fun openContact(activity: Activity, call: DialerCall) { - ContactsHelper.getContactByPhoneNumber(activity, call.contactInfo.number)?.let { - CommonUtils.showContactDetail(activity, it.id) + fun blockCaller(call: DialerCall) = call.contactInfo.number?.let { + blockCallerUseCase( + viewModelScope, + it + ) { res -> res.fold({}, ::handleBlockCaller) } } - } - fun editNumberBeforeCall(activity: Activity, call: DialerCall) { - val intent = Intent(Intent.ACTION_DIAL).apply { - data = Uri.fromParts(PhoneAccount.SCHEME_TEL, call.number, null) + fun unblockCaller(call: DialerCall) = call.contactInfo.number?.let { + unblockCallerUseCase( + viewModelScope, + it + ) { res -> res.fold({}, ::handleUnblockCaller) } } - activity.startActivity(intent) - } - - fun deleteCalls(call: DialerCall) = deleteCallsUseCase(viewModelScope, call.childCalls) { - it.fold( - {}, - ::handleDeletedDetailCalls - ) - } - - fun blockCaller(call: DialerCall) = call.contactInfo.number?.let { - blockCallerUseCase( - viewModelScope, - it - ) { res -> res.fold({}, ::handleBlockCaller) } - } - - fun unblockCaller(call: DialerCall) = call.contactInfo.number?.let { - unblockCallerUseCase( - viewModelScope, - it - ) { res -> res.fold({}, ::handleUnblockCaller) } - } - - private fun handleDetailOptions(options: List) = - detailOptions.postValue(options) - - private fun handleDeletedDetailCalls(unit: Unit) = deletedDetailCalls.postValue(Event(Unit)) - private fun handleBlockCaller(unit: Unit) = blockedCaller.postValue(Event(Unit)) - private fun handleUnblockCaller(unit: Unit) = unblockedCaller.postValue(Event(Unit)) + + private fun handleDetailOptions(options: List) = + detailOptions.postValue(options) + + private fun handleDeletedDetailCalls(unit: Unit) = deletedDetailCalls.postValue(Event(Unit)) + private fun handleBlockCaller(unit: Unit) = blockedCaller.postValue(Event(Unit)) + private fun handleUnblockCaller(unit: Unit) = unblockedCaller.postValue(Event(Unit)) } \ No newline at end of file diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/GetDetailOptions.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/GetDetailOptions.kt index efd3b7c..c638268 100644 --- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/GetDetailOptions.kt +++ b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/GetDetailOptions.kt @@ -4,14 +4,14 @@ import dev.alenajam.opendialer.core.common.exception.Failure import dev.alenajam.opendialer.core.common.functional.Either import dev.alenajam.opendialer.core.common.interactor.UseCase import dev.alenajam.opendialer.data.calls.CallOption +import dev.alenajam.opendialer.data.calls.CallsRepositoryImpl import dev.alenajam.opendialer.data.calls.DialerCall -import dev.alenajam.opendialer.data.calls.DialerRepositoryImpl import javax.inject.Inject class GetDetailOptions -@Inject constructor(private val dialerRepositoryImpl: DialerRepositoryImpl) : - UseCase>() { - override suspend fun run(params: DialerCall): Either> = - dialerRepositoryImpl.getDetailOptions(params) +@Inject constructor(private val callsRepositoryImpl: CallsRepositoryImpl) : + UseCase>() { + override suspend fun run(params: DialerCall): Either> = + callsRepositoryImpl.getDetailOptions(params) } \ No newline at end of file diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/RecentsAdapter.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/RecentsAdapter.kt deleted file mode 100644 index 9657979..0000000 --- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/RecentsAdapter.kt +++ /dev/null @@ -1,263 +0,0 @@ -package dev.alenajam.opendialer.feature.callDetail - -import android.animation.TimeInterpolator -import android.animation.ValueAnimator -import android.content.Context -import android.graphics.Color -import android.graphics.drawable.Drawable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.animation.AccelerateDecelerateInterpolator -import android.widget.ImageView -import android.widget.TextView -import androidx.cardview.widget.CardView -import androidx.core.animation.doOnEnd -import androidx.core.animation.doOnStart -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.squareup.picasso.Picasso -import com.squareup.picasso.Transformation -import dev.alenajam.opendialer.core.common.CircleTransform -import dev.alenajam.opendialer.core.common.CommonUtils -import dev.alenajam.opendialer.core.common.PermissionUtils -import dev.alenajam.opendialer.data.calls.CallOption -import dev.alenajam.opendialer.data.calls.CallType -import dev.alenajam.opendialer.data.calls.ContactInfo -import dev.alenajam.opendialer.data.calls.DialerCall -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -private val circleTransform: Transformation = CircleTransform() -private const val itemHeight = 75f -private const val optionHeight = 50 -private const val expandAnimDuration = 200L - -class RecentsAdapter( - context: Context, - private val recyclerView: RecyclerView, - private val coroutineScope: CoroutineScope, - private val onCallClick: (call: DialerCall) -> Unit, - private val onContactClick: (call: DialerCall) -> Unit, - private val onOptionClick: (call: DialerCall, option: CallOption) -> Unit, - private val updateContactInfo: (number: String?, countryIso: String?, callLogInfo: ContactInfo) -> Unit -) : RecyclerView.Adapter() { - private val incoming: Drawable? = - ContextCompat.getDrawable(context, R.drawable.icon_21) - private val outgoing: Drawable? = - ContextCompat.getDrawable(context, R.drawable.icon_16) - private val missed: Drawable? = - ContextCompat.getDrawable(context, R.drawable.icon_22) - private val voicemail: Drawable? = - ContextCompat.getDrawable(context, R.drawable.icon_09) - private val blocked: Drawable? = - ContextCompat.getDrawable(context, R.drawable.icon_18) - val calls = mutableListOf() - private var expandedItem: Int = -1 - - inner class ViewHolder(inflater: LayoutInflater, private val parent: ViewGroup) : - RecyclerView.ViewHolder(inflater.inflate(R.layout.item_call_log_new, parent, false)) { - private val card = itemView.findViewById(R.id.cardView) - private val textView = itemView.findViewById(R.id.item_call_title) - private val date = itemView.findViewById(R.id.item_call_subtitle) - private val buttonCall = itemView.findViewById(R.id.icon) - private val callsIcons = itemView.findViewById(R.id.calls_icons) - private val contactIcon = itemView.findViewById(R.id.icon_contact) - private val recyclerViewOptions = - itemView.findViewById(R.id.recyclerViewOptions) - private var animator: ValueAnimator? = null - var job: Job? = null - - private fun expand( - call: DialerCall, - holder: RecentsAdapter.ViewHolder, - expand: Boolean, - animate: Boolean? = false - ) { - val height = CommonUtils.convertDpToPixels( - itemHeight, - parent.context - ) - val expandedHeight = CommonUtils.convertDpToPixels( - itemHeight + (call.options.size * optionHeight), - parent.context - ) - val elevation = CommonUtils.convertDpToPixels(0f, parent.context) - val elevationExpanded = - CommonUtils.convertDpToPixels(4f, parent.context) - if (animate == true) { - animator?.cancel() - animator = getValueAnimator( - expand, - expandAnimDuration, - AccelerateDecelerateInterpolator() - ) { - holder.card.layoutParams.height = - (height + (expandedHeight - height) * it).toInt() - holder.card.cardElevation = (elevation + (elevationExpanded - elevation) * it) - holder.card.requestLayout() - } - if (expand) animator?.doOnStart { - holder.recyclerViewOptions.visibility = View.VISIBLE - } - animator?.doOnEnd { - animator = null - if (!expand) holder.recyclerViewOptions.visibility = View.GONE - } - animator?.start() - } else { - holder.card.layoutParams.height = (if (expand) expandedHeight else height).toInt() - holder.card.cardElevation = if (expand) elevationExpanded else elevation - holder.recyclerViewOptions.visibility = if (expand) View.VISIBLE else View.GONE - } - } - - fun bind(currentCall: DialerCall, position: Int) { - val context = parent.context - val contact = currentCall.contactInfo - - /** Update call log only if number is dialable */ - if (!currentCall.isAnonymous() && PermissionUtils.hasContactsPermission( - context - ) - ) { - job = coroutineScope.launch(Dispatchers.IO) { - updateContactInfo( - currentCall.number, - currentCall.countryIso, - currentCall.contactInfo - ) - } - } - - var name = contact.name - val number = contact.number - - if (currentCall.isAnonymous()) { - name = context.getString(R.string.anonymous) - } else if (name.isNullOrBlank()) { - name = number - } - - textView.text = name - - val isExpanded = position == expandedItem - expand(currentCall, this, isExpanded) - - Picasso.get() - .load(contact.photoUri) - .transform(circleTransform) - .into(contactIcon) - - contactIcon.setOnClickListener { onContactClick(currentCall) } - - card.setOnClickListener { - val isExpandedNow = position == expandedItem - if (!isExpandedNow) { - getExpandedItemViewHolder()?.let { - expand( - currentCall, - it, - expand = false, - animate = true - ) - } - notifyItemChanged(expandedItem) - } - - expandedItem = if (isExpandedNow) -1 else position - expand(currentCall, this, !isExpandedNow, true) - } - - callsIcons.setImageDrawable( - when (currentCall.type) { - CallType.OUTGOING -> outgoing - CallType.INCOMING, CallType.ANSWERED_EXTERNALLY -> incoming - CallType.MISSED, CallType.REJECTED -> missed - CallType.BLOCKED -> blocked - CallType.VOICEMAIL -> voicemail - } - ) - - val prettyDate = org.ocpsoft.prettytime.PrettyTime().format(currentCall.date) - - date.text = - if (currentCall.childCalls.size > 1) { - context.getString( - R.string.call_log_item_subtitle_number, - currentCall.childCalls.size, - prettyDate - ) - } else { - context.getString(R.string.call_log_item_subtitle, prettyDate) - } - - if (currentCall.isAnonymous()) { - buttonCall.visibility = View.INVISIBLE - } else { - buttonCall.setOnClickListener { onCallClick(currentCall) } - buttonCall.visibility = View.VISIBLE - } - - recyclerViewOptions.layoutManager = LinearLayoutManager(context) - recyclerViewOptions.adapter = - CallOptionsAdapter(currentCall.options) { - onOptionClick(currentCall, it) - } - } - } - - private fun getExpandedItemViewHolder(): RecentsAdapter.ViewHolder? { - return recyclerView.findViewHolderForAdapterPosition(expandedItem) as? RecentsAdapter.ViewHolder? - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = - ViewHolder(LayoutInflater.from(parent.context), parent) - - override fun getItemCount(): Int = calls.size - - override fun onBindViewHolder(holder: ViewHolder, position: Int) = - holder.bind(calls[position], position) - - override fun onViewRecycled(holder: ViewHolder) { - holder.job?.cancel() - } - - fun setData(calls: List) { - val oldCalls = this.calls.toList() - this.calls.apply { - clear() - addAll(calls) - } - if (oldCalls.isEmpty()) { - notifyDataSetChanged() - } else { - coroutineScope.launch(Dispatchers.IO) { - val diffUtil = - RecentsDiffUtil(oldCalls, calls) - val result = DiffUtil.calculateDiff(diffUtil) - withContext(Dispatchers.Main) { - result.dispatchUpdatesTo(this@RecentsAdapter) - } - } - } - } -} - -inline fun getValueAnimator( - forward: Boolean = true, - duration: Long? = null, - interpolator: TimeInterpolator? = null, - crossinline updateListener: (progress: Float) -> Unit -): ValueAnimator { - val a = if (forward) ValueAnimator.ofFloat(0f, 1f) else ValueAnimator.ofFloat(1f, 0f) - a.addUpdateListener { updateListener(it.animatedValue as Float) } - duration?.let { a.duration = it } - interpolator?.let { a.interpolator = it } - return a -} \ No newline at end of file diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/RecentsDiffUtil.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/RecentsDiffUtil.kt deleted file mode 100644 index 9baed3e..0000000 --- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/RecentsDiffUtil.kt +++ /dev/null @@ -1,31 +0,0 @@ -package dev.alenajam.opendialer.feature.callDetail - -import androidx.recyclerview.widget.DiffUtil -import dev.alenajam.opendialer.data.calls.DialerCall - -class RecentsDiffUtil( - private val oldList: List, - private val newList: List -) : DiffUtil.Callback() { - override fun getOldListSize(): Int { - return oldList.size - } - - override fun getNewListSize(): Int { - return newList.size - } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val old = oldList[oldItemPosition] - val new = newList[newItemPosition] - - return old.id == new.id - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val old = oldList[oldItemPosition] - val new = newList[newItemPosition] - - return old.contactInfo == new.contactInfo - } -} diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/Routes.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/Routes.kt new file mode 100644 index 0000000..bece7a7 --- /dev/null +++ b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/Routes.kt @@ -0,0 +1,6 @@ +package dev.alenajam.opendialer.feature.callDetail + +import kotlinx.serialization.Serializable + +@Serializable +data class CallDetailRoute(val callIds: List) \ No newline at end of file diff --git a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/UnblockCaller.kt b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/UnblockCaller.kt index b86258e..a9aad1b 100644 --- a/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/UnblockCaller.kt +++ b/feature/callDetail/src/main/java/dev/alenajam/opendialer/feature/callDetail/UnblockCaller.kt @@ -3,13 +3,13 @@ package dev.alenajam.opendialer.feature.callDetail import dev.alenajam.opendialer.core.common.exception.Failure import dev.alenajam.opendialer.core.common.functional.Either import dev.alenajam.opendialer.core.common.interactor.UseCase -import dev.alenajam.opendialer.data.calls.DialerRepositoryImpl +import dev.alenajam.opendialer.data.calls.CallsRepositoryImpl import javax.inject.Inject class UnblockCaller -@Inject constructor(private val dialerRepositoryImpl: DialerRepositoryImpl) : - UseCase() { - override suspend fun run(params: String): Either = - dialerRepositoryImpl.unblockCaller(params) +@Inject constructor(private val callsRepositoryImpl: CallsRepositoryImpl) : + UseCase() { + override suspend fun run(params: String): Either = + callsRepositoryImpl.unblockCaller(params) } \ No newline at end of file diff --git a/feature/callDetail/src/main/res/drawable-hdpi/ic_call.png b/feature/callDetail/src/main/res/drawable-hdpi/ic_call.png deleted file mode 100644 index 2506620..0000000 Binary files a/feature/callDetail/src/main/res/drawable-hdpi/ic_call.png and /dev/null differ diff --git a/feature/callDetail/src/main/res/drawable-mdpi/ic_call.png b/feature/callDetail/src/main/res/drawable-mdpi/ic_call.png deleted file mode 100644 index fb87e7c..0000000 Binary files a/feature/callDetail/src/main/res/drawable-mdpi/ic_call.png and /dev/null differ diff --git a/feature/callDetail/src/main/res/drawable-xhdpi/ic_call.png b/feature/callDetail/src/main/res/drawable-xhdpi/ic_call.png deleted file mode 100644 index 83d38c8..0000000 Binary files a/feature/callDetail/src/main/res/drawable-xhdpi/ic_call.png and /dev/null differ diff --git a/feature/callDetail/src/main/res/drawable-xxhdpi/ic_call.png b/feature/callDetail/src/main/res/drawable-xxhdpi/ic_call.png deleted file mode 100644 index 2cbdd10..0000000 Binary files a/feature/callDetail/src/main/res/drawable-xxhdpi/ic_call.png and /dev/null differ diff --git a/feature/callDetail/src/main/res/drawable-xxxhdpi/ic_call.png b/feature/callDetail/src/main/res/drawable-xxxhdpi/ic_call.png deleted file mode 100644 index 2093dfe..0000000 Binary files a/feature/callDetail/src/main/res/drawable-xxxhdpi/ic_call.png and /dev/null differ diff --git a/feature/callDetail/src/main/res/drawable/icon_09.png b/feature/callDetail/src/main/res/drawable/icon_09.png deleted file mode 100644 index 57eab87..0000000 Binary files a/feature/callDetail/src/main/res/drawable/icon_09.png and /dev/null differ diff --git a/feature/callDetail/src/main/res/drawable/icon_16.png b/feature/callDetail/src/main/res/drawable/icon_16.png deleted file mode 100644 index bf8a56c..0000000 Binary files a/feature/callDetail/src/main/res/drawable/icon_16.png and /dev/null differ diff --git a/feature/callDetail/src/main/res/drawable/icon_18.png b/feature/callDetail/src/main/res/drawable/icon_18.png deleted file mode 100644 index 21c3d9b..0000000 Binary files a/feature/callDetail/src/main/res/drawable/icon_18.png and /dev/null differ diff --git a/feature/callDetail/src/main/res/drawable/icon_21.png b/feature/callDetail/src/main/res/drawable/icon_21.png deleted file mode 100644 index b7c9f85..0000000 Binary files a/feature/callDetail/src/main/res/drawable/icon_21.png and /dev/null differ diff --git a/feature/callDetail/src/main/res/drawable/icon_22.png b/feature/callDetail/src/main/res/drawable/icon_22.png deleted file mode 100644 index 11c86bd..0000000 Binary files a/feature/callDetail/src/main/res/drawable/icon_22.png and /dev/null differ diff --git a/feature/callDetail/src/main/res/layout/fragment_call_detail.xml b/feature/callDetail/src/main/res/layout/fragment_call_detail.xml deleted file mode 100644 index 5a914cb..0000000 --- a/feature/callDetail/src/main/res/layout/fragment_call_detail.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/feature/callDetail/src/main/res/layout/item_call_details.xml b/feature/callDetail/src/main/res/layout/item_call_details.xml deleted file mode 100644 index f243e1a..0000000 --- a/feature/callDetail/src/main/res/layout/item_call_details.xml +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/feature/callDetail/src/main/res/layout/item_call_log_child.xml b/feature/callDetail/src/main/res/layout/item_call_log_child.xml deleted file mode 100644 index dcb7683..0000000 --- a/feature/callDetail/src/main/res/layout/item_call_log_child.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/feature/callDetail/src/main/res/layout/item_call_log_new.xml b/feature/callDetail/src/main/res/layout/item_call_log_new.xml deleted file mode 100644 index 1550653..0000000 --- a/feature/callDetail/src/main/res/layout/item_call_log_new.xml +++ /dev/null @@ -1,90 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/feature/callDetail/src/test/java/dev/alenajam/opendialer/feature/callDetail/ExampleUnitTest.kt b/feature/callDetail/src/test/java/dev/alenajam/opendialer/feature/callDetail/ExampleUnitTest.kt index 9459588..67c0c07 100644 --- a/feature/callDetail/src/test/java/dev/alenajam/opendialer/feature/callDetail/ExampleUnitTest.kt +++ b/feature/callDetail/src/test/java/dev/alenajam/opendialer/feature/callDetail/ExampleUnitTest.kt @@ -1,8 +1,7 @@ package dev.alenajam.opendialer.feature.callDetail -import org.junit.Test - import org.junit.Assert.* +import org.junit.Test /** * Example local unit test, which will execute on the development machine (host). @@ -10,8 +9,8 @@ import org.junit.Assert.* * See [testing documentation](http://d.android.com/tools/testing). */ class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } } \ No newline at end of file diff --git a/feature/calls/build.gradle.kts b/feature/calls/build.gradle.kts index b3b9415..0cd30ad 100644 --- a/feature/calls/build.gradle.kts +++ b/feature/calls/build.gradle.kts @@ -1,72 +1,104 @@ plugins { - id("com.android.library") - id("org.jetbrains.kotlin.android") - id("kotlin-kapt") - id("com.google.dagger.hilt.android") + id("com.android.library") + id("org.jetbrains.kotlin.android") + id("kotlin-kapt") + id("com.google.dagger.hilt.android") + alias(libs.plugins.compose.compiler) + alias(libs.plugins.kotlin.serialization) } android { - namespace = "dev.alenajam.opendialer.feature.calls" - compileSdk = 34 + namespace = "dev.alenajam.opendialer.feature.calls" + compileSdk = 34 - defaultConfig { - minSdk = 24 + defaultConfig { + minSdk = 24 - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - consumerProguardFiles("consumer-rules.pro") - } + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles("consumer-rules.pro") + } - buildTypes { - release { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + buildFeatures { + viewBinding = true + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.2" + } + compileOptions { + isCoreLibraryDesugaringEnabled = true } - } - buildFeatures { - viewBinding = true - } } dependencies { - implementation(project(":data:calls")) - implementation(project(":data:callsCache")) - implementation(project(":data:contacts")) - implementation(project(":core:common")) + implementation(project(":data:calls")) + implementation(project(":data:callsCache")) + implementation(project(":data:contacts")) + implementation(project(":core:common")) + + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.10.0") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + + implementation("com.google.dagger:hilt-android:2.48.1") + kapt("com.google.dagger:hilt-compiler:2.48.1") + androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1") + testImplementation("com.google.dagger:hilt-android-testing:2.48.1") + kaptTest("com.google.dagger:hilt-compiler:2.48.1") + implementation("com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava") - implementation("androidx.core:core-ktx:1.12.0") - implementation("androidx.appcompat:appcompat:1.6.1") - implementation("com.google.android.material:material:1.10.0") - testImplementation("junit:junit:4.13.2") - androidTestImplementation("androidx.test.ext:junit:1.1.5") - androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") + implementation("com.squareup.picasso:picasso:2.71828") + implementation("org.ocpsoft.prettytime:prettytime:4.0.1.Final") - implementation("com.google.dagger:hilt-android:2.48.1") - kapt("com.google.dagger:hilt-compiler:2.48.1") - androidTestImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptAndroidTest("com.google.dagger:hilt-compiler:2.48.1") - testImplementation("com.google.dagger:hilt-android-testing:2.48.1") - kaptTest("com.google.dagger:hilt-compiler:2.48.1") + implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.6.2") - implementation("com.squareup.picasso:picasso:2.71828") - implementation("org.ocpsoft.prettytime:prettytime:4.0.1.Final") + implementation("androidx.fragment:fragment-ktx:1.6.2") + implementation("androidx.legacy:legacy-support-v4:1.0.0") + implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.6.2") + implementation("androidx.navigation:navigation-fragment-ktx:2.7.5") + implementation("androidx.navigation:navigation-ui-ktx:2.7.5") + implementation("androidx.navigation:navigation-compose:2.7.5") + implementation("androidx.preference:preference-ktx:1.2.1") + implementation("androidx.recyclerview:recyclerview:1.3.2") - implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") - implementation("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.6.2") + val composeBom = platform("androidx.compose:compose-bom:2024.09.03") + implementation(composeBom) + androidTestImplementation(composeBom) + implementation(libs.androidx.material3) + implementation("androidx.compose.ui:ui-tooling-preview") + debugImplementation("androidx.compose.ui:ui-tooling") + androidTestImplementation("androidx.compose.ui:ui-test-junit4") + debugImplementation("androidx.compose.ui:ui-test-manifest") + implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2") + implementation("androidx.compose.runtime:runtime-livedata") - implementation("androidx.fragment:fragment-ktx:1.6.2") - implementation("androidx.legacy:legacy-support-v4:1.0.0") - implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") - implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") - implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.6.2") - implementation("androidx.lifecycle:lifecycle-reactivestreams-ktx:2.6.2") - implementation("androidx.navigation:navigation-fragment-ktx:2.7.5") - implementation("androidx.navigation:navigation-ui-ktx:2.7.5") - implementation("androidx.preference:preference-ktx:1.2.1") - implementation("androidx.recyclerview:recyclerview:1.3.2") + implementation(libs.coil.compose) + implementation(libs.compose.activity) + coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.3") + implementation(libs.hilt.navigation.compose) + implementation(libs.kotlinx.serialization) } kotlin { - jvmToolchain(17) + jvmToolchain(17) } \ No newline at end of file diff --git a/feature/calls/src/androidTest/java/dev/alenajam/opendialer/feature/calls/ExampleInstrumentedTest.kt b/feature/calls/src/androidTest/java/dev/alenajam/opendialer/feature/calls/ExampleInstrumentedTest.kt index 5176774..2c2fdad 100644 --- a/feature/calls/src/androidTest/java/dev/alenajam/opendialer/feature/calls/ExampleInstrumentedTest.kt +++ b/feature/calls/src/androidTest/java/dev/alenajam/opendialer/feature/calls/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package dev.alenajam.opendialer.feature.calls -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -15,10 +13,10 @@ import org.junit.Assert.* */ @RunWith(AndroidJUnit4::class) class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("dev.alenajam.opendialer.feature.calls.test", appContext.packageName) - } + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("dev.alenajam.opendialer.feature.calls.test", appContext.packageName) + } } \ No newline at end of file diff --git a/feature/calls/src/main/AndroidManifest.xml b/feature/calls/src/main/AndroidManifest.xml index a5918e6..44008a4 100644 --- a/feature/calls/src/main/AndroidManifest.xml +++ b/feature/calls/src/main/AndroidManifest.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/CallOptionsAdapter.kt b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/CallOptionsAdapter.kt deleted file mode 100644 index 82d6b50..0000000 --- a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/CallOptionsAdapter.kt +++ /dev/null @@ -1,52 +0,0 @@ -package dev.alenajam.opendialer.feature.calls - -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import dev.alenajam.opendialer.data.calls.CallOption - -class CallOptionsAdapter( - private var options: List = emptyList(), - private val onClick: (option: CallOption) -> Unit -) : RecyclerView.Adapter() { - inner class ViewHolder(inflater: LayoutInflater, private val parent: ViewGroup) : - RecyclerView.ViewHolder(inflater.inflate(R.layout.item_call_log_child, parent, false)) { - private val layout = itemView.findViewById(R.id.layout) - private val icon = itemView.findViewById(R.id.icon) - private val text = itemView.findViewById(R.id.text) - - fun bind(option: CallOption) { - //icon.setImageDrawable(ContextCompat.getDrawable(parent.context, option.icon)) - - val resId = when (option.id) { - CallOption.ID_CREATE_CONTACT -> R.string.create_new_contact - CallOption.ID_ADD_EXISTING -> R.string.add_to_a_contact - CallOption.ID_SEND_MESSAGE -> R.string.send_message - CallOption.ID_CALL_DETAILS -> R.string.call_details - CallOption.ID_COPY_NUMBER -> R.string.copy_number - CallOption.ID_EDIT_BEFORE_CALL -> R.string.edit_number_before_call - CallOption.ID_BLOCK_CALLER -> R.string.blockThisCaller - CallOption.ID_UNBLOCK_CALLER -> R.string.unblockThisCaller - CallOption.ID_DELETE -> R.string.delete - else -> null - } - - resId?.let { - text.text = parent.context.getString(resId) - } - - layout.setOnClickListener { onClick(option) } - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = - ViewHolder(LayoutInflater.from(parent.context), parent) - - override fun getItemCount(): Int = options.size - - override fun onBindViewHolder(holder: ViewHolder, position: Int) = - holder.bind(options[position]) -} diff --git a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/CallsScreen.kt b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/CallsScreen.kt new file mode 100644 index 0000000..c142f82 --- /dev/null +++ b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/CallsScreen.kt @@ -0,0 +1,307 @@ +package dev.alenajam.opendialer.feature.calls + +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.material.icons.outlined.Block +import androidx.compose.material.icons.outlined.CallMade +import androidx.compose.material.icons.outlined.CallMissed +import androidx.compose.material.icons.outlined.CallReceived +import androidx.compose.material.icons.outlined.History +import androidx.compose.material.icons.outlined.Message +import androidx.compose.material.icons.outlined.PersonAddAlt +import androidx.compose.material.icons.outlined.Phone +import androidx.compose.material.icons.outlined.Voicemail +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LifecycleEventEffect +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage +import dev.alenajam.opendialer.core.common.PermissionUtils +import dev.alenajam.opendialer.core.common.forwardingPainter +import dev.alenajam.opendialer.data.calls.CallType +import dev.alenajam.opendialer.data.calls.ContactInfo +import dev.alenajam.opendialer.data.calls.DialerCall +import org.ocpsoft.prettytime.PrettyTime +import java.util.Date + +@Composable +fun CallsScreen( + viewModel: CallsViewModel = hiltViewModel(), + onOpenHistory: (callIds: List) -> Unit +) { + val requestPermissions = + rememberLauncherForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> + if (PermissionUtils.recentsPermissions.all { result[it] == true }) { + viewModel.handleRuntimePermissionGranted() + } + } + + val calls = viewModel.calls.collectAsStateWithLifecycle() + val lastInvalidateCache = viewModel.lastInvalidateCache.collectAsStateWithLifecycle() + val hasPermission = viewModel.hasRuntimePermission.collectAsStateWithLifecycle() + var openRowId by remember { mutableStateOf(null) } + + LifecycleEventEffect(Lifecycle.Event.ON_RESUME) { + viewModel.attemptInvalidateCache() + } + + Surface(modifier = Modifier.fillMaxSize()) { + if (!hasPermission.value) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy( + 8.dp, + alignment = Alignment.CenterVertically + ), + ) { + Text(text = stringResource(R.string.placeholder_call_log)) + OutlinedButton( + onClick = { requestPermissions.launch(input = PermissionUtils.recentsPermissions) } + ) { + Text(text = stringResource(R.string.turn_on)) + } + } + return@Surface + } + + LazyColumn( + contentPadding = PaddingValues(bottom = 100.dp) + ) { + items(calls.value) { call -> + LaunchedEffect( + call, + lastInvalidateCache.value + ) { viewModel.updateContactInfo(call) } + val isOpen = openRowId == call.id + + CallRow(call = call, + isOpen = isOpen, + onClick = { openRowId = if (isOpen) null else call.id }, + makeCall = { viewModel.makeCall(call.contactInfo.number!!) }, + sendMessage = { viewModel.sendMessage(call.contactInfo.number!!) }, + addContact = { viewModel.addToContact(call.contactInfo.number!!) }, + openHistory = { onOpenHistory(call.childCalls.map { it.id }) } + ) + } + } + } +} + +@Composable +private fun CallRow( + call: DialerCall, + isOpen: Boolean, + onClick: () -> Unit, + makeCall: () -> Unit, + sendMessage: () -> Unit, + addContact: () -> Unit, + openHistory: () -> Unit, +) { + Surface( + onClick = onClick, tonalElevation = if (isOpen) 8.dp else 0.dp + ) { + Column { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(vertical = 16.dp, horizontal = 16.dp), + ) { + val placeholder = forwardingPainter( + painter = rememberVectorPainter(Icons.Filled.AccountCircle), + colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.primary) + ) + + AsyncImage( + model = call.contactInfo.photoUri, + contentDescription = null, + modifier = Modifier.size(50.dp), + placeholder = placeholder, + error = placeholder, + fallback = placeholder + ) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = if (call.isAnonymous()) stringResource(id = R.string.anonymous) + else if (!call.contactInfo.name.isNullOrBlank()) call.contactInfo.name!! + else call.contactInfo.number!!, + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp), + ) { + Icon( + imageVector = when (call.type) { + CallType.INCOMING, CallType.ANSWERED_EXTERNALLY -> Icons.Outlined.CallReceived + CallType.OUTGOING -> Icons.Outlined.CallMade + CallType.MISSED, CallType.REJECTED -> Icons.Outlined.CallMissed + CallType.VOICEMAIL -> Icons.Outlined.Voicemail + CallType.BLOCKED -> Icons.Outlined.Block + }, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(14.dp) + ) + + Text( + text = PrettyTime().format(call.date), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + if (!call.isAnonymous()) { + IconButton(onClick = makeCall) { + Icon( + imageVector = Icons.Outlined.Phone, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + AnimatedVisibility(visible = isOpen) { + HorizontalDivider() + + Row( + verticalAlignment = Alignment.CenterVertically + ) { + if (!call.isAnonymous()) { + if (!call.isContactSaved()) { + CallRowButton( + label = "Add contact", + icon = Icons.Outlined.PersonAddAlt, + onClick = addContact + ) + } + + CallRowButton( + label = "Message", icon = Icons.Outlined.Message, onClick = sendMessage + ) + } + + CallRowButton( + label = "History", icon = Icons.Outlined.History, onClick = openHistory + ) + } + } + } + } +} + +@Composable +private fun RowScope.CallRowButton( + label: String, icon: ImageVector, onClick: () -> Unit +) { + Surface( + onClick = onClick, modifier = Modifier.weight(1f), color = Color.Transparent + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(vertical = 16.dp) + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Text( + text = label, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +private val incomingCallMock = DialerCall( + id = 1, + number = "3331234567", + date = Date(), + type = CallType.INCOMING, + options = listOf(), + childCalls = listOf(), + contactInfo = ContactInfo( + number = "3331234567" + ) +) +private val outgoingCallMock = incomingCallMock.copy(type = CallType.OUTGOING) +private val anonymousCallMock = incomingCallMock.copy( + number = null, contactInfo = ContactInfo(number = null) +) + +@Preview(showBackground = true) +@Composable +private fun IncomingCallPreview() { + CallRow(call = incomingCallMock, + isOpen = false, + onClick = {}, + makeCall = {}, + addContact = {}, + sendMessage = {}, + openHistory = {}) +} + +@Preview(showBackground = true) +@Composable +private fun OutgoingCallPreview() { + CallRow(call = outgoingCallMock, + isOpen = false, + onClick = {}, + makeCall = {}, + addContact = {}, + sendMessage = {}, + openHistory = {}) +} + +@Preview(showBackground = true) +@Composable +private fun AnonymousCallPreview() { + CallRow(call = anonymousCallMock, + isOpen = false, + onClick = {}, + makeCall = {}, + addContact = {}, + sendMessage = {}, + openHistory = {}) +} \ No newline at end of file diff --git a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/CallsViewModel.kt b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/CallsViewModel.kt new file mode 100644 index 0000000..ffd49a3 --- /dev/null +++ b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/CallsViewModel.kt @@ -0,0 +1,142 @@ +package dev.alenajam.opendialer.feature.calls + +import android.app.Activity +import android.app.Application +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.alenajam.opendialer.core.common.CommonUtils +import dev.alenajam.opendialer.core.common.ContactsHelper +import dev.alenajam.opendialer.core.common.PermissionUtils +import dev.alenajam.opendialer.data.calls.CallsRepositoryImpl +import dev.alenajam.opendialer.data.calls.DialerCall +import dev.alenajam.opendialer.data.callsCache.CacheRepositoryImpl +import dev.alenajam.opendialer.data.contacts.ContactsRepositoryImpl +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import java.time.LocalDateTime +import javax.inject.Inject + +@HiltViewModel +class CallsViewModel +@Inject constructor( + private val callsRepository: CallsRepositoryImpl, + private val contactsRepository: ContactsRepositoryImpl, + private val app: Application, + private val cacheRepository: CacheRepositoryImpl, + private val startCacheUseCase: StartCache +) : ViewModel() { + private val _calls = MutableStateFlow>(emptyList()) + val calls: StateFlow> = _calls + private val _hasRuntimePermission = MutableStateFlow(false) + val hasRuntimePermission: StateFlow = _hasRuntimePermission + private val _lastInvalidateCache = MutableStateFlow(null) + val lastInvalidateCache: StateFlow = _lastInvalidateCache + private var shouldInvalidateCache = false + private var hasContactsRuntimePermission = false + + init { + _hasRuntimePermission.value = PermissionUtils.hasRecentsPermission(app) + hasContactsRuntimePermission = PermissionUtils.hasContactsPermission(app) + getCalls() + getContacts() + } + + fun getCalls() { + if (!hasRuntimePermission.value) return + + viewModelScope.launch { + callsRepository.getCalls().collect { calls -> + _calls.value = DialerCall.mapList(calls) + } + } + } + + fun getContacts() { + if (!hasContactsRuntimePermission) return + + viewModelScope.launch { + contactsRepository.getContacts().collect { shouldInvalidateCache = true } + } + } + + fun handleRuntimePermissionGranted() { + _hasRuntimePermission.value = true + getCalls() + } + + fun sendMessage(activity: Activity, call: DialerCall) = + CommonUtils.makeSms(activity, call.contactInfo.number) + + fun sendMessage(number: String) = + CommonUtils.makeSms(app, number) + + fun makeCall(activity: Activity, number: String) = CommonUtils.makeCall(activity, number) + fun makeCall(number: String) = CommonUtils.makeCall(app, number) + + fun callDetail(call: DialerCall) { + //navController.navigateToCallDetail(call.childCalls.map { it.id }) + } + + fun createContact(activity: Activity, call: DialerCall) = + CommonUtils.createContact(activity, call.contactInfo.number) + + fun addToContact(activity: Activity, call: DialerCall) = + CommonUtils.addContactAsExisting(activity, call.contactInfo.number) + + fun addToContact(number: String) = + CommonUtils.addContactAsExisting(app, number) + + fun openContact(activity: Activity, call: DialerCall) { + ContactsHelper.getContactByPhoneNumber(activity, call.contactInfo.number)?.let { + CommonUtils.showContactDetail(activity, it.id) + } + } + + fun updateContactInfo(call: DialerCall) { + if (call.isAnonymous() || !hasContactsRuntimePermission) { + return + } + + val callLogInfo = call.contactInfo + + cacheRepository.requestUpdateContactInfo( + viewModelScope, + call.number, + call.countryIso, + callLogInfo = dev.alenajam.opendialer.data.callsCache.ContactInfo( + name = callLogInfo.name, + number = callLogInfo.number, + photoUri = callLogInfo.photoUri, + type = callLogInfo.type, + label = callLogInfo.label, + lookupUri = callLogInfo.lookupUri, + normalizedNumber = callLogInfo.normalizedNumber, + formattedNumber = callLogInfo.formattedNumber, + geoDescription = callLogInfo.geoDescription, + photoId = callLogInfo.photoId + ) + ) + } + + fun startCache() { + startCacheUseCase(viewModelScope, Unit) { /* TODO handle failure */ } + } + + fun stopCache() { + cacheRepository.stop() + } + + fun invalidateCache() { + cacheRepository.invalidate() + _lastInvalidateCache.value = LocalDateTime.now() + } + + fun attemptInvalidateCache() { + if (shouldInvalidateCache) { + invalidateCache() + shouldInvalidateCache = false + } + } +} \ No newline at end of file diff --git a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/DialerViewModel.kt b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/DialerViewModel.kt deleted file mode 100644 index 9da5575..0000000 --- a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/DialerViewModel.kt +++ /dev/null @@ -1,98 +0,0 @@ -package dev.alenajam.opendialer.feature.calls - -import android.app.Activity -import android.app.Application -import androidx.lifecycle.LiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.asLiveData -import androidx.lifecycle.viewModelScope -import androidx.navigation.NavController -import dagger.hilt.android.lifecycle.HiltViewModel -import dev.alenajam.opendialer.core.common.CommonUtils -import dev.alenajam.opendialer.core.common.ContactsHelper -import dev.alenajam.opendialer.core.common.navigateToCallDetail -import dev.alenajam.opendialer.data.calls.ContactInfo -import dev.alenajam.opendialer.data.calls.DialerCall -import dev.alenajam.opendialer.data.calls.DialerRepositoryImpl -import dev.alenajam.opendialer.data.callsCache.CacheRepositoryImpl -import dev.alenajam.opendialer.data.contacts.DialerContact -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.flowOn -import kotlinx.coroutines.flow.map -import javax.inject.Inject - -@HiltViewModel -class DialerViewModel -@Inject constructor( - dialerRepository: DialerRepositoryImpl, - contactsRepository: dev.alenajam.opendialer.data.contacts.DialerRepositoryImpl, - app: Application, - private val cacheRepository: CacheRepositoryImpl, - private val startCacheUseCase: StartCache -) : ViewModel() { - val calls: LiveData> = dialerRepository - .getCalls(app.contentResolver) - .map { DialerCall.mapList(it) } - .flowOn(Dispatchers.IO) - .asLiveData() - - val contacts: LiveData> = contactsRepository - .getContacts(app.contentResolver) - .map { DialerContact.mapList(it) } - .flowOn(Dispatchers.IO) - .asLiveData() - - fun sendMessage(activity: Activity, call: DialerCall) = - CommonUtils.makeSms(activity, call.contactInfo.number) - - // - fun makeCall(activity: Activity, number: String) = CommonUtils.makeCall(activity, number) - - fun callDetail(navController: NavController, call: DialerCall) { - navController.navigateToCallDetail(call.childCalls.map { it.id }) - } - - fun createContact(activity: Activity, call: DialerCall) = - CommonUtils.createContact(activity, call.contactInfo.number) - - fun addToContact(activity: Activity, call: DialerCall) = - CommonUtils.addContactAsExisting(activity, call.contactInfo.number) - - fun openContact(activity: Activity, call: DialerCall) { - ContactsHelper.getContactByPhoneNumber(activity, call.contactInfo.number)?.let { - CommonUtils.showContactDetail(activity, it.id) - } - } - - fun updateContactInfo(number: String?, countryIso: String?, callLogInfo: ContactInfo) { - cacheRepository.requestUpdateContactInfo( - viewModelScope, - number, - countryIso, - callLogInfo = dev.alenajam.opendialer.data.callsCache.ContactInfo( - name = callLogInfo.name, - number = callLogInfo.number, - photoUri = callLogInfo.photoUri, - type = callLogInfo.type, - label = callLogInfo.label, - lookupUri = callLogInfo.lookupUri, - normalizedNumber = callLogInfo.normalizedNumber, - formattedNumber = callLogInfo.formattedNumber, - geoDescription = callLogInfo.geoDescription, - photoId = callLogInfo.photoId - ) - ) - } - - fun startCache() { - startCacheUseCase(viewModelScope, Unit) { /* TODO handle failure */ } - } - - fun stopCache() { - cacheRepository.stop() - } - - fun invalidateCache() { - cacheRepository.invalidate() - } -} \ No newline at end of file diff --git a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsAdapter.kt b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsAdapter.kt deleted file mode 100644 index 9b8b870..0000000 --- a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsAdapter.kt +++ /dev/null @@ -1,254 +0,0 @@ -package dev.alenajam.opendialer.feature.calls - -import android.animation.TimeInterpolator -import android.animation.ValueAnimator -import android.content.Context -import android.graphics.Color -import android.graphics.drawable.Drawable -import android.util.DisplayMetrics -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.animation.AccelerateDecelerateInterpolator -import android.widget.ImageView -import android.widget.TextView -import androidx.cardview.widget.CardView -import androidx.core.animation.doOnEnd -import androidx.core.animation.doOnStart -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.squareup.picasso.Picasso -import com.squareup.picasso.Transformation -import dev.alenajam.opendialer.core.common.CircleTransform -import dev.alenajam.opendialer.core.common.PermissionUtils -import dev.alenajam.opendialer.data.calls.CallOption -import dev.alenajam.opendialer.data.calls.CallType -import dev.alenajam.opendialer.data.calls.ContactInfo -import dev.alenajam.opendialer.data.calls.DialerCall -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.ocpsoft.prettytime.PrettyTime - -private val circleTransform: Transformation = CircleTransform() -private const val itemHeight = 75f -private const val optionHeight = 50 -private const val expandAnimDuration = 200L - -class RecentsAdapter( - context: Context, - private val recyclerView: RecyclerView, - private val coroutineScope: CoroutineScope, - private val onCallClick: (call: DialerCall) -> Unit, - private val onContactClick: (call: DialerCall) -> Unit, - private val onOptionClick: (call: DialerCall, option: CallOption) -> Unit, - private val updateContactInfo: (number: String?, countryIso: String?, callLogInfo: ContactInfo) -> Unit -) : RecyclerView.Adapter() { - private val incoming: Drawable? = ContextCompat.getDrawable(context, R.drawable.icon_21) - private val outgoing: Drawable? = ContextCompat.getDrawable(context, R.drawable.icon_16) - private val missed: Drawable? = ContextCompat.getDrawable(context, R.drawable.icon_22) - private val voicemail: Drawable? = ContextCompat.getDrawable(context, R.drawable.icon_09) - private val blocked: Drawable? = ContextCompat.getDrawable(context, R.drawable.icon_18) - val calls = mutableListOf() - private var expandedItem: Int = -1 - - inner class ViewHolder(inflater: LayoutInflater, private val parent: ViewGroup) : - RecyclerView.ViewHolder(inflater.inflate(R.layout.item_call_log_new, parent, false)) { - private val card = itemView.findViewById(R.id.cardView) - private val textView = itemView.findViewById(R.id.item_call_title) - private val date = itemView.findViewById(R.id.item_call_subtitle) - private val buttonCall = itemView.findViewById(R.id.icon) - private val callsIcons = itemView.findViewById(R.id.calls_icons) - private val contactIcon = itemView.findViewById(R.id.icon_contact) - private val recyclerViewOptions = - itemView.findViewById(R.id.recyclerViewOptions) - private var animator: ValueAnimator? = null - var job: Job? = null - - private fun expand( - call: DialerCall, - holder: RecentsAdapter.ViewHolder, - expand: Boolean, - animate: Boolean? = false - ) { - val height = convertDpToPixels(itemHeight, parent.context) - val expandedHeight = convertDpToPixels( - itemHeight + (call.options.size * optionHeight), - parent.context - ) - val elevation = convertDpToPixels(0f, parent.context) - val elevationExpanded = convertDpToPixels(4f, parent.context) - if (animate == true) { - animator?.cancel() - animator = getValueAnimator( - expand, - expandAnimDuration, - AccelerateDecelerateInterpolator() - ) { - holder.card.layoutParams.height = - (height + (expandedHeight - height) * it).toInt() - holder.card.cardElevation = (elevation + (elevationExpanded - elevation) * it) - holder.card.requestLayout() - } - if (expand) animator?.doOnStart { - holder.recyclerViewOptions.visibility = View.VISIBLE - } - animator?.doOnEnd { - animator = null - if (!expand) holder.recyclerViewOptions.visibility = View.GONE - } - animator?.start() - } else { - holder.card.layoutParams.height = (if (expand) expandedHeight else height).toInt() - holder.card.cardElevation = if (expand) elevationExpanded else elevation - holder.recyclerViewOptions.visibility = if (expand) View.VISIBLE else View.GONE - } - } - - fun bind(currentCall: DialerCall, position: Int) { - val context = parent.context - val contact = currentCall.contactInfo - - /** Update call log only if number is dialable */ - if (!currentCall.isAnonymous() && PermissionUtils.hasContactsPermission(context)) { - job = coroutineScope.launch(Dispatchers.IO) { - updateContactInfo( - currentCall.number, - currentCall.countryIso, - currentCall.contactInfo - ) - } - } - - var name = contact.name - val number = contact.number - - if (currentCall.isAnonymous()) { - name = context.getString(R.string.anonymous) - } else if (name.isNullOrBlank()) { - name = number - } - - textView.text = name - - val isExpanded = position == expandedItem - expand(currentCall, this, isExpanded) - - Picasso.get() - .load(contact.photoUri) - .transform(circleTransform) - .into(contactIcon) - - contactIcon.setOnClickListener { onContactClick(currentCall) } - - card.setOnClickListener { - val isExpandedNow = position == expandedItem - if (!isExpandedNow) { - getExpandedItemViewHolder()?.let { - expand( - currentCall, - it, - expand = false, - animate = true - ) - } - notifyItemChanged(expandedItem) - } - - expandedItem = if (isExpandedNow) -1 else position - expand(currentCall, this, !isExpandedNow, true) - } - - callsIcons.setImageDrawable( - when (currentCall.type) { - CallType.OUTGOING -> outgoing - CallType.INCOMING, CallType.ANSWERED_EXTERNALLY -> incoming - CallType.MISSED, CallType.REJECTED -> missed - CallType.BLOCKED -> blocked - CallType.VOICEMAIL -> voicemail - } - ) - - val prettyDate = PrettyTime().format(currentCall.date) - - date.text = - if (currentCall.childCalls.size > 1) { - context.getString( - R.string.call_log_item_subtitle_number, - currentCall.childCalls.size, - prettyDate - ) - } else { - context.getString(R.string.call_log_item_subtitle, prettyDate) - } - - if (currentCall.isAnonymous()) { - buttonCall.visibility = View.INVISIBLE - } else { - buttonCall.setOnClickListener { onCallClick(currentCall) } - buttonCall.visibility = View.VISIBLE - } - - recyclerViewOptions.layoutManager = LinearLayoutManager(context) - recyclerViewOptions.adapter = CallOptionsAdapter(currentCall.options) { - onOptionClick(currentCall, it) - } - } - } - - private fun getExpandedItemViewHolder(): RecentsAdapter.ViewHolder? { - return recyclerView.findViewHolderForAdapterPosition(expandedItem) as? RecentsAdapter.ViewHolder? - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder = - ViewHolder(LayoutInflater.from(parent.context), parent) - - override fun getItemCount(): Int = calls.size - - override fun onBindViewHolder(holder: ViewHolder, position: Int) = - holder.bind(calls[position], position) - - override fun onViewRecycled(holder: ViewHolder) { - holder.job?.cancel() - } - - fun setData(calls: List) { - val oldCalls = this.calls.toList() - this.calls.apply { - clear() - addAll(calls) - } - if (oldCalls.isEmpty()) { - notifyDataSetChanged() - } else { - coroutineScope.launch(Dispatchers.IO) { - val diffUtil = RecentsDiffUtil(oldCalls, calls) - val result = DiffUtil.calculateDiff(diffUtil) - withContext(Dispatchers.Main) { - result.dispatchUpdatesTo(this@RecentsAdapter) - } - } - } - } -} - -inline fun getValueAnimator( - forward: Boolean = true, - duration: Long? = null, - interpolator: TimeInterpolator? = null, - crossinline updateListener: (progress: Float) -> Unit -): ValueAnimator { - val a = if (forward) ValueAnimator.ofFloat(0f, 1f) else ValueAnimator.ofFloat(1f, 0f) - a.addUpdateListener { updateListener(it.animatedValue as Float) } - duration?.let { a.duration = it } - interpolator?.let { a.interpolator = it } - return a -} - -fun convertDpToPixels(dp: Float, context: Context): Float { - return dp * (context.resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT) -} \ No newline at end of file diff --git a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsDiffUtil.kt b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsDiffUtil.kt deleted file mode 100644 index cda88fe..0000000 --- a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsDiffUtil.kt +++ /dev/null @@ -1,31 +0,0 @@ -package dev.alenajam.opendialer.feature.calls - -import androidx.recyclerview.widget.DiffUtil -import dev.alenajam.opendialer.data.calls.DialerCall - -class RecentsDiffUtil( - private val oldList: List, - private val newList: List -) : DiffUtil.Callback() { - override fun getOldListSize(): Int { - return oldList.size - } - - override fun getNewListSize(): Int { - return newList.size - } - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val old = oldList[oldItemPosition] - val new = newList[newItemPosition] - - return old.id == new.id - } - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - val old = oldList[oldItemPosition] - val new = newList[newItemPosition] - - return old.contactInfo == new.contactInfo - } -} diff --git a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsFragment.kt b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsFragment.kt deleted file mode 100644 index 7e40a17..0000000 --- a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/RecentsFragment.kt +++ /dev/null @@ -1,174 +0,0 @@ -package dev.alenajam.opendialer.feature.calls - -import android.app.Activity -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.lifecycleScope -import androidx.navigation.fragment.findNavController -import androidx.recyclerview.widget.LinearLayoutManager -import dagger.hilt.android.AndroidEntryPoint -import dev.alenajam.opendialer.core.common.PermissionUtils -import dev.alenajam.opendialer.data.calls.CallOption -import dev.alenajam.opendialer.data.calls.DialerCall -import dev.alenajam.opendialer.feature.calls.databinding.FragmentRecentsBinding -import kotlinx.coroutines.ExperimentalCoroutinesApi - -@AndroidEntryPoint -class RecentsFragment : Fragment() { - private val viewModel: DialerViewModel by viewModels() - lateinit var adapter: RecentsAdapter - private var notCalledNumber = "" - private var refreshNeeded = false - private var _binding: FragmentRecentsBinding? = null - private val binding get() = _binding!! - - @ExperimentalCoroutinesApi - private val requestPermissions = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { data -> - /** Ensure that all permissions were allowed */ - if (PermissionUtils.recentsPermissions.all { data[it] == true }) { - /** Hide permission prompt */ - binding.permissionPrompt.visibility = View.GONE - - observeCalls() - } - } - - private val requestMakeCallPermissions = - registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { data -> - /** Ensure that all permissions were allowed */ - if (!data.containsValue(false)) { - /** Retry call, if necessary */ - if (notCalledNumber.isNotBlank()) { - makeCall(notCalledNumber) - } - } - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - _binding = FragmentRecentsBinding.inflate(inflater, container, false) - return binding.root - } - - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - - override fun onStart() { - super.onStart() - viewModel.startCache() - } - - override fun onStop() { - super.onStop() - viewModel.stopCache() - } - - @ExperimentalCoroutinesApi - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - context?.let { - adapter = RecentsAdapter( - it, - binding.recyclerViewCallLog, - coroutineScope = lifecycleScope, - onCallClick = { call -> call.contactInfo.number?.let { num -> makeCall(num) } }, - onContactClick = { call -> openContact(call) }, - onOptionClick = { call, option -> activity?.handleOptionClick(call, option) }, - updateContactInfo = viewModel::updateContactInfo - ) - binding.recyclerViewCallLog.adapter = adapter - binding.recyclerViewCallLog.layoutManager = LinearLayoutManager(context) - } - - if (PermissionUtils.hasRecentsPermission(context)) { - observeCalls() - } else { - /** Show permission prompt */ - binding.permissionPrompt.visibility = View.VISIBLE - binding.buttonPermission.setOnClickListener { - requestPermissions.launch(PermissionUtils.recentsPermissions) - } - } - - if (PermissionUtils.hasContactsPermission(context)) { - observeContacts() - } - } - - override fun onResume() { - super.onResume() - refreshData() - } - - private fun observeCalls() { - /** Ensure that observable isn't observed already */ - if (!viewModel.calls.hasObservers()) { - viewModel.calls.observe(viewLifecycleOwner) { - handleCalls(it) - refreshNeeded = true - } - } - } - - private fun observeContacts() { - viewModel.contacts.observe(viewLifecycleOwner) { - refreshNeeded = true - } - } - - private fun makeCall(number: String) { - if (PermissionUtils.hasMakeCallPermission(context)) { - activity?.let { viewModel.makeCall(it, number) } - notCalledNumber = "" - } else { - notCalledNumber = number - requestMakeCallPermissions.launch(PermissionUtils.makeCallPermissions) - } - } - - private fun openContact(call: DialerCall) = activity?.let { viewModel.openContact(it, call) } - - private fun Activity.handleOptionClick( - call: DialerCall, - option: CallOption - ) = - when (option.id) { - CallOption.ID_SEND_MESSAGE -> viewModel.sendMessage(this, call) - CallOption.ID_CALL_DETAILS -> viewModel.callDetail( - findNavController(), - call - ) - - CallOption.ID_CREATE_CONTACT -> viewModel.createContact( - this, - call - ) - - CallOption.ID_ADD_EXISTING -> viewModel.addToContact(this, call) - else -> Unit - } - - private fun handleCalls(calls: List) { - adapter.setData(calls) - } - - private fun refreshData() { - if (refreshNeeded) { - refreshNeeded = false - viewModel.invalidateCache() - adapter.notifyDataSetChanged() - } - } -} \ No newline at end of file diff --git a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/StartCache.kt b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/StartCache.kt index 76abfdc..d61fa1a 100644 --- a/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/StartCache.kt +++ b/feature/calls/src/main/java/dev/alenajam/opendialer/feature/calls/StartCache.kt @@ -8,5 +8,5 @@ import javax.inject.Inject class StartCache @Inject constructor(private val cacheRepositoryImpl: CacheRepositoryImpl) : UseCase() { - override suspend fun run(params: Unit): Either = cacheRepositoryImpl.start() + override suspend fun run(params: Unit): Either = cacheRepositoryImpl.start() } \ No newline at end of file diff --git a/feature/calls/src/main/res/drawable-hdpi/ic_call.png b/feature/calls/src/main/res/drawable-hdpi/ic_call.png deleted file mode 100644 index 2506620..0000000 Binary files a/feature/calls/src/main/res/drawable-hdpi/ic_call.png and /dev/null differ diff --git a/feature/calls/src/main/res/drawable-mdpi/ic_call.png b/feature/calls/src/main/res/drawable-mdpi/ic_call.png deleted file mode 100644 index fb87e7c..0000000 Binary files a/feature/calls/src/main/res/drawable-mdpi/ic_call.png and /dev/null differ diff --git a/feature/calls/src/main/res/drawable-xhdpi/ic_call.png b/feature/calls/src/main/res/drawable-xhdpi/ic_call.png deleted file mode 100644 index 83d38c8..0000000 Binary files a/feature/calls/src/main/res/drawable-xhdpi/ic_call.png and /dev/null differ diff --git a/feature/calls/src/main/res/drawable-xxhdpi/ic_call.png b/feature/calls/src/main/res/drawable-xxhdpi/ic_call.png deleted file mode 100644 index 2cbdd10..0000000 Binary files a/feature/calls/src/main/res/drawable-xxhdpi/ic_call.png and /dev/null differ diff --git a/feature/calls/src/main/res/drawable-xxxhdpi/ic_call.png b/feature/calls/src/main/res/drawable-xxxhdpi/ic_call.png deleted file mode 100644 index 2093dfe..0000000 Binary files a/feature/calls/src/main/res/drawable-xxxhdpi/ic_call.png and /dev/null differ diff --git a/feature/calls/src/main/res/drawable/icon_09.png b/feature/calls/src/main/res/drawable/icon_09.png deleted file mode 100644 index 57eab87..0000000 Binary files a/feature/calls/src/main/res/drawable/icon_09.png and /dev/null differ diff --git a/feature/calls/src/main/res/drawable/icon_16.png b/feature/calls/src/main/res/drawable/icon_16.png deleted file mode 100644 index bf8a56c..0000000 Binary files a/feature/calls/src/main/res/drawable/icon_16.png and /dev/null differ diff --git a/feature/calls/src/main/res/drawable/icon_18.png b/feature/calls/src/main/res/drawable/icon_18.png deleted file mode 100644 index 21c3d9b..0000000 Binary files a/feature/calls/src/main/res/drawable/icon_18.png and /dev/null differ diff --git a/feature/calls/src/main/res/drawable/icon_21.png b/feature/calls/src/main/res/drawable/icon_21.png deleted file mode 100644 index b7c9f85..0000000 Binary files a/feature/calls/src/main/res/drawable/icon_21.png and /dev/null differ diff --git a/feature/calls/src/main/res/drawable/icon_22.png b/feature/calls/src/main/res/drawable/icon_22.png deleted file mode 100644 index 11c86bd..0000000 Binary files a/feature/calls/src/main/res/drawable/icon_22.png and /dev/null differ diff --git a/feature/calls/src/main/res/layout/fragment_recents.xml b/feature/calls/src/main/res/layout/fragment_recents.xml deleted file mode 100644 index 0b7c7b1..0000000 --- a/feature/calls/src/main/res/layout/fragment_recents.xml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - -