Fix resources when language is changed - android

I already know how to change the language of my application (updating the configuration). My code also check if the configuration is changed by the system and "fix it" in the ´onCreate´ method. I even have created a ListPreference to let the user decide the language with one that my app supports (and saves the decision).
Let's say I have 3 activities (A, B and SettingsActivity). Activity A can start activities B and SettingsActivity. Activity B can start SettingsActivity. If the user changes the language inside SettingsActivity, I can update its resources (in this case Strings) without any problem using this code:
//if (Build.VERSION_CODES.HONEYCOMB <= Build.VERSION.SDK_INT) {
// Disabled because it blinks and looks bad
// recreate();
// } else {
startActivity(getIntent());
finish();
overridePendingTransition(0, 0);
// }
However, I'm unable to change the already open activities because I have no reference to them from SettingsActivity.
My question: is there any clean way to update the resources or recreate the already open activities? If I don't find a better solution, my approach will be one of the above:
Start activities using startActivityForResultand return a code to trigger the code I already use to recreate the activity.
Check inside the onResume method if the current language has changed and do the same thing.

At the end what I did was this:
#Override
protected void onStart() {
super.onStart();
if (!locale.equals(getResources().getConfiguration().locale)) {
finish();
startActivity(getIntent());
overridePendingTransition(0, 0);
return;
}
}
Where locale is a variable assigned in my onCreate method:
private Locale locale;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((Application) getApplication()).refreshLanguage();
locale = getResources().getConfiguration().locale;
setContentView(R.layout.activity_main);
//moar code
}
Finally, for the sake of posting code, here is my refreshLanguage method(s):
boolean refreshLanguage() {
return refreshLanguage(PreferenceManager.getDefaultSharedPreferences(this));
}
boolean refreshLanguage(SharedPreferences sharedPreferences) {
if (sharedPreferences.contains("language")) {
int languageIndex = Integer.parseInt(sharedPreferences.getString("language", "0"));
if (!getResources().getConfiguration().locale.equals(languages[languageIndex])) {
Configuration config = new Configuration();
config.locale = languages[languageIndex];
getResources().updateConfiguration(config, null);
return true;
}
}
return false;
}
Notice that I use onStart rather than onResume because I'm not switching between transparent activities or using dialogs.

Related

Splash Screen Recreating After Calling AppCompatDelegate.setDefaultNightMode

I am using the AppCompatDelegate.setDefaultNightMode(mode); to set the Night Mode in my Android Application, whenever the user chooses any mode that preference configuration in the Shared Preferences on their device, now while I use the shared preferences to set the UI mode when the app starts from the Splash Screen Activity, the activity is getting recreated and then there are 2 instances of my app, as the Splash Screen intents to Landing Activity.
Here is my SplashScreen.java
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash_screen);
SharedPreferences prefs = getSharedPreferences(UI_MODE, MODE_PRIVATE);
name = prefs.getString("uiMode", "System");
applyUI();
fireSplashScreen();
}
private void applyUI() {
if (name.equals("Dark")){
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
}
else if (name.equals("Light")){
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
}
else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
}
private void fireSplashScreen() {
Intent i = new Intent(SplashScreen.this, Landing.class);
startActivity(i);
finish();
}
What can I do to avoid creating multiple instances of the Landing Activity?
Make sure to call AppCompatDelegate.setDefaultNightMode() as soon as possible. E.g. before calling super.onCreate(). Ideally, you would want to call it in Application.onCreate(). Also, make sure to use the latest version of AppCompat (at least 1.1.0) or you might face this issue anyway.
See this answer for more details.

Restarting app without worrying about onSaveInstanceState

