1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.localepicker; 18 19 import static com.android.settings.flags.Flags.localeNotificationEnabled; 20 21 import android.app.FragmentTransaction; 22 import android.app.LocaleManager; 23 import android.app.NotificationChannel; 24 import android.app.NotificationManager; 25 import android.app.PendingIntent; 26 import android.app.settings.SettingsEnums; 27 import android.content.Intent; 28 import android.content.pm.PackageManager; 29 import android.net.Uri; 30 import android.os.Bundle; 31 import android.os.LocaleList; 32 import android.os.SystemClock; 33 import android.text.TextUtils; 34 import android.util.Log; 35 import android.view.MenuItem; 36 import android.view.View; 37 import android.widget.FrameLayout; 38 import android.widget.ListView; 39 40 import androidx.core.app.NotificationCompat; 41 import androidx.core.view.ViewCompat; 42 43 import com.android.internal.app.LocalePickerWithRegion; 44 import com.android.internal.app.LocaleStore; 45 import com.android.settings.R; 46 import com.android.settings.applications.AppLocaleUtil; 47 import com.android.settings.applications.appinfo.AppLocaleDetails; 48 import com.android.settings.core.SettingsBaseActivity; 49 import com.android.settings.overlay.FeatureFactory; 50 import com.android.settingslib.core.instrumentation.MetricsFeatureProvider; 51 52 public class AppLocalePickerActivity extends SettingsBaseActivity 53 implements LocalePickerWithRegion.LocaleSelectedListener, MenuItem.OnActionExpandListener { 54 private static final String TAG = AppLocalePickerActivity.class.getSimpleName(); 55 private static final String CHANNEL_ID_SUGGESTION = "suggestion"; 56 private static final String CHANNEL_ID_SUGGESTION_TO_USER = "Locale suggestion"; 57 private static final int SIM_LOCALE = 1 << 0; 58 private static final int SYSTEM_LOCALE = 1 << 1; 59 private static final int APP_LOCALE = 1 << 2; 60 private static final int IME_LOCALE = 1 << 3; 61 static final String EXTRA_APP_LOCALE = "app_locale"; 62 static final String EXTRA_NOTIFICATION_ID = "notification_id"; 63 64 private String mPackageName; 65 private LocalePickerWithRegion mLocalePickerWithRegion; 66 private AppLocaleDetails mAppLocaleDetails; 67 private View mAppLocaleDetailContainer; 68 private NotificationController mNotificationController; 69 private MetricsFeatureProvider mMetricsFeatureProvider; 70 71 @Override onCreate(Bundle savedInstanceState)72 public void onCreate(Bundle savedInstanceState) { 73 super.onCreate(savedInstanceState); 74 Uri data = getIntent().getData(); 75 if (data == null) { 76 Log.d(TAG, "There is no uri data."); 77 finish(); 78 return; 79 } 80 mPackageName = data.getSchemeSpecificPart(); 81 if (TextUtils.isEmpty(mPackageName)) { 82 Log.d(TAG, "There is no package name."); 83 finish(); 84 return; 85 } 86 87 if (!canDisplayLocaleUi()) { 88 Log.w(TAG, "Not allow to display Locale Settings UI."); 89 finish(); 90 return; 91 } 92 93 setTitle(R.string.app_locale_picker_title); 94 getActionBar().setDisplayHomeAsUpEnabled(true); 95 mMetricsFeatureProvider = FeatureFactory.getFeatureFactory().getMetricsFeatureProvider(); 96 mNotificationController = NotificationController.getInstance(this); 97 98 mLocalePickerWithRegion = LocalePickerWithRegion.createLanguagePicker( 99 this, 100 this, 101 false /* translate only */, 102 null, 103 mPackageName, 104 this); 105 mAppLocaleDetails = AppLocaleDetails.newInstance(mPackageName, getUserId()); 106 mAppLocaleDetailContainer = launchAppLocaleDetailsPage(); 107 // Launch Locale picker part. 108 launchLocalePickerPage(); 109 } 110 111 @Override onOptionsItemSelected(MenuItem item)112 public boolean onOptionsItemSelected(MenuItem item) { 113 if (item.getItemId() == android.R.id.home) { 114 super.onBackPressed(); 115 return true; 116 } 117 return super.onOptionsItemSelected(item); 118 } 119 120 @Override onLocaleSelected(LocaleStore.LocaleInfo localeInfo)121 public void onLocaleSelected(LocaleStore.LocaleInfo localeInfo) { 122 if (localeInfo == null || localeInfo.getLocale() == null || localeInfo.isSystemLocale()) { 123 setAppDefaultLocale(""); 124 } else { 125 logLocaleSource(localeInfo); 126 setAppDefaultLocale(localeInfo.getLocale().toLanguageTag()); 127 broadcastAppLocaleChange(localeInfo); 128 } 129 finish(); 130 } 131 132 @Override onMenuItemActionCollapse(MenuItem item)133 public boolean onMenuItemActionCollapse(MenuItem item) { 134 mAppBarLayout.setExpanded(false /*expanded*/, false /*animate*/); 135 ViewCompat.setNestedScrollingEnabled(mAppLocaleDetails.getListView(), true); 136 ViewCompat.setNestedScrollingEnabled(mLocalePickerWithRegion.getListView(), true); 137 return true; 138 } 139 140 @Override onMenuItemActionExpand(MenuItem item)141 public boolean onMenuItemActionExpand(MenuItem item) { 142 mAppBarLayout.setExpanded(false /*expanded*/, false /*animate*/); 143 ViewCompat.setNestedScrollingEnabled(mAppLocaleDetails.getListView(), false); 144 ViewCompat.setNestedScrollingEnabled(mLocalePickerWithRegion.getListView(), false); 145 return true; 146 } 147 148 /** Sets the app's locale to the supplied language tag */ setAppDefaultLocale(String languageTag)149 private void setAppDefaultLocale(String languageTag) { 150 Log.d(TAG, "setAppDefaultLocale: " + languageTag); 151 LocaleManager localeManager = getSystemService(LocaleManager.class); 152 if (localeManager == null) { 153 Log.w(TAG, "LocaleManager is null, cannot set default app locale"); 154 return; 155 } 156 localeManager.setApplicationLocales(mPackageName, LocaleList.forLanguageTags(languageTag)); 157 } 158 broadcastAppLocaleChange(LocaleStore.LocaleInfo localeInfo)159 private void broadcastAppLocaleChange(LocaleStore.LocaleInfo localeInfo) { 160 if (!localeNotificationEnabled()) { 161 Log.w(TAG, "Locale notification is not enabled"); 162 return; 163 } 164 if (localeInfo.isAppCurrentLocale()) { 165 return; 166 } 167 try { 168 String localeTag = localeInfo.getLocale().toLanguageTag(); 169 int uid = getPackageManager().getApplicationInfo(mPackageName, 170 PackageManager.GET_META_DATA).uid; 171 boolean launchNotification = mNotificationController.shouldTriggerNotification( 172 uid, localeTag); 173 if (launchNotification) { 174 triggerNotification( 175 mNotificationController.getNotificationId(localeTag), 176 getString(R.string.title_system_locale_addition, 177 localeInfo.getFullNameNative()), 178 getString(R.string.desc_system_locale_addition), 179 localeTag); 180 mMetricsFeatureProvider.action(this, 181 SettingsEnums.ACTION_NOTIFICATION_FOR_SYSTEM_LOCALE); 182 } 183 } catch (PackageManager.NameNotFoundException e) { 184 Log.e(TAG, "Unable to find info for package: " + mPackageName); 185 } 186 } 187 triggerNotification( int notificationId, String title, String description, String localeTag)188 private void triggerNotification( 189 int notificationId, 190 String title, 191 String description, 192 String localeTag) { 193 NotificationManager notificationManager = getSystemService(NotificationManager.class); 194 final boolean channelExist = 195 notificationManager.getNotificationChannel(CHANNEL_ID_SUGGESTION) != null; 196 197 // Create an alert channel if it does not exist 198 if (!channelExist) { 199 NotificationChannel channel = 200 new NotificationChannel( 201 CHANNEL_ID_SUGGESTION, 202 CHANNEL_ID_SUGGESTION_TO_USER, 203 NotificationManager.IMPORTANCE_DEFAULT); 204 channel.setSound(/* sound */ null, /* audioAttributes */ null); // silent notification 205 notificationManager.createNotificationChannel(channel); 206 } 207 final NotificationCompat.Builder builder = 208 new NotificationCompat.Builder(this, CHANNEL_ID_SUGGESTION) 209 .setSmallIcon(R.drawable.ic_settings_language) 210 .setAutoCancel(true) 211 .setContentTitle(title) 212 .setContentText(description) 213 .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) 214 .setContentIntent( 215 createPendingIntent(localeTag, notificationId, false)) 216 .setDeleteIntent( 217 createPendingIntent(localeTag, notificationId, true)); 218 notificationManager.notify(notificationId, builder.build()); 219 } 220 createPendingIntent(String locale, int notificationId, boolean isDeleteIntent)221 private PendingIntent createPendingIntent(String locale, int notificationId, 222 boolean isDeleteIntent) { 223 Intent intent = isDeleteIntent 224 ? new Intent(this, NotificationCancelReceiver.class) 225 : new Intent(this, NotificationActionActivity.class) 226 .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); 227 228 intent.putExtra(EXTRA_APP_LOCALE, locale) 229 .putExtra(EXTRA_NOTIFICATION_ID, notificationId); 230 int flag = PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT; 231 int elapsedTime = (int) SystemClock.elapsedRealtimeNanos(); 232 233 return isDeleteIntent 234 ? PendingIntent.getBroadcast(this, elapsedTime, intent, flag) 235 : PendingIntent.getActivity(this, elapsedTime, intent, flag); 236 } 237 launchAppLocaleDetailsPage()238 private View launchAppLocaleDetailsPage() { 239 FrameLayout appLocaleDetailsContainer = new FrameLayout(this); 240 appLocaleDetailsContainer.setId(R.id.layout_app_locale_details); 241 getSupportFragmentManager() 242 .beginTransaction() 243 .replace(R.id.layout_app_locale_details, mAppLocaleDetails) 244 .commit(); 245 return appLocaleDetailsContainer; 246 } 247 launchLocalePickerPage()248 private void launchLocalePickerPage() { 249 // LocalePickerWithRegion use android.app.ListFragment. Thus, it can not use 250 // getSupportFragmentManager() to add this into container. 251 android.app.FragmentManager fragmentManager = getFragmentManager(); 252 fragmentManager.registerFragmentLifecycleCallbacks( 253 new android.app.FragmentManager.FragmentLifecycleCallbacks() { 254 @Override 255 public void onFragmentViewCreated( 256 android.app.FragmentManager fm, 257 android.app.Fragment f, View v, Bundle s) { 258 super.onFragmentViewCreated(fm, f, v, s); 259 ListView listView = (ListView) v.findViewById(android.R.id.list); 260 if (listView != null) { 261 listView.addHeaderView(mAppLocaleDetailContainer); 262 } 263 } 264 }, true); 265 fragmentManager.beginTransaction() 266 .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN) 267 .replace(R.id.content_frame, mLocalePickerWithRegion) 268 .commit(); 269 } 270 canDisplayLocaleUi()271 private boolean canDisplayLocaleUi() { 272 try { 273 PackageManager packageManager = getPackageManager(); 274 return AppLocaleUtil.canDisplayLocaleUi(this, 275 packageManager.getApplicationInfo(mPackageName, 0), 276 packageManager.queryIntentActivities(AppLocaleUtil.LAUNCHER_ENTRY_INTENT, 277 PackageManager.GET_META_DATA)); 278 } catch (PackageManager.NameNotFoundException e) { 279 Log.e(TAG, "Unable to find info for package: " + mPackageName); 280 } 281 282 return false; 283 } 284 logLocaleSource(LocaleStore.LocaleInfo localeInfo)285 private void logLocaleSource(LocaleStore.LocaleInfo localeInfo) { 286 if (!localeInfo.isSuggested() || localeInfo.isAppCurrentLocale()) { 287 return; 288 } 289 int localeSource = 0; 290 if (hasSuggestionType(localeInfo, 291 LocaleStore.LocaleInfo.SUGGESTION_TYPE_SYSTEM_AVAILABLE_LANGUAGE)) { 292 localeSource |= SYSTEM_LOCALE; 293 } 294 if (hasSuggestionType(localeInfo, 295 LocaleStore.LocaleInfo.SUGGESTION_TYPE_OTHER_APP_LANGUAGE)) { 296 localeSource |= APP_LOCALE; 297 } 298 if (hasSuggestionType(localeInfo, LocaleStore.LocaleInfo.SUGGESTION_TYPE_IME_LANGUAGE)) { 299 localeSource |= IME_LOCALE; 300 } 301 if (hasSuggestionType(localeInfo, LocaleStore.LocaleInfo.SUGGESTION_TYPE_SIM)) { 302 localeSource |= SIM_LOCALE; 303 } 304 mMetricsFeatureProvider.action(this, 305 SettingsEnums.ACTION_CHANGE_APP_LANGUAGE_FROM_SUGGESTED, localeSource); 306 } 307 hasSuggestionType(LocaleStore.LocaleInfo localeInfo, int suggestionType)308 private static boolean hasSuggestionType(LocaleStore.LocaleInfo localeInfo, 309 int suggestionType) { 310 return localeInfo.isSuggestionOfType(suggestionType); 311 } 312 } 313