1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "chrome/browser/android/foreign_session_helper.h"
6
7#include <jni.h>
8
9#include "base/android/jni_string.h"
10#include "base/prefs/pref_service.h"
11#include "base/prefs/scoped_user_pref_update.h"
12#include "chrome/browser/android/tab_android.h"
13#include "chrome/browser/chrome_notification_types.h"
14#include "chrome/browser/profiles/profile_android.h"
15#include "chrome/browser/sessions/session_restore.h"
16#include "chrome/browser/sync/open_tabs_ui_delegate.h"
17#include "chrome/browser/sync/profile_sync_service.h"
18#include "chrome/browser/sync/profile_sync_service_factory.h"
19#include "chrome/browser/ui/android/tab_model/tab_model.h"
20#include "chrome/browser/ui/android/tab_model/tab_model_list.h"
21#include "chrome/common/pref_names.h"
22#include "chrome/common/url_constants.h"
23#include "content/public/browser/notification_source.h"
24#include "content/public/browser/web_contents.h"
25#include "jni/ForeignSessionHelper_jni.h"
26
27using base::android::ScopedJavaGlobalRef;
28using base::android::ScopedJavaLocalRef;
29using base::android::AttachCurrentThread;
30using base::android::ConvertUTF16ToJavaString;
31using base::android::ConvertUTF8ToJavaString;
32using base::android::ConvertJavaStringToUTF8;
33using browser_sync::OpenTabsUIDelegate;
34using browser_sync::SyncedSession;
35
36namespace {
37
38OpenTabsUIDelegate* GetOpenTabsUIDelegate(Profile* profile) {
39  ProfileSyncService* service = ProfileSyncServiceFactory::GetInstance()->
40      GetForProfile(profile);
41
42  // Only return the delegate if it exists and it is done syncing sessions.
43  if (!service || !service->ShouldPushChanges())
44    return NULL;
45
46  return service->GetOpenTabsUIDelegate();
47}
48
49bool ShouldSkipTab(const SessionTab& session_tab) {
50    if (session_tab.navigations.empty())
51      return true;
52
53    int selected_index = session_tab.current_navigation_index;
54    if (selected_index < 0 ||
55        selected_index >= static_cast<int>(session_tab.navigations.size()))
56      return true;
57
58    const ::sessions::SerializedNavigationEntry& current_navigation =
59        session_tab.navigations.at(selected_index);
60
61    if (current_navigation.virtual_url().is_empty())
62      return true;
63
64    return false;
65}
66
67bool ShouldSkipWindow(const SessionWindow& window) {
68  for (std::vector<SessionTab*>::const_iterator tab_it = window.tabs.begin();
69      tab_it != window.tabs.end(); ++tab_it) {
70    const SessionTab &session_tab = **tab_it;
71    if (!ShouldSkipTab(session_tab))
72      return false;
73  }
74  return true;
75}
76
77bool ShouldSkipSession(const browser_sync::SyncedSession& session) {
78  for (SyncedSession::SyncedWindowMap::const_iterator it =
79      session.windows.begin(); it != session.windows.end(); ++it) {
80    const SessionWindow  &window = *(it->second);
81    if (!ShouldSkipWindow(window))
82      return false;
83  }
84  return true;
85}
86
87void CopyTabsToJava(
88    JNIEnv* env,
89    const SessionWindow& window,
90    ScopedJavaLocalRef<jobject>& j_window) {
91  for (std::vector<SessionTab*>::const_iterator tab_it = window.tabs.begin();
92      tab_it != window.tabs.end(); ++tab_it) {
93    const SessionTab &session_tab = **tab_it;
94
95    if (ShouldSkipTab(session_tab))
96      continue;
97
98    int selected_index = session_tab.current_navigation_index;
99    DCHECK(selected_index >= 0);
100    DCHECK(selected_index < static_cast<int>(session_tab.navigations.size()));
101
102    const ::sessions::SerializedNavigationEntry& current_navigation =
103        session_tab.navigations.at(selected_index);
104
105    GURL tab_url = current_navigation.virtual_url();
106
107    Java_ForeignSessionHelper_pushTab(
108        env, j_window.obj(),
109        ConvertUTF8ToJavaString(env, tab_url.spec()).Release(),
110        ConvertUTF16ToJavaString(env, current_navigation.title()).Release(),
111        session_tab.timestamp.ToJavaTime(),
112        session_tab.tab_id.id());
113  }
114}
115
116void CopyWindowsToJava(
117    JNIEnv* env,
118    const SyncedSession& session,
119    ScopedJavaLocalRef<jobject>& j_session) {
120  for (SyncedSession::SyncedWindowMap::const_iterator it =
121      session.windows.begin(); it != session.windows.end(); ++it) {
122    const SessionWindow &window = *(it->second);
123
124    if (ShouldSkipWindow(window))
125      continue;
126
127    ScopedJavaLocalRef<jobject> last_pushed_window;
128    last_pushed_window.Reset(
129        Java_ForeignSessionHelper_pushWindow(
130            env, j_session.obj(),
131            window.timestamp.ToJavaTime(),
132            window.window_id.id()));
133
134    CopyTabsToJava(env, window, last_pushed_window);
135  }
136}
137
138}  // namespace
139
140static jlong Init(JNIEnv* env, jclass clazz, jobject profile) {
141  ForeignSessionHelper* foreign_session_helper = new ForeignSessionHelper(
142      ProfileAndroid::FromProfileAndroid(profile));
143  return reinterpret_cast<intptr_t>(foreign_session_helper);
144}
145
146ForeignSessionHelper::ForeignSessionHelper(Profile* profile)
147    : profile_(profile) {
148  ProfileSyncService* service = ProfileSyncServiceFactory::GetInstance()->
149      GetForProfile(profile);
150  registrar_.Add(this, chrome::NOTIFICATION_SYNC_CONFIGURE_DONE,
151                 content::Source<ProfileSyncService>(service));
152  registrar_.Add(this, chrome::NOTIFICATION_FOREIGN_SESSION_UPDATED,
153                 content::Source<Profile>(profile));
154  registrar_.Add(this, chrome::NOTIFICATION_FOREIGN_SESSION_DISABLED,
155                 content::Source<Profile>(profile));
156}
157
158ForeignSessionHelper::~ForeignSessionHelper() {
159}
160
161void ForeignSessionHelper::Destroy(JNIEnv* env, jobject obj) {
162  delete this;
163}
164
165jboolean ForeignSessionHelper::IsTabSyncEnabled(JNIEnv* env, jobject obj) {
166  ProfileSyncService* service = ProfileSyncServiceFactory::GetInstance()->
167      GetForProfile(profile_);
168  return service && service->GetActiveDataTypes().Has(syncer::PROXY_TABS);
169}
170
171void ForeignSessionHelper::SetOnForeignSessionCallback(JNIEnv* env,
172                                                       jobject obj,
173                                                       jobject callback) {
174  callback_.Reset(env, callback);
175}
176
177void ForeignSessionHelper::Observe(
178    int type, const content::NotificationSource& source,
179    const content::NotificationDetails& details) {
180  if (callback_.is_null())
181    return;
182
183  JNIEnv* env = AttachCurrentThread();
184
185  switch (type) {
186    case chrome::NOTIFICATION_FOREIGN_SESSION_DISABLED:
187      // Tab sync is disabled, so clean up data about collapsed sessions.
188      profile_->GetPrefs()->ClearPref(
189          prefs::kNtpCollapsedForeignSessions);
190      // Purposeful fall through.
191    case chrome::NOTIFICATION_SYNC_CONFIGURE_DONE:
192    case chrome::NOTIFICATION_FOREIGN_SESSION_UPDATED:
193      Java_ForeignSessionCallback_onUpdated(env, callback_.obj());
194      break;
195    default:
196      NOTREACHED();
197  }
198}
199
200jboolean ForeignSessionHelper::GetForeignSessions(JNIEnv* env,
201                                                  jobject obj,
202                                                  jobject result) {
203  OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(profile_);
204  if (!open_tabs)
205    return false;
206
207  std::vector<const browser_sync::SyncedSession*> sessions;
208  if (!open_tabs->GetAllForeignSessions(&sessions))
209    return false;
210
211  // Use a pref to keep track of sessions that were collapsed by the user.
212  // To prevent the pref from accumulating stale sessions, clear it each time
213  // and only add back sessions that are still current.
214  DictionaryPrefUpdate pref_update(profile_->GetPrefs(),
215                                   prefs::kNtpCollapsedForeignSessions);
216  DictionaryValue* pref_collapsed_sessions = pref_update.Get();
217  scoped_ptr<DictionaryValue> collapsed_sessions(
218      pref_collapsed_sessions->DeepCopy());
219  pref_collapsed_sessions->Clear();
220
221  ScopedJavaLocalRef<jobject> last_pushed_session;
222  ScopedJavaLocalRef<jobject> last_pushed_window;
223
224  // Note: we don't own the SyncedSessions themselves.
225  for (size_t i = 0; i < sessions.size(); ++i) {
226    const browser_sync::SyncedSession &session = *(sessions[i]);
227    if (ShouldSkipSession(session))
228      continue;
229
230    const bool is_collapsed = collapsed_sessions->HasKey(session.session_tag);
231
232    if (is_collapsed)
233      pref_collapsed_sessions->SetBoolean(session.session_tag, true);
234
235    last_pushed_session.Reset(
236        Java_ForeignSessionHelper_pushSession(
237            env,
238            result,
239            ConvertUTF8ToJavaString(env, session.session_tag).Release(),
240            ConvertUTF8ToJavaString(env, session.session_name).Release(),
241            session.device_type,
242            session.modified_time.ToJavaTime()));
243
244    CopyWindowsToJava(env, session, last_pushed_session);
245  }
246
247  return true;
248}
249
250jboolean ForeignSessionHelper::OpenForeignSessionTab(JNIEnv* env,
251                                                     jobject obj,
252                                                     jobject j_tab,
253                                                     jstring session_tag,
254                                                     jint session_tab_id,
255                                                     jint j_disposition) {
256  OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(profile_);
257  if (!open_tabs) {
258    LOG(ERROR) << "Null OpenTabsUIDelegate returned.";
259    return false;
260  }
261
262  const SessionTab* session_tab;
263
264  if (!open_tabs->GetForeignTab(ConvertJavaStringToUTF8(env, session_tag),
265                                session_tab_id,
266                                &session_tab)) {
267    LOG(ERROR) << "Failed to load foreign tab.";
268    return false;
269  }
270
271  if (session_tab->navigations.empty()) {
272    LOG(ERROR) << "Foreign tab no longer has valid navigations.";
273    return false;
274  }
275
276  TabAndroid* tab_android = TabAndroid::GetNativeTab(env, j_tab);
277  if (!tab_android)
278    return false;
279  content::WebContents* web_contents = tab_android->web_contents();
280  if (!web_contents)
281    return false;
282
283  WindowOpenDisposition disposition =
284      static_cast<WindowOpenDisposition>(j_disposition);
285  SessionRestore::RestoreForeignSessionTab(web_contents,
286                                           *session_tab,
287                                           disposition);
288
289  return true;
290}
291
292void ForeignSessionHelper::DeleteForeignSession(JNIEnv* env, jobject obj,
293                                                jstring session_tag) {
294  OpenTabsUIDelegate* open_tabs = GetOpenTabsUIDelegate(profile_);
295  if (open_tabs)
296    open_tabs->DeleteForeignSession(ConvertJavaStringToUTF8(env, session_tag));
297}
298
299// static
300bool ForeignSessionHelper::RegisterForeignSessionHelper(JNIEnv* env) {
301  return RegisterNativesImpl(env);
302}
303