How do I simply just restart my ENTIRE app instead of trying to worry about saving the instance perfectly in onSaveInstanceState and reinitializing everything perfectly when resumed/restored in onRestoreInstanceState? (this can quickly become error prone)
UPDATE 10.1.16
I chose to do this in onCreate since onRestoreInstanceState behaves oddly sometimes.
This method is based on the fact that the onCreate(Bundle) is null unless the activity is being revived in which case it is whatever onSaveInstanceState(Bundle) set it to.
I set TWO flags. One in onSaveInstanceState in the Bundle so to know that it is a valid Bundle set by me. The other in the class itself to determine if onCreate was called because of recreation or rotation. And so in onCreate I checked to see if onSaveInstanceState is not null, check the Bundle flag, and check bInit (which defaults to false). If both flags are true then it means android dumped and destroyed our apps memory and the safest way to ensure everything is initialized again in a linear-style application is to just restart it and launch the beginning activity.
public class SomeMiddleActivity extends AppCompatActivity
{
private static boolean bInit = false; // only way it will be false again is if android cleared our memory and we are recreating
#Override
public void onSaveInstanceState(Bundle state)
{
// set a flag so that onCreate knows this is valid
state.putBoolean("StateSaved", true);
super.onSaveInstanceState(state);
}
#Override
protected void onCreate(Bundle savedInstanceState)
{
// this must be called first always for some reason
super.onCreate(savedInstanceState);
if (savedInstanceState != null)
{
if (savedInstanceState.getBoolean("StateSaved", false) && !bInit)
{
// we were recreated... start app over
Intent intent = new Intent(getApplicationContext(), Startup.class);
startActivity(intent);
finish();
return;
}
}
bInit = true; // this will stay true until android has cleared our memory
.......
}
Hope this helps someone and although this has worked thus far, if anyone has a different suggestion let me know.
And FYI: the onSaveInstanceState(Bundle, PersistableBundle) version of onSaveInstanceState is never called ever so I dont know why they even implement it. (?)
REFERENCES:
ACCORDING TO ANDROID DOCUMENTATION
onCreate
Bundle: If the activity is being re-initialized after previously being shut down then this Bundle contains the data it most recently supplied in onSaveInstanceState(Bundle). Note: Otherwise it is null.
Try implementing this way
private final String IS_RE_CREATED = "is_re_created";
#Override
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean(IS_RE_CREATED, true);
super.onSaveInstanceState(outState);
}
#Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState.containsKey(IS_RE_CREATED)) {
boolean isRecreated = savedInstanceState.getBoolean(IS_RE_CREATED, false);
if (isRecreated) restartApplication(this);
}
}
public void restartApplication(Context context) {
String packageName = context.getPackageName();
PackageManager packageManager = context.getPackageManager();
// Intent to start launcher activity and closing all previous ones
Intent restartIntent = packageManager.getLaunchIntentForPackage(packageName);
restartIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
restartIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
context.startActivity(restartIntent);
// Kill Current Process
Process.killProcess(Process.myPid());
System.exit(0);
}
Note: It is not a recommended to forcefully restart application.
How do I simply just restart my app instead of trying to worry about saving the instance
You mean the current activity? Do nothing (Don't implement onSaveInstanceState and onRestoreInstanceState).
The activity gets created automatically when changes happen. If there is no saved instance state, the activity won't restore any data.
Edit:
I think I came across similar issue too few weeks earlier, where I've to kill all the activities in the back stack and open a fresh new activity.
// Start Main Activity
Intent intent = new Intent(this, MainActivity.class);
finishAffinity();
startActivity(intent);
Use finishAffinity(). This works on > API 16.
When you kill all the activities in the back stack and open the main activity, it is kind of similar to restarting your app.

Changing app language in runtime

I'm trying to implement app language switch on the runtime, once the user made language changes in app preferences. I have this code in my PreferenceFragment:
public class Fragment_Preferences extends PreferenceFragment {
private SharedPreferences.OnSharedPreferenceChangeListener prefListener;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getActivity());
prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
Log.i("Pref changed", "Settings key changed: " + key);
if(key.equals("language_preference"))
{
String system_language = Locale.getDefault().getLanguage().toUpperCase();
String preference_language = Common_Methods.get_preference_language(getActivity());
Toast.makeText(getActivity(), "Pref changed: "+preference_language, Toast.LENGTH_SHORT).show();
Common_Methods.set_app_interface_language(getActivity(), system_language, preference_language);
}
}
};
prefs.registerOnSharedPreferenceChangeListener(prefListener);
}
}
This is my set_app_interface_language method in Common_Methods class:
public static void set_app_interface_language(Context context, String system_language, String preference_language)
{
if(!preference_language.equals(system_language))
{
Locale locale = new Locale(preference_language.toLowerCase());
Locale.setDefault(locale);
Configuration config = new Configuration();
config.locale = locale;
context.getResources().updateConfiguration(config, context.getResources().getDisplayMetrics());
}
}
I get the Toast message when I change language in preferences. I know that this method works since I call it also from my Fragment_Main. But the language doesn't change on the runtime - I have to exit the app and reopen it, only then I see the changes.
So how can I make the app language change on the runtime, without restarting the app? Thanks!
OK, I think I solved this problem: I call for Common_Methods.set_app_interface_language not in SharedPreferences.OnSharedPreferenceChangeListener, but rather in onRestart method of my Fragment_Main.
I also changed set_app_interface_language to return new config. And once it's returned - I pass it to onConfigurationChanged method to recreate fragment. Now, I did ran into Performing pause of activity that is not resumed... error message once my device's screen turned off. After googling a little about it, I realized that it's non-fatal exception, but I still used Handler to postpone recreate() for 1 millisecond and let the Fragment restart properly. I also set another method in my Common_Methods to check if there were any changes made to the app language and recreate the fragment only if the method returns true; I call this method in onRestart That gave the app some performance boost, since now there's no need to recreate the fragment every time the app restarts.

