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