1 /*
2  * Copyright 2013, Leanplum, Inc. All rights reserved.
3  *
4  * Licensed to the Apache Software Foundation (ASF) under one
5  * or more contributor license agreements.  See the NOTICE file
6  * distributed with this work for additional information
7  * regarding copyright ownership.  The ASF licenses this file
8  * to you under the Apache License, Version 2.0 (the
9  * "License"); you may not use this file except in compliance
10  * with the License.  You may obtain a copy of the License at
11  *
12  *        http://www.apache.org/licenses/LICENSE-2.0
13  *
14  * Unless required by applicable law or agreed to in writing,
15  * software distributed under the License is distributed on an
16  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17  * KIND, either express or implied.  See the License for the
18  * specific language governing permissions and limitations
19  * under the License.
20  */
21 
22 package com.leanplum;
23 
24 import android.app.Activity;
25 import android.app.Application;
26 import android.app.Application.ActivityLifecycleCallbacks;
27 import android.content.res.Resources;
28 import android.os.Build;
29 import android.os.Bundle;
30 
31 import com.leanplum.annotations.Parser;
32 import com.leanplum.callbacks.PostponableAction;
33 import com.leanplum.internal.ActionManager;
34 import com.leanplum.internal.LeanplumInternal;
35 import com.leanplum.internal.LeanplumUIEditorWrapper;
36 import com.leanplum.internal.OsHandler;
37 import com.leanplum.internal.Util;
38 
39 import java.util.Collections;
40 import java.util.HashSet;
41 import java.util.LinkedList;
42 import java.util.Queue;
43 import java.util.Set;
44 
45 /**
46  * Utility class for handling activity lifecycle events. Call these methods from your activity if
47  * you don't extend one of the Leanplum*Activity classes.
48  *
49  * @author Andrew First
50  */
51 public class LeanplumActivityHelper {
52   /**
53    * Whether any of the activities are paused.
54    */
55   static boolean isActivityPaused;
56   private static Set<Class> ignoredActivityClasses;
57 
58   /**
59    * Whether lifecycle callbacks were registered. This is only supported on Android OS &gt;= 4.0.
60    */
61   private static boolean registeredCallbacks;
62 
63   static Activity currentActivity;
64 
65   private final Activity activity;
66   private LeanplumResources res;
67   private LeanplumInflater inflater;
68 
69   private static final Queue<Runnable> pendingActions = new LinkedList<>();
70   private static final Runnable runPendingActionsRunnable = new Runnable() {
71     @Override
72     public void run() {
73       runPendingActions();
74     }
75   };
76 
LeanplumActivityHelper(Activity activity)77   public LeanplumActivityHelper(Activity activity) {
78     this.activity = activity;
79     Leanplum.setApplicationContext(activity.getApplicationContext());
80     Parser.parseVariables(activity);
81   }
82 
83   /**
84    * Retrieves the currently active activity.
85    */
getCurrentActivity()86   public static Activity getCurrentActivity() {
87     return currentActivity;
88   }
89 
90   /**
91    * Retrieves if the activity is paused.
92    */
isActivityPaused()93   public static boolean isActivityPaused() {
94     return isActivityPaused;
95   }
96 
97   /**
98    * Enables lifecycle callbacks for Android devices with Android OS &gt;= 4.0
99    */
enableLifecycleCallbacks(final Application app)100   public static void enableLifecycleCallbacks(final Application app) {
101     Leanplum.setApplicationContext(app.getApplicationContext());
102     if (Build.VERSION.SDK_INT < 14) {
103       return;
104     }
105     app.registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() {
106       @Override
107       public void onActivityStopped(Activity activity) {
108         try {
109           onStop(activity);
110         } catch (Throwable t) {
111           Util.handleException(t);
112         }
113       }
114 
115       @Override
116       public void onActivityResumed(final Activity activity) {
117         try {
118           if (Leanplum.isInterfaceEditingEnabled()) {
119             // Execute runnable in next frame to ensure that all system stuff is setup, before
120             // applying UI edits.
121             OsHandler.getInstance().post(new Runnable() {
122               @Override
123               public void run() {
124                 LeanplumUIEditorWrapper.getInstance().applyInterfaceEdits(activity);
125               }
126             });
127           }
128           onResume(activity);
129           if (Leanplum.isScreenTrackingEnabled()) {
130             Leanplum.advanceTo(activity.getLocalClassName());
131           }
132         } catch (Throwable t) {
133           Util.handleException(t);
134         }
135       }
136 
137       @Override
138       public void onActivityPaused(Activity activity) {
139         try {
140           onPause(activity);
141         } catch (Throwable t) {
142           Util.handleException(t);
143         }
144       }
145 
146       @Override
147       public void onActivityStarted(Activity activity) {
148       }
149 
150       @Override
151       public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
152       }
153 
154       @Override
155       public void onActivityDestroyed(Activity activity) {
156       }
157 
158       @Override
159       public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
160       }
161 
162     });
163     registeredCallbacks = true;
164   }
165 
getLeanplumResources()166   public LeanplumResources getLeanplumResources() {
167     return getLeanplumResources(null);
168   }
169 
getLeanplumResources(Resources baseResources)170   public LeanplumResources getLeanplumResources(Resources baseResources) {
171     if (res != null) {
172       return res;
173     }
174     if (baseResources == null) {
175       baseResources = activity.getResources();
176     }
177     if (baseResources instanceof LeanplumResources) {
178       return (LeanplumResources) baseResources;
179     }
180     res = new LeanplumResources(baseResources);
181     return res;
182   }
183 
184   /**
185    * Sets the view from a layout file.
186    */
setContentView(final int layoutResID)187   public void setContentView(final int layoutResID) {
188     if (inflater == null) {
189       inflater = LeanplumInflater.from(activity);
190     }
191     activity.setContentView(inflater.inflate(layoutResID));
192   }
193 
194   @SuppressWarnings("unused")
onPause(Activity activity)195   private static void onPause(Activity activity) {
196     isActivityPaused = true;
197   }
198 
199   /**
200    * Call this when your activity gets paused.
201    */
onPause()202   public void onPause() {
203     try {
204       if (!registeredCallbacks) {
205         onPause(activity);
206       }
207     } catch (Throwable t) {
208       Util.handleException(t);
209     }
210   }
211 
onResume(Activity activity)212   public static void onResume(Activity activity) {
213     isActivityPaused = false;
214     currentActivity = activity;
215     if (LeanplumInternal.isPaused() || LeanplumInternal.hasStartedInBackground()) {
216       Leanplum.resume();
217       LocationManager locationManager = ActionManager.getLocationManager();
218       if (locationManager != null) {
219         locationManager.updateGeofencing();
220         locationManager.updateUserLocation();
221       }
222     }
223 
224     // Pending actions execution triggered, but Leanplum.start() may not be done yet.
225     LeanplumInternal.addStartIssuedHandler(runPendingActionsRunnable);
226   }
227 
228   /**
229    * Call this when your activity gets resumed.
230    */
onResume()231   public void onResume() {
232     try {
233       if (!registeredCallbacks) {
234         onResume(activity);
235       }
236     } catch (Throwable t) {
237       Util.handleException(t);
238     }
239   }
240 
onStop(Activity activity)241   private static void onStop(Activity activity) {
242     // onStop is called when the activity gets hidden, and is
243     // called after onPause.
244     //
245     // However, if we're switching to another activity, that activity
246     // will call onResume, so we shouldn't pause if that's the case.
247     //
248     // Thus, we can call pause from here, only if all activities are paused.
249     if (isActivityPaused) {
250       Leanplum.pause();
251       LocationManager locationManager = ActionManager.getLocationManager();
252       if (locationManager != null) {
253         locationManager.updateGeofencing();
254       }
255     }
256     if (currentActivity != null && currentActivity.equals(activity)) {
257       // Don't leak activities.
258       currentActivity = null;
259     }
260   }
261 
262   /**
263    * Call this when your activity gets stopped.
264    */
onStop()265   public void onStop() {
266     try {
267       if (!registeredCallbacks) {
268         onStop(activity);
269       }
270     } catch (Throwable t) {
271       Util.handleException(t);
272     }
273   }
274 
275   /**
276    * Enqueues a callback to invoke when an activity reaches in the foreground.
277    */
queueActionUponActive(Runnable action)278   public static void queueActionUponActive(Runnable action) {
279     try {
280       if (currentActivity != null && !currentActivity.isFinishing() && !isActivityPaused &&
281           (!(action instanceof PostponableAction) || !isActivityClassIgnored(currentActivity))) {
282         action.run();
283       } else {
284         synchronized (pendingActions) {
285           pendingActions.add(action);
286         }
287       }
288     } catch (Throwable t) {
289       Util.handleException(t);
290     }
291   }
292 
293   /**
294    * Runs any pending actions that have been queued.
295    */
runPendingActions()296   private static void runPendingActions() {
297     if (isActivityPaused || currentActivity == null) {
298       // Trying to run pending actions, but no activity is resumed. Skip.
299       return;
300     }
301 
302     Queue<Runnable> runningActions;
303     synchronized (pendingActions) {
304       runningActions = new LinkedList<>(pendingActions);
305       pendingActions.clear();
306     }
307     for (Runnable action : runningActions) {
308       // If postponable callback and current activity should be skipped, then postpone.
309       if (action instanceof PostponableAction && isActivityClassIgnored(currentActivity)) {
310         pendingActions.add(action);
311       } else {
312         action.run();
313       }
314     }
315   }
316 
317   /**
318    * Whether or not an activity is configured to not show messages.
319    *
320    * @param activity The activity to check.
321    * @return Whether or not the activity is ignored.
322    */
isActivityClassIgnored(Activity activity)323   private static boolean isActivityClassIgnored(Activity activity) {
324     return ignoredActivityClasses != null && ignoredActivityClasses.contains(activity.getClass());
325   }
326 
327   /**
328    * Does not show messages for the provided activity classes.
329    *
330    * @param activityClasses The activity classes to not show messages on.
331    */
deferMessagesForActivities(Class... activityClasses)332   public static void deferMessagesForActivities(Class... activityClasses) {
333     // Check if valid arguments are provided.
334     if (activityClasses == null || activityClasses.length == 0) {
335       return;
336     }
337     // Lazy instantiate activityClasses set.
338     if (ignoredActivityClasses == null) {
339       ignoredActivityClasses = new HashSet<>(activityClasses.length);
340     }
341     // Add all class names to set.
342     Collections.addAll(ignoredActivityClasses, activityClasses);
343   }
344 }
345