Change language on runtime in Android

I have to change the language on runtime in Android (yes, I know that this is not a good behaviour, but this is a requirement...).
So I have a basic class, from which every activity extends. This class has the following function:
public static void changeLanguage(Context context) {
Resources res = context.getResources();
/*
* Change locale settings in the app.
*/
DisplayMetrics dm = res.getDisplayMetrics();
/*
* Store and load data in this preferences
*/
android.content.res.Configuration conf = res.getConfiguration();
String[] localArray = res.getStringArray(R.array.language_short_array);
if (localArray != null) {
SharedPreferences settings = context.getSharedPreferences(
MyService.APP_ID, MODE_PRIVATE);
conf.locale = new Locale(localArray[settings.getInt(
PREFERED_LANGUAGE_KEY, 0)]);
res.updateConfiguration(conf, dm);
}
}
I will call this method in onCreate:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
changeLanguage(this);
}
This is in the super-class. My Activities extends from it and call super.onCreate at first. After this call, they set their layout and initializes their settings...
I thought that my lines of code would make it. But I have the following problem: Sometimes, the activity changes the language and sometimes not!
If I set a debug breakpoint on it and after the programm pauses I press continue, everything works fine. So I think, in some cases, where my Application is "slow enough", the language will change correctly, whereas the language won't change if the application is too fast...
Is there any solution of my problem? How can I be sure, that my language will change correctly in any time?
Thanks a lot!
Edit: Here is an example for a class which extends from my super-class
public class MainMenuActivity extends BaseActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_start);
}
}
changeLanguage(this); only needs to be called when the language has changed or when the App is loaded.. res.updateConfiguration(conf, dm); updates the global config and is specific to your app instance not your activity instance.
When you change the locale in an Activity you have to recreate that Activity to see your language change. This can be easily done by forcing an orientation change then forcing it back like this:
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
If you hit back after a language change you will see the old language because onCreate is not called. You will have to detect in onResume that the language changed and force a recreate of the Activity.
-= EDIT =-
Using Screen Orientation to reload the Activity has proven to be a bit buggy on some devices. I am now using this to reload the current Activity:
public static void resetScreen(Activity activity) {
if (activity != null) {
if (Build.VERSION.SDK_INT >= 11) {
activity.recreate();
} else {
Intent intent = activity.getIntent();
activity.overridePendingTransition(0, 0);
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
activity.finish();
activity.overridePendingTransition(0, 0);
activity.startActivity(intent);
}
}
}
I think you need to post an example of an Activity that extends your superclass. Calling changeLanguage in onCreate() seems suspicious to me, though. That's only going to run when the app is first initialized. To change the language once your app is loaded, you'd have to stop it and re-create it.

Programmatically relaunch/recreate an activity?

