forked from mozilla-mobile/android-components
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
For mozilla-mobile#12565 Implement the common part of search widget i…
…n Android Components
- Loading branch information
1 parent
eaabbcb
commit a2bace4
Showing
25 changed files
with
982 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
apply plugin: 'com.android.library' | ||
apply plugin: 'kotlin-android' | ||
|
||
android { | ||
compileSdkVersion config.compileSdkVersion | ||
|
||
defaultConfig { | ||
minSdkVersion config.minSdkVersion | ||
targetSdkVersion config.targetSdkVersion | ||
} | ||
|
||
buildTypes { | ||
release { | ||
minifyEnabled false | ||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||
} | ||
} | ||
} | ||
|
||
dependencies { | ||
implementation project(':support-base') | ||
implementation project(':ui-colors') | ||
implementation Dependencies.androidx_core_ktx | ||
|
||
testImplementation project(':support-test') | ||
testImplementation Dependencies.androidx_test_core | ||
testImplementation Dependencies.androidx_test_junit | ||
testImplementation Dependencies.testing_robolectric | ||
testImplementation Dependencies.testing_mockito | ||
} | ||
apply from: '../../../publish.gradle' | ||
ext.configurePublish(config.componentsGroupId, archivesBaseName, project.ext.description) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Add project specific ProGuard rules here. | ||
# You can control the set of applied configuration files using the | ||
# proguardFiles setting in build.gradle. | ||
# | ||
# For more details, see | ||
# http://developer.android.com/guide/developing/tools/proguard.html | ||
|
||
# If your project uses WebView with JS, uncomment the following | ||
# and specify the fully qualified class name to the JavaScript interface | ||
# class: | ||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview { | ||
# public *; | ||
#} | ||
|
||
# Uncomment this to preserve the line number information for | ||
# debugging stack traces. | ||
#-keepattributes SourceFile,LineNumberTable | ||
|
||
# If you keep the line number information, uncomment this to | ||
# hide the original source file name. | ||
#-renamesourcefileattribute SourceFile |
10 changes: 10 additions & 0 deletions
10
components/feature/searchwidget/src/main/AndroidManifest.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<manifest package="mozilla.components.feature.searchwidget" | ||
xmlns:android="http://schemas.android.com/apk/res/android" | ||
xmlns:tools="http://schemas.android.com/tools"> | ||
|
||
<!-- Needed to interact with all apps installed on a device --> | ||
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" | ||
tools:ignore="QueryAllPackagesPermission" /> | ||
|
||
</manifest> |
274 changes: 274 additions & 0 deletions
274
...chwidget/src/main/java/mozilla/components/feature/searchwidget/AppSearchWidgetProvider.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,274 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
package mozilla.components.feature.searchwidget | ||
|
||
import android.app.PendingIntent | ||
import android.appwidget.AppWidgetManager | ||
import android.appwidget.AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH | ||
import android.appwidget.AppWidgetProvider | ||
import android.content.ComponentName | ||
import android.content.Context | ||
import android.content.Intent | ||
import android.os.Build | ||
import android.os.Bundle | ||
import android.speech.RecognizerIntent | ||
import android.view.View | ||
import android.widget.RemoteViews | ||
import androidx.annotation.Dimension | ||
import androidx.annotation.Dimension.DP | ||
import androidx.annotation.VisibleForTesting | ||
import androidx.appcompat.content.res.AppCompatResources | ||
import androidx.core.graphics.drawable.toBitmap | ||
import mozilla.components.feature.searchwidget.VoiceSearchActivity.Companion.SPEECH_PROCESSING | ||
|
||
abstract class AppSearchWidgetProvider : AppWidgetProvider() { | ||
|
||
override fun onUpdate( | ||
context: Context, | ||
appWidgetManager: AppWidgetManager, | ||
appWidgetIds: IntArray | ||
) { | ||
val textSearchIntent = createTextSearchIntent(context) | ||
val voiceSearchIntent = createVoiceSearchIntent(context) | ||
|
||
appWidgetIds.forEach { appWidgetId -> | ||
updateWidgetLayout( | ||
context = context, | ||
appWidgetId = appWidgetId, | ||
appWidgetManager = appWidgetManager, | ||
voiceSearchIntent = voiceSearchIntent, | ||
textSearchIntent = textSearchIntent | ||
) | ||
} | ||
} | ||
|
||
override fun onAppWidgetOptionsChanged( | ||
context: Context, | ||
appWidgetManager: AppWidgetManager, | ||
appWidgetId: Int, | ||
newOptions: Bundle? | ||
) { | ||
val textSearchIntent = createTextSearchIntent(context) | ||
val voiceSearchIntent = createVoiceSearchIntent(context) | ||
|
||
updateWidgetLayout( | ||
context = context, | ||
appWidgetId = appWidgetId, | ||
appWidgetManager = appWidgetManager, | ||
voiceSearchIntent = voiceSearchIntent, | ||
textSearchIntent = textSearchIntent | ||
) | ||
} | ||
|
||
/** | ||
* Builds pending intent that opens the browser and starts a new text search. | ||
*/ | ||
abstract fun createTextSearchIntent(context: Context): PendingIntent | ||
|
||
/** | ||
* If the microphone will appear on the Search Widget and the user can perform a voice search | ||
*/ | ||
abstract fun shouldShowVoiceSearch(context: Context): Boolean | ||
|
||
/** | ||
* Activity that extends VoiceSearchActivity | ||
*/ | ||
abstract fun voiceSearchActivity(): VoiceSearchActivity | ||
|
||
/** | ||
* Builds pending intent that starts a new voice search. | ||
*/ | ||
@VisibleForTesting | ||
internal fun createVoiceSearchIntent(context: Context): PendingIntent? { | ||
if (!shouldShowVoiceSearch(context)) { | ||
return null | ||
} | ||
|
||
val voiceIntent = Intent(context, voiceSearchActivity()::class.java).apply { | ||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK | ||
putExtra(SPEECH_PROCESSING, true) | ||
} | ||
|
||
val intentSpeech = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH) | ||
|
||
return intentSpeech.resolveActivity(context.packageManager)?.let { | ||
PendingIntent.getActivity( | ||
context, | ||
REQUEST_CODE_VOICE, voiceIntent, IntentUtils.defaultIntentPendingFlags | ||
) | ||
} | ||
} | ||
|
||
private fun updateWidgetLayout( | ||
context: Context, | ||
appWidgetId: Int, | ||
appWidgetManager: AppWidgetManager, | ||
voiceSearchIntent: PendingIntent?, | ||
textSearchIntent: PendingIntent | ||
) { | ||
val currentWidth = | ||
appWidgetManager.getAppWidgetOptions(appWidgetId).getInt(OPTION_APPWIDGET_MIN_WIDTH) | ||
val layoutSize = getLayoutSize(currentWidth) | ||
// It's not enough to just hide the microphone on the "small" sized widget due to its design. | ||
// The "small" widget needs a complete redesign, meaning it needs a new layout file. | ||
val showMic = (voiceSearchIntent != null) | ||
val layout = getLayout(layoutSize, showMic) | ||
val text = getText(layoutSize, context) | ||
|
||
val views = | ||
createRemoteViews(context, layout, textSearchIntent, voiceSearchIntent, text) | ||
appWidgetManager.updateAppWidget(appWidgetId, views) | ||
} | ||
|
||
private fun createRemoteViews( | ||
context: Context, | ||
layout: Int, | ||
textSearchIntent: PendingIntent, | ||
voiceSearchIntent: PendingIntent?, | ||
text: String? | ||
): RemoteViews { | ||
return RemoteViews(context.packageName, layout).apply { | ||
setIcon(context) | ||
when (layout) { | ||
R.layout.mozac_search_widget_extra_small_v1, | ||
R.layout.mozac_search_widget_extra_small_v2, | ||
R.layout.mozac_search_widget_small_no_mic -> { | ||
setOnClickPendingIntent( | ||
R.id.mozac_button_search_widget_new_tab, | ||
textSearchIntent | ||
) | ||
} | ||
R.layout.mozac_search_widget_small -> { | ||
setOnClickPendingIntent( | ||
R.id.mozac_button_search_widget_new_tab, | ||
textSearchIntent | ||
) | ||
setOnClickPendingIntent( | ||
R.id.mozac_button_search_widget_voice, | ||
voiceSearchIntent | ||
) | ||
} | ||
R.layout.mozac_search_widget_medium, | ||
R.layout.mozac_search_widget_large -> { | ||
setOnClickPendingIntent( | ||
R.id.mozac_button_search_widget_new_tab, | ||
textSearchIntent | ||
) | ||
setOnClickPendingIntent( | ||
R.id.mozac_button_search_widget_voice, | ||
voiceSearchIntent | ||
) | ||
setOnClickPendingIntent( | ||
R.id.mozac_button_search_widget_new_tab_icon, | ||
textSearchIntent | ||
) | ||
setTextViewText(R.id.mozac_button_search_widget_new_tab, text) | ||
// Unlike "small" widget, "medium" and "large" sizes do not have separate layouts | ||
// that exclude the microphone icon, which is why we must hide it accordingly here. | ||
if (voiceSearchIntent == null) { | ||
setViewVisibility(R.id.mozac_button_search_widget_voice, View.GONE) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
private fun RemoteViews.setIcon(context: Context) { | ||
// gradient color available for android:fillColor only on SDK 24+ | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | ||
setImageViewResource( | ||
R.id.mozac_button_search_widget_new_tab_icon, | ||
R.drawable.mozac_ic_launcher_foreground | ||
) | ||
} else { | ||
setImageViewBitmap( | ||
R.id.mozac_button_search_widget_new_tab_icon, | ||
AppCompatResources.getDrawable( | ||
context, | ||
R.drawable.mozac_ic_launcher_foreground | ||
)?.toBitmap() | ||
) | ||
} | ||
|
||
val appName = context.getString(R.string.app_name) | ||
setContentDescription( | ||
R.id.mozac_button_search_widget_new_tab_icon, | ||
context.getString(R.string.search_widget_content_description, appName) | ||
) | ||
} | ||
|
||
// Cell sizes obtained from the actual dimensions listed in search widget specs | ||
companion object { | ||
private const val DP_EXTRA_SMALL = 64 | ||
private const val DP_SMALL = 100 | ||
private const val DP_MEDIUM = 192 | ||
private const val DP_LARGE = 256 | ||
private const val REQUEST_CODE_VOICE = 1 | ||
|
||
fun updateAllWidgets(context: Context, widgetClassNameApp: AppSearchWidgetProvider) { | ||
val widgetManager = AppWidgetManager.getInstance(context) | ||
val widgetIds = widgetManager.getAppWidgetIds( | ||
ComponentName( | ||
context, | ||
widgetClassNameApp::class.java | ||
) | ||
) | ||
|
||
if (widgetIds.isNotEmpty()) { | ||
context.sendBroadcast( | ||
Intent(context, widgetClassNameApp::class.java).apply { | ||
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE | ||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds) | ||
} | ||
) | ||
} | ||
} | ||
|
||
@VisibleForTesting | ||
internal fun getLayoutSize(@Dimension(unit = DP) dp: Int) = when { | ||
dp >= DP_LARGE -> SearchWidgetProviderSize.LARGE | ||
dp >= DP_MEDIUM -> SearchWidgetProviderSize.MEDIUM | ||
dp >= DP_SMALL -> SearchWidgetProviderSize.SMALL | ||
dp >= DP_EXTRA_SMALL -> SearchWidgetProviderSize.EXTRA_SMALL_V2 | ||
else -> SearchWidgetProviderSize.EXTRA_SMALL_V1 | ||
} | ||
|
||
/** | ||
* Get the layout resource to use for the search widget. | ||
*/ | ||
@VisibleForTesting | ||
internal fun getLayout(size: SearchWidgetProviderSize, showMic: Boolean) = when (size) { | ||
SearchWidgetProviderSize.LARGE -> R.layout.mozac_search_widget_large | ||
SearchWidgetProviderSize.MEDIUM -> R.layout.mozac_search_widget_medium | ||
SearchWidgetProviderSize.SMALL -> { | ||
if (showMic) { | ||
R.layout.mozac_search_widget_small | ||
} else { | ||
R.layout.mozac_search_widget_small_no_mic | ||
} | ||
} | ||
SearchWidgetProviderSize.EXTRA_SMALL_V2 -> R.layout.mozac_search_widget_extra_small_v2 | ||
SearchWidgetProviderSize.EXTRA_SMALL_V1 -> R.layout.mozac_search_widget_extra_small_v1 | ||
} | ||
|
||
/** | ||
* Get the text to place in the search widget | ||
*/ | ||
@VisibleForTesting | ||
internal fun getText(layout: SearchWidgetProviderSize, context: Context) = when (layout) { | ||
SearchWidgetProviderSize.MEDIUM -> context.getString(R.string.search_widget_text_short) | ||
SearchWidgetProviderSize.LARGE -> context.getString(R.string.search_widget_text_long) | ||
else -> null | ||
} | ||
} | ||
} | ||
|
||
enum class SearchWidgetProviderSize { | ||
EXTRA_SMALL_V1, | ||
EXTRA_SMALL_V2, | ||
SMALL, | ||
MEDIUM, | ||
LARGE, | ||
} |
23 changes: 23 additions & 0 deletions
23
...feature/searchwidget/src/main/java/mozilla/components/feature/searchwidget/IntentUtils.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
package mozilla.components.feature.searchwidget | ||
|
||
import android.app.PendingIntent | ||
import android.os.Build | ||
|
||
object IntentUtils { | ||
|
||
/** | ||
* Since Android 12 we need to set PendingIntent mutability explicitly, but Android 6 can be the minimum version | ||
* This additional requirement improves your app's security. | ||
* FLAG_IMMUTABLE -> Flag indicating that the created PendingIntent should be immutable. | ||
*/ | ||
val defaultIntentPendingFlags | ||
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { | ||
PendingIntent.FLAG_IMMUTABLE | ||
} else { | ||
0 // No flags. Default behavior. | ||
} | ||
} |
Oops, something went wrong.