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