After I do some change in my database, that involves significant change in my views, I would like to redraw, re-execute onCreate.
How is that possible?
UPDATE: Android SDK 11 added a recreate() method to activities.
I've done that by simply reusing the intent that started the activity. Define an intent starterIntent in your class and assign it in onCreate() using starterIntent = getIntent();. Then when you want to restart the activity, call finish(); startActivity(starterIntent);
It isn't a very elegant solution, but it's a simple way to restart your activity and force it to reload everything.
Call the recreate method of the activity.
Option 1
Call recreate() on your Activity.
However this method causes a flashing black screen to appear during the activity re-creation.
Option 2
finish();
startActivity(getIntent());
No "flashing" black screen here, but you'll see a transition between the old and the new instances with a not-so-pleasant black background. We can do better.
Option 3
To fix this, we can add a call to overridePendingTransition() :
finish();
startActivity(getIntent());
overridePendingTransition(0, 0);
Good bye black screen, but in my case I still see some kind of transition (a fade animation), on a colored background this time. That's because you're finishing the current instance of your activity before the new one is created and becomes fully visible, and the in-between color is the value of the windowBackground theme attribute.
Option 4
startActivity(getIntent());
finish();
Calling finish() after startActivity() will use the default transition between activities, often with a little slide-in animation. But the transition is still visible.
Option 5
startActivity(getIntent());
finish();
overridePendingTransition(0, 0);
To me, this is the best solution because it restarts the activity without any visible transition, like if nothing happened.
It could be useful if, for example, in your app you expose a way to change the display language independently of the system's language. In this case, whenever the user changes your app's language you'll probably want to restart your activity without transition, making the language switch look instantaneous.
Combining some answers here you can use something like the following.
class BaseActivity extends SherlockFragmentActivity
{
// Backwards compatible recreate().
#Override
public void recreate()
{
if (android.os.Build.VERSION.SDK_INT >= 11)
{
super.recreate();
}
else
{
startActivity(getIntent());
finish();
}
}
}
Testing
I tested it a bit, and there are some problems:
If the activity is the lowest one on the stack, calling startActivity(...); finish(); just exist the app and doesn't restart the activity.
super.recreate() doesn't actually act the same way as totally recreating the activity. It is equivalent to rotating the device so if you have any Fragments with setRetainInstance(true) they won't be recreated; merely paused and resumed.
So currently I don't believe there is an acceptable solution.
When I need to restart an activity, I use following code. Though it is not recommended.
Intent intent = getIntent();
finish();
startActivity(intent);
for API before 11 you cannot use recreate(). I solved in this way:
Bundle temp_bundle = new Bundle();
onSaveInstanceState(temp_bundle);
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("bundle", temp_bundle);
startActivity(intent);
finish();
and in onCreate..
#Override
public void onCreate(Bundle savedInstanceState) {
if (getIntent().hasExtra("bundle") && savedInstanceState==null){
savedInstanceState = getIntent().getExtras().getBundle("bundle");
}
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//code
}
After looking for the gingerbread implement for recreate, I'd like to use following codes (for gingerbread):
activity.mMainThread.mAppThread.scheduleRelaunchActivity(activity.mToken, null, null, 0, false, null);
For these codes, it's from the implementation in higher api.
public void recreate() {
if (mParent != null) {
throw new IllegalStateException("Can only be called on top-level activity");
}
if (Looper.myLooper() != mMainThread.getLooper()) {
throw new IllegalStateException("Must be called from main thread");
}
mMainThread.requestRelaunchActivity(mToken, null, null, 0, false, null, false);
}
Api-10 has no requestRelaunchActivity, however, from the diff, i found this:
public final void scheduleRelaunchActivity(IBinder token,
List<ResultInfo> pendingResults, List<Intent> pendingNewIntents,
int configChanges, boolean notResumed, Configuration config) {
- ActivityClientRecord r = new ActivityClientRecord();
-
- r.token = token;
- r.pendingResults = pendingResults;
- r.pendingIntents = pendingNewIntents;
- r.startsNotResumed = notResumed;
- r.createdConfig = config;
-
- synchronized (mPackages) {
- mRelaunchingActivities.add(r);
- }
-
- queueOrSendMessage(H.RELAUNCH_ACTIVITY, r, configChanges);
+ requestRelaunchActivity(token, pendingResults, pendingNewIntents,
+ configChanges, notResumed, config, true);
}
So I think I could use scheduleRelaunchActivity instead of requestRelaunchActivity.
And I have written them using reflect:
package me.piebridge.util;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.res.Configuration;
import android.os.Build;
import android.os.IBinder;
public class GingerBreadUtil {
private static Field scanField(Class<?> clazz, String... names) {
for (String name : names) {
Field field;
try {
field = clazz.getDeclaredField(name);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException e) {
}
try {
field = clazz.getField(name);
field.setAccessible(true);
return field;
} catch (NoSuchFieldException e) {
}
}
return null;
}
public static void recreate(Activity activity) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
recreateHC(activity);
} else {
try {
recreateGB(activity);
} catch (InvocationTargetException e) {
e.getTargetException().printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static void recreateHC(Activity activity) {
((Activity) activity).recreate();
}
private static void recreateGB(Activity activity) throws IllegalArgumentException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
Field Activity$mToken = scanField(Activity.class, "mToken");
IBinder mToken = (IBinder) Activity$mToken.get(activity);
Field Activity$mMainThread = scanField(Activity.class, "mMainThread");
Object mMainThread = Activity$mMainThread.get(activity);
Field ActivityThread$mAppThread = scanField(mMainThread.getClass(), "mAppThread");
Object mAppThread = ActivityThread$mAppThread.get(mMainThread);
Method method = mAppThread.getClass().getMethod("scheduleRelaunchActivity",
IBinder.class, List.class, List.class, int.class, boolean.class, Configuration.class);
method.invoke(mAppThread, mToken, null, null, 0, false, null);
}
}
I'm using these codes for the back-porting of xposed framework.
Call the recreate() method from where you want to recreate your activity . This method will destroy current instance of Activity with onDestroy() and then recreate activity with onCreate().
If this is your problem, you should probably implement another way to do the view filling in your Activity. Instead of re running onCreate() you should make it so onCreate() calls your filling method with some argument. When the data changes, the filling method should get called with another argument.
The way I resolved it is by using Fragments. These are backwards compatible until API 4 by using the support library.
You make a "wrapper" layout with a FrameLayout in it.
Example:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
Then you make a FragmentActivity in wich you can replace the FrameLayout any time you want.
Example:
public class SampleFragmentActivity extends FragmentActivity
{
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.wrapper);
// Check that the activity is using the layout version with
// the fragment_container FrameLayout
if (findViewById(R.id.fragment_container) != null)
{
// However, if we're being restored from a previous state,
// then we don't need to do anything and should return or else
// we could end up with overlapping fragments.
if (savedInstanceState != null)
{
return;
}
updateLayout();
}
}
private void updateLayout()
{
Fragment fragment = new SampleFragment();
fragment.setArguments(getIntent().getExtras());
// replace original fragment by new fragment
getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, fragment).commit();
}
In the Fragment you inflate/replace you can use the onStart and onCreateView like you normaly would use the onCreate of an activity.
Example:
public class SampleFragment extends Fragment
{
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
{
return inflater.inflate(R.layout.yourActualLayout, container, false);
}
#Override
public void onStart()
{
// do something with the components, or not!
TextView text = (TextView) getActivity().findViewById(R.id.text1);
super.onStart();
}
}
Also depending on your situation, you may need getActivity().recreate(); instead of just recreate().
For example, you should use it if you are doing recreate() in the class which has been created inside class of activity.
In case you want to use recreate and target Android versions lower than 11, use ActivityCompat.recreate(...) using the platform support APIs.
I once made a test app that uploads, deletes, and then redownloads the database file using firebase cloud storage.
To display the data in database, the following code was the only solution I found. Neither recreate() nor finish() worked in this case.
Intent intent = new Intent(getApplicationContext(), MainActivity.class);
startActivity(intent);
System.exit(0);
If you want to pass a parameter to onCreate() then you have to create a new intent with adding extra and call StartActivity with it. Here is a simple example which i did using this way.
String eczSabit = sa.getItem(position).getValue();
if(!Util.IsNullOrEmpty(eczSabit)){
sabit = Long.parseLong(eczSabit);
Intent intent = new Intent(eczaneSegmentasyon.this,eczaneSegmentasyon.class);
intent.putExtra("sabit", sabit);
startActivity(intent);
}
i found out the best way to refresh your Fragment when data change
if you have a button "search", you have to initialize your ARRAY list inside the button
mSearchBtn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
mList = new ArrayList<Node>();
firebaseSearchQuery.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for (DataSnapshot dataSnapshot1 : dataSnapshot.getChildren()) {
Node p = dataSnapshot1.getValue(Node .class);
mList.add(p);
}
YourAdapter = new NodeAdapter(getActivity(), mList);
mRecyclerView.setAdapter(YourAdapter );
}
If you're just looking to re-do your view, I had the exact same issue. In the onResume function try putting this:
mView = new AndroidPinballView(getApplication());
This was also in my onCreate(), so putting this in the onResume worked for me :)

Categories

Resources