1 // Copyright 2010-2018, Google Inc.
2 // All rights reserved.
3 //
4 // Redistribution and use in source and binary forms, with or without
5 // modification, are permitted provided that the following conditions are
6 // met:
7 //
8 //     * Redistributions of source code must retain the above copyright
9 // notice, this list of conditions and the following disclaimer.
10 //     * Redistributions in binary form must reproduce the above
11 // copyright notice, this list of conditions and the following disclaimer
12 // in the documentation and/or other materials provided with the
13 // distribution.
14 //     * Neither the name of Google Inc. nor the names of its
15 // contributors may be used to endorse or promote products derived from
16 // this software without specific prior written permission.
17 //
18 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 
30 // A class handling the converter on the session layer.
31 
32 #include "session/session_converter.h"
33 
34 #include <algorithm>
35 #include <limits>
36 #include <string>
37 
38 #include "base/flags.h"
39 #include "base/logging.h"
40 #include "base/port.h"
41 #include "base/text_normalizer.h"
42 #include "base/util.h"
43 #include "composer/composer.h"
44 #include "config/config_handler.h"
45 #include "converter/converter_interface.h"
46 #include "converter/converter_util.h"
47 #include "converter/segments.h"
48 #include "protocol/commands.pb.h"
49 #include "protocol/config.pb.h"
50 #include "request/conversion_request.h"
51 #include "session/internal/candidate_list.h"
52 #include "session/internal/session_output.h"
53 #include "session/session_usage_stats_util.h"
54 #include "transliteration/transliteration.h"
55 #include "usage_stats/usage_stats.h"
56 
57 using mozc::usage_stats::UsageStats;
58 
59 #ifdef OS_ANDROID
60 const bool kDefaultUseActualConverterForRealtimeConversion = false;
61 #else
62 const bool kDefaultUseActualConverterForRealtimeConversion = true;
63 #endif  // OS_ANDROID
64 
65 DEFINE_bool(use_actual_converter_for_realtime_conversion,
66             kDefaultUseActualConverterForRealtimeConversion,
67             "If true, use the actual (non-immutable) converter for real "
68             "time conversion.");
69 
70 namespace mozc {
71 namespace session {
72 
73 namespace {
74 
75 using mozc::commands::Request;
76 using mozc::config::Config;
77 using mozc::config::ConfigHandler;
78 
79 const size_t kDefaultMaxHistorySize = 3;
80 
GetCandidateShortcuts(config::Config::SelectionShortcut selection_shortcut)81 const char *GetCandidateShortcuts(
82     config::Config::SelectionShortcut selection_shortcut) {
83   // Keyboard shortcut for candidates.
84   const char *kShortcut123456789 = "123456789";
85   const char *kShortcutASDFGHJKL = "asdfghjkl";
86   const char *kNoShortcut = "";
87 
88   const char *shortcut = kNoShortcut;
89   switch (selection_shortcut) {
90     case config::Config::SHORTCUT_123456789:
91       shortcut = kShortcut123456789;
92       break;
93     case config::Config::SHORTCUT_ASDFGHJKL:
94       shortcut = kShortcutASDFGHJKL;
95       break;
96     case config::Config::NO_SHORTCUT:
97       break;
98     default:
99       LOG(WARNING) << "Unknown shortcuts type: " << selection_shortcut;
100       break;
101   }
102   return shortcut;
103 }
104 
105 }  // namespace
106 
107 const size_t SessionConverter::kConsumedAllCharacters =
108     std::numeric_limits<size_t>::max();
109 
SessionConverter(const ConverterInterface * converter,const Request * request,const Config * config)110 SessionConverter::SessionConverter(const ConverterInterface *converter,
111                                    const Request *request,
112                                    const Config *config)
113     : SessionConverterInterface(),
114       state_(COMPOSITION),
115       converter_(converter),
116       segments_(new Segments),
117       segment_index_(0),
118       result_(new commands::Result),
119       candidate_list_(new CandidateList(true)),
120       candidate_list_visible_(false),
121       request_(request),
122       client_revision_(0) {
123   conversion_preferences_.use_history = true;
124   conversion_preferences_.max_history_size = kDefaultMaxHistorySize;
125   conversion_preferences_.request_suggestion = true;
126   candidate_list_->set_page_size(request->candidate_page_size());
127   SetConfig(config);
128 }
129 
~SessionConverter()130 SessionConverter::~SessionConverter() {}
131 
CheckState(SessionConverterInterface::States states) const132 bool SessionConverter::CheckState(
133     SessionConverterInterface::States states) const {
134   return ((state_ & states) != NO_STATE);
135 }
136 
IsActive() const137 bool SessionConverter::IsActive() const {
138   return CheckState(SUGGESTION | PREDICTION | CONVERSION);
139 }
140 
conversion_preferences() const141 const ConversionPreferences &SessionConverter::conversion_preferences() const {
142   return conversion_preferences_;
143 }
144 
Convert(const composer::Composer & composer)145 bool SessionConverter::Convert(const composer::Composer &composer) {
146   return ConvertWithPreferences(composer, conversion_preferences_);
147 }
148 
ConvertWithPreferences(const composer::Composer & composer,const ConversionPreferences & preferences)149 bool SessionConverter::ConvertWithPreferences(
150     const composer::Composer &composer,
151     const ConversionPreferences &preferences) {
152   DCHECK(CheckState(COMPOSITION | SUGGESTION | CONVERSION));
153 
154   segments_->set_request_type(Segments::CONVERSION);
155   SetConversionPreferences(preferences, segments_.get());
156 
157   const ConversionRequest conversion_request(&composer, request_, config_);
158   if (!converter_->StartConversionForRequest(conversion_request,
159                                              segments_.get())) {
160     LOG(WARNING) << "StartConversionForRequest() failed";
161     ResetState();
162     return false;
163   }
164 
165   segment_index_ = 0;
166   state_ = CONVERSION;
167   candidate_list_visible_ = false;
168   UpdateCandidateList();
169   InitializeSelectedCandidateIndices();
170   return true;
171 }
172 
GetReadingText(const string & source_text,string * reading)173 bool SessionConverter::GetReadingText(const string &source_text,
174                                       string *reading) {
175   DCHECK(reading);
176   reading->clear();
177   Segments reverse_segments;
178   if (!converter_->StartReverseConversion(&reverse_segments, source_text)) {
179     return false;
180   }
181   if (reverse_segments.segments_size() == 0) {
182     LOG(WARNING) << "no segments from reverse conversion";
183     return false;
184   }
185   for (size_t i = 0; i < reverse_segments.segments_size(); ++i) {
186     const mozc::Segment &segment = reverse_segments.segment(i);
187     if (segment.candidates_size() == 0) {
188       LOG(WARNING) << "got an empty segment from reverse conversion";
189       return false;
190     }
191     reading->append(segment.candidate(0).value);
192   }
193   return true;
194 }
195 
196 namespace {
GetT13nAttributes(const transliteration::TransliterationType type)197 Attributes GetT13nAttributes(const transliteration::TransliterationType type) {
198   Attributes attributes = NO_ATTRIBUTES;
199   switch (type) {
200     case transliteration::HIRAGANA:  // "ひらがな"
201       attributes = HIRAGANA;
202       break;
203     case transliteration::FULL_KATAKANA:  // "カタカナ"
204       attributes = (FULL_WIDTH | KATAKANA);
205       break;
206     case transliteration::HALF_ASCII:  // "ascII"
207       attributes = (HALF_WIDTH | ASCII);
208       break;
209     case transliteration::HALF_ASCII_UPPER:  // "ASCII"
210       attributes = (HALF_WIDTH | ASCII | UPPER);
211       break;
212     case transliteration::HALF_ASCII_LOWER:  // "ascii"
213       attributes = (HALF_WIDTH | ASCII | LOWER);
214       break;
215     case transliteration::HALF_ASCII_CAPITALIZED:  // "Ascii"
216       attributes = (HALF_WIDTH | ASCII | CAPITALIZED);
217       break;
218     case transliteration::FULL_ASCII:  // "ascII"
219       attributes = (FULL_WIDTH | ASCII);
220       break;
221     case transliteration::FULL_ASCII_UPPER:  // "ASCII"
222       attributes = (FULL_WIDTH | ASCII | UPPER);
223       break;
224     case transliteration::FULL_ASCII_LOWER:  // "ascii"
225       attributes = (FULL_WIDTH | ASCII | LOWER);
226       break;
227     case transliteration::FULL_ASCII_CAPITALIZED:  // "Ascii"
228       attributes = (FULL_WIDTH | ASCII | CAPITALIZED);
229       break;
230     case transliteration::HALF_KATAKANA:  // "カタカナ"
231       attributes = (HALF_WIDTH | KATAKANA);
232       break;
233     default:
234       LOG(ERROR) << "Unknown type: " << type;
235       break;
236   }
237   return attributes;
238 }
239 }  // namespace
240 
ConvertToTransliteration(const composer::Composer & composer,const transliteration::TransliterationType type)241 bool SessionConverter::ConvertToTransliteration(
242     const composer::Composer &composer,
243     const transliteration::TransliterationType type) {
244   DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION | CONVERSION));
245   if (CheckState(PREDICTION)) {
246     // TODO(komatsu): A better way is to transliterate the key of the
247     // focused candidate.  However it takes a long time.
248     Cancel();
249     DCHECK(CheckState(COMPOSITION));
250   }
251 
252   Attributes query_attr =
253       (GetT13nAttributes(type) &
254        (HALF_WIDTH | FULL_WIDTH | ASCII | HIRAGANA | KATAKANA));
255 
256   if (CheckState(COMPOSITION | SUGGESTION)) {
257     if (!Convert(composer)) {
258       LOG(ERROR) << "Conversion failed";
259       return false;
260     }
261 
262     // TODO(komatsu): This is a workaround to transliterate the whole
263     // preedit as a single segment.  We should modify
264     // converter/converter.cc to enable to accept mozc::Segment::FIXED
265     // from the session layer.
266     if (segments_->conversion_segments_size() != 1) {
267       string composition;
268       GetPreedit(0, segments_->conversion_segments_size(), &composition);
269       ResizeSegmentWidth(composer, Util::CharsLen(composition));
270     }
271 
272     DCHECK(CheckState(CONVERSION));
273     candidate_list_->MoveToAttributes(query_attr);
274   } else {
275     DCHECK(CheckState(CONVERSION));
276     const Attributes current_attr =
277         candidate_list_->GetDeepestFocusedCandidate().attributes();
278 
279     if ((query_attr & current_attr & ASCII) &&
280         ((((query_attr & HALF_WIDTH) && (current_attr & FULL_WIDTH))) ||
281          (((query_attr & FULL_WIDTH) && (current_attr & HALF_WIDTH))))) {
282       query_attr |= (current_attr & (UPPER | LOWER | CAPITALIZED));
283     }
284 
285     candidate_list_->MoveNextAttributes(query_attr);
286   }
287   candidate_list_visible_ = false;
288   // Treat as top conversion candidate on usage stats.
289   selected_candidate_indices_[segment_index_] = 0;
290   SegmentFocus();
291   return true;
292 }
293 
ConvertToHalfWidth(const composer::Composer & composer)294 bool SessionConverter::ConvertToHalfWidth(const composer::Composer &composer) {
295   DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION | CONVERSION));
296   if (CheckState(PREDICTION)) {
297     // TODO(komatsu): A better way is to transliterate the key of the
298     // focused candidate.  However it takes a long time.
299     Cancel();
300     DCHECK(CheckState(COMPOSITION));
301   }
302 
303   string composition;
304   if (CheckState(COMPOSITION | SUGGESTION)) {
305     composer.GetStringForPreedit(&composition);
306   } else {
307     composition = GetSelectedCandidate(segment_index_).value;
308   }
309 
310   // TODO(komatsu): make a function to return a logical sum of ScriptType.
311   // If composition_ is "あbc", it should be treated as Katakana.
312   if (Util::ContainsScriptType(composition, Util::KATAKANA) ||
313       Util::ContainsScriptType(composition, Util::HIRAGANA) ||
314       Util::ContainsScriptType(composition, Util::KANJI) ||
315       Util::IsKanaSymbolContained(composition)) {
316     return ConvertToTransliteration(composer, transliteration::HALF_KATAKANA);
317   } else {
318     return ConvertToTransliteration(composer, transliteration::HALF_ASCII);
319   }
320 }
321 
SwitchKanaType(const composer::Composer & composer)322 bool SessionConverter::SwitchKanaType(const composer::Composer &composer) {
323   DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION | CONVERSION));
324   if (CheckState(PREDICTION)) {
325     // TODO(komatsu): A better way is to transliterate the key of the
326     // focused candidate.  However it takes a long time.
327     Cancel();
328     DCHECK(CheckState(COMPOSITION));
329   }
330 
331   Attributes attributes = NO_ATTRIBUTES;
332   if (CheckState(COMPOSITION | SUGGESTION)) {
333     if (!Convert(composer)) {
334       LOG(ERROR) << "Conversion failed";
335       return false;
336     }
337 
338     // TODO(komatsu): This is a workaround to transliterate the whole
339     // preedit as a single segment.  We should modify
340     // converter/converter.cc to enable to accept mozc::Segment::FIXED
341     // from the session layer.
342     if (segments_->conversion_segments_size() != 1) {
343       string composition;
344       GetPreedit(0, segments_->conversion_segments_size(), &composition);
345       const ConversionRequest conversion_request(&composer, request_, config_);
346       converter_->ResizeSegment(segments_.get(),
347                                 conversion_request,
348                                 0, Util::CharsLen(composition));
349       UpdateCandidateList();
350     }
351 
352     attributes = (FULL_WIDTH | KATAKANA);
353   } else {
354     const Attributes current_attributes =
355         candidate_list_->GetDeepestFocusedCandidate().attributes();
356     // "漢字" -> "かんじ" -> "カンジ" -> "カンジ" -> "かんじ" -> ...
357     if (current_attributes & HIRAGANA) {
358       attributes = (FULL_WIDTH | KATAKANA);
359     } else if ((current_attributes & KATAKANA) &&
360                (current_attributes & FULL_WIDTH)) {
361       attributes = (HALF_WIDTH | KATAKANA);
362     } else {
363       attributes = HIRAGANA;
364     }
365   }
366 
367   DCHECK(CheckState(CONVERSION));
368   candidate_list_->MoveNextAttributes(attributes);
369   candidate_list_visible_ = false;
370   // Treat as top conversion candidate on usage stats.
371   selected_candidate_indices_[segment_index_] = 0;
372   SegmentFocus();
373   return true;
374 }
375 
376 namespace {
377 
378 // Prepend the candidates to the first conversion segment.
PrependCandidates(const Segment & previous_segment,const string & preedit,Segments * segments)379 void PrependCandidates(const Segment &previous_segment,
380                        const string &preedit,
381                        Segments *segments) {
382   DCHECK(segments);
383 
384   // TODO(taku) want to have a method in converter to make an empty segment
385   if (segments->conversion_segments_size() == 0) {
386     segments->clear_conversion_segments();
387     Segment *segment = segments->add_segment();
388     segment->Clear();
389     segment->set_key(preedit);
390   }
391 
392   DCHECK_EQ(1, segments->conversion_segments_size());
393   Segment *segment = segments->mutable_conversion_segment(0);
394   DCHECK(segment);
395 
396   const size_t cands_size = previous_segment.candidates_size();
397   for (size_t i = 0; i < cands_size; ++i) {
398     Segment::Candidate *candidate = segment->push_front_candidate();
399     candidate->CopyFrom(previous_segment.candidate(cands_size - i - 1));
400   }
401   *(segment->mutable_meta_candidates()) = previous_segment.meta_candidates();
402 }
403 }  // namespace
404 
405 
Suggest(const composer::Composer & composer)406 bool SessionConverter::Suggest(const composer::Composer &composer) {
407   return SuggestWithPreferences(composer, conversion_preferences_);
408 }
409 
SuggestWithPreferences(const composer::Composer & composer,const ConversionPreferences & preferences)410 bool SessionConverter::SuggestWithPreferences(
411     const composer::Composer &composer,
412     const ConversionPreferences &preferences) {
413   DCHECK(CheckState(COMPOSITION | SUGGESTION));
414   candidate_list_visible_ = false;
415 
416   // Normalize the current state by resetting the previous state.
417   ResetState();
418 
419   // If we are on a password field, suppress suggestion.
420   if (!preferences.request_suggestion ||
421       composer.GetInputFieldType() == commands::Context::PASSWORD) {
422     return false;
423   }
424 
425   // Initialize the segments for suggestion.
426   SetConversionPreferences(preferences, segments_.get());
427 
428   ConversionRequest conversion_request(&composer, request_, config_);
429   const size_t cursor = composer.GetCursor();
430   if (cursor == composer.GetLength() || cursor == 0 ||
431       !request_->mixed_conversion()) {
432     conversion_request.set_create_partial_candidates(
433         request_->auto_partial_suggestion());
434     conversion_request.set_use_actual_converter_for_realtime_conversion(
435         FLAGS_use_actual_converter_for_realtime_conversion);
436     if (!converter_->StartSuggestionForRequest(conversion_request,
437                                                segments_.get())) {
438       // TODO(komatsu): Because suggestion is a prefix search, once
439       // StartSuggestion returns false, this GetSuggestion always
440       // returns false.  Refactor it.
441       VLOG(1) << "StartSuggestionForRequest() returns no suggestions.";
442       // Clear segments and keep the context
443       converter_->CancelConversion(segments_.get());
444       return false;
445     }
446   } else {
447     // create_partial_candidates is false because auto partial suggestion
448     // should be activated only when the cursor is at the tail or head from
449     // the view point of UX.
450     // use_actual_converter_for_realtime_conversion is also false because of
451     // implementation reason. If the flag is true, all the composition
452     // characters will be used in the below process, which conflicts
453     // with *partial* prediction.
454     if (!converter_->StartPartialSuggestionForRequest(conversion_request,
455                                                       segments_.get())) {
456       VLOG(1) << "StartPartialSuggestionForRequest() returns no suggestions.";
457       // Clear segments and keep the context
458       converter_->CancelConversion(segments_.get());
459       return false;
460     }
461   }
462   DCHECK_EQ(1, segments_->conversion_segments_size());
463 
464   // Copy current suggestions so that we can merge
465   // prediction/suggestions later
466   previous_suggestions_.CopyFrom(segments_->conversion_segment(0));
467 
468   // TODO(komatsu): the next line can be deleted.
469   segment_index_ = 0;
470   state_ = SUGGESTION;
471   UpdateCandidateList();
472   candidate_list_visible_ = true;
473   InitializeSelectedCandidateIndices();
474   return true;
475 }
476 
477 
Predict(const composer::Composer & composer)478 bool SessionConverter::Predict(const composer::Composer &composer) {
479   return PredictWithPreferences(composer, conversion_preferences_);
480 }
481 
IsEmptySegment(const Segment & segment) const482 bool SessionConverter::IsEmptySegment(const Segment &segment) const {
483   return ((segment.candidates_size() == 0) &&
484           (segment.meta_candidates_size() == 0));
485 }
486 
PredictWithPreferences(const composer::Composer & composer,const ConversionPreferences & preferences)487 bool SessionConverter::PredictWithPreferences(
488     const composer::Composer &composer,
489     const ConversionPreferences &preferences) {
490   // TODO(komatsu): DCHECK should be
491   // DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION));
492   DCHECK(CheckState(COMPOSITION | SUGGESTION | CONVERSION | PREDICTION));
493   ResetResult();
494 
495   // Initialize the segments for prediction
496   segments_->set_request_type(Segments::PREDICTION);
497   SetConversionPreferences(preferences, segments_.get());
498 
499   const bool predict_first =
500       !CheckState(PREDICTION) && IsEmptySegment(previous_suggestions_);
501 
502   const bool predict_expand =
503       (CheckState(PREDICTION) &&
504        !IsEmptySegment(previous_suggestions_) &&
505        candidate_list_->size() > 0 &&
506        candidate_list_->focused() &&
507        candidate_list_->focused_index() == candidate_list_->last_index());
508 
509   segments_->clear_conversion_segments();
510 
511   if (predict_expand || predict_first) {
512     ConversionRequest conversion_request(&composer, request_, config_);
513     conversion_request.set_use_actual_converter_for_realtime_conversion(
514         FLAGS_use_actual_converter_for_realtime_conversion);
515     if (!converter_->StartPredictionForRequest(conversion_request,
516                                                segments_.get())) {
517       LOG(WARNING) << "StartPredictionForRequest() failed";
518 
519       // TODO(komatsu): Perform refactoring after checking the stability test.
520       //
521       // If predict_expand is true, it means we have prevous_suggestions_.
522       // So we can use it as the result of this prediction.
523       if (predict_first) {
524         ResetState();
525         return false;
526       }
527     }
528   }
529 
530   // Merge suggestions and prediction
531   string preedit;
532   composer.GetQueryForPrediction(&preedit);
533   PrependCandidates(previous_suggestions_, preedit, segments_.get());
534 
535   segment_index_ = 0;
536   state_ = PREDICTION;
537   UpdateCandidateList();
538   candidate_list_visible_ = true;
539   InitializeSelectedCandidateIndices();
540 
541   return true;
542 }
543 
ExpandSuggestion(const composer::Composer & composer)544 bool SessionConverter::ExpandSuggestion(const composer::Composer &composer) {
545   return ExpandSuggestionWithPreferences(composer, conversion_preferences_);
546 }
547 
ExpandSuggestionWithPreferences(const composer::Composer & composer,const ConversionPreferences & preferences)548 bool SessionConverter::ExpandSuggestionWithPreferences(
549     const composer::Composer &composer,
550     const ConversionPreferences &preferences) {
551   DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION));
552   if (CheckState(COMPOSITION)) {
553     // Client can send EXPAND_SUGGESTION command when on composition mode.
554     // In such case we do nothing.
555     VLOG(1) << "ExpandSuggestion does nothing on composition mode.";
556     return false;
557   }
558 
559   ResetResult();
560 
561   // Expand suggestion.
562   // Current implementation is hacky.
563   // We want prediction candidates,
564   // but want to set candidates' category SUGGESTION.
565   // TODO(matsuzakit or yamaguchi): Refactor following lines,
566   //     after implemention of partial conversion.
567 
568   // Initialize the segments for prediction.
569   SetConversionPreferences(preferences, segments_.get());
570 
571   string preedit;
572   composer.GetQueryForPrediction(&preedit);
573 
574   // We do not need "segments_->clear_conversion_segments()".
575   // Without this statement we can add additional candidates into
576   // existing segments.
577 
578   ConversionRequest conversion_request(&composer, request_, config_);
579 
580   const size_t cursor = composer.GetCursor();
581   if (cursor == composer.GetLength() || cursor == 0 ||
582       !request_->mixed_conversion()) {
583     conversion_request.set_create_partial_candidates(
584         request_->auto_partial_suggestion());
585     conversion_request.set_use_actual_converter_for_realtime_conversion(
586         FLAGS_use_actual_converter_for_realtime_conversion);
587     // This is abuse of StartPrediction().
588     // TODO(matsuzakit or yamaguchi): Add ExpandSuggestion method
589     //    to Converter class.
590     if (!converter_->StartPredictionForRequest(conversion_request,
591                                                segments_.get())) {
592       LOG(WARNING) << "StartPredictionForRequest() failed";
593     }
594   } else {
595     // c.f. SuggestWithPreferences for ConversionRequest flags.
596     if (!converter_->StartPartialPredictionForRequest(conversion_request,
597                                                       segments_.get())) {
598       VLOG(1) << "StartPartialPredictionForRequest() returns no suggestions.";
599       // Clear segments and keep the context
600       converter_->CancelConversion(segments_.get());
601       return false;
602     }
603   }
604   // Overwrite the request type to SUGGESTION.
605   // Without this logic, a candidate gets focused that is unexpected behavior.
606   segments_->set_request_type(Segments::SUGGESTION);
607 
608   // Merge suggestions and predictions.
609   PrependCandidates(previous_suggestions_, preedit, segments_.get());
610 
611   segment_index_ = 0;
612   // Call AppendCandidateList instead of UpdateCandidateList because
613   // we want to keep existing candidates.
614   // As a result, ExpandSuggestionWithPreferences adds expanded suggestion
615   // candidates at the tail of existing candidates.
616   AppendCandidateList();
617   candidate_list_visible_ = true;
618   return true;
619 }
620 
MaybeExpandPrediction(const composer::Composer & composer)621 void SessionConverter::MaybeExpandPrediction(
622     const composer::Composer &composer) {
623   DCHECK(CheckState(PREDICTION | CONVERSION));
624 
625   // Expand the current suggestions and fill with Prediction results.
626   if (!CheckState(PREDICTION) ||
627       IsEmptySegment(previous_suggestions_) ||
628       !candidate_list_->focused() ||
629       candidate_list_->focused_index() != candidate_list_->last_index()) {
630     return;
631   }
632 
633   DCHECK(CheckState(PREDICTION));
634   ResetResult();
635 
636   const size_t previous_index = candidate_list_->focused_index();
637   if (!PredictWithPreferences(composer, conversion_preferences_)) {
638     return;
639   }
640 
641   DCHECK_LT(previous_index, candidate_list_->size());
642   candidate_list_->MoveToId(candidate_list_->candidate(previous_index).id());
643   UpdateSelectedCandidateIndex();
644 }
645 
Cancel()646 void SessionConverter::Cancel() {
647   DCHECK(CheckState(PREDICTION | CONVERSION));
648   ResetResult();
649 
650   // Clear segments and keep the context
651   converter_->CancelConversion(segments_.get());
652   ResetState();
653 }
654 
Reset()655 void SessionConverter::Reset() {
656   DCHECK(CheckState(COMPOSITION | SUGGESTION | PREDICTION | CONVERSION));
657 
658   // Even if composition mode, call ResetConversion
659   // in order to clear history segments.
660   converter_->ResetConversion(segments_.get());
661 
662   if (CheckState(COMPOSITION)) {
663     return;
664   }
665 
666   ResetResult();
667   // Reset segments (and its internal context)
668   ResetState();
669 }
670 
Commit(const composer::Composer & composer,const commands::Context & context)671 void SessionConverter::Commit(const composer::Composer &composer,
672                               const commands::Context &context) {
673   DCHECK(CheckState(PREDICTION | CONVERSION));
674   ResetResult();
675 
676   if (!UpdateResult(0, segments_->conversion_segments_size(), NULL)) {
677     Cancel();
678     ResetState();
679     return;
680   }
681 
682   for (size_t i = 0; i < segments_->conversion_segments_size(); ++i) {
683     converter_->CommitSegmentValue(segments_.get(),
684                                    i,
685                                    GetCandidateIndexForConverter(i));
686   }
687   CommitUsageStats(state_, context);
688   ConversionRequest conversion_request(&composer, request_, config_);
689   converter_->FinishConversion(conversion_request, segments_.get());
690   ResetState();
691 }
692 
CommitSuggestionInternal(const composer::Composer & composer,const commands::Context & context,size_t * consumed_key_size)693 bool SessionConverter::CommitSuggestionInternal(
694     const composer::Composer &composer,
695     const commands::Context &context,
696     size_t *consumed_key_size) {
697   DCHECK(consumed_key_size);
698   DCHECK(CheckState(SUGGESTION));
699   ResetResult();
700   string preedit;
701   composer.GetStringForPreedit(&preedit);
702 
703   if (!UpdateResult(0, segments_->conversion_segments_size(),
704                     consumed_key_size)) {
705     // Do not need to call Cancel like Commit because the current
706     // state is SUGGESTION.
707     ResetState();
708     return false;
709   }
710 
711   const size_t preedit_length = Util::CharsLen(preedit);
712 
713   // TODO(horo): When we will support hardware keyboard and introduce
714   // shift+enter keymap in Android, this if condition may be insufficient.
715   if (request_->zero_query_suggestion() &&
716       *consumed_key_size < composer.GetLength()) {
717     // A candidate was chosen from partial suggestion.
718     converter_->CommitPartialSuggestionSegmentValue(
719         segments_.get(),
720         0,
721         GetCandidateIndexForConverter(0),
722         Util::SubString(preedit, 0, *consumed_key_size),
723         Util::SubString(preedit,
724                         *consumed_key_size,
725                         preedit_length - *consumed_key_size));
726     CommitUsageStats(SessionConverterInterface::SUGGESTION, context);
727     InitializeSelectedCandidateIndices();
728     // One or more segments must exist because new segment is inserted
729     // just after the commited segment.
730     DCHECK_GT(segments_->conversion_segments_size(), 0);
731   } else {
732     // Not partial suggestion so let's reset the state.
733     converter_->CommitSegmentValue(segments_.get(),
734                                    0,
735                                    GetCandidateIndexForConverter(0));
736     CommitUsageStats(SessionConverterInterface::SUGGESTION, context);
737     ConversionRequest conversion_request(&composer, request_, config_);
738     converter_->FinishConversion(conversion_request, segments_.get());
739     DCHECK_EQ(0, segments_->conversion_segments_size());
740     ResetState();
741   }
742   return true;
743 }
744 
CommitSuggestionByIndex(const size_t index,const composer::Composer & composer,const commands::Context & context,size_t * consumed_key_size)745 bool SessionConverter::CommitSuggestionByIndex(
746     const size_t index,
747     const composer::Composer &composer,
748     const commands::Context &context,
749     size_t *consumed_key_size) {
750   DCHECK(CheckState(SUGGESTION));
751   if (index >= candidate_list_->size()) {
752     LOG(ERROR) << "index is out of the range: " << index;
753     return false;
754   }
755   candidate_list_->MoveToPageIndex(index);
756   UpdateSelectedCandidateIndex();
757   return CommitSuggestionInternal(composer, context, consumed_key_size);
758 }
759 
CommitSuggestionById(const int id,const composer::Composer & composer,const commands::Context & context,size_t * consumed_key_size)760 bool SessionConverter::CommitSuggestionById(
761     const int id,
762     const composer::Composer &composer,
763     const commands::Context &context,
764     size_t *consumed_key_size) {
765   DCHECK(CheckState(SUGGESTION));
766   if (!candidate_list_->MoveToId(id)) {
767     // Don't use CandidateMoveToId() method, which overwrites candidates.
768     // This is harmful for EXPAND_SUGGESTION session command.
769     LOG(ERROR) << "No id found";
770     return false;
771   }
772   UpdateSelectedCandidateIndex();
773   return CommitSuggestionInternal(composer, context, consumed_key_size);
774 }
775 
CommitHeadToFocusedSegments(const composer::Composer & composer,const commands::Context & context,size_t * consumed_key_size)776 void SessionConverter::CommitHeadToFocusedSegments(
777     const composer::Composer &composer,
778     const commands::Context &context,
779     size_t *consumed_key_size) {
780   CommitSegmentsInternal(
781       composer, context, segment_index_ + 1, consumed_key_size);
782 }
783 
CommitFirstSegment(const composer::Composer & composer,const commands::Context & context,size_t * consumed_key_size)784 void SessionConverter::CommitFirstSegment(
785     const composer::Composer &composer,
786     const commands::Context &context,
787     size_t *consumed_key_size) {
788   CommitSegmentsInternal(composer, context, 1, consumed_key_size);
789 }
790 
CommitSegmentsInternal(const composer::Composer & composer,const commands::Context & context,size_t segments_to_commit,size_t * consumed_key_size)791 void SessionConverter::CommitSegmentsInternal(
792     const composer::Composer &composer,
793     const commands::Context &context,
794     size_t segments_to_commit,
795     size_t *consumed_key_size) {
796   DCHECK(CheckState(PREDICTION | CONVERSION));
797   DCHECK(segments_->conversion_segments_size() >= segments_to_commit);
798   ResetResult();
799   candidate_list_visible_ = false;
800   *consumed_key_size = 0;
801 
802   // If the number of segments is one, just call Commit.
803   if (segments_->conversion_segments_size() == segments_to_commit) {
804     Commit(composer, context);
805     return;
806   }
807 
808   // Store the first conversion segment to the result.
809   if (!UpdateResult(0, segments_to_commit, NULL)) {
810     // If the selected candidate of the first segment has the command
811     // attribute, Cancel is performed instead of Commit.
812     Cancel();
813     ResetState();
814     return;
815   }
816 
817   std::vector<size_t> candidate_ids;
818   for (size_t i = 0; i < segments_to_commit; ++i) {
819     // Get the i-th (0 origin) conversion segment and the selected candidate.
820     Segment *segment = segments_->mutable_conversion_segment(i);
821     if (segment == NULL) {
822       LOG(ERROR) << "There is no segment on position " << i;
823       return;
824     }
825 
826     // Accumulate the size of i-th segment's key.
827     // The caller will remove corresponding characters from the composer.
828     *consumed_key_size += Util::CharsLen(segment->key());
829 
830     // Collect candidate's id for each segment.
831     candidate_ids.push_back(GetCandidateIndexForConverter(i));
832   }
833   converter_->CommitSegments(segments_.get(), candidate_ids);
834 
835   // Commit the [0, segments_to_commit - 1] conversion segment.
836   CommitUsageStatsWithSegmentsSize(state_, context, segments_to_commit);
837 
838   // Adjust the segment_index, since the [0, segment_to_commit - 1] segments
839   // disappeared.
840   // Note that segment_index_ is unsigned.
841   segment_index_ = segment_index_ > segments_to_commit
842       ? segment_index_ - segments_to_commit : 0;
843   UpdateCandidateList();
844 }
845 
CommitPreedit(const composer::Composer & composer,const commands::Context & context)846 void SessionConverter::CommitPreedit(const composer::Composer &composer,
847                                      const commands::Context &context) {
848   string key, preedit, normalized_preedit;
849   composer.GetQueryForConversion(&key);
850   composer.GetStringForSubmission(&preedit);
851   TextNormalizer::NormalizeText(preedit, &normalized_preedit);
852   SessionOutput::FillPreeditResult(preedit, result_.get());
853 
854   ConverterUtil::InitSegmentsFromString(key, normalized_preedit,
855                                         segments_.get());
856 
857   CommitUsageStats(SessionConverterInterface::COMPOSITION, context);
858   ConversionRequest conversion_request(&composer, request_, config_);
859   converter_->FinishConversion(conversion_request, segments_.get());
860   ResetState();
861 }
862 
CommitHead(size_t count,const composer::Composer & composer,size_t * consumed_key_size)863 void SessionConverter::CommitHead(
864     size_t count, const composer::Composer &composer,
865     size_t *consumed_key_size) {
866   string preedit;
867   composer.GetStringForSubmission(&preedit);
868   if (count > preedit.length()) {
869     *consumed_key_size = preedit.length();
870   } else {
871     *consumed_key_size = count;
872   }
873   preedit = Util::SubString(preedit, 0, *consumed_key_size);
874   string composition;
875   TextNormalizer::NormalizeText(preedit, &composition);
876   SessionOutput::FillPreeditResult(composition, result_.get());
877 }
878 
Revert()879 void SessionConverter::Revert() {
880   converter_->RevertConversion(segments_.get());
881 }
882 
SegmentFocusInternal(size_t index)883 void SessionConverter::SegmentFocusInternal(size_t index) {
884   DCHECK(CheckState(PREDICTION | CONVERSION));
885   candidate_list_visible_ = false;
886   if (CheckState(PREDICTION)) {
887     return;  // Do nothing.
888   }
889   ResetResult();
890 
891   if (segment_index_ == index) {
892     return;
893   }
894 
895   SegmentFix();
896   segment_index_ = index;
897   UpdateCandidateList();
898 }
899 
SegmentFocusRight()900 void SessionConverter::SegmentFocusRight() {
901   if (segment_index_ + 1 >= segments_->conversion_segments_size()) {
902     // If |segment_index_| is at the tail of the segments,
903     // focus on the head.
904     SegmentFocusLeftEdge();
905   } else {
906     SegmentFocusInternal(segment_index_ + 1);
907   }
908 }
909 
SegmentFocusLast()910 void SessionConverter::SegmentFocusLast() {
911   const size_t r_edge = segments_->conversion_segments_size() - 1;
912   SegmentFocusInternal(r_edge);
913 }
914 
SegmentFocusLeft()915 void SessionConverter::SegmentFocusLeft() {
916   if (segment_index_ <= 0) {
917     // If |segment_index_| is at the head of the segments,
918     // focus on the tail.
919     SegmentFocusLast();
920   } else {
921     SegmentFocusInternal(segment_index_ - 1);
922   }
923 }
924 
SegmentFocusLeftEdge()925 void SessionConverter::SegmentFocusLeftEdge() {
926   SegmentFocusInternal(0);
927 }
928 
ResizeSegmentWidth(const composer::Composer & composer,int delta)929 void SessionConverter::ResizeSegmentWidth(const composer::Composer &composer,
930                                           int delta) {
931   DCHECK(CheckState(PREDICTION | CONVERSION));
932   candidate_list_visible_ = false;
933   if (CheckState(PREDICTION)) {
934     return;  // Do nothing.
935   }
936   ResetResult();
937 
938   const ConversionRequest conversion_request(&composer, request_, config_);
939   if (!converter_->ResizeSegment(segments_.get(),
940                                  conversion_request,
941                                  segment_index_, delta)) {
942     return;
943   }
944 
945   UpdateCandidateList();
946   // Clears selected index of a focused segment and trailing segments.
947   // TODO(hsumita): Keep the indices if the segment type is FIXED_VALUE.
948   selected_candidate_indices_.resize(segments_->conversion_segments_size());
949   std::fill(selected_candidate_indices_.begin() + segment_index_ + 1,
950             selected_candidate_indices_.end(), 0);
951   UpdateSelectedCandidateIndex();
952 }
953 
SegmentWidthExpand(const composer::Composer & composer)954 void SessionConverter::SegmentWidthExpand(const composer::Composer &composer) {
955   ResizeSegmentWidth(composer, 1);
956 }
957 
SegmentWidthShrink(const composer::Composer & composer)958 void SessionConverter::SegmentWidthShrink(const composer::Composer &composer) {
959   ResizeSegmentWidth(composer, -1);
960 }
961 
962 const Segment::Candidate *
GetSelectedCandidateOfFocusedSegment() const963 SessionConverter::GetSelectedCandidateOfFocusedSegment() const {
964   if (!candidate_list_->focused()) {
965     return NULL;
966   }
967   const Candidate &cand = candidate_list_->focused_candidate();
968   const Segment &seg = segments_->conversion_segment(segment_index_);
969   return &seg.candidate(cand.id());
970 }
971 
CandidateNext(const composer::Composer & composer)972 void SessionConverter::CandidateNext(const composer::Composer &composer) {
973   DCHECK(CheckState(PREDICTION | CONVERSION));
974   ResetResult();
975 
976   MaybeExpandPrediction(composer);
977   candidate_list_->MoveNext();
978   candidate_list_visible_ = true;
979   UpdateSelectedCandidateIndex();
980   SegmentFocus();
981 }
982 
CandidateNextPage()983 void SessionConverter::CandidateNextPage() {
984   DCHECK(CheckState(PREDICTION | CONVERSION));
985   ResetResult();
986 
987   candidate_list_->MoveNextPage();
988   candidate_list_visible_ = true;
989   UpdateSelectedCandidateIndex();
990   SegmentFocus();
991 }
992 
CandidatePrev()993 void SessionConverter::CandidatePrev() {
994   DCHECK(CheckState(PREDICTION | CONVERSION));
995   ResetResult();
996 
997   candidate_list_->MovePrev();
998   candidate_list_visible_ = true;
999   UpdateSelectedCandidateIndex();
1000   SegmentFocus();
1001 }
1002 
CandidatePrevPage()1003 void SessionConverter::CandidatePrevPage() {
1004   DCHECK(CheckState(PREDICTION | CONVERSION));
1005   ResetResult();
1006 
1007   candidate_list_->MovePrevPage();
1008   candidate_list_visible_ = true;
1009   UpdateSelectedCandidateIndex();
1010   SegmentFocus();
1011 }
1012 
CandidateMoveToId(const int id,const composer::Composer & composer)1013 void SessionConverter::CandidateMoveToId(
1014     const int id, const composer::Composer &composer) {
1015   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1016   ResetResult();
1017 
1018   if (CheckState(SUGGESTION)) {
1019     // This method makes a candidate focused but SUGGESTION state cannot
1020     // have focused candidate.
1021     // To solve this conflict we call Predict() method to transit to
1022     // PREDICTION state, on which existence of focused candidate is acceptable.
1023     Predict(composer);
1024   }
1025   DCHECK(CheckState(PREDICTION | CONVERSION));
1026 
1027   candidate_list_->MoveToId(id);
1028   candidate_list_visible_ = false;
1029   UpdateSelectedCandidateIndex();
1030   SegmentFocus();
1031 }
1032 
CandidateMoveToPageIndex(const size_t index)1033 void SessionConverter::CandidateMoveToPageIndex(const size_t index) {
1034   DCHECK(CheckState(PREDICTION | CONVERSION));
1035   ResetResult();
1036 
1037   candidate_list_->MoveToPageIndex(index);
1038   candidate_list_visible_ = false;
1039   UpdateSelectedCandidateIndex();
1040   SegmentFocus();
1041 }
1042 
CandidateMoveToShortcut(const char shortcut)1043 bool SessionConverter::CandidateMoveToShortcut(const char shortcut) {
1044   DCHECK(CheckState(PREDICTION | CONVERSION));
1045 
1046   if (!candidate_list_visible_) {
1047     VLOG(1) << "Candidate list is not displayed.";
1048     return false;
1049   }
1050 
1051   const string shortcuts(GetCandidateShortcuts(selection_shortcut_));
1052   if (shortcuts.empty()) {
1053     VLOG(1) << "No shortcuts";
1054     return false;
1055   }
1056 
1057   // Check if the input character is in the shortcut.
1058   // TODO(komatsu): Support non ASCII characters such as Unicode and
1059   // special keys.
1060   const string::size_type index = shortcuts.find(shortcut);
1061   if (index == string::npos) {
1062     VLOG(1) << "shortcut is not a member of shortcuts.";
1063     return false;
1064   }
1065 
1066   if (!candidate_list_->MoveToPageIndex(index)) {
1067     VLOG(1) << "shortcut is out of the range.";
1068     return false;
1069   }
1070   UpdateSelectedCandidateIndex();
1071   ResetResult();
1072   SegmentFocus();
1073   return true;
1074 }
1075 
SetCandidateListVisible(bool visible)1076 void SessionConverter::SetCandidateListVisible(bool visible) {
1077   candidate_list_visible_ = visible;
1078 }
1079 
PopOutput(const composer::Composer & composer,commands::Output * output)1080 void SessionConverter::PopOutput(
1081     const composer::Composer &composer, commands::Output *output) {
1082   FillOutput(composer, output);
1083   updated_command_ = Segment::Candidate::DEFAULT_COMMAND;
1084   ResetResult();
1085 }
1086 
1087 namespace {
MaybeFillConfig(Segment::Candidate::Command command,const config::Config & base_config,commands::Output * output)1088 void MaybeFillConfig(Segment::Candidate::Command command,
1089                      const config::Config &base_config,
1090                      commands::Output *output) {
1091   if (command == Segment::Candidate::DEFAULT_COMMAND) {
1092     return;
1093   }
1094 
1095   *output->mutable_config() = base_config;
1096   switch (command) {
1097     case Segment::Candidate::ENABLE_INCOGNITO_MODE:
1098       output->mutable_config()->set_incognito_mode(true);
1099       break;
1100     case Segment::Candidate::DISABLE_INCOGNITO_MODE:
1101       output->mutable_config()->set_incognito_mode(false);
1102       break;
1103     case Segment::Candidate::ENABLE_PRESENTATION_MODE:
1104       output->mutable_config()->set_presentation_mode(true);
1105       break;
1106     case Segment::Candidate::DISABLE_PRESENTATION_MODE:
1107       output->mutable_config()->set_presentation_mode(false);
1108       break;
1109     default:
1110       LOG(WARNING) << "Unknown command: " << command;
1111       break;
1112   }
1113 }
1114 }  // namespace
1115 
FillOutput(const composer::Composer & composer,commands::Output * output) const1116 void SessionConverter::FillOutput(
1117     const composer::Composer &composer, commands::Output *output) const {
1118   if (output == NULL) {
1119     LOG(ERROR) << "output is NULL.";
1120     return;
1121   }
1122   if (result_->has_value()) {
1123     FillResult(output->mutable_result());
1124   }
1125   if (CheckState(COMPOSITION)) {
1126     if (!composer.Empty()) {
1127       session::SessionOutput::FillPreedit(composer,
1128                                           output->mutable_preedit());
1129     }
1130   }
1131 
1132   MaybeFillConfig(updated_command_, *config_, output);
1133 
1134   if (!IsActive()) {
1135     return;
1136   }
1137 
1138   // Composition on Suggestion
1139   if (CheckState(SUGGESTION)) {
1140     // When the suggestion comes from zero query suggestion, the
1141     // composer is empty.  In that case, preedit is not rendered.
1142     if (!composer.Empty()) {
1143       session::SessionOutput::FillPreedit(composer,
1144                                           output->mutable_preedit());
1145     }
1146   } else if (CheckState(PREDICTION | CONVERSION)) {
1147     // Conversion on Prediction or Conversion
1148     FillConversion(output->mutable_preedit());
1149   }
1150   // Candidate list
1151   if (CheckState(SUGGESTION | PREDICTION | CONVERSION) &&
1152       candidate_list_visible_) {
1153     FillCandidates(output->mutable_candidates());
1154   }
1155 
1156   // All candidate words
1157   if (CheckState(SUGGESTION | PREDICTION | CONVERSION)) {
1158     FillAllCandidateWords(output->mutable_all_candidate_words());
1159   }
1160 }
1161 
1162 // static
SetConversionPreferences(const ConversionPreferences & preferences,Segments * segments)1163 void SessionConverter::SetConversionPreferences(
1164     const ConversionPreferences &preferences,
1165     Segments *segments) {
1166   segments->set_user_history_enabled(preferences.use_history);
1167   segments->set_max_history_segments_size(preferences.max_history_size);
1168 }
1169 
Clone() const1170 SessionConverter* SessionConverter::Clone() const {
1171   SessionConverter *session_converter =
1172       new SessionConverter(converter_, request_, config_);
1173 
1174   // Copy the members in order of their declarations.
1175   session_converter->state_ = state_;
1176   // TODO(team): copy of |converter_| member.
1177   // We cannot copy the member converter_ from SessionConverterInterface because
1178   // it doesn't (and shouldn't) define a method like GetConverter(). At the
1179   // moment it's ok because the current design guarantees that the converter is
1180   // singleton. However, we should refactor such bad design; see also the
1181   // comment right above.
1182   session_converter->segments_->CopyFrom(*segments_);
1183   session_converter->segment_index_ = segment_index_;
1184   session_converter->previous_suggestions_.CopyFrom(previous_suggestions_);
1185   session_converter->conversion_preferences_ = conversion_preferences();
1186   session_converter->result_->CopyFrom(*result_);
1187   session_converter->request_ = request_;
1188   session_converter->config_ = config_;
1189   session_converter->use_cascading_window_ = use_cascading_window_;
1190   session_converter->selected_candidate_indices_ = selected_candidate_indices_;
1191 
1192   if (session_converter->CheckState(SUGGESTION | PREDICTION | CONVERSION)) {
1193     // UpdateCandidateList() is not simple setter and it uses some members.
1194     session_converter->UpdateCandidateList();
1195     session_converter->candidate_list_->MoveToId(candidate_list_->focused_id());
1196     session_converter->SetCandidateListVisible(candidate_list_visible_);
1197   }
1198 
1199   return session_converter;
1200 }
1201 
ResetResult()1202 void SessionConverter::ResetResult() {
1203   result_->Clear();
1204 }
1205 
ResetState()1206 void SessionConverter::ResetState() {
1207   state_ = COMPOSITION;
1208   segment_index_ = 0;
1209   previous_suggestions_.clear();
1210   candidate_list_visible_ = false;
1211   candidate_list_->Clear();
1212   selected_candidate_indices_.clear();
1213 }
1214 
SegmentFocus()1215 void SessionConverter::SegmentFocus() {
1216   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1217   converter_->FocusSegmentValue(segments_.get(),
1218                                 segment_index_,
1219                                 GetCandidateIndexForConverter(segment_index_));
1220 }
1221 
SegmentFix()1222 void SessionConverter::SegmentFix() {
1223   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1224   converter_->CommitSegmentValue(segments_.get(),
1225                                  segment_index_,
1226                                  GetCandidateIndexForConverter(segment_index_));
1227 }
1228 
GetPreedit(const size_t index,const size_t size,string * preedit) const1229 void SessionConverter::GetPreedit(const size_t index,
1230                                   const size_t size,
1231                                   string *preedit) const {
1232   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1233   DCHECK(index + size <= segments_->conversion_segments_size());
1234   DCHECK(preedit);
1235 
1236   preedit->clear();
1237   for (size_t i = index; i < size; ++i) {
1238     if (CheckState(CONVERSION)) {
1239       // In conversion mode, all the key of candidates is same.
1240       preedit->append(segments_->conversion_segment(i).key());
1241     } else {
1242       DCHECK(CheckState(SUGGESTION | PREDICTION));
1243       // In suggestion or prediction modes, each key may have
1244       // different keys, so content_key is used although it is
1245       // possibly dropped the conjugational word (ex. the content_key
1246       // of "はしる" is "はし").
1247       preedit->append(GetSelectedCandidate(i).content_key);
1248     }
1249   }
1250 }
1251 
GetConversion(const size_t index,const size_t size,string * conversion) const1252 void SessionConverter::GetConversion(const size_t index,
1253                                      const size_t size,
1254                                      string *conversion) const {
1255   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1256   DCHECK(index + size <= segments_->conversion_segments_size());
1257   DCHECK(conversion);
1258 
1259   conversion->clear();
1260   for (size_t i = index; i < size; ++i) {
1261     conversion->append(GetSelectedCandidateValue(i));
1262   }
1263 }
1264 
GetConsumedPreeditSize(const size_t index,const size_t size) const1265 size_t SessionConverter::GetConsumedPreeditSize(const size_t index,
1266                                                 const size_t size) const {
1267   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1268   DCHECK(index + size <= segments_->conversion_segments_size());
1269 
1270   if (CheckState(SUGGESTION | PREDICTION)) {
1271     DCHECK_EQ(1, size);
1272     const Segment &segment = segments_->conversion_segment(0);
1273     const int id = GetCandidateIndexForConverter(0);
1274     const Segment::Candidate &candidate = segment.candidate(id);
1275     return (candidate.attributes & Segment::Candidate::PARTIALLY_KEY_CONSUMED)
1276                ? candidate.consumed_key_size : kConsumedAllCharacters;
1277   }
1278 
1279   DCHECK(CheckState(CONVERSION));
1280   size_t result = 0;
1281   for (size_t i = index; i < size; ++i) {
1282     const int id = GetCandidateIndexForConverter(i);
1283     const Segment::Candidate &candidate =
1284         segments_->conversion_segment(i).candidate(id);
1285     DCHECK(!(candidate.attributes &
1286              Segment::Candidate::PARTIALLY_KEY_CONSUMED));
1287     result += Util::CharsLen(segments_->conversion_segment(i).key());
1288   }
1289   return result;
1290 }
1291 
MaybePerformCommandCandidate(const size_t index,const size_t size)1292 bool SessionConverter::MaybePerformCommandCandidate(
1293     const size_t index,
1294     const size_t size) {
1295   // If a candidate has the command attribute, Cancel is performed
1296   // instead of Commit after executing the specified action.
1297   for (size_t i = index; i < size; ++i) {
1298     const int id = GetCandidateIndexForConverter(i);
1299     const Segment::Candidate &candidate =
1300         segments_->conversion_segment(i).candidate(id);
1301     if (candidate.attributes & Segment::Candidate::COMMAND_CANDIDATE) {
1302       switch (candidate.command) {
1303         case Segment::Candidate::DEFAULT_COMMAND:
1304           // Do nothing
1305           break;
1306         case Segment::Candidate::ENABLE_INCOGNITO_MODE:
1307         case Segment::Candidate::DISABLE_INCOGNITO_MODE:
1308         case Segment::Candidate::ENABLE_PRESENTATION_MODE:
1309         case Segment::Candidate::DISABLE_PRESENTATION_MODE:
1310           updated_command_ = candidate.command;
1311           break;
1312         default:
1313           LOG(WARNING) << "Unknown command: " << candidate.command;
1314           break;
1315       }
1316       return true;
1317     }
1318   }
1319   return false;
1320 }
1321 
UpdateResult(size_t index,size_t size,size_t * consumed_key_size)1322 bool SessionConverter::UpdateResult(size_t index, size_t size,
1323                                     size_t *consumed_key_size) {
1324   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1325 
1326   // If command candidate is performed, result is not updated and
1327   // returns false.
1328   if (MaybePerformCommandCandidate(index, size)) {
1329     return false;
1330   }
1331 
1332   string preedit, conversion;
1333   GetPreedit(index, size, &preedit);
1334   GetConversion(index, size, &conversion);
1335   if (consumed_key_size) {
1336     *consumed_key_size = GetConsumedPreeditSize(index, size);
1337   }
1338   SessionOutput::FillConversionResult(preedit, conversion, result_.get());
1339   return true;
1340 }
1341 
1342 namespace {
1343 // Convert transliteration::TransliterationType to id used in the
1344 // converter.  The id number are negative values, and 0 of
1345 // transliteration::TransliterationType is bound for -1 of the id.
GetT13nId(const transliteration::TransliterationType type)1346 int GetT13nId(const transliteration::TransliterationType type) {
1347   return -(type + 1);
1348 }
1349 }  // namespace
1350 
AppendCandidateList()1351 void SessionConverter::AppendCandidateList() {
1352   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1353 
1354   // Meta candidates are added iff |candidate_list_| is empty.
1355   // This is because if |candidate_list_| is not empty we cannot decide
1356   // where to add meta candidates, especially use_cascading_window flag
1357   // is true (If there are two or more sub candidate lists, and existent
1358   // meta candidates are not located in the same list (distributed over
1359   // some lists), the most appropriate location to be added new meta candidates
1360   // cannot be decided).
1361   const bool add_meta_candidates = (candidate_list_->size() == 0);
1362 
1363   const Segment &segment = segments_->conversion_segment(segment_index_);
1364   for (size_t i = candidate_list_->next_available_id();
1365        i < segment.candidates_size();
1366        ++i) {
1367     candidate_list_->AddCandidate(i, segment.candidate(i).value);
1368     // if candidate has spelling correction attribute,
1369     // always display the candidate to let user know the
1370     // miss spelled candidate.
1371     if (i < 10 &&
1372         (segment.candidate(i).attributes &
1373          Segment::Candidate::SPELLING_CORRECTION)) {
1374       candidate_list_visible_ = true;
1375     }
1376   }
1377 
1378   const bool focused = (
1379       segments_->request_type() != Segments::SUGGESTION &&
1380       segments_->request_type() != Segments::PARTIAL_SUGGESTION &&
1381       segments_->request_type() != Segments::PARTIAL_PREDICTION);
1382   candidate_list_->set_focused(focused);
1383 
1384   if (segment.meta_candidates_size() == 0) {
1385     // For suggestion mode, it is natural that T13N is not initialized.
1386     if (CheckState(SUGGESTION)) {
1387       return;
1388     }
1389     // For other modes, records |segment| just in case.
1390     VLOG(1) << "T13N is not initialized: " << segment.key();
1391     return;
1392   }
1393 
1394   if (!add_meta_candidates) {
1395     return;
1396   }
1397 
1398   // Set transliteration candidates
1399   CandidateList *transliterations;
1400   if (use_cascading_window_) {
1401     const bool kNoRotate = false;
1402     transliterations = candidate_list_->AllocateSubCandidateList(kNoRotate);
1403     transliterations->set_focused(true);
1404 
1405     const char kT13nLabel[] = "そのほかの文字種";
1406     transliterations->set_name(kT13nLabel);
1407   } else {
1408     transliterations = candidate_list_.get();
1409   }
1410 
1411   // Add transliterations.
1412   for (size_t i = 0; i < transliteration::NUM_T13N_TYPES; ++i) {
1413     const transliteration::TransliterationType type =
1414       transliteration::TransliterationTypeArray[i];
1415     transliterations->AddCandidateWithAttributes(
1416         GetT13nId(type),
1417         segment.meta_candidate(i).value,
1418         GetT13nAttributes(type));
1419   }
1420 }
1421 
UpdateCandidateList()1422 void SessionConverter::UpdateCandidateList() {
1423   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1424   candidate_list_->Clear();
1425   AppendCandidateList();
1426 }
1427 
GetCandidateIndexForConverter(const size_t segment_index) const1428 int SessionConverter::GetCandidateIndexForConverter(
1429     const size_t segment_index) const {
1430   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1431   // If segment_index does not point to the focused segment, the value
1432   // should be always zero.
1433   if (segment_index != segment_index_) {
1434     return 0;
1435   }
1436   return candidate_list_->focused_id();
1437 }
1438 
GetSelectedCandidateValue(const size_t segment_index) const1439 string SessionConverter::GetSelectedCandidateValue(
1440     const size_t segment_index) const {
1441   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1442   const int id = GetCandidateIndexForConverter(segment_index);
1443   const Segment::Candidate &candidate =
1444       segments_->conversion_segment(segment_index).candidate(id);
1445   if (candidate.attributes & Segment::Candidate::COMMAND_CANDIDATE) {
1446     // Return an empty string, however this path should not be reached.
1447     return "";
1448   }
1449   return candidate.value;
1450 }
1451 
GetSelectedCandidate(const size_t segment_index) const1452 const Segment::Candidate &SessionConverter::GetSelectedCandidate(
1453     const size_t segment_index) const {
1454   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1455   const int id = GetCandidateIndexForConverter(segment_index);
1456   return segments_->conversion_segment(segment_index).candidate(id);
1457 }
1458 
FillConversion(commands::Preedit * preedit) const1459 void SessionConverter::FillConversion(commands::Preedit *preedit) const {
1460   DCHECK(CheckState(PREDICTION | CONVERSION));
1461   SessionOutput::FillConversion(*segments_,
1462                                 segment_index_,
1463                                 candidate_list_->focused_id(),
1464                                 preedit);
1465 }
1466 
FillResult(commands::Result * result) const1467 void SessionConverter::FillResult(commands::Result *result) const {
1468   result->CopyFrom(*result_);
1469 }
1470 
FillCandidates(commands::Candidates * candidates) const1471 void SessionConverter::FillCandidates(commands::Candidates *candidates) const {
1472   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1473   if (!candidate_list_visible_) {
1474     return;
1475   }
1476 
1477   // The position to display the candidate window.
1478   size_t position = 0;
1479   string conversion;
1480   for (size_t i = 0; i < segment_index_; ++i) {
1481     position += Util::CharsLen(GetSelectedCandidate(i).value);
1482   }
1483 
1484   // Temporarily added to see if this condition is really satisfied in the
1485   // real world or not.
1486 #ifdef CHANNEL_DEV
1487   CHECK_LT(0, segments_->conversion_segments_size());
1488 #endif  // CHANNEL_DEV
1489   const Segment &segment = segments_->conversion_segment(segment_index_);
1490   SessionOutput::FillCandidates(
1491       segment, *candidate_list_, position, candidates);
1492 
1493   // Shortcut keys
1494   if (CheckState(PREDICTION | CONVERSION)) {
1495     SessionOutput::FillShortcuts(GetCandidateShortcuts(selection_shortcut_),
1496                                  candidates);
1497   }
1498 
1499   // Store category
1500   switch (segments_->request_type()) {
1501     case Segments::CONVERSION:
1502       candidates->set_category(commands::CONVERSION);
1503       break;
1504     case Segments::PREDICTION:
1505       candidates->set_category(commands::PREDICTION);
1506       break;
1507     case Segments::SUGGESTION:
1508       candidates->set_category(commands::SUGGESTION);
1509       break;
1510     case Segments::PARTIAL_PREDICTION:
1511       // Not PREDICTION because we do not want to get focused candidate.
1512       candidates->set_category(commands::SUGGESTION);
1513       break;
1514     case Segments::PARTIAL_SUGGESTION:
1515       candidates->set_category(commands::SUGGESTION);
1516       break;
1517     default:
1518       LOG(WARNING) << "Unknown request type: " << segments_->request_type();
1519       candidates->set_category(commands::CONVERSION);
1520       break;
1521   }
1522 
1523   if (candidates->has_usages()) {
1524     candidates->mutable_usages()->set_category(commands::USAGE);
1525   }
1526   if (candidates->has_subcandidates()) {
1527     // TODO(komatsu): Subcandidate is not always for transliterations.
1528     // The category of the subcandidates should be checked.
1529     candidates->mutable_subcandidates()->set_category(
1530         commands::TRANSLITERATION);
1531   }
1532 
1533   // Store display type
1534   candidates->set_display_type(commands::MAIN);
1535   if (candidates->has_usages()) {
1536     candidates->mutable_usages()->set_display_type(commands::CASCADE);
1537   }
1538   if (candidates->has_subcandidates()) {
1539     // TODO(komatsu): Subcandidate is not always for transliterations.
1540     // The category of the subcandidates should be checked.
1541     candidates->mutable_subcandidates()->set_display_type(commands::CASCADE);
1542   }
1543 
1544   // Store footer.
1545   SessionOutput::FillFooter(candidates->category(), candidates);
1546 }
1547 
1548 
FillAllCandidateWords(commands::CandidateList * candidates) const1549 void SessionConverter::FillAllCandidateWords(
1550     commands::CandidateList *candidates) const {
1551   DCHECK(CheckState(SUGGESTION | PREDICTION | CONVERSION));
1552   commands::Category category;
1553   switch (segments_->request_type()) {
1554     case Segments::CONVERSION:
1555       category = commands::CONVERSION;
1556       break;
1557     case Segments::PREDICTION:
1558       category = commands::PREDICTION;
1559       break;
1560     case Segments::SUGGESTION:
1561       category = commands::SUGGESTION;
1562       break;
1563     case Segments::PARTIAL_PREDICTION:
1564       // Not PREDICTION because we do not want to get focused candidate.
1565       category = commands::SUGGESTION;
1566       break;
1567     case Segments::PARTIAL_SUGGESTION:
1568       category = commands::SUGGESTION;
1569       break;
1570     default:
1571       LOG(WARNING) << "Unknown request type: " << segments_->request_type();
1572       category = commands::CONVERSION;
1573       break;
1574   }
1575 
1576   const Segment &segment = segments_->conversion_segment(segment_index_);
1577   SessionOutput::FillAllCandidateWords(
1578       segment, *candidate_list_, category, candidates);
1579 }
1580 
SetRequest(const commands::Request * request)1581 void SessionConverter::SetRequest(const commands::Request *request) {
1582   request_ = request;
1583   candidate_list_->set_page_size(request->candidate_page_size());
1584 }
1585 
SetConfig(const config::Config * config)1586 void SessionConverter::SetConfig(const config::Config *config) {
1587   config_ = config;
1588   updated_command_ = Segment::Candidate::DEFAULT_COMMAND;
1589   selection_shortcut_ =  config->selection_shortcut();
1590   use_cascading_window_ = config->use_cascading_window();
1591 }
1592 
OnStartComposition(const commands::Context & context)1593 void SessionConverter::OnStartComposition(const commands::Context &context) {
1594   bool revision_changed = false;
1595   if (context.has_revision()) {
1596     revision_changed = (context.revision() != client_revision_);
1597     client_revision_ = context.revision();
1598   }
1599   if (!context.has_preceding_text()) {
1600     // In this case, reset history segments when the revision is mismatched.
1601     if (revision_changed) {
1602       converter_->ResetConversion(segments_.get());
1603     }
1604     return;
1605   }
1606 
1607   const string &preceding_text = context.preceding_text();
1608   // If preceding text is empty, it is OK to reset the history segments by
1609   // calling ResetConversion.
1610   if (preceding_text.empty()) {
1611     converter_->ResetConversion(segments_.get());
1612     return;
1613   }
1614 
1615   // Hereafter, we keep the existing history segments as long as it is
1616   // consistent with the preceding text even when revision_changed is true.
1617   string history_text;
1618   for (size_t i = 0; i < segments_->segments_size(); ++i) {
1619     const Segment &segment = segments_->segment(i);
1620     if (segment.segment_type() != Segment::HISTORY) {
1621       break;
1622     }
1623     if (segment.candidates_size() == 0) {
1624       break;
1625     }
1626     history_text.append(segment.candidate(0).value);
1627   }
1628 
1629   if (!history_text.empty()) {
1630     // Compare |preceding_text| with |history_text| to check if the history
1631     // segments are still valid or not.
1632     DCHECK(!preceding_text.empty());
1633     DCHECK(!history_text.empty());
1634     if (preceding_text.size() > history_text.size()) {
1635       if (Util::EndsWith(preceding_text, history_text)) {
1636         // History segments seem to be consistent with preceding text.
1637         return;
1638       }
1639     } else {
1640       if (Util::EndsWith(history_text, preceding_text)) {
1641         // History segments seem to be consistent with preceding text.
1642         return;
1643       }
1644     }
1645   }
1646 
1647   // Here we reconstruct history segments from |preceding_text| regardless
1648   // of revision mismatch. If it fails the history segments is cleared anyway.
1649   converter_->ReconstructHistory(segments_.get(), preceding_text);
1650 }
1651 
UpdateSelectedCandidateIndex()1652 void SessionConverter::UpdateSelectedCandidateIndex() {
1653   int index;
1654   const Candidate &focused_candidate = candidate_list_->focused_candidate();
1655   if (focused_candidate.IsSubcandidateList()) {
1656     const int t13n_index =
1657         focused_candidate.subcandidate_list().focused_index();
1658     index = -1 - t13n_index;
1659   } else {
1660     // TODO(hsumita): Use id instead of focused index.
1661     index = candidate_list_->focused_index();
1662   }
1663   selected_candidate_indices_[segment_index_] = index;
1664 }
1665 
InitializeSelectedCandidateIndices()1666 void SessionConverter::InitializeSelectedCandidateIndices() {
1667   selected_candidate_indices_.clear();
1668   selected_candidate_indices_.resize(segments_->conversion_segments_size());
1669 }
1670 
UpdateCandidateStats(const string & base_name,int32 index)1671 void SessionConverter::UpdateCandidateStats(const string &base_name,
1672                                             int32 index) {
1673   string prefix;
1674   if (index < 0) {
1675     prefix = "TransliterationCandidates";
1676     index = -1 - index;
1677   } else {
1678     prefix = base_name + "Candidates";
1679   }
1680 
1681   if (index <= 9) {
1682     const string stats_name = prefix + std::to_string(index);
1683     UsageStats::IncrementCount(stats_name);
1684   } else {
1685     const string stats_name = prefix + "GE10";
1686     UsageStats::IncrementCount(stats_name);
1687   }
1688 }
1689 
CommitUsageStats(SessionConverterInterface::State commit_state,const commands::Context & context)1690 void SessionConverter::CommitUsageStats(
1691     SessionConverterInterface::State commit_state,
1692     const commands::Context &context) {
1693   size_t commit_segment_size = 0;
1694   switch (commit_state) {
1695     case COMPOSITION:
1696       commit_segment_size = 0;
1697       break;
1698     case SUGGESTION:
1699     case PREDICTION:
1700       commit_segment_size = 1;
1701       break;
1702     case CONVERSION:
1703       commit_segment_size = segments_->conversion_segments_size();
1704       break;
1705     default:
1706       LOG(DFATAL) << "Unexpected state: " << commit_state;
1707   }
1708   CommitUsageStatsWithSegmentsSize(commit_state, context, commit_segment_size);
1709 }
1710 
CommitUsageStatsWithSegmentsSize(SessionConverterInterface::State commit_state,const commands::Context & context,size_t commit_segments_size)1711 void SessionConverter::CommitUsageStatsWithSegmentsSize(
1712     SessionConverterInterface::State commit_state,
1713     const commands::Context &context,
1714     size_t commit_segments_size) {
1715   CHECK_LE(commit_segments_size, selected_candidate_indices_.size());
1716 
1717   string stats_str;
1718   switch (commit_state) {
1719     case COMPOSITION:
1720       stats_str = "Composition";
1721       break;
1722     case SUGGESTION:
1723     case PREDICTION:
1724       // Suggestion related usage stats are collected as Prediction.
1725       stats_str = "Prediction";
1726       UpdateCandidateStats(stats_str, selected_candidate_indices_[0]);
1727       break;
1728     case CONVERSION:
1729       stats_str = "Conversion";
1730       for (size_t i = 0; i < commit_segments_size; ++i) {
1731         UpdateCandidateStats(stats_str,
1732                              selected_candidate_indices_[i]);
1733       }
1734       break;
1735     default:
1736       LOG(DFATAL) << "Unexpected state: " << commit_state;
1737       stats_str = "Unknown";
1738   }
1739 
1740   UsageStats::IncrementCount("Commit");
1741   UsageStats::IncrementCount("CommitFrom" + stats_str);
1742 
1743   if (stats_str != "Unknown") {
1744     if (SessionUsageStatsUtil::HasExperimentalFeature(context,
1745                                                       "chrome_omnibox")) {
1746       UsageStats::IncrementCount("CommitFrom" + stats_str + "InChromeOmnibox");
1747     }
1748     if (SessionUsageStatsUtil::HasExperimentalFeature(context,
1749                                                       "google_search_box")) {
1750       UsageStats::IncrementCount(
1751           "CommitFrom" + stats_str + "InGoogleSearchBox");
1752     }
1753   }
1754 
1755   const std::vector<int>::iterator it = selected_candidate_indices_.begin();
1756   selected_candidate_indices_.erase(it, it + commit_segments_size);
1757 }
1758 
1759 }  // namespace session
1760 }  // namespace mozc
1761