diff --git a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties.xlf b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties.xlf index b9582503229..fe93e12c1d2 100644 --- a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties.xlf +++ b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties.xlf @@ -1,12 +1,12 @@ - +
- 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 - +
@@ -1236,7 +1236,7 @@ New Mark - Mark/Gate + Mark/Gate name Error updating race course: {0} @@ -2942,6 +2942,9 @@ Password + + Search tag + Do you really want to remove the leaderboard: ''{0}''? @@ -3038,6 +3041,9 @@ Tracker Status + + Course Status + Can only be edited before starting tracking @@ -4494,6 +4500,54 @@ Move fix + + Manage2Sail Event + + + There are no events yet. + + + Please select a result import provider... + + + Build version + + + Server Information + + + Server Configuration + + + Standalone server + + + Send... + + + PassId + + + Created at + + + Logical timepoint + + + Info + + + Wind fixes + + + 2x Mark + + + {0,number,#,###}-{1,number,#,###} of {2,number,#,###} + + + {0,number,#,###}-{1,number,#,###} of over {2,number,#,###} +
\ No newline at end of file diff --git a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties.xlf b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties.xlf new file mode 100644 index 00000000000..816ca89ac7b --- /dev/null +++ b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties.xlf @@ -0,0 +1,6064 @@ + + + +
+ + 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 + + + + + +
+ + + Manage2Sail + Manage2Sail + + + Race Manager App + レースマネージャアプリ + + + TracTrac Events + TracTrac イベント + + + SwissTiming Events + SwissTiming イベント + + + History of tracked events + 追跡イベントの履歴 + + + General + 一般 + + + List Races + レース一覧 + + + List Regattas + レガッタ一覧 + + + Wind + 風 + + + Maneuver + マニューバー + + + This is the Wind Panel which so far is completely empty. + これは風パネルであり、今のところ完全に空となっています。 + + + Refresh + リフレッシュ + + + Remove + 削除 + + + Wind Source + 風源 + + + Dampening Interval + 制動間隔 + + + Map + マップ + + + Race + レース + + + Races + レース + + + tracked + 追跡済 + + + Linked Races + リンクされているレース + + + Time + 計時 + + + Time the competitor sailed on this leg. For the first leg this is counted from when the competitor passed the start line. + このレグを帆走した競技者を計時します。第 1 レグについては、競技者がスタートラインを通過したときにカウントされます。 + + + Play speed + 再生速度 + + + Use "1" for regular play-back speed, "2" for double-speed playback and so on + 通常の再生速度は "1"、2 倍速の再生は "2" と、以下同様に指定 + + + Live + ライブ + + + Replay Mode + 再生モード + + + Delay + 遅延 + + + Leave empty to replay at arbitrary times; enter seconds (e.g., "60" for one minute or "0" for none) of delay before live data feed + 任意の時間に再生する場合は空白のままにします。ライブデータフィード前の遅延を秒数で入力してください (例: "60" で 1 分間、"0" で遅延なし) + + + Speed (kn) + 速力 (kt) + + + From (deg) + 開始 (度) + + + There are no races yet. + レースがまだありません。 + + + Σ + Σ + + + Accumulated + 累積 + + + Competitor + 競技者 + + + Competitors + 競技者 + + + {0}/{1} competitors selected. + {0}/{1} の競技者が選択されました。 + + + No such leaderboard + そのようなリーダーボードはありません + + + Leaderboard Configuration + リーダーボード設定 + + + New... + 新規... + + + Edit Points... + ポイント編集... + + + Race Names in Selected Leaderboard + 選択されたリーダーボード上のレース名 + + + Add... + 追加... + + + Rename... + 名称変更... + + + Tracked Race Connected to Selected Race Name + 選択されたレース名に結び付けられた追跡レース + + + Link to Column + 列にリンク + + + Unlink + リンク解除 + + + Leaderboard Name + リーダーボード名 + + + Cancel + 取消 + + + Please enter a name + 名称を入力してください + + + Please enter a boat class + 艇種を入力してください + + + Discard one more race starting with how many started races + レース除外 (スタート済レースから) + + + Leaderboard with this name already exists + この名称のリーダーボードは登録されています + + + Discard thresholds must be numeric + 破棄しきい値は数値でなければなりません + + + Discard thresholds must be ascending + 破棄しきい値は昇順でなければなりません + + + Discarding + 破棄 + + + Starting from how many races + レース数から開始 + + + Rename Leaderboard + リーダーボード名称変更 + + + Add Column to Leaderboard + リーダーボードに列を追加 + + + Please enter a name for new race column + 新規レース列の名称を入力してください + + + OK + OK + + + Medal Race + メダルレース + + + Rename Race + レース名称変更 + + + Open Selected Leaderboard + 選択したリーダーボードを開く + + + Rank + ランク + + + Competitor''s rank in the race + レースにおける競技者のランク + + + Regatta Rank + レガッタランク + + + Competitor''s rank in the regatta + レガッタにおける競技者のランク + + + Overall Rank + 全体ランク + + + Competitor''s rank in the series + シリーズにおける競技者のランク + + + Regatta Rank + レガッタランク + + + - + - + + + + + + + + + L + L + + + Distance + 距離 + + + ∅ Speed + ∅ 速力 + + + The average speed over ground during this leg,\ncalculated with the division of the distance traveled by the time needed. + このレグ中の平均対地速力は、\n必要とした時間で移動距離を割ったもので計算されます。 + + + The average speed over ground during this race,\ncalculated with the division of the distance traveled by the time needed. + このレース中の平均対地速力は、\n必要とした時間で移動距離を割ったもので計算されます。 + + + Rank Gain + ランクゲイン + + + The difference of the position from the current and the previous leg. + 現在および前のログからのポジションの変化です。 + + + Sailing Analytics + Sailing Analytics + + + Leaderboard + リーダーボード + + + Leaderboards + リーダーボード + + + Leaderboard Settings + リーダーボード設定 + + + Settings + 設定 + + + Select at least one leg detail + レグ詳細を少なくとも 1 つ選択 + + + SOG + SOG + + + The current speed over ground. + 現在の対地速力です。 + + + ETA + ETA + + + The estimated time that the competitor needs to reach the next waypoint, based on the competitor''s current velocity made good, considering the true wind direction. + 真風向を考慮して、競技者の現在の VMG (有効速度) に基づいて競技者が次の変針点に到達するために必要な推定時間です。 + + + Gap to leader + 首位艇までの距離差 + + + Estimates the (calculated) arrival time at the current position of the boat that is farthest ahead,\nbased on average VMG for the current leg and with performance equal to that of the race leader for subsequent legs.\nThe gap tells how much earlier the competitor would have had to be where she currently is\nin order to rank equal to the race leader at the projected arrival at the current position of the boat farthest ahead. + 最も遠くの前方にいる艇の現在位置での (計算) 到着時刻を、\n現在レグの平均 VMG および後続レグに対するレース首位艇と同等の性能に基づいて推定します。\nこの距離差は、最も遠くの前方にいる艇の現在位置に到着が予測される時点においてレース首位艇と同じランクとなるために\n競技者が現在位置する場所までどれだけ以前にいなければならなかったかを示します。 + + + VMG + VMG + + + The velocity made good, which is the windward/leeward speed for upwind/downwind legs, respectively, or the along-track speed for reaching legs. + アップウィンド/ダウンウィンドレグに対する風上/風下速度である VMG、またはリーチングレグに対するアロングトラック速度です。 + + + Windward distance to go + ウィンワード残り距離 + + + Windward/leeward distance from the competitor''s current position to the end of the leg for upwind/downwind legs, respectively, or the along-track distance for reaching legs. + アップウィンド/ダウンウィンドレグに対する競技者の現在位置からレグの最後までのウィンワード/リーワード距離、またはリーチングレグのアロングトラック距離です。 + + + Gap + 距離差 + + + Windward/leeward distance of the competitor to the competitor farthest ahead in the race for upwind/downwind legs, respectively, or the along-track distance for reaching legs.\nNote that the competitor that is farthest ahead is not necessarily leading the race if handicap ranking is in place. + アップウィンド/ダウンウィンドレグに対するレース内で最も遠くの前方にいる競技者までの競技者のウィンワード/リーワード距離、またはリーチングレグに対するアロングトラック距離です。\n最も遠くの前方にいる競技者は、ハンディキャップランキングが実施されている場合には必ずしもレースの首位艇ではないことに注意してください。 + + + Name + 名称 + + + Leave empty for default + デフォルトでは空のままにします + + + Track New event + 新規イベント追跡 + + + Trackable Races + 追跡可能レース + + + Track Wind + 風の追跡 + + + Regatta name + レガッタ名 + + + Event name + イベント名 + + + Event + イベント + + + Start time + 開始時間 + + + Start of tracking + 追跡の開始 + + + End of tracking + 追跡の終了 + + + Regatta + レガッタ + + + Regattas + レガッタ + + + Start tracking + 追跡開始 + + + Stop tracking + 追跡停止 + + + Connections + 接続 + + + History of connections + 接続の履歴 + + + Define a new connection + 新規接続の定義 + + + Tracked races + 追跡レース + + + Can send requests + 要求を送信可能 + + + Connect and read races + 接続してレース読込 + + + Ports + ポート + + + Live data + ライブデータ + + + Stored data + 格納データ + + + URIs + URI + + + JSON URL + JSON URL + + + Port Live Data + ポートライブデータ + + + Port Stored Data + ポート格納データ + + + Live URI + ライブ URI + + + Stored URI + 格納 URL + + + Events Currently Tracked + 現在追跡中のイベント + + + Hostname + ホスト名 + + + Correct Wind Bearing by Declination + 偏角別の適切な風方位 + + + Tracking Started + 追跡開始 + + + Pause automatic refresh + 自動リフレッシュを中断 + + + Refresh automatically + 自動リフレッシュ + + + Delay (s) + 遅延 + + + Delay between auto-updates in seconds: + 自動更新間の遅延秒数: + + + The delay must be greater than zero + 遅延はゼロより大きい必要 + + + Choose update interval of at least one second + 1 秒以上の更新間隔を選択 + + + Error setting wind for race + レースの風の設定でエラー発生 + + + Error fetching wind information for race + レースの風情報の取込でエラー発生 + + + Error while trying to set "wind starts upwind" flag + "アップウィンドの風で開始" フラグを設定しようとしてエラーが発生 + + + in event + イベント: + + + to + to + + + to + to + + + Show estimated wind + 推定された風の表示 + + + Timing + タイミング + + + Race Details + レース詳細 + + + Overall Details + 全体詳細 + + + Leg Details + レグ詳細 + + + Up + 上 + + + Down + 下 + + + Port + ポート + + + Race Started + レーススタート済 + + + Show only selected competitors + 選択した競技者のみ表示 + + + Show info box for selected competitors + 選択した競技者の情報ボックスを表示 + + + Tail length (s) + テール長 + + + Which races would you like to display? + どのレースを表示しますか + + + Length (s) + 長さ + + + Stress Test + ストレステスト + + + Maneuvers + マニューバー + + + The number of the maneuvers sailed in this leg. + このレグで帆走したマニューバーの数です。 + + + The number of maneuvers sailed in this race. + このレースで帆走したマニューバーの数です。 + + + Tacks + タック + + + Jibes + ジャイブ + + + Penalty Circles + ペナルティーサークル + + + Medalrace value not allowed + メダルレース値は不可 + + + Configuration + 設定 + + + Maneuvers + マニューバー + + + Choose chart + チャートの選択 + + + Distance Traveled + 移動した距離 + + + The distance traveled from the leg start to the end\nor to the current time point, if the leg isn''t finished. + レグ始点からレグ終点まで、またはレグが\nフィニッシュしていない場合は現時点までに移動した距離です。 + + + The distance traveled from the race start to the end\nor to the current time point, if the race isn''t finished. + レース開始からレース終了まで、またはレースが\nフィニッシュしていない場合は現時点までに移動した距離です。 + + + Velocity Made Good + VMG (有効速度) + + + Speed Over Ground + 対地速力 + + + Gap To Leader + 首位艇までの距離差 + + + Points to load + ロードするポイント + + + Speed + 速力 + + + Distance + 距離 + + + in + in + + + s + s + + + s/NM + s/NM + + + m + m + + + ° + ° + + + Close + 閉じる + + + Compare competitors + 競技者比較 + + + Description + 内容説明 + + + Sail number + セールナンバー + + + Country + 国 + + + Unable to find the IOC 3-letter-codes. + IOC 3 文字コードを検索できませんせした。 + + + Add + 追加 + + + Delete + 削除 + + + Show charts + チャート表示 + + + A race with that name already exists. + その名称のレースはすでに登録されています。 + + + A race with that name already exists in this regatta. + その名称のレースはこのレガッタにすでに登録されています。 + + + Head up + ヘッドアップ + + + Bear away + ベアアウェイ + + + Tack + タック + + + The amount of tacks sailed in this race. + このレースで帆走したタックの数です。 + + + Jibe + ジャイブ + + + The amount of jibes sailed in this race. + このレースで帆走したジャイブの数です。 + + + Penalty circle + ペナルティーサークル + + + The amount of penalty circles sailed in this race. + このレースで帆走したペナルティー回転の数です。 + + + Mark passing + マーク通過 + + + Mark + マーク + + + Unknown maneuver + 不明のマニューバー + + + Douglas peucker points + Douglas peucker ポイント + + + Filter races by name + レースをフィルタリングする名称 + + + Filter media by name + メディアをフィルタリングする名称 + + + Track settings + 設定追跡 + + + Edit + 編集 + + + Edit competitors + 競技者編集 + + + Edit scores + スコア編集 + + + Open in Browser + ブラウザで開く + + + Remove + 削除 + + + Edit Series + シリーズ編集 + + + Edit or rename race + レース編集/名称変更 + + + Unlink race + レースのリンク解除 + + + Remove race + レース削除 + + + Export as XML + XML としてエクスポート + + + Open Coach Dashboard + コーチダッシュボードを開く + + + Filter leaderboards by name + リーダーボードをフィルタリングする名称 + + + Actions + アクション + + + Is linked + リンク済 + + + Yes + はい + + + No + いいえ + + + Hover over a point for details + 詳細についてはポイントをホバー + + + No selection + 未選択 + + + Race starts with upwind leg + アップウィンドレグでレーススタート + + + Events + イベント + + + Settings for {0} + {0} の設定 + + + No events were found + イベントが見つかりませんでした + + + No event selected + イベントが選択されていません + + + No leaderboards were found + リーダーボードが見つかりませんでした + + + Search for events + イベントの検索 + + + Location + 場所 + + + From + 開始 + + + Until + 終了 + + + Only live events + ライブイベントのみ + + + Location not available + 場所がありません + + + Start date not available + 開始日付がありません + + + Start date + 開始日付 + + + End date + 終了日付 + + + Unknown maneuver + 不明のマニューバー + + + the tail length must be a number greater than zero. + テール長はゼロより大きい数でなければなりません。 + + + Failed to load race information: {0} + レース情報のロードに失敗: {0} + + + {0} passed {1} at {2} + {0} が {2} に {1} を通過 + + + {0} at {1}: {2} + {1} での {0}: {2} + + + Do you really want to remove the race ''{0}''? Note: all race logs for this race will also permanently be removed! + レース ''{0}'' を削除しますか。注記: このレースのレースログもすべて永続的に削除されます。 + + + Show details + 詳細表示 + + + Hide leaderboard + リーダーボード非表示 + + + Hide leaderboards + リーダーボード非表示 + + + Show leaderboard + リーダーボード表示 + + + Hide races + レース非表示 + + + Show map + マップ表示 + + + Hide map + マップ非表示 + + + No leaderboard selected + リーダーボード未選択 + + + Leaderboard groups + リーダーボードグループ + + + Filter leaderboard groups by name + リーダーボードグループをフィルタリングする名称 + + + Create leaderboard group + リーダーボードグループ登録 + + + Leaderboard Group Configuration + リーダーボードグループ設定 + + + Legs + レグ + + + On Leg + オンレグ + + + The number of the leg on which the competitor currently is, starting with 1 + 競技者が現在競技しているレグの番号 (1 から開始) + + + Leaderboard Group + リーダーボードグループ + + + Please enter a non-empty description + 空でない説明を入力してください + + + A leaderboard group with this name already exists. + この名称のリーダーボードグループはすでに登録されています。 + + + Details of leaderboard group + リーダーボードグループの詳細 + + + Edit + 編集 + + + Save + 保存 + + + Abort + 中止 + + + No leaderboard group with the name {0} found + {0} という名称のリーダーボードグループは見つかりませんでした + + + Overview + 概要 + + + Welcome to SAP Sailing Analytics + SAP Sailing Analytics へようこそ + + + SAP brings sailing fans closer to the on-water action than ever before.\ The live-leaderboard enhances the fan experience through real-time rankings, boat speeds and winning margins. Follow every race and never miss a passing maneuver with SAP.\ This is the simple way to watch live sail racing from wherever you are. + SAP は、水上で行われている動きをセーリングファンにとってかつてないほど身近なものにします。\nこの実況リーダーボードでランキング、艇速、艇差をリアルタイムに把握してファンとしての体験を充実したものにできます。SAP を使ってすべてのレースを追いかければ、追い抜きのマニューバーを見逃すことはありません。\nこれこそ、セーリング競技を生で簡単に視聴する方法なのです。 + + + Home + ホーム + + + Click and drag in the plot area to zoom in + 拡大するプロット領域をクリックしてドラッグ + + + Wind speed (kts) + 風速 (kt) + + + All times in UTC + 時刻はすべて UTC + + + Auto zoom to + 自動拡大: + + + Boats + 艇 + + + Buoys + ブイ + + + Wind sensors + 風センサー + + + Off + オフ + + + Tails + テール + + + Zoom only to selected competitors + 選択した競技者にのみズーム + + + Bearing + 方位 + + + Zoom + ズーム + + + Step size must be greater than 0 + ステップサイズは 0 より大きい必要 + + + Step size in seconds + ステップサイズ秒数 + + + Refresh interval (s) + リフレッシュ間隔 + + + Refresh interval must be greater than {0} seconds + リフレッシュ間隔は {0} 秒より長いことが必要 + + + Competitor Chart + 競技者チャート + + + To show the competitor charts, select at least one competitor + 競技者チャートを表示するには、少なくとも 1 つの競技者を選択してください + + + To send invitation E-Mails, select at least one competitor + 招待電子メールを送信するには、少なくとも 1 つの競技者を選択してください + + + Direction change + 方向変更 + + + Speed change + 艇速変更 + + + deg + 度 + + + Untracked + 未追跡 + + + Delay for live mode: + ライブモードの遅延: + + + Not available + 利用不可 + + + Details + 詳細 + + + No group selected + グループ未選択 + + + Combined + 複合 + + + Course Layout + コースレイアウト + + + Estimation + 推定 + + + Measured + 計測 + + + Manual + 手動 + + + Race Committee + レース委員会 + + + Click on the chart to set the time + 時刻を設定するチャートをクリック + + + Position + 位置 + + + Wind Sources Used + 使用風源 + + + Error trying to update wind sources to exclude for race {0}: {1} + レース {0} を除外するために風源を更新しようとしてエラーが発生: {1} + + + Feedback + フィードバック + + + Start/Stop playing + 再生開始/停止 + + + Jump back to live time point + ライブ時点に戻る + + + Slow play speed down or even run backwards + 再生速度を下げるか巻き戻し + + + Speed play speed up or run forward again + 再生速度を上げるか早送り + + + Show/Hide {0} + 表示/非表示 {0} + + + Angle to wind + 風とのアングル + + + Races in leaderboard + リーダーボードにあるレース + + + Leaderboards in group + リーダーボードグループ + + + Replication + 複製 + + + Error fetching replica data: {0} + 複製データ取込でエラー発生: {0} + + + ∅ XTE + ∅ XTE + + + The average cross track error to the great circle segment connecting leg start with leg end + レグ始点からレグ終点までの大圏航路 (円曲線) との間の平均クロストラックエラー + + + ∅ XTE +/- + ∅ XTE +/- + + + The average position on the course as a signed distance from the course middle line; 0 meaning on the course middle line; negative values mean left-hand side, positive values mean right-hand side. + コース中央航路からの距離 (+/- 符号付) で表されたコース上の平均位置: 0 はコース中央航路を、マイナスの値は左側を、プラスの値は右側をそれぞれ意味します。 + + + The average absolute cross track error to the great circle segments connecting leg start with leg end for all upwind legs of this race, ignoring to which side of the course middle line the competitor is + このレースのすべてのアップウィンドレグに対するレグ始点からレグ終点までの大圏航路 (円曲線) との間の平均絶対クロストラックエラー (競技者がコース中央航路のどちら側にいるかは無視) + + + The signed average cross track error to the great circle segments connecting leg start with leg end for all upwind legs of this race; negative values mean left of the course middle line, positive values mean right. + このレースのすべてのアップウィンドレグに対するレグ始点からレグ終点までの大圏航路 (円曲線) との間の +/- 符号付の平均クロストラックエラー: マイナスの値はコース中央航路の左側、プラスの値は右側を意味します。 + + + Hostname of Master Instance + マスタインスタンスのホスト名 + + + Error starting replication from host {0} / exchange {1}: {2} + ホスト {0} / エクスチェンジ {1} からの複製開始でエラー発生: {2} + + + Help lines + ヘルプライン + + + Start line + スタートライン + + + Finish line + フィニッシュライン + + + Advantage line + アドバンテージライン + + + Course middle line + コース中央航路 + + + Messaging port number (0 for default) + メッセージングポート番号 (初期値は 0) + + + Servlet port number + サーブレットポート番号 + + + Registered at {0} + 登録時刻 {0} + + + Replicating from master {0}:{2}, messaging port {3}:{1}, exchange {4} + マスタ {0}:{2}、メッセージングポート {3}:{1}、エクスチェンジ {4} から複製 + + + Show wind speed data + 風速データ表示 + + + Show wind directions data + 風向データ表示 + + + Fleet + フリート + + + Boat Class + 艇種 + + + Set delay to live + ライブに遅延を設定 + + + Please enter a venue. + 開催地を入力してください。 + + + Please enter a start date and an end date. + 開始日付と終了日付を入力してください。 + + + The start date must be before the end date. + 開始日付は終了日付より前でなければなりません。 + + + An event with this name already exists. + この名称のイベントはすでに登録されています。 + + + Venue + 開催地 + + + Please select a boat class. + 艇種を入力してください。 + + + A regatta with this name already exists. + この名称のレガッタはすでに登録されています。 + + + A series with this name already exists. + この名称のシリーズはすでに登録されています。 + + + A fleet with this name already exists. + この名称のフリートはすでに登録されています。 + + + Fleets + フリート + + + Series + シリーズ + + + Split fleet + フリート分割 + + + Color + 色 + + + Medal Series + メダルシリーズ + + + No color + 色なし + + + Please select a regatta. + レガッタを選択してください。 + + + Add fleet + フリート追加 + + + Do you really want to remove the regatta "{0}"? This will also remove all leaderboards for this regatta! + レガッタ "{0}" を削除しますか。削除すると、このレガッタのためのリーダーボードもすべて削除されます。 + + + Error loading score correction providers: {0} + スコア修正プロバイダのロードでエラー発生: {0} + + + Select result list to import from + インポート元の結果一覧を選択 + + + of + of + + + Error obtaining score corrections from provider {0} and event {1} for boat class {2} published on {3}: {4} + プロバイダ {0} からのスコア修正および艇種 {2} のイベント {1} (公表日付 {3}) の取得でエラー発生: {4} + + + Assign race numbers of official results to leaderboard race columns + 公式結果のレース番号をリーダーボードのレース列に割り当ててください + + + Import official results... + 公式結果をインポートしています... + + + Select all + 全選択 + + + Apply + 適用 + + + Successfully updated scores + スコアが正常に更新されました + + + Error updating scores for leaderboard {0}: {1} + リーダーボード {0} のスコアを更新しようとしてエラーが発生: {1} + + + Maneuver types to show when competitor is clicked + 競技者をクリックしたときに表示するマニューバータイプ + + + Simulate as live race + ライブレースとしてシミュレート + + + Offset before start in minutes: + スタート前のオフセット (分): + + + The selected races contain boat classes which are not the same as the boat class ''{0}'' of the selected regatta. No races will be loaded. + 選択したレースに、選択したレガッタの艇種 ''{0}'' と同じでない艇種が含まれています。レースはロードされません。 + + + There is at least one regatta for the selected boat classes. Really create default regatta(s) for those race(s)? + 選択した艇種に対して少なくとも 1 つのレガッタがあります。このレースに対してレガッタを初期登録しますか。 + + + Reload + リロード + + + Add Regatta... + レガッタ追加... + + + Import Regattas... + レガッタのインポート... + + + Exchange Name + エクスチェンジ名 + + + Course + コース + + + Course layout + コースレイアウト + + + Buoy + ブイ + + + Gate + ゲート + + + Buoys + ブイ + + + Waypoint + 変針点 + + + Show all competitors + 全競技者表示 + + + Boat tails + ボートテール + + + Start line to first Mark Triangle + 最初のマークトライアングルへのスタートライン + + + Result import URLs + 結果インポート URL + + + Error refreshing result import URL list: {0} + 結果インポート URL 一覧をリフレッシュしようとしてエラーが発生: {0} + + + Successfully updated result import URL list + 結果インポート URL 一覧を正常に更新 + + + Error removing result import URLs: {0} + 結果インポート URL を削除しようとしてエラーが発生: {0} + + + Please enter a URL. + URL を入力してください。 + + + Add result import URL + 結果インポート URL の追加 + + + Error adding result import URL: {0} + 結果インポート URL を追加しようとしてエラーが発生: {0} + + + Remote Server Instances + リモートサーバインスタンス + + + Local Server + ローカルサーバ + + + Error refreshing SailingServer instances: {0} + SailingServer インスタンスをリフレッシュしようとしてエラーが発生: {0} + + + Successfully updated SailingServer instances + SailingServer インスタンスを正常に更新 + + + Error removing SailingServer instances: {0} + SailingServer インスタンスを削除しようとしてエラーが発生: {0} + + + Add SailingServer instance + SailingServer インスタンスの追加 + + + Error adding SailingServer instance: {0} + SailingServer インスタンスを追加しようとしてエラーが発生: {0} + + + Error trying to obtain leaderboard contents: {0} + リーダーボード内容を取得しようとしてエラーが発生: {0} + + + Last Score Update + 最新スコア更新 + + + This website is not optimized for your browser. Please switch to a newer one like Firefox or Chrome. + この Web サイトはご使用のブラウザには最適化されていません。Firefox や Chrome などの新しいブラウザに切り替えてください。 + + + New Mark + 新規マーク + + + Mark/Gate name + マーク/ゲート + + + Error updating race course: {0} + レースコースを更新しようとしてエラーが発生: {0} + + + Successfully updated race course + レースコースを正常に更新 + + + Select two marks to create a gate + ゲートを登録するために 2 つのマークを選択 + + + Error trying to obtain the marks of the race course: {0} + レースコースのマークを取得しようとしてエラーが発生: {0} + + + Error trying to obtain the race course: {0} + レースコースを取得しようとしてエラーが発生: {0} + + + Insert waypoint before selected + 選択前に変針点を挿入 + + + Insert waypoint after selected + 選択後に変針点を挿入 + + + Add Gate + ゲート追加 + + + Low Point System + 低得点方式 + + + High Point System + 高得点方式 + + + High Point System ESS Overall (10) + 高得点方式 ESS 全体 (10) + + + High Point System ESS Overall (12) + 高得点方式 ESS 全体 (12) + + + Score in points. Winner of an act scores 12 points, 2nd - 11 points, .... Any tie in the overall Extreme Sailing Series is broken in favour of the competitor with the most Act wins. If that does not break the tie, the result in the last Act will be used. + 得点数によるスコアです。アクトの勝者が 12 点、2 位が 11 点などと得点します。Extreme Sailing シリーズ全体で同点となった場合は、より多くのアクトで勝者となった競技者を上位とします。それでも同じ場合には、最終アクトの結果を用います。 + + + High Point System, Last Race Breaks Tie + 高得点方式、同点の場合は最終レースで順位決定 + + + Low Point System, Winner Gets Zero + 低得点方式、1 位が 0 点 + + + High Point System, Winner Gets 5 Points + 高得点方式、1 位が 5 点 + + + High Point System, Winner Gets 6 Points + 高得点方式、1 位が 6 点 + + + High Point System, Winner Gets 8 Points + 高得点方式、1 位が 8 点 + + + High Point System, Winner 8 Points, Interpolated + 高得点方式、1 位が 8 点、補間 + + + Low Point with Eliminations and Rounds, Winner Gets 0.7 Points + 低得点方式、予選およびラウンドあり、1 位が 0.7 点 + + + A variant of the low-point scoring scheme, using eliminations consisting of rounds.\nEach round shall be modeled as a series in the regatta, and the fleet ordering counts backwards\nstarting with the highest number for the first round.\nIf a final and losers final take place, use 1 and 2 as their ordering. Start with 3 for the semi-final round. + ラウンドから構成される予選を用いた低得点方式のバリエーションです。\n各ラウンドがレガッタ内のシリーズとして形成されることになり、フリート順序は\n最初のラウンドで最も大きい数から開始してカウントダウンします。\n決勝や最下位決定戦が行われる場合は、その順序として 1 および 2 を使用します。準決勝ラウンドは 3 から始めます。 + + + Low Point, ties broken by act point sum, then last act + 低得点方式、アクト総得点、次に最終アクトによって順位を決定 + + + Instead of the usual count-back system used for low point schemes to break ties, competitors with equal point sums are compared by comparing the sum of the points they scored across all acts. If this still leaves a tie, the tie is broken by the last act. + 順位決定のための低得点方式に使用される通常のカウントダウン方式ではなく、総得点が同点の競技者が全アクト共通で得点した総得点で比較されます。これでも同順位となる場合は、最終アクトによって順位を決定します。 + + + High Point, winner gets 1 point + 高得点方式、1 位が 1 点 + + + High Point, winner gets 10 points + 高得点方式、1 位が 10 点 + + + High Point, winner gets 10 points (or 8) + 高得点方式、1 位が 10 点 (または 8 点) + + + Score in points. Winner scores 10 points, 2nd - 9 points, .... A tie is broken in favour of the competitor having won most races, if tie remains then tie is broken by the rank in the last race scored. + 得点数によるスコアです。1 位が 10 点、2 位が 9 点などと得点します。同点となった場合は、より多くのレースで勝者となった競技者を上位とします。それでも同順位となる場合には、最終レースでの得点でのランクで順位を決定します。 + + + High Point, winner gets 12 points (or 8) + 高得点方式、1 位が 12 点 (または 8 点) + + + Score in points. Winner scores 12 points, 2nd - 11 points, .... A tie is broken in favour of the competitor having won most races, if tie remains then tie is broken by the rank in the last race scored. + 得点数によるスコアです。1 位が 12 点、2 位が 11 点などと得点します。同点となった場合は、より多くのレースで勝者となった競技者を上位とします。それでも同順位となる場合には、最終レースでの得点でのランクで順位を決定します。 + + + Scoring system + 得点方式 + + + Create flexible leaderboard + フレキシブルリーダーボード登録 + + + Create regatta leaderboard + レガッタリーダーボード登録 + + + Create default regatta leaderboard + レガッタリーダーボード初期登録 + + + Edit flexible leaderboard + フレキシブルリーダーボード編集 + + + Edit regatta leaderboard + レガッタリーダーボード編集 + + + Default regatta + レガッタ初期値 + + + Use Overall leaderboard + 全体リーダーボード使用 + + + Overall Standings + 全体順位表 + + + Type + タイプ + + + Add races + レース追加 + + + races with name prefix + レース - 名称接頭辞: + + + Max SOG + 最大 SOG + + + The competitor''s greatest speed over ground measured in the regatta. + このレガッタで計測されたこの競技者の最大対地速力です。 + + + Total time downwind + ダウンウィンド合計時間 + + + The total time the competitor sailed downwind in the regatta. + レガッタ内で競技者がダウンウィンドで帆走した合計時間です。 + + + Total time upwind + アップウィンド合計時間 + + + The total time the competitor sailed upwind in the regatta. + レガッタ内で競技者がアップウィンドで帆走した合計時間です。 + + + Total time reaching + リーチング合計時間 + + + The total time the competitor sailed on reaching legs in the regatta. + レガッタ内で競技者がリーチングレグで帆走送した合計時間です。 + + + Total time + 合計時間 + + + The total time the competitor sailed in the regatta, counted from race start time. + レーススタート時刻から計時した、レガッタ内で競技者が帆走した合計時間です。 + + + Total distance + 合計距離 + + + The total distance sailed in the regatta. + このレガッタで帆走した合計距離です。 + + + ∅ SOG + ∅ SOG + + + Average speed over ground across the regatta. + このレガッタ全体の平均対地速力です。 + + + hh:mm:ss + hh:mm:ss + + + Add wind data + 風データ追加 + + + The value of ''{0}'' must be between {1} and {2}. + ''{0}'' の値は {1} と {2} の間になければなりません。 + + + The value of ''{0}'' must be greater than {1}. + ''{0}'' の値は {1} より大きくなければなりません。 + + + optional + オプション + + + Please enter a value. + 値を入力してください。 + + + Latitude + 緯度 + + + Longitude + 経度 + + + Add Event + イベント追加 + + + Publication URL + 公開 URL + + + Suppress + 非表示 + + + Is public + 公開 + + + Unsuppress + 非表示解除 + + + Suppressed Competitors + 非表示競技者 + + + Audio & Video + 音声と動画 + + + URL + URL + + + MIME Type + MIME タイプ + + + Title + 表題 + + + Duration (h:mm:ss.ms) + 時間 (h:mm:ss.ms) + + + Add Audio/Video + 音声/動画追加 + + + Add Youtube + YouTube 追加 + + + Youtube Id + YouTube ID + + + Do you really want to remove media track ''{0}''? + メディアトラック ''{0}'' を削除しますか + + + Please select a series + シリーズを選択してください + + + Please select a race + レースを選択してください + + + Display groups in reverse order + 逆順序でグループを表示 + + + Configure URL + URL 設定 + + + Configure a leaderboard and obtain the resulting URL from the link + リーダーボードを設定し、リンクから結果 URL を取得 + + + Embedded + 埋込 + + + Additional URL Settings + 追加 URL 設定 + + + Restrict to single selected race + 単一の選択レースに制限 + + + Expand selected race + 選択レースを拡大 + + + Allow for race details + レース詳細を許容 + + + Number of races + レース数 + + + Please enter the number of last races to be displayed + 表示する最終レース数を入力してください + + + Choose the way you select races + レース選択方法の選択 + + + Select from all races + 全レースから選択 + + + Select a number of races + レースの数を選択 + + + Number of most current races to show + 表示する最新レース数 + + + Always expand last race shown + 表示されている最後のレースを常に拡大 + + + Factor + 係数 + + + Error updating the medal race setting: {0} + メダルレース設定の更新でエラー発生: {0} + + + Maneuver loss + マニューバーロス + + + ∅ Maneuver Loss + ∅ マニューバーロス + + + The average distance loss during any mneuver. + マニューバー中の平均距離ロスです。 + + + ∅ Tack Loss + ∅ タックロス + + + The average distance loss during tacks. + タック中の平均距離ロスです。 + + + ∅ Jibe Loss + ∅ ジャイブロス + + + The average distance loss during jibes. + ジャイブ中の平均距離ロスです。 + + + Shape + シェイプ + + + Pattern + パターン + + + Legend + 凡例 + + + live + ライブ + + + SwissTiming Archived Events + SwissTiming アーカイブ済イベント + + + Error fetching chart data: {0} + チャートデータの取込でエラー発生: {0} + + + Loading competitor data... + 競技者データをロードしています... + + + {0} after race {1} + レース {1} 後の {0} + + + Ranking + ランキング + + + Total points + 総得点 + + + Competitor''s total points in the race + 当該レースの競技者総得点 + + + Competitor''s total points in the regatta + 当該レガッタの競技者総得点 + + + Show rank chart + ランクチャート表示 + + + Overall Leaderboard + 全体リーダーボード + + + Error trying to obtain overall leaderboards: {0} + 全体リーダーボードを取得しようとしてエラーが発生: {0} + + + finished + フィニッシュ済 + + + upcoming + 予定 + + + Next + 次 + + + in progress + 進行中 + + + passed to {0} + {0} に受け渡し済 + + + Gap Change + 距離差の変化 + + + The change of the gap to the leader since the last leg start.\nA negative number means that the gap was decreased;\na positive number means that it was increased. + 最後のレグがスタートしてからの首位艇との距離差の変化です。\nマイナスの数字は距離差が減少したことを意味します。\nプラスの数字は距離差が増加したことを意味します。 + + + Mark rounded to + マーク回航 + + + Side to which the mark at leg start was rounded. + レグ始点でマークを回航する側です。 + + + Race {0} is live + レース {0} はライブです + + + Races {0} are live + レース {0} はライブです + + + Regatta {0} is live + レガッタ {0} はライブです + + + Regattas {0} are live + レガッタ {0} はライブです + + + kts + kt + + + {0,number,#0.0} kts + {0,number,#0.0} kt + + + {0,number,#0.0} - {1,number,#0.0} kts + {0,number,#0.0} - {1,number,#0.0} kt + + + Generate Polar Sheet + ポーラーシート生成 + + + Polar Diagrams + ポーラーダイアグラム + + + Polar Sheet + ポーラーシート + + + Minimum datapoints per graph + グラフ別の最小データポイント数 + + + All graphs with less underlying datapoints are not shown visually.\nThis way there are no distracting diagrams shown for\nwindspeeds lacking a sufficient amount of data. + 基礎となるデータポイントが少ないグラフはすべては表示されません。\nこのようにして、十分なデータ量を欠いている\n風速で注意を逸らすダイアグラムは表示されません。 + + + Minimum datapoints per angle + アングル別の最小データポイント数 + + + In each graph only data about angles with at least the given amount\nof underlying datapoints will be shown. Insignificant\nparts of the diagram can be hidden using this threshold. + 各グラフには、定められた数のデータポイントが少なくとも含まれている\nアングルに関するデータのみが表示されます。ダイアグラムの\n重要でない部分は、このしきい値を使用して非表示にすることができます。 + + + Minimum wind confidence value + 最小風信頼値 + + + Only fixes with wind having a confidence over the given threshold are\nused. This confidence is between 0 and 1. + 指定されたしきい値について信頼のある、風のフィックスのみが\n使用されます。この信頼は 0 から 1 までの範囲内です。 + + + Minimum confidence measure + 最小信頼測度 + + + The minimum confidence measure for a point shown in the graph.\nIf it''s below the given threshold, the point won''t show.\nThe measure is between 0 and 1 and determined\nusing statistical measures. + グラフに表示されたポイントの最小信頼測度です。\n指定されたしきい値より小さい場合には、ポイントは表示されません。\nこの測度は 0 から 1 までの範囲内で、\n統計的尺度を使用して決定されます。 + + + Use only wind gauge data for wind speed + 風速に風速計データのみ使用 + + + By taking only the speed data of wind gauges into account\nwe exclude guessed manually entered wind speeds, which can falsify the polar diagram. + 風速計の風速データのみを考慮することにより、\nポーラーダイアグラムを誤らせる可能性のある、推量により手動で入力された風速が除外されます。 + + + Use only estimation for wind direction + 風向に推定値のみ使用 + + + By using the accurate wind estimation for the wind direction,\nwe fix polar diagrams that are rotated due to invalid wind\ngauge data. + 風向に風の正確な推定値を使用することにより、\n無効な風速計データによって回転された\nポーラーダイアグラムを修正します。 + + + Remove outliers (Distance based approach) + 異常値の削除 (距離ベースのアプローチ) + + + Outliers are removed by a distance based approach.\nThe parameters can be adjusted below. + 異常値は、距離ベースのアプローチによって削除されます。\nパラメータを次のとおり調整することができます。 + + + Outlier detection neighborhood radius + 異常値検出近傍半径 + + + The radius used in the distance based outlier\ndetection algorithm. + 異常値検出アルゴリズムに基づく\n距離で使用される半径です。 + + + Minimum outlier detection neighborhood percentage + 最小異常値検出近傍率 + + + The percentage of neightborhood datapoints / overall datapoints\nneeds to be at less than the given value for the point to be considered an outlier. + 近傍データポイント/全体データポイントの比率は\n異常値と考慮されるには当該ポイントに対して指定された値よりも小さい必要があります。 + + + Number of histogram columns per wind range + 風範囲別のヒストグラム列の数 + + + Wind stepping in knots + ウィンドステッピング (kt) + + + Max. distance to wind level + 風レベルへの最大距離 + + + Sets a max radius for which fixes will be assigned to the wind levels. + フィックスが風レベルに割り当てられる対象の最大半径を設定します。 + + + Split Windsources + 風源の分割 + + + Splitting Windsources allows a better search for bad wind gauges.\nThe statistical correctness cannot be guaranteed. The same fix can\ncount into the resulting polar sheet multiple times (for different wind gauges). + 風源の分割により、不良な風速計に対してより良い検索を行うことが可能です。\n統計的な正確さは保証できません。同一フィックスを\n結果として得られるポーラーシートに複数回カウントすることができます (異なる風速計に対して)。 + + + Apply minimum wind confidence + 最小風信頼度の適用 + + + If checked the value in the box above will be used as the minimum valid wind confidence during generation. + 選択すると、上にあるボックスの値が、生成時に最小有効風信頼度として使用されます。 + + + Distribution in the wind range. + 風範囲の分布 + + + Please hover over the options to view tooltips where available. + ツールチップが利用できる場所でそれを表示するには、オプションの上をホバーしてください。 + + + Names of the members of this competitor. + この競技者の乗員の名前です。 + + + Shows the flag of the country and the sail number of the competitor. + 競技者の国の国旗と競技者のセールナンバーを表示します。 + + + The overall rank of this competitor in the regatta. + レガッタにおけるこの競技者の全体ランクです。 + + + Points carried over from previous races. + 以前のレースから持ち越された得点です。 + + + The total of all points of a competitor in the regatta.\nSorting based on rank, not points. + レガッタにおける競技者のすべての得点の合計です。\nソートは得点ではなくランクに基づきます。 + + + Wind data + 風データ + + + GPS data + GPS データ + + + Status + ステータス + + + No data found + データが見つかりませんでした + + + Display name + 名称表示 + + + Histogram + ヒストグラム + + + Number of data points + データポイント数 + + + Angle: {0}; Total number of data-points: {1}; Coefficiant of Variation: {2}; Confidence Measure: {3} + アングル: {0}、データポイントの合計数: {1}、変動係数: {2}、信頼基準: {3} + + + Buoy zone + ブイゾーン + + + Radius (m) + 半径 (m) + + + Filter regattas by name + レガッタをフィルタリングする名称 + + + There are no regattas yet. + レガッタがまだありません。 + + + Add course area + コース領域追加 + + + Course areas + コース領域 + + + Course area + コース領域 + + + Please enter a course area. + コース領域を入力してください。 + + + Please select a course area. + コース領域を選択してください。 + + + Upper flag + 上側の信号旗 + + + Lower flag + 下側の信号旗 + + + This is a regatta leaderboard. You can only add races to this leaderboard directly in the regatta definition. + これはレガッタのリーダーボードです。レガッタ定義でこのリーダーボードに直接に追加できるのはレースのみです。 + + + Correct Score + スコア修正 + + + Correct score for competitor {0} in race {1} + レース {1} での競技者 {0} のスコアを修正 + + + Penalty/Redress + ペナルティー/救済 + + + Net points + 正味得点 + + + Time point must be after start of tracking. + 時点は追跡の開始後でなければなりません。 + + + Time point must be before end of tracking. + 時点は追跡の終了前でなければなりません。 + + + Regatta used for the tracked race + 追跡レースに使用されるレガッタ + + + TracTrac Server Update URI + TracTrac サーバ更新 URI + + + Passing instructions + 通過指示書 + + + Refresh now + 今すぐリフレッシュ + + + Stop updating + 更新停止 + + + Start updating + 更新開始 + + + Current time: + 現在時刻: + + + TracTrac username + TracTrac ユーザ名 + + + TracTrac password + TracTrac パスワード + + + Equals + 等しい + + + Not equals to + 等しくない + + + Less than + 未満 + + + Less or equals + 以下 + + + Greater than + 超過 + + + Greater or equals + 以上 + + + Contains + 包含 + + + Not Contains + 非包含 + + + Starts with + 開始: + + + Ends with + 終了: + + + Nationality + 国籍 + + + The number must be greater than zero. + この数はゼロより大きい必要 + + + Distance to line at Start + スタート時のラインまでの距離 + + + Distance to line 5s before start + スタート 5 秒前のラインまでの距離 + + + SOG at Start + スタート時の SOG + + + SOG 5s before start + スタート 5 秒前の SOG + + + SOG when Starting + スタート時の SOG + + + Distance to start line at race start time + レーススタート時刻のスタートラインまでの距離 + + + Distance to start line five seconds before the start + スタート 5 秒前のスタートラインまでの距離 + + + Speed over ground at race start time + レーススタート時刻の対地速力 + + + Speed over ground five seconds before the start + スタート 5 秒前の対地速力 + + + Speed over ground when the competitor passed the start + 競技者がスタートを通過したときの対地速力 + + + Start Tack + タック開始 + + + Tack on which the competitor started + 競技者が開始したタック + + + Port + 左舷 + + + Starboard + 右舷 + + + Port + 左舷 + + + Starboard + 右舷 + + + Please enter a number. + 番号を入力してください。 + + + Competitors filter + 競技者フィルタ + + + Flag status + 信号旗ステータス + + + Info + 情報 + + + Wind Fixes (first 3, last 3, true values) + 風フィックス (当初 3、最終 3、真値) + + + No Wind-Fixes available. + 風フィックスが利用可能でありません。 + + + Dist. Stb-Side of Line when Starting + スタート時のラインの右舷側距離 + + + Distance to starboard side of start line or to the single start mark when the competitor passed the start + スタートラインの右舷側までの距離、または競技者がスタートを通過したときの単一スタートマークまでの距離 + + + Add filter + フィルタ追加 + + + Edit filter + フィルタ編集 + + + Available filters + 利用可能フィルタ + + + Create a filter set to filter the list of competitors. + 競技者の一覧をフィルタリングするフィルタセットを登録してください。 + + + A filter with this name already exists. + この名称のフィルタはすでに登録されています。 + + + Add at least one filter criteria. + フィルタ基準を少なくとも 1 つ選択してください。 + + + Filter criteria + フィルタ基準 + + + Select a filter criteria + フィルタ基準の選択 + + + Filter name + フィルタ名 + + + operator + 演算子 + + + value + 値 + + + The nationality must be a two letter ISO code or a three letter IOC code. + 国籍は 2 文字の ISO コードまたは 3 文字の IOC コードでなければなりません。 + + + Race rank + レースランク + + + Waypoints + 変針点 + + + Disable filter + フィルタ無効化 + + + Enable filter + フィルタ有効化 + + + Race Status + レースステータス + + + Loading (on a slow internet connection this can take some minutes)... + ロードしています (スピードの遅いインターネット接続では時間がかかる場合があります)... + + + Races with state HIDDEN + 非表示ステータスのレース + + + Invalid date format. Use: {0} + 日付書式が無効です。使用: {0} + + + Selected competitors + 選択された競技者 + + + Filter nothing + フィルタなし + + + Race Top {0} + レース上位 {0} + + + Total Top {0} + 合計上位 {0} + + + Simulator + シミュレータ + + + Optionsbar + オプションバー + + + Predict + 予測 + + + Setup + 設定 + + + Strength (in kn) + 風力 (kn) + + + Oscillation + 振動 + + + Oscillating + 振動中 + + + Amplitude(in Grad) + 振幅 (勾配) + + + Frequency(in 1/h) + 周期 (1/h) + + + Noise + 雑音 + + + Sailing + 帆走 + + + Optimality + 最適性 + + + Maximum Turns + 最大ターン + + + Race Direction + レース方向 + + + Upwind + アップウィンド + + + Downwind + ダウンウィンド + + + Strategies + 戦略 + + + Omniscient + 全知 + + + Opportunist + オポチュニスト + + + Mixed + 混合 + + + Maps + マップ + + + Start-End + 開始/終了 + + + Course + コース + + + Summary + サマリー + + + Replay + リプレイ + + + Display + 表示 + + + Polar diagram + ポーラーダイアグラム + + + Show + 表示 + + + hide + 非表示 + + + Left wind + レフトウィンド + + + Right wind + ライトウィンド + + + strategyOption1 + strategyOption1 + + + strategyOption2 + strategyOption2 + + + strategyOption3 + strategyOption3 + + + Leg + レグ + + + Race + レース + + + Competitor + 競技者 + + + Keep Bearings + 方位を維持 + + + What-If Course + What-If コース + + + Edit Regatta... + レガッタ編集... + + + Please select a sailing event... + 帆走イベントを選択してください... + + + Please select a course area.. + コース領域を選択してください.. + + + Area overview + 領域概要 + + + Course design overview + コースデザイン概要 + + + Regatta overview + レガッタ概要 + + + Additional settings + 追加設定 + + + Show following course areas + 以下のコース領域を表示 + + + Show following regattas + 以下のレガッタを表示 + + + Show only races of same day + 同一のレースのみ表示 + + + Show currently running races + 現在競技中のレースのみ表示 + + + Series defines result discarding rules + シリーズにより結果取消ルールを定義 + + + Add series + シリーズ追加 + + + Regatta defines result discarding rules + レガッタにより結果取消ルールを定義 + + + Series starts with zero score + セロスコアでシリーズ開始 + + + Master Data Import + マスタデータインポート + + + Success\nCreated {0} leaderboard group(s) and {1} leaderboard(s) and {2} event(s) and {3} regatta(s). + 成功\n{0} リーダーボードグループ、{1} リーダーボード、{2} イベント、{3} レガッタを登録しました。 + + + Had to overwrite the following regattas, you will need to retrack their races: + 以下のレガッタを上書きするには、以下のレースを再追跡する必要があります: + + + Regatta Overview Configuration + レガッタ概要設定 + + + Starts with non-discardable carry-forward + 取消不可の持ち越しがあるスタート + + + Connect to replication master server + 複製マスタサーバに接続 + + + Connect + 接続 + + + Connect to Master + マスタに接続 + + + Messaging Hostname + メッセージングホスト名 + + + Hostname where the messaging queue is running on. + メッセージキューを実行中のホスト名です。 + + + Hostname where the replication master is running on. + 複製マスタを実行中のホスト名です。 + + + The name of the exchange queue. It must be the same as configured in master. + エクスチェンジキューの名称です。マスタで設定されたものと同じでなければなりません。 + + + The port of the messaging exchange queue. This is used for all data after initial load. + メッセージングエクスチェンジキューのポートです。これは初期ロード後の全データに使用されます。 + + + Servlet port number that is used to connect to register this replica and receive initial load. + この複製登録し、初期ロードを受け取るための接続に使用されるサーブレットポート番号です。 + + + Replicas registered (this server is master): + 登録された複製 (このサーバがマスタ): + + + Connections to a master (this server is a replica): + マスタへの接続 (このサーバが複製): + + + No connections to a master server configured. Click the Connect to Master button to configure one. + マスタサーバへの接続が設定されていません。マスタへの接続ボタンをクリックして設定してください。 + + + No connections from a replica found. You can connect replicas by using their AdminConsole. + 複製からの接続が見つかりませんでした。複製の接続はその管理コンソールを使用して行うことができます。 + + + Stop Replication + 複製の停止 + + + As link + リンクとして + + + Pending + 保留 + + + Running + 実行中 + + + Finishing + フィニッシュ中 + + + Finished + フィニッシュ済 + + + Scheduled + スケジュール済 + + + Start phase + スタートフェーズ + + + General recall + リコール生成 + + + Postponed + 延期 + + + Abandoned + 中止 + + + Drop connection to all replicas (Dangerous!) + すべての複製への接続をドロップ (危険です) + + + ATTENTION: This server is configured to be a replica. Do NOT change anything or this will lead to strange effects! + 注意: このサーバは複製となるよう設定されています。何も変更しないでください。そうしないと、予想しない動作がもたらされます。 + + + Drop connection + 接続のドロップ + + + Time Control + 時間管理 + + + The filter will show all competitors matching the criterias: + このフィルタは基準に一致するすべての競技者を表示します: + + + Protest time + 抗議時間 + + + ends + 終了予定 + + + ended + 終了 + + + Flags + 信号旗 + + + Last update + 最終更新 + + + No more racing today + 本日はもうレースはありません + + + Further signals ashore + 陸上の大会本部にさらに信号旗を掲揚 + + + Early starters + フライング艇 + + + Showing details for race {0} + レース {0} の詳細表示 + + + Start at + スタート時刻 + + + Finish at + フィニッシュ時刻 + + + Protest ends at + 抗議最終時刻 + + + Unknown + 不明 + + + Race log + レースログ + + + Regatta log + レガッタログ + + + Refresh race log + レースログのリフレッシュ + + + Race log successfully reloaded from database + レースログがデータベースから正常にリロード済 + + + Reload all race logs + 全レースログのリロード + + + Export + エクスポート + + + Sideline + サイドライン + + + Flag displayed - eight minutes till start - the startprocedure is a gate start + 信号旗の掲揚 - スタートまで 8 分 - スタート手順はゲートスタート + + + Flag displayed - five minutes till start + 信号旗の掲揚 - スタートまで 5 分 + + + Start mode flag - four minutes till start + スタート前の信号旗 - スタートまで 4 分 + + + Start mode flag - disqualification for early starters - four minutes till start + スタート前の信号旗 - フライング艇の失格 - スタートまで 4 分 + + + Start mode flag - penalties for early starters - four minutes till start + スタート前の信号旗 - フライング艇のペナルティー - スタートまで 4 分 + + + Start mode flag - one minute till start + スタート前の信号旗 - スタートまで 1 分 + + + Start mode flag - disqualification for early starters - one minute till start + スタート前の信号旗 - フライング艇の失格 - スタートまで 1 分 + + + Start mode flag - start with penalties for early starters - one minute till start + スタート前の信号旗 - フライング艇のペナルティーありのスタート - スタートまで 1 分 + + + Flag removed - race starting signal + 信号旗の降下 - レースのスタート信号 + + + Individual recall - had early starters + 個別のリコール - フライング艇あり + + + General recall - had too many early starters - start procedure will be repeated + ゼネラルリコール - フライング艇が多すぎる - スタート手順のやり直し + + + Flag displayed - first competitor passed the finishing line + 信号旗の掲揚 - 先頭の競技者がフィニッシュラインを通過 + + + Flag removed - last competitor passed the finishing line or the finish time limit is passed + 信号旗の降下 - 最終の競技者がフィニッシュラインを通過、またはフィニッシュ時間制限を超過 + + + Flag removed - gate is closed + 信号旗の降下 - ゲート閉鎖 + + + Flag displayed - start postponed - no more racing today + 信号旗の掲揚 - スタート延期 - 本日のレースは終了 + + + Flag displayed - start postponed - further signals ashore + 信号旗の掲揚 - スタート延期 - 陸上の大会本部にさらに信号旗を掲揚 + + + Flag displayed - start postponed + 信号旗の掲揚 - スタート延期 + + + Flag removed - start procedure starts in one minute + 信号旗の降下 - 1 分後にスタート手順が開始 + + + Flag displayed - race abandoned - no more racing today + 信号旗の掲揚 - レース中止 - 本日のレースは終了 + + + Flag displayed - race abandoned - further signals ashore + 信号旗の掲揚 - レース中止 - 陸上の大会本部にさらに信号旗を掲揚 + + + Flag displayed - race abandoned + 信号旗の掲揚 - レース中止 + + + Protest time started + 抗議時間開始 + + + Protest time ended + 抗議時間終了 + + + Three minutes till start + スタートまで 3 分 + + + Two minutes till start + スタートまで 2 分 + + + One minute till start + スタートまで 1 分 + + + Race starting signal + レース開始信号 + + + Data from before/after race + レース前/後からのデータ + + + All fixes (including outliers) + 全フィックス (異常値を含む) + + + Remote host: + リモートホスト: + + + Fetch Leaderboard Group List + リーダーボードグループ一覧の取込 + + + Not a valid URL for fetching leaderboardgroups: {0} + リーダーボードグループ取込に有効な URL でありません: {0} + + + GET leaderboardgroups request failed for host {0} with error {1} + リーダーボードグループ取得の要求がホスト {0} に対してエラー {1} で失敗しました + + + No data was returned by the server + データがサーバから戻されませんでした + + + GET leaderboardgroups request failed with Server error. Please see server logs for more information. + リーダーボード取得の要求がサーバエラーで失敗しました。詳細については、サーバログを参照してください。 + + + Leaderboard Groups: + リーダーボードグループ: + + + Override existing data if names and ids match + 名称と ID が一致する場合に既存データを上書き + + + Import selected leaderboard groups + 選択したリーダーボードグループのインポート + + + You have to select at least one leaderboard group to import + インポートするリーダーボードグループを少なくとも 1 つ選択する必要があります + + + Deselect all + 全選択解除 + + + Polar Diagram + ポーラーダイアグラム + + + GPS Lag + GPS ラグ + + + Ratio between time since last received GPS position and average sampling interval + 最後に GPS 位置を受信してからの時間と平均サンプリング間隔の間の比率 + + + Tracking quality + 追跡品質 + + + Leg Type + レグタイプ + + + Sail Number + セールナンバー + + + Series leaderboard + シリーズリーダーボード + + + Regatta leaderboards + レガッタリーダーボード + + + Clear Selection + 選択のクリア + + + Running + 実行中 + + + Run + 実行 + + + Done + 実行済 + + + Last finished + 最終フィニッシュ + + + Run + 実行 + + + times + 回 + + + Data amount + データ量 + + + ∅ Cleaned Server Time + ∅ サーバ時間のクリア + + + ∅ Cleaned Overall Time + ∅ 全体時間のクリア + + + Data Mining Benchmark Results + データマイニングベンチマーク結果 + + + Server Time + サーバ時間 + + + Confidence values need to be between 0 and 1. + 信頼値は 0 から 1 までの間である必要があります。 + + + Minimum data size values cannot be negative. + 最小データサイズ値はマイナスにはできません。 + + + The number of histogram columns needs to be more than one. + ヒストグラム列の数は複数である必要があります。 + + + The outlier radius needs to be a positive number. + 半径の異常値はプラスの数である必要があります。 + + + The outlier neighborhood percentage needs to be positive and max. 1. + 近傍率はプラスで最大 1 である必要があります。 + + + Parameter fields cannot be empty. + パラメータ項目は空白にはできません。 + + + Error occured while trying to add\na new series to the chart. + 新規シリーズをチャートに追加\nしようとしてエラーが発生しました。 + + + Generating Polar Sheet... + ポーラーシートを生成しています... + + + Data Size + データサイズ + + + Generation finished! + 生成が終了しました + + + Generating + 生成中 + + + Remove all sheets + 全シート削除 + + + Remove all + 全削除 + + + Select sheet + シート選択 + + + Cleaned Server Time + サーバ時間のクリア + + + Overall Time + 全体時間 + + + Cleaned Overall Time + 全体時間のクリア + + + Data Mining Result + データマイニング結果 + + + Group by + グループキー + + + Calculate statistic + 統計の計算 + + + Worked through {0} data entries in {1} seconds + {0} データエントリを {1} 秒で処理済 + + + No query selected + クエリ未選択 + + + Run automatically + 自動的に実行 + + + Upload + アップロード + + + Import Wind from Expedition + Expedition から風をインポート + + + Boat Id: + 艇 ID: + + + Do you really want to import wind into the selected {0} races?\nOnly races with applicable time window will be affected anyway. + 選択した {0} レースに風をインポートしますか。\n影響を受けるのは適用可能な時間ウィンドウにあるレースのみです。 + + + Do you really want to import wind into all races?\nOnly races with applicable time window will be affected anyway. + すべてのレースに風をインポートしますか。\n影響を受けるのは適用可能な時間ウィンドウにあるレースのみです。 + + + Imported wind from: {0} to: {1}. {2} races affected. + {0} から {1} に風をインポートしました。{2} レースが影響を受けます。 + + + Race: {0} ({1}) - {2} fixes from: {3} to: {4} + レース: {0} ({1}) - {2} フィックス 開始: {3} 終了: {4} + + + Error: {0} + エラー: {0} + + + Toggles the visibility of the wind speed for all data sources + すべてのデータソースに対する風速の表示を切替 + + + Toggles the visibility of the wind direction for all data sources + すべてのデータソースに対する風向の表示を切替 + + + Combines all other wind sources to one data set + 他のすべての風源を 1 つのデータセットに結合 + + + A value based on the course layout (under the assumption, that the race starts with an upwind leg) + コースレイアウトに基づく値 (アップウィンドレグでレースがスタートするという想定) + + + Calculated data from the competitor tracks + 競技者追跡から計算されたデータ + + + Data captured from mobile measuring systems + モバイル計測システムから取得されたデータ + + + Manually set data values + 手動で設定されたデータ値 + + + Data manually set from the race officers + レースオフィサーにより手動で設定されたデータ + + + Sheet name + シート名 + + + You can name your polar sheet using the text field.\nA name will be generated using the boat class\nif no name is provided. + テキスト項目を使用してポーラーシートに名称を付ける\nことができます。名称が与えられていない場合は\n艇種を使用して生成されます。 + + + Time Downwind + ダウンウィンド時間 + + + Time Reaching + リーチング時間 + + + Time Upwind + アップウィンド時間 + + + Total time traveled in this race, starting to measure when competitor passes start waypoint\n(not when the race starts) + 競技者がスタート変針店を通過したときに計測を開始した、このレースで移動した合計時間\n(レースのスタート時ではなく) + + + Total time traveled downwind in this race + このレースでダウンウィンドで移動した合計時間 + + + Total time traveled reaching in this race + このレースでリーチングで移動した合計時間 + + + Total time traveled upwind in this race + このレースでアップウィンドで移動した合計時間 + + + No statistic to calculate is selected + 計算する統計が選択されていません + + + The grouper script is empty + 分類スクリプトが空です + + + No dimension to group by is selected + グループ別にする次元が選択されていません + + + No grouper type is selected + 分類タイプが選択されていません + + + No data retriever is selected + データリトリーバが選択されていません + + + No query possible, because + クエリが可能でありません。理由: + + + Data Mining + データマイニング + + + An error occured running the query + クエリ実行でエラーが発生しました + + + Hide toolbar + ツールバー非表示 + + + Show series leaderboards + シリーズリーダーボード表示 + + + Show overall leaderboard + 全体リーダーボード表示 + + + length: {0}m\nangle to wind: {1}°\n{2} side has {3}m advantage + 長さ: {0} m\n風とのアングル: {1}°\n{2} 側に {3} m のアドバンテージ + + + Do you really want to remove the events? + イベントを削除しますか + + + Do you really want to remove event {0}? + イベント {0} を削除しますか + + + Line + ライン + + + Offset + オフセット + + + Do you really want to remove the selected competitors? + 選択した競技者を削除しますか + + + Do you really want to remove competitor {0}? + 競技者 {0} を削除しますか + + + Filter competitors by name, boat class, sail number or ID + 競技者をフィルタリングする名称、艇種、セールナンバー、または ID + + + Edit Competitor + 競技者編集 + + + Please enter a sail number + セールナンバーを入力してください + + + Successfully allowed reload of competitor(s) {0}. + 競技者 {0} のリロードが許可されました。 + + + ID + ID + + + Allow reload + リロード許可 + + + Compress + 圧縮 + + + Use only if exporting server instance is at least\nrunning with commit 0fbf6071dea125bec4a56dee55d61c99def4a62e. + エクスポートサーバインスタンスが少なくとも\nコミット 0fbf6071dea125bec4a56dee55d61c99def4a62e で実行中の場合にのみ使用してください。 + + + Query Runner + クエリランナー + + + Rerun query after refresh + リフレッシュ後にクエリを再実行 + + + Refresh interval mustn''t be empty + リフレッシュ間隔は空であってはなりません + + + Selection Tables + 選択テーブル + + + Add Configuration + 設定追加 + + + There are no configurations. + 設定がありません + + + Error retrieving configuration data from server. + サーバからの設定データの取得でエラーが発生しました。 + + + Allowed course areas + 許容コース領域 + + + Minimum rounds for Course + コースの最小周回数 + + + Maximum rounds for Course + コースの最大周回数 + + + Results mail recipient + 結果メール受信者 + + + Device Configuration + デバイス設定 + + + None + なし + + + Course designer + コースデザイナー + + + Racing procedure + 競技手順 + + + Course names + コース名 + + + Generate + 生成 + + + Don''t overwrite + 上書きしない + + + Set regatta configuration + レガッタ設定実行 + + + Regatta configuration + レガッタ設定 + + + Activate individual recall + 個別リコール有効化 + + + Activate result entry + 結果エントリ有効化 + + + RRS26 start + RRS26 開始 + + + Activate configuration + 設定有効化 + + + Class flag + クラス旗 + + + Sets the class flag used for {0}. Choose ''Don''t Overwrite'' to activate the procedure''s default flag. + {0} に使用するクラス旗を選定します。手順にある初期値の旗を有効化するには、''上書きしない'' を選択してください。 + + + When activated the Race Officer is able to call for individual recall. + 有効化されると、レースオフィサーが個別リコールをコールすることができます。 + + + When activated the Race Officer is able to enter results during and after the finishing phase. + 有効化されると、レースオフィサーがフィニッシュフェーズ中およびその後に結果を入力することができます。 + + + Startmode flags + スタート前の信号旗 + + + Sets the available start mode flags. Choose none to enable the procedure''s default flags. + 利用可能なスタート前の信号旗を設定します。手順のデフォルト信号旗を有効化するには何も選択しないでください。 + + + Gate Start + ゲートスタート + + + Activate Pathfinder + 先導艇の有効化 + + + When activated the Race Officer is able to set the Pathfinder boat. + 有効化されると、レースオフィサーが先導艇を設定することができます。 + + + ESS Start + ESS スタート + + + Basic Start + 基本スタート + + + League Start + リーグスタート + + + Configured + 設定済 + + + Activate regatta configuration + レガッタ設定の有効化 + + + Enabling this option will provide a base regatta configuration for all options unconfigured in the regatta-specific configuration. + このオプションを有効化すると、レガッタ固有の設定で未設定のすべてのオプションに対して基本レガッタ設定が与えられます。 + + + Set start time + スタート時刻の設定 + + + Current pass + 現在のパス + + + Select a new start time for the race. If you want to set the start time to a timepoint in the past then you have to enable the checkbox to update the pass count. Please be aware of the fact that changing the pass count will have you loose all information from the previous pass (like flag events). + レースの新規スタート時刻を選択してください。スタート時刻を過去の時点に設定する場合は、パスカウントを更新するチェックボックスを有効化する必要があります。パスカウントを変更すると (信号旗イベントなどの) 以前のパスからの情報がすべて解放されるということに注意してください。 + + + Set the start and end of tracking for the race. + レース追跡の開始および終了を設定してください。 + + + Activate additional golf down time + 追加ゴルフダウンタイムの有効化 + + + Activate additional golf down time + 追加ゴルフダウンタイムの有効化 + + + Invalid color: {0} + 無効な色: {0} + + + Igtimi Accounts + Igtimi アカウント + + + Error fetching all Igtimi accounts: {0} + 全 Igtimi アカウントの取込でエラー発生: {0} + + + Add Igtimi Account + Igtimi アカウント追加 + + + Error authorizing access to Igtimi user {0}: {1} + Igtimi ユーザ {0} へのアクセス権限付与でエラー発生: {1} + + + Successfully authorized access to Igtimi user {0} + Igtimi ユーザ {0} へのアクセス権限が付与されました + + + Could not authorize user {0}; invalid user name or password + ユーザ {0} への権限付与ができませんでした (ユーザ名またはパスワードが無効) + + + Error getting Igtimi authorization URL: {0} + Igtimi 権限 URL 取得でエラー発生: {0} + + + Add Igtimi User + Igtimi ユーザ追加 + + + Do you really want to remove the leaderboards? + リーダーボードを削除しますか + + + Error trying to remove Igtimi account for user {0} + ユーザ {0} の Igtimi アカウントを削除しようとしてエラーが発生 + + + Successfully removed Igtimi account for user {0} + ユーザ {0} の Igtimi アカウントが削除されました + + + e-Mail address must not be empty + 電子メールアドレスは空であってはなりません + + + e-mail address + 電子メールアドレス + + + Password + パスワード + + + Search tag + 検索タグ + + + Do you really want to remove the leaderboard: ''{0}''? + リーダーボード ''{0}'' を削除しますか。 + + + This is a meta leaderboard. It can''t be changed here. + これはメタリーダーボードです。ここで変更することはできません。 + + + Failed to set new start time. This can happen when the same start time is already present in the race log or someone has added a start time that is newer (in terms of a pass id and logical time) than the one that you just added. + 新規スタート時刻の設定に失敗しました。これは、同じスタート時刻がすでにレースログに存在しているか、いま追加したものよりも (パス ID および論理時間の点で) 新しいスタート時間を誰かが追加している場合に発生することがあります。 + + + Do you really want to remove Igtimi account granting access to data of user {0}? + ユーザ {0} のデータへのアクセスを付与する Igtimi アカウントを削除しますか。 + + + Import Wind from Igtimi + Igtimi から風をインポート + + + See the ''Igtimi'' tab for account settings determining which data from Igtimi will be considered. + ''Igtimi'' タブで、Igtimi からのどのデータを考慮するかを指定するアカウント設定を参照してください。 + + + Import Wind from Igtimi + Igtimi から風をインポート + + + Error importing wind data from Igtimi: {0} + Igtimi からの風データのインポートでエラー発生: {0} + + + The following number of fixes were imported for the listed races: {0} + 一覧されたレースに対して次の数のフィックスがインポートされました: {0} + + + {0} to start + スタートまで {0} + + + {0} since start + スタートから {0} + + + Distance to start line + スタートラインまでの距離 + + + The angle between the competitors direction and the wind + 競技者の方向と風の間のアングル + + + Beat Angle + ビートアングル + + + You can also view the overall diagram for the available boat classes. + 利用可能な艇種の全体図も表示することができます。 + + + Show Diagram + ダイアグラム表示 + + + Run the query automatically, after you changed e.g. the statistic or the grouping. + 統計やグルーピングなどを変更した後にクエリを自動的に実行します。 + + + Reruns the query after the tables have been refreshed. + テーブルがリフレッシュされた後にクエリを再実行します。 + + + Query Definition Provider + クエリ定義プロバイダ + + + Statistic Provider + 統計プロバイダ + + + Calculate the + 計算: + + + Grouping Provider + グルーピングプロバイダ + + + News and Release History + 新規およびリリース履歴 + + + Split fleets scored contiguously + 連続的に得点するフリートの分割 + + + Add RaceLog Tracker + RaceLog トラッカー追加 + + + Add RaceLog Trackers + RaceLog トラッカー追加 + + + Denote for RaceLog tracking + RaceLog 追跡の表示 + + + Start RaceLog Tracking + RaceLog 追跡の開始 + + + Filter by Leaderboard + リーダーボードによるフィルタリング + + + Remove Leaderboard filter + リーダーボードフィルタ削除 + + + Competitor Registrations + 競技者登録 + + + Tracker Status + トラッカーステータス + + + Course Status + コースステータス + + + Can only be edited before starting tracking + 追跡開始前にのみ編集可能 + + + Define Course Layout + コースレイアウト定義 + + + Map Devices to Competitors and Marks + デバイスを競技者およびマークにマッピング + + + Edit {0} + {0} を編集 + + + Add {0} + {0} を追加 + + + Remove {0} + {0} を削除 + + + Please enter a {0} + {0} を入力してください + + + Ping {0} position + ping {0} 位置 + + + Please enter a valid value for {0} ({1}) + {0} の有効な値を入力してください ({1}) + + + Copy course and competitors to other races + コースと競技者を他のレースにコピー + + + Set tracking times + 追跡時間の設定 + + + Now select the race column and fleet to copy the course to. + ここで、コースのコピー先のレース列およびフリートを選択してください。 + + + Select a single row in the table, to which to copy the course. + コースのコピー先の、テーブル内の単一行を選択してください。 + + + Device ID + デバイス ID + + + Device Type + デバイスタイプ + + + Mapped to Type + マッピング先のタイプ + + + Mapped to + マッピング先 + + + Device Mappings + デバイスマッピング + + + At most one end of the mapping''s time range may be left open (left unset) + 多くとも、マッピングの時間範囲の終了の 1 つは未指定のままにすることができます (左を未設定) + + + Please select an item to map the device to + デバイスのマッピング先のアイテムを選択してください + + + Device + デバイス + + + Time Range + 時間範囲 + + + Close open end of Time Range + 時間範囲の未指定側を閉じる + + + Set Timepoint that closes open time range + 未指定の時間範囲を閉じる時点の設定 + + + The start of the time range must lie before its end + 時間範囲の開始はその終了より前でなければなりません + + + Overall Progress + 全体進行 + + + Event Overview + イベント概要 + + + Start + スタート + + + Show added scores for each entry in leaderboard + リーダーボードにあるエントリごとに追加されたスコアの表示 + + + ratio + 率 + + + Import Wind + 風のインポート + + + Unchecking this option will drastically lower the amount of data transfered. + このオプションを指定解除すると、転送されるデータ量が著しく小さくなります。 + + + Import Device Configurations + デバイス設定のインポート + + + Checking this will import ALL device configurations on the remote server. + これを指定すると、リモートサーバ上のすべてのデバイス設定がインポートされます。 + + + Author name + 作成者名 + + + Author priority + 作成者優先度 + + + Remove Denotation + 表示の削除 + + + At least one of the selected races ({0}) is missing competitor registrations. Proceed anyway? + 選択したレース ({0}) のうち少なくとも 1 つで競技者登録が不足しています。続行しますか。 + + + Please select a control point + コントロールポイントを選択してください + + + 2-mark-controlpoint + 2 マークコントロールポイント + + + Registered Competitors + 登録済競技者 + + + Competitor Pool + 競技者プール + + + Select at least one + 少なくとも 1 つ選択 + + + Select the Races + レースの選択 + + + Import Fixes + フィックスのインポート + + + No remote server instances registered yet. + リモートサーバインスタンスがまだ登録されていません。 + + + Registered remote server instances + 登録済リモートサーバインスタンス + + + Filter Events by name: + イベントをフィルタリングする名称 + + + Leaderboards (except those of selected group) + リーダーボード (選択したグループのものを除く) + + + Available Leaderboard Groups + 利用可能リーダーボードグループ + + + Leaderboard Groups Assigned to Event + イベントに割当済のリーダーボードグループ + + + Add races + レース追加 + + + with name prefix + - 名称接頭辞: + + + Please select a series first. + 最初にシリーズを選択してください + + + in {0,number} days + {0,number} 日間 + + + tomorrow + 明日 + + + today + 今日 + + + Share by mail + メールによるシェア + + + Share on Twitter + Twitter でシェア + + + Share on Facebook + Facebook でシェア + + + {0} in {1} on {2} + {1} の {0} を {2} で + String eventSharingShortText(String eventName, String venue, String dateString); + + + I''d like to share {0} in {1} on {2}. Details on: \n{3} + {1} の {0} を {2} でシェアします。詳細: \n{3} + String eventSharingLongText(String eventName, String venue, String dateString, String url); + + + I''d like to share series {0}. Details: {1} + シリーズ {0} をシェアします。詳細: {1} + String seriesSharingLongText(String seriesName, String url); + + + {0} + {0} + String seriesSharingShortText(String seriesName); + + + Competitors Analytics + 競技者分析 + + + There are no photos or videos available yet. + 写真または動画がまだ利用可能でありません。 + + + Media + メディア + + + Photos + 写真 + + + Videos + 動画 + + + Image + 画像 + + + Images + 画像 + + + Video + 動画 + + + Flag image URL (image renders with 18x12px) + 旗画像 URL (画像を 18x12px でレンダリング) + + + Image URLs + 画像 URL + + + Video URLs + 動画 URL + + + Sponsor Image URLs + スポンサー画像 URL + + + The current selection isn''t valid. + 現在の選択は有効でありません。 + + + Advance Pass ID + 予選通過 ID + + + Please select a scoring result... + 得点結果を選択してください... + + + Time zone + タイムゾーン + + + Official Website URL + 公式 Web サイト URL + + + Sailors Info Website URL + セーラー情報 Web サイト URL + + + Event Overview URL + イベント概要 URL + + + Logo Image URL + ロゴ画像 URL + + + Show Mappings for pinged Marks + ping したマークのマッピング表示 + + + Regatta name must not contain slashes + レガッタ名にスラッシュが含まれていてはなりません + + + Number of races scored + スコアリングされたレース数 + + + Error fetching wind streamlet data: {0} + 風細流データの取込でエラー発生: {0} + + + Show wind streamlets overlay + 風細流オーバーレイ表示 + + + Color streamlets according to wind speed + 風速に従った色細流 + + + Show simulation overlay + シミュレーションオーバーレイ表示 + + + Time Comparison:\nRace is the leg duration measured from GPS, Omniscient is the course if one would know everything about the wind, Opportunist are courses if one decides to take a turn measuring the distance to the next mark, 1-Turners are the courses if one knows everything about the wind and just takes one turn + 時間比較:\nレースは GPS から計測されたレグ持続時間、全知は風についてすべてわかっている場合のコース、オポチュニストは次マークまでの距離を計測するためにターンする場合のコース、ワンターナーは風についてすべてわかっていて 1 ターンするだけの場合のコース + + + Races scored + スコアが記録されたレース + + + Number of races the competitor has completed or gotten a score for in this leaderboard. + このリーダーボードにおいて競技者が完了したか、得点を獲得したレースの数 + + + Average number of operations per message + メッセージごとの操作の平均数 + + + Show uncorrected net points + 未修正正味得点表示 + + + Set start time received + 設定済スタート時刻を受信 + + + This sets the startTimeReceived of the selected TrackedRace that isn''t persistent. This means that the new value will be forgotten after the server has been restarted. + これにより、永続的でない選択した TrackedRace の startTimeReceived が設定されます。したがって、サーバが再起動された後は新規の値は忘れられます。 + + + Last score correction time + 最終得点修正時刻 + + + Last score correction comment + 最終得点修正コメント + + + Set time to ''now'' + 時刻を ''現在'' に設定 + + + Use start time inference from start mark passings + スタートマーク通過からの推定スタート時刻を使用 + + + Number of queue messages sent + 送信済キューメッセージ数 + + + Total Size + 合計サイズ + + + Average size per message in bytes + メッセージごとの平均サイズ (バイト) + + + Total number of operations + 操作の合計数 + + + Current speed over ground in knots while leg is still running; average speed over ground when leg is completed + レグをまだ競技中の現対地速力 (kt)、レグ完了時の平均対地速力 + + + SOG (∅ at end) + SOG (終了時 ∅) + + + Connectors + コネクタ + + + Video + 動画 + + + Competitor Search Filter + 競技者検索フィルタ + + + Search + 検索 + + + Go to event overview + イベント概要にジャンプ + + + Go to the overview and see all Races in one Leaderboard + 概要にジャンプし、1 つのリーダーボードで全レースを参照 + + + No such event + そのようなイベントなし + + + The leaderboard {0} is not contained in leaderboard group {1}. + リーダーボード {0} はリーダーボードグループ {1} に含まれていません。 + + + The leaderboard group {0} is not contained in event {1}. + リーダーボードグループ {0} はイベント {1} に含まれていません。 + + + Race is Live + レースがライブ中 + + + Back to Live + ライブに戻る + + + Wind Chart + 風チャート + + + No Videos + 動画なし + + + Show Video + 動画表示 + + + Click to show video: {0} + クリックして動画を表示: {0} + + + Hide Video + 動画非表示 + + + Click to hide video + クリックして動画を非表示 + + + {0} Videos + {0} 動画 + + + Click to select video + クリックして動画を選択 + + + Manage Media + メディア管理 + + + Configure audio and video clips, synchronize times + 音声および動画クリックを設定して、時間を同期 + + + Show all + 全表示 + + + Visibility + 可視性 + + + Enter carried points for competitor {0} + 競技者 {0} の持ち越し得点を入力 + + + Advanced + 予選通過 + + + based on + 基準 + + + Retrieve with + 検索キー + + + Mapping Details + マッピング詳細 + + + If you are using the Tracking App, you can add the device mapping also by selecting a competitor/mark, setting the from and to times, and then scanning this QRCode. + 追跡アプリを使用している場合は、競技者/マークを選択し、開始時刻と終了時刻を設定してから、この QRCode をスキャンすることによってデバイスマッピングも追加することができます。 + + + QRCode + QRCode + + + Enter course area name... + コース領域名を入力... + + + Enter course name... + コース名を入力... + + + Enter image URL... + 画像 URL を入力... + + + Enter video URL... + 動画 URL を入力... + + + Enter sponsor image URL... + スポンサー画像 URL を入力... + + + Enter race name... + レース名を入力... + + + An error occurred while attempting to contact the server. Please check your network connection and try again. + サーバにアクセスしようとしてエラーが発生しました。ネットワーク接続を確認して再試行してください。 + + + Remote Procedure Call + リモートプロシージャコール + + + Server replies + サーバ応答 + + + Error communicating with server + サーバとの通信でエラー発生 + + + User Management + ユーザ管理 + + + Regatta Structure Import + レガッタ構成のインポート + + + filtered by + フィルタキー + + + Edit Series + シリーズ編集 + + + Please select at least one regatta + レガッタを少なくとも 1 つ選択してください + + + Please select an event + イベントを選択してください + + + Error trying to get events: {0} + イベント {0} を取得しようとしてエラーが発生 + + + Show x-y-diagram + x-y ダイアグラム表示 + + + x-y-Diagram + x-y ダイアグラム + + + Boat Speed + 艇速 + + + Confidence + 信頼 + + + Create default settings for all regattas... + 全レガッタの初期設定を登録... + + + No overall event selected... + 全体イベント未選択... + + + Error trying to create new event {0}: {1} + 新規イベント {0} を登録しようとしてエラーが発生: {1} + + + Create New Event + 新規イベント登録 + + + No fleets defined. + フリートが定義されていません。 + + + Successfully created regattas + レガッタが登録されました + + + Error trying to register races {0} for tracking: {1}. Check live/stored URI syntax. + 追跡するレース {0} を登録しようとしてエラーが発生: {1}。ライブ/格納 URI 構文をチェックしてください。 + + + Error determining availability of polar / VPP data for race {0}: {1} + レース {0} のポーラー/VPP データの利用可能性の決定でエラー発生: {1} + + + Error + エラー + + + File Storage + ファイルストレージ + + + Active + 有効 + + + Score in points. Winner of an act scores 10 points, 2nd - 9 points, .... Any tie in the overall Extreme Sailing Series is broken in favour of the competitor with the most Act wins. If that does not break the tie, the result in the last Act will be used. + 得点数によるスコアです。アクトの勝者が 10 点、2 位が 9 点などと得点します。Extreme Sailing シリーズ全体で同点となった場合は、より多くのアクトで勝者となった競技者を上位とします。それでも同じ場合には、最終アクトの結果を用います。 + + + Start delay + スタート遅延 + + + Time between the start of the race and the time the competitor passed the start waypoint. Negative time suggests OCS. + レースのスタートと、競技者がスタート変針点を通過した時刻の間の時間です。マイナスの時間は OCS を意味します。 + + + Use internal mark passing algorithm + 内部マーク通過アルゴリズムの使用 + + + Edit Mark Passings + マーク通過編集 + + + Edit Mark Positions + マーク位置編集 + + + Remove fixed mark passing + 固定マーク通過を削除 + + + Set time as mark passing + マーク通過の時刻を設定 + + + Suppress mark passing starting with current + 現在のもので開始したマーク通過を非表示 + + + Remove suppressed mark passing + 非表示のマーク通過を削除 + + + Error removing fixed mark passing: {0} + 固定マーク通過の削除でエラー発生: {0} + + + Error setting fixed mark passing: {0} + 固定マーク通過の設定でエラー発生: {0} + + + Error suppressing mark passings: {0} + マーク通過の非表示でエラー発生: {0} + + + Error removing suppressed mark passings: {0} + 非表示のマーク通過の削除でエラー発生: {0} + + + Error trying to obtain mark passings: {0} + マーク通過を取得しようとしてエラーが発生: {0} + + + Error trying to obtain race log mark passing data: {0} + レースログのマーク通過データを取得しようとしてエラーが発生: {0} + + + E-Mail + 電子メール + + + Please enter an E-Mail. + 電子メールを入力してください + + + Invite competitors + 競技者の招待 + + + Invite selected competitors + 選択した競技者の招待 + + + Not all competitors provide an E-Mail + 一部の競技者が電子メールを未提供 + + + Sending Mails failed + メール送信に失敗 + + + Successfully sent E-Mails + 電子メールを正常に送信済 + + + Select Event for Invitation + 招待のイベント選択 + + + Image URL + 画像 URL + + + Upload + アップロード + + + Remove operation: {0} {1} + 操作の削除: {0} {1} + + + Upload successful + 正常にアップロード済 + + + File upload: {0} {1}.\nIn case of error please check that your File Storage service is properly configured. + ファイルアップロード: {0} {1}\nエラー発生の場合は、ファイルストレージサービスが適切に設定されていることを確認してください。 + + + Remove uploaded file + アップロード済ファイルの削除 + + + Sail Id + 帆走 ID + + + Competitor Full Name + 競技者氏名 + + + Loading dimension values + 次元値ロード中 + + + Running Query + クエリ実行中 + + + Invite Buoy Tenders + ブイ入札の招待 + + + or multiple E-Mails separated by comma + または複数の電子メールをカンマで区切り + + + True course over ground in degrees + 真の対地方位角度 (度) + + + COG + COG + + + Distance (w/ Gate Start) + 距離 (ゲートスタートでの) + + + The distance traveled from the leg start to the end\nor to the current time point, if the leg isn''t finished.\nIf the leg includes a gate start, the distance from the pin end to the starting position is included\nto make competitors comparable even when started at different times. + レグ始点からレグ終点まで、またはレグをフィニッシュ\nしていない場合は現時点までに移動した距離です。\nレグにゲートスタートが含まれる場合は、異なる時刻にスタートしたときでも\n競技者を比較するためにピンエンドからスタート位置までの距離が含まれます。 + + + The distance traveled from the race start to the end\nor to the current time point, if the race isn''t finished. For a gate start, the distance from the pin end to the starting position is included\nto make competitors comparable even when started at different times. + レース開始からレース終了まで、またはレースがフィニッシュ\nしていない場合は現時点までに移動した距離です。ゲートスタートの場合は、異なる時刻にスタートしたときでも\n競技者を比較するためにピンエンドからスタート位置までの距離が含まれます。 + + + Results + 結果 + + + Group Name + グループ名 + + + Value (Ascending) + 値 (昇順) + + + Value (Descending) + 値 (降順) + + + Sort by + ソートキー + + + Dashboard + ダッシュボード + + + Wind Bot is not available. + ウィンドボットは利用できません。 + + + To receive live wind data from wind measurement units, ensure that the wind bot is switched on and connected to the SAP Sailing Analytics. + 風計測ユニットからライブ風データを受信するには、ウィンドボットがオンになっていて、SAP Sailing Analytics に接続されていることを確認してください。 + + + Wind Bot + ウィンドボット + + + TWS + TWS + + + TWD + TWD + + + kts + kt + + + deg + 度 + + + live + ライブ + + + average + 平均 + + + ({0} minutes) + ({0} 分) + + + Tap on chart to change interval + チャートをタップして間隔を変更 + + + Select Competitor + 競技者選択 + + + Start Analyses + 分析開始 + + + New start analysis available + 新規スタート分析が利用可能 + + + No Start Analyses available. + 利用可能なスタート分析がありません。 + + + There is a Start Analysis created from each race start. Please stand by till racing has been started. + それぞれのレーススタートから作成されたスタート分析が存在しています。レースがスタートするまでお待ちください。 + + + No Startline Advantages available. + スタートラインアドバンテージは利用できません。 + + + For calculating the startline advantage there needs to be a course set by the race commitee. + スタートラインアドバンテージを計算するには、レース委員会が設定したコースが必要です。 + + + No Startline Advantage available. + スタートラインアドバンテージは利用できません。 + + + For calculating the startline advantage there needs to be a course set by the race commitee. + スタートラインアドバンテージを計算するには、レース委員会が設定したコースが必要です。 + + + Startline Advantages by Wind + 風によるスタートラインアドバンテージ + + + Startline Advantage by Geometry + 配置によるスタートラインアドバンテージ + + + Rank at first mark + 第一マークでのランク + + + Team + チーム + + + Distance to line in m + ラインまでの距離 (m) + + + Speed in kts + 速力 (kt) + + + Advantage in m + アドバンテージ (m) + + + s + s + + + Distance to rc boat in m + RC 艇までの距離 (m) + + + Pin End + ピンエンド + + + RC Boat + RC 艇 + + + (fixed) + (固定) + + + (suppressed) + (非表示) + + + Wind-Up (show wind from top of map) + ウィンドアップ (マップの最上部から風を表示) + + + Filter by + フィルタキー + + + Current filter selection + 現在のフィルタ選択 + + + I''m not capable of generating a code for this identifier. + この ID のコードを生成することができません。 + + + Server URL + サーバ URL + + + Rotated {0} degrees from true north.\nTap/click to toggle between wind-up and north-up map display + 真北から {0} 度を回転。\nタップ/クリックしてウィンドアップとノースアップの間のマップ表示を切替 + + + Start to first mark ({0}m) + スタートから第一マークまで ({0}m) + + + The Data-Mining components have been updated + データマイニングコンポーネントが更新されました + + + Click Reload to reload the components now. This will discard the current displayed data and run a default query.\nClick Close to do nothing. The Data-Mining won''t work correctly, until the components have been reloaded. + リロードをクリックして、コンポーネントをリロードします。これにより、現在表示されているデータが破棄され、デフォルトクエリが実行されます。\n何も行わない場合は閉じるをクリックします。データマイニングは、コンポーネントがリロードされるまでは正しく機能しません。 + + + There isn''t any data for the event yet. + このイベントにまだデータが何もありません。 + + + {0,number} countries + {0,number} カ国 + + + {0,number} country + {0,number} カ国 + + + {0,number} races + {0,number} レース + + + {0,number} race + {0,number} レース + + + {0,number} tracked races + {0,number} 追跡レース + + + {0,number} tracked race + {0,number} 追跡レース + + + {0,number} events + {0,number} イベント + + + {0,number} event + {0,number} イベント + + + {0,number} competitors + {0,number} 競技者 + + + Course area: {0} + コース領域: {0} + + + {0,number} competitor + {0,number} 競技者 + + + Leg {0,number}/{1,number} + レグ {0,number}/{1,number} + + + Clone + クローン + + + Changed + 変更済 + + + Matcher + 一致 + + + Any + 任意 + + + URL-based result providers + URL ベースの結果プロバイダ + + + Please select a URL-based result provider... + URL ベースの結果プロバイダを選択してください... + + + Sample URL: {0} + サンプル URL: {0} + + + SAP Sports on + SAP Sports: + + + Follow SAP Sports + SAP Sports をフォロー + + + Live now + ライブ中 + + + Analyze race + レースを分析 + + + Next race starts in + 次のレースのスタート: + + + {0} starts in + {0} がスタート: + + + Planned + 計画済 + + + Abandoned + 中止 + + + Postponed + 延期 + + + Starts in {0,number} min + スタートまで {0,number} 分 + + + Starting now + スタートします + + + Latest News + 最新ニュース + + + Result update + 結果更新 + + + {0} won race + {0} がレースに勝利 + + + Time on Time Factor + Time on Time 係数 + + + Time on Distance Allowance + Time on Distance 修正時間 + + + Ranking + ランキング + + + One-Design + ワンデザイン + + + ORC Performance Curve + ORC 性能曲線 + + + Time-on-Time/Time-on-Distance + Time on Time/Time on Distance + + + One-Design: First across the line wins;\nin-race ranks based on windward positions (upwind/downwind) or along-course position (reaching) + ワンデザイン: ラインを最初に通過した艇が勝者、\nウィンワード位置 (アップウィンド/ダウンウィンド) またはアロングコース位置 (リーチング) に基づくレース内ランク + + + ORC Performance Curve: Sophisticated handicapping scheme that awards time allowances based on a VPP/polar assuming pre-defined legs'' angles to the wind;\nbased on these allowances, an "implied wind" is calculated which is the basis for ranking.\nImplied wind can be calculated at any time in the race and is therefore the basis for in-race ranking. + ORC 性能曲線: 風に対して事前定義したレグのアングルを仮定して VPP/ポーラーに基づいて修正時間を与える洗練されたハンディキャップ付与方式です。\nこれらの修正時間に基づいて、ランキングの基礎となる "風の推定" が計算されます。\n風の推定は、レース中にいつでも計算でき、したがってレース中ランキングの基礎となります。 + + + Time-on-Time / Time-on-Distance: Final ranking by calculated time;\ntime is calculated by optionally applying a factor to the actual time (defaulting to 1.0)\nand subtracting an allowance in seconds per nautical mile,\napplied to the course length.\nIn-race ranks based on expected arrival at fastest boat's current position\nassuming current leg's average VMG/VMC. + Time on Time / Time on Distance: 計算された時間による最終ランキングです。\n時間は実際の時間にオプションで係数 (初期値は 1.0) を適用して、\nコース長に適用される、海里当りの修正時間 (秒単位) を\n減じて計算されます。\nレース中のランクは、現在のレグの平均 VMG/VMC を仮定して\n最速艇の現在の位置への到着の予想に基づきます。 + + + ToT Factor + ToT 係数 + + + ToD Allowance + ToD 修正時間 + + + Time-on-Time factor; actual time is multiplied with this factor in calculated time formula + Time on Time 係数、実際の時間は計算された時間公式をこの係数で乗算 + + + Time-on-Distance allowance in seconds per nautical mile;\ndistance sailed is multiplied with this allowance, and the resulting seconds are subtracted from the actual time sailed\n(optionally calculated by applying the time-on-time factor) in the calculated time formula + 海里ごとの Time on Distance 修正時間 (秒)、\n帆走した距離をこの修正時間で乗じ、結果として得られた秒数を、計算された時間公式において\n帆走した実際の時間から減算 (オプションで Time on Time 係数を適用して計算) + + + Calculated Time + 計算時間 + + + Calculated time according to competitor''s handicap\nbased on time since start of race and (windward) distance traveled + レースのスタートからの時間および移動した (ウィンワード) 距離に基づいて\n競技者のハンディキャップに従って計算された時間 + + + Calc.Time @ Fastest + 計算時間 @ 最速 + + + Calculated time at estimated arrival at the competitor that is currently farthest ahead in the race;\nestimation based on average VMG in current leg and equal (calculated) performance to fastest boat in all subsequent legs + レースにおいて現在最も遠くの前方にいる競技者の推定到着で計算された時間、\n推定は、現レグでの平均 VMG および後続のすべてのレグでの最速艇と同等の (計算された) 性能に依拠 + + + Sailor Info + 帆走情報 + + + Official Results, Protests,\nNotice Board, Sailor weather: + 公式結果、抗議、\n掲示板、帆走中天候: + + + Event Quickfinder + イベントクイックファインダ + + + Regatta Quickfinder + レガッタクイックファインダ + + + Results Quickfinder + 結果クイックファインダ + + + Races Quickfinder + レースクイックファインダ + + + Statistics + 統計 + + + Impressions + 印象 + + + This content is not available on mobile.\nSwitch to the desktop version? + この内容はモバイルでは利用できません。\nデスクトップバージョンに切り替えますか。 + + + Go to desktop version + デスクトップバージョンにジャンプ + + + Go back + 戻る + + + Show all {0} + 全 {0} を表示 + + + Collapse {0} + {0} を圧縮 + + + GPS fixes + GPS フィックス + + + Fastest + 最速 + + + Wind fixes + 風フィックス + + + Sailed miles + 帆走マイル + + + {0,number,#0.0} mil + {0,number,#0.0} 百万 + + + {0,number,#0.0} bil + {0,number,#0.0} 十億 + + + There are no results available yet. + 結果はまだ利用可能でありません。 + + + There are no news available. + 利用可能なニュースがありません。 + + + {0,number,#0.00} + {0,number,#0.00} + + + Open Leaderboard + リーダーボードを開く + + + Now + 現在 + + + {0} min ago + {0} 分前 + + + {0} h ago + {0} 時間前 + + + Winner + 勝者 + + + Duration + 期間 + + + Finished Races + フィニッシュしたレース + + + Please select one competitor + 競技者を 1 つ選択してください + + + All + 全 + + + Points + ポイント + + + # Races + # レース + + + Course Geometry + コース配置 + + + {0}° to wind + 風と {0}° + + + Shows the points along with the number of races per competitor + 競技者別にレース数とともに得点を表示 + + + {0,number}/{1,number} + {0,number}/{1,number} + + + There are no finished races yet + フィニッシュしたレースがまだありません。 + + + Races overview + レース概要 + + + List format + レース一覧 + + + Competition format + 競技日 + + + Empty + 空 + + + Run a Query + クエリを実行 + + + Latest Regatta Standings + レガッタ最新順位表 + + + Plain Text + プレーンテキスト + + + Column Chart + 縦棒グラフ + + + Choose Presentation + プレゼンテーション選択 + + + Can''t display data of type {0} + {0} タイプのデータは表示できません。 + + + Shown Decimals + 小数点表示 + + + Open fullscreen view + 全画面ビューを開く + + + Close fullscreen view + 全画面ビューを閉じる + + + Progress + 進行 + + + {0,number} videos + {0,number} 動画 + + + {0,number} video + {0,number} 動画 + + + {0,number} photos + {0,number} 写真 + + + {0,number} photo + {0,number} 写真 + + + {0} events have taken place + {0} イベントが行われました + + + One event has taken place + 1 つのイベントが行われました + + + Race Office + レース事務所 + + + Analyze + 分析 + + + Data Mining Settings + データマイニング設定 + + + Multi Results Presenter + 複数結果表示 + + + Plain Results Presenter + プレーン結果表示ツール + + + Results Chart + 結果チャート + + + Tabbed Results Presenter + タブ結果表示ツール + + + Polar Results Presenter + ポーラー結果表示ツール + + + Data Retrieval + データ取得 + + + Watch + 視聴 + + + Analyze + 分析 + + + Shortcut to denote all races for racelog tracking + レースログ追跡の全レース表示へのショートカット + + + Register Competitors + 競技者の登録 + + + Do you want to create a default Regatta Leaderboard? + レガッタリーダーボードを初期登録しますか + + + Copy Course + コースのコピー + + + Copy Competitors + 競技者のコピー + + + Smartphone Tracking + スマートフォン追跡 + + + {0,number} flights + {0,number} フライト + + + {0,number} flight + {0,number} フライト + + + View Query Definition + クエリ定義の表示 + + + Query Definition Viewer + クエリ定義ビューア + + + Group Average (Ascending) + グループ平均 (昇順) + + + Group Average (Descending) + グループ平均 (降順) + + + Group Median (Ascending) + グループ中央値 (昇順) + + + Group Median (Descending) + グループ中央値 (降順) + + + {0,number} results found for ''{1}'' + {0,number} 結果が ''{1}'' に見つかりました + + + {0,number} result found for ''{1}'' + {0,number} 結果が ''{1}'' に見つかりました + + + Run Predefined Query + 事前定義クエリを実行 + + + Select Predefined Query + 事前定義クエリを選択 + + + Predefined Query Runner + 事前定義クエリランナー + + + Developer Options + 開発者オプション + + + Copy to Clipboard + クリップボードにコピー + + + Code + コード + + + Use Class.getName() for type names + タイプ名に class.getName() を使用 + + + More robust against changes in the code base, but the code snippet can only be used in the scope where the classes are available. + コードベースでの変更に対してより堅牢ですが、コードスニペットはクラスが利用できる範囲でのみ使用できます。 + + + Use string literals for type names + タイプ名に文字列リテラルを使用 + + + The code snippet can be used anywhere, but it will break if the code base changes. + このコードスニペットはどこでも使用できますが、コードベースが変更される場合は中断します。 + + + Error loading data. Trying again in a few moments. + データのロードでエラーが発生しました。しばらくしてから再試行してください。 + + + Add gallery photo + ギャラリーフォト追加 + + + Add stage image + ステージ画像追加 + + + Add teaser image + ティーザー画像追加 + + + + No images defined yet + 画像未定義 + + + The size of the {0} image does not fit. The width should be between {1} and {2} px and the height between {3} and {4} px. + {0} 画像のサイズが適合しません。幅は {1} と {2} px の間、高さは {3} と {4} px の間である必要があります。 + + + The width and height of the image could not be retrieved yet. + 画像の幅と高さをまだ取得することができません。 + + + Please only upload content you have all usage rights for. Remember that content may be cropped. + すべての使用権を持っているコンテンツのみをアップロードしてください。コンテンツは途中から切れてしまう場合があることを忘れないでください。 + + + Register Competitors on Race (e.g. for split-fleet racing) + レースへの競技者の登録 (例: フリート分割レースに対して) + + + Do you want to register competitors directly on the Race? + レースに競技者を直接に登録しますか + + + Do you want to disable competitor registrations on the Race? + レースへの競技者登録を無効にしますか + + + Warning! This setting will lead to competitors for this race being registered on a per race basis. Competitors will not be automatically registered for this race, when registering them on the corresponding regatta. + 警告: この設定を行うと、このレースの競技者がレースごとに登録されることにつながります。競技者は、該当するレガッタに登録されるときに、このレースに対して自動的には登録されません。 + + + Warning! This will reactivate competior registrations on the regatta. All competitor registrations from the regatta will automatically be used as competitor registration for this race. + 警告: これにより、レガッタへの競技者登録が再有効化されます。レガッタからのすべての競技者登録が、このレースに対する競技者登録として自動的に使用されます。 + + + Competitor Registrations are disabled for this race. Enable by checking the checkbox to the left. + 競技者登録はこのレースに対して無効化されています。左側のチェックボックスを選択して有効化します。 + + + Add Mark to Regatta + マークをレガッタに追加 + + + Select a leaderboard group... + リーダーボードグループを選択... + + + Please select + 選択: + + + This page requires a valid regatta, race column and fleet name to identify the race to show. + このページには、表示するレースを識別するために有効なレガッタ、レース列、およびフリート名が必要です。 + + + Could not obtain a race with name {1} for fleet {2} for a regatta with name {0}: {3} + 名称 {0} のレガッタに対してフリート {2} の名称 {1} でレースが取得できませんでした: {3} + + + Error trying to create the embedded map: {0} + 埋込マップを登録しようとしてエラーが発生: {0} + + + Top {0} + 上位 {0} + + + Transparent buffer lines on hover of helplines + ヘルプ行のホバー上の透過バッファ行 + + + Buffer line stroke weight + バッファ行ストローク幅 + + + Profile + プロファイル + + + Show only competitors registered in Log + ログに登録された競技者のみ表示 + + + This will reset your current, unsaved changes to the competitors.\nDo you really want to continue? + これにより、現在の、競技者に対して未保存の変更内容がリセットされます。\n続行しますか。 + + + Removal deavticated, marks are used in races {0} + 削除は無効化され、マークはレース {0} で使用されます + + + Would you like to create a default LeaderboardGroup for your event? + イベントにリーダーボードグループを初期登録しますか + + + Please create at least one mapping by first selecting a track on the left hand side and then selecting a mark or competitor on the right hand side + 左側にあるトラックを最初に選択してから右側にあるマークまたは競技者を選択してマッピングを少なくとも 1 つ登録してください + + + Matcher type + 一致タイプ + + + Matching devices + 一致デバイス + + + Add Device + デバイス追加 + + + Create Device Configuration + デバイス設定登録 + + + Specify for which device the new configuration should apply + 新規設定を適用するデバイスを指定 + + + Create + 登録 + + + There is already a configuration for this device. + このデバイスの設定がすでに存在しています。 + + + Enter an identifier name + 識別のための名称を入力してください + + + Administration + 管理 + + + Please select a leaderboard group + リーダーボードグループを選択してください + + + Link Leaderboard to Leaderboard Group of Event + イベントのリーダーボードグループへのリーダーボードのリンク + + + Link leaderboard {0} to a leaderboard group of event {1} + リーダーボード {0} をイベントのリーダーボードグループ {1} にリンクしてください + + + Failed to link leaderboard {0} to leaderboard group {1} + リーダーボード {0} のリーダーボードグループ {1} へのリンクに失敗しました + + + Create default regatta + レガッタ初期登録 + + + Do you want to create a default Regatta? + レガッタを初期登録しますか + + + Default + 初期値 + + + The default link is used unless a language specific overwrite exists. If no default exists, there will be no link for languages that do not explicitly have one set. + 言語依存の上書きがなければデフォルトのリンクが使用されます。デフォルト未登録の場合は、明示的に 1 セットも有していない言語に対するリンクはありません。 + + + QR-Sync + QR 同期 + + + There is no identifier set. + ID が設定されていません。 + + + Could not obtain access token for user {0} + ユーザ {0} のアクセストークンを取得できませんでした + + + Device Name + デバイス名 + + + Please enter a valid Server URL + 有効なサーバ URL を入力してください + + + Server URL empty + サーバ URL が空 + + + Marks + マーク + + + Add new fix + 新規フィックス追加 + + + Please select another timepoint. There already is a fix at the timepoint of the timeslider. + 別の時点を選択してください。タイムスライダの時点にフィックスがすでに存在します。 + + + Confirm move + 移動を確認 + + + Confirm new fix + 新規フィックス確認 + + + Select a fix position by dragging the map. Then confirm or cancel with the buttons below. + マップをドラッグしてフィックス位置を選択してから、確認するか下記のボタンで取り消してください。 + + + Please select a mark from the list on the left. + 左側の一覧からマークを選択してください。 + + + Mark fixes + フィックスのマーク + + + utm_source=sapsailing&utm_medium=solutions&utm_content=2016.02&utm_campaign=solutions&pcampaignid=2016.sapsailing.solutions + utm_source=sapsailing&utm_medium=solutions&utm_content=2016.02&utm_campaign=solutions&pcampaignid=2016.sapsailing.solutions + + + https://play.google.com/store/apps/details?id=com.sap.sailing.android.tracking.app + https://play.google.com/store/apps/details?id=com.sap.sailing.android.tracking.app + + + https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app + https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app + + + https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app + https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app + + + https://play.google.com/intl/en/badges/images/generic/en-play-badge.png + https://play.google.com/intl/en/badges/images/generic/en-play-badge.png + + + http://appstore.com/sapsailinsight + http://appstore.com/sapsailinsight + + + http://appstore.com/stgtrainingstagebuch + http://appstore.com/stgtrainingstagebuch + + + _en + _en + + + Distance from average position + 平均位置からの距離 + + + Use a touch optimized UI to move the fix + タッチ最適化 UI を使用してフィックスを移動 + + + Delete fix + フィックス削除 + + + The deletion of this fix is currently not possible. + このフィックスの削除は現在は可能でありません。 + + + Loading mark fixes... + フィックスのマークをロードしています... + + + Fix successfully added. + フィックスが追加されました。 + + + Fix position successfully edited. + フィックス位置が編集されました。 + + + Fix successfully removed. + フィックスが削除されました。 + + + Drag to change position\nRight-click to get menu to delete + ドラッグして位置を変更\n右クリックして削除するメニューを取得 + + + Move fix + フィックスを移動 + + + Manage2Sail Event + Manage2Sail イベント + + + There are no events yet. + イベントがまだありません。 + + + Please select a result import provider... + 結果インポートプロバイダを選択してください... + + + Build version + ビルドバージョン + + + Server Information + サーバ情報 + + + Server Configuration + サーバ設定 + + + Standalone server + スタンドアロンサーバ + + + Send... + 送信... + + + PassId + パス ID + + + Created at + 登録時刻 + + + Logical timepoint + 論理タイムポイント + + + Info + 情報 + + + Wind fixes + 風フィックス + + + 2x Mark + 2x マーク + + + {0,number,#,###}-{1,number,#,###} of {2,number,#,###} + {0,number,#,###}-{1,number,#,###} of {2,number,#,###} + + + {0,number,#,###}-{1,number,#,###} of over {2,number,#,###} + {0,number,#,###}-{1,number,#,###} of over {2,number,#,###} + + +
+
\ No newline at end of file diff --git a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties.xlf b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties.xlf index 67bdad17514..f32bf1875e4 100644 --- a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties.xlf +++ b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties.xlf @@ -1,5991 +1,6063 @@ - +
- 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 - +
Manage2Sail - Manage2Sail + Manage2Sail Race Manager App - Приложение диспетчера гонок + Приложение Race Manager TracTrac Events - События TracTrac + События TracTrac SwissTiming Events - События SwissTiming + События SwissTiming History of tracked events - История отслеживаемых событий + История отслеживаемых событий General - Общее + Общее List Races - Список гонок + Список гонок List Regattas - Список регат + Список регат Wind - Ветер + Ветер Maneuver - Маневр + Маневр This is the Wind Panel which so far is completely empty. - Это панель ветров, которая пока что совсем пуста. + Это панель ветров, которая пока что совсем пуста. Refresh - Обновить + Обновить Remove - Удалить + Удалить Wind Source - Источник ветра + Источник ветра Dampening Interval - Интервал смягчения + Интервал смягчения Map - Карта + Карта Race - Гонка + Гонка Races - Гонки + Гонки tracked - отслеживаемые + отслеживаемые Linked Races - Связанные гонки + Связанные гонки Time - Время + Время Time the competitor sailed on this leg. For the first leg this is counted from when the competitor passed the start line. - Время плавания данного участника на данном этапе. Для первого этапа отсчитывается от момента пересечения участником стартовой линии. + Время плавания данного участника на данном этапе. Для первого этапа отсчитывается от момента пересечения участником стартовой линии. Play speed - Скорость воспроизведения + Скорость воспроизведения Use "1" for regular play-back speed, "2" for double-speed playback and so on - "1" — обычная скорость воспроизведения, "2" — удвоенная скорость воспроизведения и т. д. + "1" — обычная скорость воспроизведения, "2" — удвоенная скорость воспроизведения и т. д. Live - Активно + Активно Replay Mode - Режим воспроизведения + Режим воспроизведения Delay - Задержка + Задержка Leave empty to replay at arbitrary times; enter seconds (e.g., "60" for one minute or "0" for none) of delay before live data feed - Оставьте пустым для воспроизведения в произвольное время; введите число секунд задержки перед потоком оперативных данных (например, "60" — одна минута задержки, "0" — без задержки) + Оставьте пустым для воспроизведения в произвольное время; введите число секунд задержки перед потоком оперативных данных (например, "60" — одна минута задержки, "0" — без задержки) Speed (kn) - Скорость (уз) + Скорость (уз) From (deg) - Источник (град.) + Источник (град.) There are no races yet. - Гонок еще нет. + Гонок еще нет. Σ - Σ + Σ Accumulated - Накоплено + Накоплено Competitor - Участник + Участник Competitors - Участники + Участники {0}/{1} competitors selected. - Выбрано участников: {0}/{1}. + Выбрано участников: {0}/{1}. No such leaderboard - Нет такой таблицы лидеров + Нет такой таблицы лидеров Leaderboard Configuration - Конфигурация таблицы лидеров + Конфигурация таблицы лидеров New... - Создать... + Создать... Edit Points... - Изменить точки... + Изменить точки... Race Names in Selected Leaderboard - Названия гонок в выбранной таблице лидеров + Названия гонок в выбранной таблице лидеров Add... - Добавить... + Добавить... Rename... - Переименовать... + Переименовать... Tracked Race Connected to Selected Race Name - Отслеживаемая гонка связана с выбранным названием гонки + Отслеживаемая гонка связана с выбранным названием гонки Link to Column - Связать со столбцом + Связать со столбцом Unlink - Отменить связывание + Отменить связывание Leaderboard Name - Название таблицы лидеров + Название таблицы лидеров Cancel - Отмена + Отмена Please enter a name - Введите название + Введите название Please enter a boat class - Введите класс лодки + Введите класс лодки Discard one more race starting with how many started races - Исключить еще одну гонку, начиная с числа стартов в гонках + Исключить еще одну гонку, начиная с числа стартов в гонках Leaderboard with this name already exists - Таблица лидеров с таким названием уже существует + Таблица лидеров с таким названием уже существует Discard thresholds must be numeric - Пороги исключения должны быть числовыми + Пороги исключения должны быть числовыми Discard thresholds must be ascending - Пороги исключения должны возрастать + Пороги исключения должны возрастать Discarding - Исключение + Исключение Starting from how many races - Начиная с числа гонок + Начиная с числа гонок Rename Leaderboard - Переименовать таблицу лидеров + Переименовать таблицу лидеров Add Column to Leaderboard - Добавить столбец в таблицу лидеров + Добавить столбец в таблицу лидеров Please enter a name for new race column - Введите название для столбца новой гонки + Введите название для столбца новой гонки OK - ОК + ОК Medal Race - Гонка на медали + Гонка на медали Rename Race - Переименовать гонку + Переименовать гонку Open Selected Leaderboard - Открыть выбранную таблицу лидеров + Открыть выбранную таблицу лидеров Rank - Ранг + Ранг Competitor''s rank in the race - Ранг участника в данной гонке + Ранг участника в данной гонке Regatta Rank - Ранг в регате + Ранг в регате Competitor''s rank in the regatta - Ранг участника в регате + Ранг участника в регате Overall Rank - Общий ранг + Общий ранг Competitor''s rank in the series - Ранг участника в серии + Ранг участника в серии Regatta Rank - Ранг в регате + Ранг в регате - - - + - + - + + + L - Э + Э Distance - Дистанция + Дистанция ∅ Speed - ∅ Скорость + ∅ Скорость The average speed over ground during this leg,\ncalculated with the division of the distance traveled by the time needed. - Средняя скорость относительно грунта на данном этапе:\nотношение пройденного расстояния к затраченному времени. + Средняя скорость относительно грунта на данном этапе:\nотношение пройденного расстояния к затраченному времени. The average speed over ground during this race,\ncalculated with the division of the distance traveled by the time needed. - Средняя скорость в течение гонки,\nвычисляемая делением пройденного расстояния на затраченное время. + Средняя скорость в течение гонки,\nвычисляемая делением пройденного расстояния на затраченное время. Rank Gain - Повышение ранга + Повышение ранга The difference of the position from the current and the previous leg. - Разница в положении между текущим и предыдущим этапами. + Разница в положении между текущим и предыдущим этапами. Sailing Analytics - Аналитика плавания + Sailing Analytics Leaderboard - Таблица лидеров + Таблица лидеров Leaderboards - Таблицы лидеров + Таблицы лидеров Leaderboard Settings - Параметры таблицы лидеров + Параметры таблицы лидеров Settings - Параметры + Параметры Select at least one leg detail - Выберите подробности минимум одного этапа + Выберите подробности минимум одного этапа SOG - SOG + SOG The current speed over ground. - Текущая скорость относительно грунта. + Текущая скорость относительно грунта. ETA - ETA + ETA The estimated time that the competitor needs to reach the next waypoint, based on the competitor''s current velocity made good, considering the true wind direction. - Ожидаемое время достижения участником следующей путевой точки при эффективной скорости лавирования (VMG) с учетом направления истинного ветра. + Ожидаемое время достижения участником следующей путевой точки при эффективной скорости лавирования (VMG) с учетом направления истинного ветра. Gap to leader - Отставание от лидера + Отставание от лидера Estimates the (calculated) arrival time at the current position of the boat that is farthest ahead,\nbased on average VMG for the current leg and with performance equal to that of the race leader for subsequent legs.\nThe gap tells how much earlier the competitor would have had to be where she currently is\nin order to rank equal to the race leader at the projected arrival at the current position of the boat farthest ahead. - Оценивает самое ранее (расчетное) время прибытия лодки в текущую позицию,\nисходя из среднего значения VMG для текущего этапа и эффективности, равной эффективности лидера гонки, для последующих этапов.\nПоказывает, насколько раньше нужно было прийти в текущую позицию,\nчтобы иметь ранг лидера гонки в расчетное время прибытия в текущую позицию лидирующей лодки. + Оценивает самое ранее (расчетное) время прибытия лодки в текущую позицию,\nисходя из среднего значения VMG для текущего этапа и эффективности, равной эффективности лидера гонки, для последующих этапов.\nПоказывает, насколько раньше нужно было прийти в текущую позицию,\nчтобы иметь ранг лидера гонки в расчетное время прибытия в текущую позицию лидирующей лодки. VMG - VMG + VMG The velocity made good, which is the windward/leeward speed for upwind/downwind legs, respectively, or the along-track speed for reaching legs. - Эффективная скорость лавирования, то есть скорость хода переменными галсами или полным ветром на этапах с соответствующими условиями. + Эффективная скорость лавирования, то есть скорость хода переменными галсами или полным ветром на этапах с соответствующими условиями. Windward distance to go - Осталось пройти + Осталось пройти Windward/leeward distance from the competitor''s current position to the end of the leg for upwind/downwind legs, respectively, or the along-track distance for reaching legs. - Расстояние от текущей позиции участника до конца этапа с учетом направления ветра. + Расстояние от текущей позиции участника до конца этапа с учетом направления ветра. Gap - Отставание + Отставание Windward/leeward distance of the competitor to the competitor farthest ahead in the race for upwind/downwind legs, respectively, or the along-track distance for reaching legs.\nNote that the competitor that is farthest ahead is not necessarily leading the race if handicap ranking is in place. - Расстояние от участника до первой лодки в гонке с учетом направления ветра.\nПервая лодка не обязательно лидирует в гонке, если применяется ранжирование с гандикапом. + Расстояние от участника до первой лодки в гонке с учетом направления ветра.\nПервая лодка не обязательно лидирует в гонке, если применяется ранжирование с гандикапом. Name - Название + Название Leave empty for default - Чтобы задать название по умолчанию, оставьте пустым + Чтобы задать название по умолчанию, оставьте пустым Track New event - Отслеживать новое событие + Отслеживать новое событие Trackable Races - Гонки с возможностью отслеживания + Гонки с возможностью отслеживания Track Wind - Отслеживать ветер + Отслеживать ветер Regatta name - Название регаты + Название регаты Event name - Название события + Название события Event - Событие + Событие Start time - Время начала + Время начала Start of tracking - Начало отслеживания + Начало отслеживания End of tracking - Окончание отслеживания + Окончание отслеживания Regatta - Регата + Регата Regattas - Регаты + Регаты Start tracking - Начать отслеживание + Начать отслеживание Stop tracking - Прекратить отслеживание + Прекратить отслеживание Connections - Подключение + Подключения History of connections - История подключений + История подключений Define a new connection - Определить новое подключение + Определить новое подключение Tracked races - Отслеживаемые гонки + Отслеживаемые гонки Can send requests - Может отправлять запросы + Может отправлять запросы Connect and read races - Подключиться и считать гонки + Подключиться и считать гонки Ports - Порты + Порты Live data - Оперативные данные + Оперативные данные Stored data - Хранимые данные + Хранимые данные URIs - URI + URI JSON URL - URL-адрес JSON + URL-адрес JSON Port Live Data - Порт оперативных данных + Порт оперативных данных Port Stored Data - Порт хранимых данных + Порт хранимых данных Live URI - URI оперативных данных + URI оперативных данных Stored URI - URI хранимых данных + URI хранимых данных Events Currently Tracked - Отслеживаемые события + Отслеживаемые события Hostname - Имя хоста + Имя хоста Correct Wind Bearing by Declination - Корректировать направление ветра по отклонению + Корректировать направление ветра по отклонению Tracking Started - Отслеживание начато + Отслеживание начато Pause automatic refresh - Приостановить автоматическое обновление + Приостановить автоматическое обновление Refresh automatically - Обновлять автоматически + Обновлять автоматически Delay (s) - Задержка (с) + Задержка (с) Delay between auto-updates in seconds: - Задержка между автообновлениями в секундах: + Задержка между автообновлениями в секундах: The delay must be greater than zero - Задержка должна быть больше нуля + Задержка должна быть больше нуля Choose update interval of at least one second - Выберите интервал обновления не менее одной секунды + Выберите интервал обновления не менее одной секунды Error setting wind for race - Ошибка при установке ветра для гонки + Ошибка при установке ветра для гонки Error fetching wind information for race - Ошибка при извлечении сведений о ветре для гонки + Ошибка при извлечении сведений о ветре для гонки Error while trying to set "wind starts upwind" flag - Ошибка при попытке установить флаг "ветер начинается навстречу" + Ошибка при попытке установить флаг "ветер начинается навстречу" in event - в событии + в событии to - на + на to - на + на Show estimated wind - Показать оценку ветра + Показать оценку ветра Timing - Хронометраж + Хронометраж Race Details - Подробности гонки + Подробности гонки Overall Details - Общие подробности + Общие подробности Leg Details - Подробности + Подробности Up - Вверх + Вверх Down - Вниз + Вниз Port - Порт + Порт Race Started - Гонка начата + Гонка начата Show only selected competitors - Показать только выбранных участников + Показать только выбранных участников Show info box for selected competitors - Показать окно сведений для выбранных участников + Показать окно сведений для выбранных участников Tail length (s) - Длина хвоста (с) + Длина хвоста (с) Which races would you like to display? - Какие гонки вы хотите просмотреть? + Какие гонки вы хотите просмотреть? Length (s) - Длина (с) + Длина (с) Stress Test - Стресс-тест + Стресс-тест Maneuvers - Маневры + Маневры The number of the maneuvers sailed in this leg. - Число совершенных маневров на данном этапе. + Число совершенных маневров на данном этапе. The number of maneuvers sailed in this race. - Число совершенных маневров в данной гонке. + Число совершенных маневров в данной гонке. Tacks - Поворотов оверштаг + Поворотов оверштаг Jibes - Поворотов фордевинд + Поворотов фордевинд Penalty Circles - Штрафных кругов + Штрафных кругов Medalrace value not allowed - Значение гонки на медали не разрешено + Значение гонки на медали не разрешено Configuration - Конфигурация + Конфигурация Maneuvers - Маневры + Маневры Choose chart - Выберите диаграмму + Выберите диаграмму Distance Traveled - Пройденная дистанция + Пройденная дистанция The distance traveled from the leg start to the end\nor to the current time point, if the leg isn''t finished. - Расстояние, пройденное от старта этапа до конца\nили до текущего момента, если этап не завершен. + Расстояние, пройденное от старта этапа до конца\nили до текущего момента, если этап не завершен. The distance traveled from the race start to the end\nor to the current time point, if the race isn''t finished. - Расстояние, пройденное от старта гонки до конца\nили до текущего момента, если гонка не завершена. + Расстояние, пройденное от старта гонки до конца\nили до текущего момента, если гонка не завершена. Velocity Made Good - Эффективная скорость лавирования (VMG) + Эффективная скорость лавирования (VMG) Speed Over Ground - Скорость относительно грунта (SOG) + Скорость относительно грунта (SOG) Gap To Leader - Отставание от лидера + Отставание от лидера Points to load - Точки для загрузки + Точки для загрузки Speed - Скорость + Скорость Distance - Дистанция + Дистанция in - в + в s - с + с s/NM - с/NM + с/NM m - м + м ° - ° + ° Close - Закрыть + Закрыть Compare competitors - Сравнить участников + Сравнить участников Description - Описание + Описание Sail number - Номер на парусе + Номер на парусе Country - Страна + Страна Unable to find the IOC 3-letter-codes. - Не удалось найти 3-буквенные коды IOC. + Не удалось найти 3-буквенные коды IOC. Add - Добавить + Добавить Delete - Удалить + Удалить Show charts - Показать диаграммы + Показать диаграммы A race with that name already exists. - Гонка с таким названием уже существует. + Гонка с таким названием уже существует. A race with that name already exists in this regatta. - Гонка с таким названием уже существует в данной регате. + Гонка с таким названием уже существует в данной регате. Head up - Приведение к ветру + Приведение к ветру Bear away - Увал под ветер + Увал под ветер Tack - Оверштаг + Оверштаг The amount of tacks sailed in this race. - Число совершенных поворотов оверштаг в данной гонке. + Число совершенных поворотов оверштаг в данной гонке. Jibe - Фордевинд + Фордевинд The amount of jibes sailed in this race. - Число совершенных поворотов фордевинд в данной гонке. + Число совершенных поворотов фордевинд в данной гонке. Penalty circle - Штрафной круг + Штрафной круг The amount of penalty circles sailed in this race. - Число штрафных кругов, пройденных в данной гонке. + Число штрафных кругов, пройденных в данной гонке. Mark passing - Прохождение отметки + Прохождение отметки Mark - Отметка + Отметка Unknown maneuver - Неизвестный маневр + Неизвестный маневр Douglas peucker points - Точки Дугласа – Пекера + Точки Дугласа – Пекера Filter races by name - Фильтровать гонки по названию + Фильтровать гонки по названию Filter media by name - Фильтровать носители по названию + Фильтровать по названию Track settings - Параметры отслеживания + Параметры отслеживания Edit - Изменить + Изменить Edit competitors - Изменить участников + Изменить участников Edit scores - Изменить оценки + Изменить оценки Open in Browser - Открыть в браузере + Открыть в браузере Remove - Удалить + Удалить Edit Series - Изменить серию + Изменить серию Edit or rename race - Изменить или переименовать гонку + Изменить или переименовать гонку Unlink race - Отменить связывание гонки + Отменить связывание гонки Remove race - Удалить гонку + Удалить гонку Export as XML - Экспортировать как XML + Экспортировать как XML Open Coach Dashboard - Открыть панель тренера + Открыть панель тренера Filter leaderboards by name - Фильтровать таблицы лидеров по названию + Фильтровать таблицы лидеров по названию Actions - Действия + Действия Is linked - Связано + Связано Yes - Да + Да No - Нет + Нет Hover over a point for details - Наведите указатель для просмотра подробностей + Наведите указатель для просмотра подробностей No selection - Ничего не выбрано + Ничего не выбрано Race starts with upwind leg - Гонка начинается против ветра + Гонка начинается против ветра Events - События + События Settings for {0} - Параметры для {0} + {0} — параметры No events were found - События не найдены + События не найдены No event selected - Не выбрано событие + Не выбрано событие No leaderboards were found - Таблицы лидеров не найдены + Таблицы лидеров не найдены Search for events - Поиск событий + Поиск событий Location - Место + Место From - С + С Until - По + По Only live events - Только текущие события + Только текущие события Location not available - Место недоступно + Место недоступно Start date not available - Дата начала недоступна + Дата начала недоступна Start date - Дата начала + Дата начала End date - Дата окончания + Дата окончания Unknown maneuver - Неизвестный маневр + Неизвестный маневр the tail length must be a number greater than zero. - длина хвоста должна быть задана числом больше нуля. + длина хвоста должна быть задана числом больше нуля. Failed to load race information: {0} - Не удалось загрузить сведения о гонке: {0} + Не удалось загрузить сведения о гонке: {0} {0} passed {1} at {2} - {0} пройдено {1} в {2} + {0} пройдено {1} в {2} {0} at {1}: {2} - {0} в {1}: {2} + {0} в {1}: {2} Do you really want to remove the race ''{0}''? Note: all race logs for this race will also permanently be removed! - Действительно удалить гонку ''{0}''? Примечание: все журналы для этой гонки также будут окончательно удалены! + Действительно удалить гонку ''{0}''? Примечание: все журналы для этой гонки также будут окончательно удалены! Show details - Показать подробности + Показать подробности Hide leaderboard - Скрыть таблицу лидеров + Скрыть таблицу лидеров Hide leaderboards - Скрыть таблицы лидеров + Скрыть таблицы лидеров Show leaderboard - Показать таблицу лидеров + Показать таблицу лидеров Hide races - Скрыть гонки + Скрыть гонки Show map - Показать карту + Показать карту Hide map - Скрыть карту + Скрыть карту No leaderboard selected - Не выбрана таблица лидеров + Не выбрана таблица лидеров Leaderboard groups - Группы таблиц лидеров + Группы таблиц лидеров Filter leaderboard groups by name - Фильтровать группы таблиц лидеров по названию + Фильтровать группы таблиц лидеров по названию Create leaderboard group - Создать группу таблиц лидеров + Создать группу таблиц лидеров Leaderboard Group Configuration - Конфигурация группы таблиц лидеров + Конфигурация группы таблиц лидеров Legs - Отрезки + Отрезки On Leg - На отрезке + На отрезке The number of the leg on which the competitor currently is, starting with 1 - Номер отрезка, на котором сейчас находится участник, начиная с 1. + Номер отрезка, на котором сейчас находится участник, начиная с 1. Leaderboard Group - Группа таблиц лидеров + Группа таблиц лидеров Please enter a non-empty description - Введите непустое описание + Введите непустое описание A leaderboard group with this name already exists. - Группа таблиц лидеров с таким названием уже существует. + Группа таблиц лидеров с таким названием уже существует. Details of leaderboard group - Подробности группы таблиц лидеров + Подробности группы таблиц лидеров Edit - Изменить + Изменить Save - Сохранить + Сохранить Abort - Прервать + Прервать No leaderboard group with the name {0} found - Группа таблиц лидеров названием {0} не найдена + Группа таблиц лидеров названием {0} не найдена Overview - Обзор + Обзор Welcome to SAP Sailing Analytics - Вас приветствует SAP Sailing Analytics + Вас приветствует SAP Sailing Analytics SAP brings sailing fans closer to the on-water action than ever before.\ The live-leaderboard enhances the fan experience through real-time rankings, boat speeds and winning margins. Follow every race and never miss a passing maneuver with SAP.\ This is the simple way to watch live sail racing from wherever you are. - SAP позволяет любителям парусного спорта подойти к происходящему на воде ближе, чем когда-либо ранее.\ Онлайн-таблица лидеров содержит обновляемые в режиме реального времени данные о рангах участников, скоростях лодок и преимуществе лидеров. Следите за каждой гонкой и не пропустите ни одного маневра с SAP.\ Это удобный способ наблюдения за парусными гонками в реальном времени откуда угодно. + SAP позволяет любителям парусного спорта подойти к происходящему на воде ближе, чем когда-либо ранее.\ Онлайн-таблица лидеров содержит обновляемые в режиме реального времени данные о рангах участников, скоростях лодок и преимуществе лидеров. Следите за каждой гонкой и не пропустите ни одного маневра с SAP.\ Это удобный способ наблюдения за парусными гонками в реальном времени откуда угодно. Home - Главная + Главная Click and drag in the plot area to zoom in - Щелкните и перетащите в область визуализации для увеличения + Щелкните и перетащите в область визуализации для увеличения Wind speed (kts) - Скорость ветра (уз) + Скорость ветра (уз) All times in UTC - Время UTC + Время UTC Auto zoom to - Автоувеличение до + Автоувеличение до Boats - Лодки + Лодки Buoys - Буи + Буи Wind sensors - Датчики ветра + Датчики ветра Off - Выкл. + Выкл. Tails - Хвосты + Хвосты Zoom only to selected competitors - Увеличить только до выбранных участников + Увеличить только до выбранных участников Bearing - Ветер + Ветер Zoom - Увеличить + Увеличить Step size must be greater than 0 - Размер шага должен быть больше 0 + Размер шага должен быть больше 0 Step size in seconds - Размер шага в секундах + Размер шага в секундах Refresh interval (s) - Интервал обновления (с) + Интервал обновления (с) Refresh interval must be greater than {0} seconds - Интервал обновления должен быть больше {0} с + Интервал обновления должен быть больше {0} с Competitor Chart - Диаграмма участника + Диаграмма участника To show the competitor charts, select at least one competitor - Чтобы просмотреть диаграммы участника, выберите хотя бы одного участника + Чтобы просмотреть диаграммы участника, выберите хотя бы одного участника To send invitation E-Mails, select at least one competitor - Чтобы отправить приглашения по эл. почте, выберите хотя бы одного участника + Чтобы отправить приглашения по эл. почте, выберите хотя бы одного участника Direction change - Изменение направления + Изменение направления Speed change - Изменение скорости + Изменение скорости deg - град. + град. Untracked - Не отслеживается + Не отслеживается Delay for live mode: - Задержка для оперативного режима: + Задержка для оперативного режима: Not available - Недоступно + Недоступно Details - Подробности + Подробности No group selected - Не выбрана группа + Не выбрана группа Combined - Объединено + Объединено Course Layout - Схема курса + Схема курса Estimation - Расчетная + Расчетная Measured - Измеренная + Измеренная Manual - Вручную + Вручную Race Committee - Гоночный комитет + Гоночный комитет Click on the chart to set the time - Щелкните диаграмму, чтобы установить время + Щелкните диаграмму, чтобы установить время Position - Позиция + Позиция Wind Sources Used - Используемые источники ветра + Используемые источники ветра Error trying to update wind sources to exclude for race {0}: {1} - Ошибка при попытке обновить источники ветра к исключению для гонки {0}: {1} + Ошибка при попытке обновить источники ветра к исключению для гонки {0}: {1} Feedback - Обратная связь + Обратная связь Start/Stop playing - Начать/остановить воспроизведение + Начать/остановить воспроизведение Jump back to live time point - Вернуться к текущей отметке времени + Вернуться к текущей отметке времени Slow play speed down or even run backwards - Замедлить воспроизведение или даже запустить в обратном направлении + Замедлить воспроизведение или даже запустить в обратном направлении Speed play speed up or run forward again - Ускорить воспроизведение или снова запустить в прямом направлении + Ускорить воспроизведение или снова запустить в прямом направлении Show/Hide {0} - Показать/скрыть {0} + Показать/скрыть {0} Angle to wind - Угол к ветру + Угол к ветру Races in leaderboard - Гонки в таблице лидеров + Гонки в таблице лидеров Leaderboards in group - Таблицы лидеров в группе + Таблицы лидеров в группе Replication - Репликация + Репликация Error fetching replica data: {0} - Ошибка извлечения данных реплики: {0} + Ошибка извлечения данных реплики: {0} ∅ XTE - ∅ XTE + ∅ XTE The average cross track error to the great circle segment connecting leg start with leg end - Среднее боковое уклонение к большому круговому сегменту, соединяющему начало отрезка с концом отрезка + Среднее боковое уклонение к большому круговому сегменту, соединяющему начало отрезка с концом отрезка ∅ XTE +/- - ∅ XTE +/- + ∅ XTE +/- The average position on the course as a signed distance from the course middle line; 0 meaning on the course middle line; negative values mean left-hand side, positive values mean right-hand side. - Средняя позиция на курсе как расстояние (со знаком) от средней линии курса; отрицательные значения означают уклонение влево, положительные — вправо. + Средняя позиция на курсе как расстояние (со знаком) от средней линии курса; отрицательные значения означают уклонение влево, положительные — вправо. The average absolute cross track error to the great circle segments connecting leg start with leg end for all upwind legs of this race, ignoring to which side of the course middle line the competitor is - Среднее абсолютное боковое уклонение к большим круговым сегментам, соединяющим начало отрезка с концом отрезка, для всех наветренных отрезках данной гонки без учета стороны участника от средней линии курса + Среднее абсолютное боковое уклонение к большим круговым сегментам, соединяющим начало отрезка с концом отрезка, для всех наветренных отрезках данной гонки без учета стороны участника от средней линии курса The signed average cross track error to the great circle segments connecting leg start with leg end for all upwind legs of this race; negative values mean left of the course middle line, positive values mean right. - Боковое уклонение (со знаком) к большим круговым сегментам, соединяющим начало отрезка с концом отрезка, для всех наветренных отрезков данной гонки; отрицательные значения означают уклонение влево от средней линии курса, положительные — вправо. + Боковое уклонение (со знаком) к большим круговым сегментам, соединяющим начало отрезка с концом отрезка, для всех наветренных отрезков данной гонки; отрицательные значения означают уклонение влево от средней линии курса, положительные — вправо. Hostname of Master Instance - Имя хоста главного экземпляра + Имя хоста главного экземпляра Error starting replication from host {0} / exchange {1}: {2} - Ошибка запуска репликации с хоста {0}/Exchange {1}: {2} + Ошибка запуска репликации с хоста {0}/Exchange {1}: {2} Help lines - Вспомогательные линии + Вспомогательные линии Start line - Стартовая линия + Стартовая линия Finish line - Финишная линия + Финишная линия Advantage line - Лини преимущества + Лини преимущества Course middle line - Средняя линия курса + Средняя линия курса Messaging port number (0 for default) - Номер порта обмена сообщениями (0 по умолчанию) + Номер порта обмена сообщениями (0 по умолчанию) Servlet port number - Номер порта сервлета + Номер порта сервлета Registered at {0} - Зарегистрировано в {0} + Зарегистрировано в {0} Replicating from master {0}:{2}, messaging port {3}:{1}, exchange {4} - Репликация с главного экземпляра {0}:{2}, порт обмена сообщениями {3}:{1}, Exchange {4} + Репликация с главного экземпляра {0}:{2}, порт обмена сообщениями {3}:{1}, Exchange {4} Show wind speed data - Показать данные о скорости ветра + Показать данные о скорости ветра Show wind directions data - Показать данные о направлениях ветра + Показать данные о направлениях ветра Fleet - Флот + Флот Boat Class - Класс лодок + Класс лодок Set delay to live - Установить задержку оперативных данных + Установить задержку оперативных данных Please enter a venue. - Введите место. + Введите место. Please enter a start date and an end date. - Введите даты начала и окончания. + Введите даты начала и окончания. The start date must be before the end date. - Дата начала должна быть раньше даты окончания. + Дата начала должна быть раньше даты окончания. An event with this name already exists. - Событие с таким названием уже существует. + Событие с таким названием уже существует. Venue - Место + Место Please select a boat class. - Выберите класс лодки. + Выберите класс лодки. A regatta with this name already exists. - Регата с таким названием уже существует. + Регата с таким названием уже существует. A series with this name already exists. - Серия с таким названием уже существует. + Серия с таким названием уже существует. A fleet with this name already exists. - Флот с таким названием уже существует. + Флот с таким названием уже существует. Fleets - Флоты + Флоты Series - Серия + Серия Split fleet - Разделить флот + Разделить флот Color - Цвет + Цвет Medal Series - Серия на медали + Серия на медали No color - Без цвета + Без цвета Please select a regatta. - Выберите регату. + Выберите регату. Add fleet - Добавить флот + Добавить флот Do you really want to remove the regatta "{0}"? This will also remove all leaderboards for this regatta! - Действительно удалить регату "{0}"? При этом также будут удалены все таблицы лидеров для данной регаты! + Действительно удалить регату "{0}"? При этом также будут удалены все таблицы лидеров для данной регаты! Error loading score correction providers: {0} - Ошибка при загрузке поставщиков корректировки оценок: {0} + Ошибка при загрузке поставщиков корректировки оценок: {0} Select result list to import from - Выберите список результатов для импорта + Выберите список результатов для импорта of - из + из Error obtaining score corrections from provider {0} and event {1} for boat class {2} published on {3}: {4} - Ошибка при получении корректировок оценок от поставщика {0} и события {1} для класса лодок {2}, опубликованных в {3}: {4} + Ошибка при получении корректировок оценок от поставщика {0} и события {1} для класса лодок {2}, опубликованных в {3}: {4} Assign race numbers of official results to leaderboard race columns - Присвойте номера гонок официальных результатов столбцам гонок таблицы лидеров + Присвойте номера гонок официальных результатов столбцам гонок таблицы лидеров Import official results... - Импорт официальных результатов... + Импорт официальных результатов... Select all - Выбрать все + Выбрать все Apply - Применить + Применить Successfully updated scores - Оценки успешно обновлены + Оценки успешно обновлены Error updating scores for leaderboard {0}: {1} - Ошибка при обновлении оценок для таблицы лидеров {0}: {1} + Ошибка при обновлении оценок для таблицы лидеров {0}: {1} Maneuver types to show when competitor is clicked - Типы маневров для отображения при щелчке участника + Типы маневров для отображения при щелчке участника Simulate as live race - Симулировать как текущую гонку + Симулировать как текущую гонку Offset before start in minutes: - Сдвиг перед стартом в минутах: + Сдвиг перед стартом в минутах: The selected races contain boat classes which are not the same as the boat class ''{0}'' of the selected regatta. No races will be loaded. - Выбранные гонки содержат классы лодок, не совпадающие с классом лодок ''{0}'' выбранной регаты. Гонки не будут загружены. + Выбранные гонки содержат классы лодок, не совпадающие с классом лодок ''{0}'' выбранной регаты. Гонки не будут загружены. There is at least one regatta for the selected boat classes. Really create default regatta(s) for those race(s)? - Для выбранных классов лодок имеется минимум одна регата. Создать регаты по умолчанию для соответствующих гонок? + Для выбранных классов лодок имеется минимум одна регата. Создать регаты по умолчанию для соответствующих гонок? Reload - Перезагрузить + Перезагрузить Add Regatta... - Добавить регату... + Добавить регату... Import Regattas... - Импортировать регаты... + Импортировать регаты... Exchange Name - Имя Exchange + Имя Exchange Course - Курс + Курс Course layout - Схема курса + Схема курса Buoy - Буй + Буй Gate - Ворота + Ворота Buoys - Буи + Буи Waypoint - Путевая точка + Путевая точка Show all competitors - Показать всех участников + Показать всех участников Boat tails - Хвосты лодок + Хвосты лодок Start line to first Mark Triangle - Линия старта до треугольника первой отметки + Линия старта до треугольника первой отметки Result import URLs - URL-адреса импорта результатов + URL-адреса импорта результатов Error refreshing result import URL list: {0} - Ошибка при обновлении списка URL-адресов импорта результатов: {0} + Ошибка при обновлении списка URL-адресов импорта результатов: {0} Successfully updated result import URL list - Успешно обновлен список URL-адресов импорта результатов + Успешно обновлен список URL-адресов импорта результатов Error removing result import URLs: {0} - Ошибка при удалении URL-адресов импорта результатов: {0} + Ошибка при удалении URL-адресов импорта результатов: {0} Please enter a URL. - Введите URL-адрес. + Введите URL-адрес. Add result import URL - Добавьте URL-адрес импорта результатов + Добавьте URL-адрес импорта результатов Error adding result import URL: {0} - Ошибка при добавлении URL-адресов импорта результатов: {0} + Ошибка при добавлении URL-адресов импорта результатов: {0} Remote Server Instances - Удаленные экземпляры сервера + Удаленные экземпляры сервера Local Server - Локальный сервер + Локальный сервер Error refreshing SailingServer instances: {0} - Ошибка при обновлении экземпляров SailingServer: {0} + Ошибка при обновлении экземпляров SailingServer: {0} Successfully updated SailingServer instances - Экземпляры SailingServer успешно обновлены + Экземпляры SailingServer успешно обновлены Error removing SailingServer instances: {0} - Ошибка при удалении экземпляров SailingServer: {0} + Ошибка при удалении экземпляров SailingServer: {0} Add SailingServer instance - Добавить экземпляр SailingServer + Добавить экземпляр SailingServer Error adding SailingServer instance: {0} - Ошибка при добавлении экземпляра SailingServer: {0} + Ошибка при добавлении экземпляра SailingServer: {0} Error trying to obtain leaderboard contents: {0} - Ошибка при попытке получить содержимое таблицы лидеров: {0} + Ошибка при попытке получить содержимое таблицы лидеров: {0} Last Score Update - Последнее обновление оценок + Последнее обновление оценок This website is not optimized for your browser. Please switch to a newer one like Firefox or Chrome. - Этот веб-сайт не оптимизирован для вашего браузера. Перейдите на более новый, такой как Firefox или Chrome. + Этот веб-сайт не оптимизирован для вашего браузера. Перейдите на более новый, такой как Firefox или Chrome. New Mark - Новая отметка + Новая отметка - Mark/Gate - Отметка/ворота + Mark/Gate name + Название отметки/ворот Error updating race course: {0} - Ошибка при обновлении курса гонки: {0} + Ошибка при обновлении курса гонки: {0} Successfully updated race course - Курс гонки успешно обновлен + Курс гонки успешно обновлен Select two marks to create a gate - Выберите две отметки, чтобы создать ворота + Выберите две отметки, чтобы создать ворота Error trying to obtain the marks of the race course: {0} - Ошибка при попытке получить отметки курса гонки: {0} + Ошибка при попытке получить отметки курса гонки: {0} Error trying to obtain the race course: {0} - Ошибка при попытке получить курс гонки: {0} + Ошибка при попытке получить курс гонки: {0} Insert waypoint before selected - Вставка путевой точки перед выбранной + Вставить путевую точку перед выбранной Insert waypoint after selected - Вставка путевой точки после выбранной + Вставить путевую точку после выбранной Add Gate - Добавить ворота + Добавить ворота Low Point System - Система низких баллов + Система низких баллов High Point System - Система высоких баллов + Система высоких баллов High Point System ESS Overall (10) - Итоговая оценка ESS по системе высоких баллов (10) + Итоговая оценка ESS по системе высоких баллов (10) High Point System ESS Overall (12) - Итоговая оценка ESS по системе высоких баллов (12) + Итоговая оценка ESS по системе высоких баллов (12) Score in points. Winner of an act scores 12 points, 2nd - 11 points, .... Any tie in the overall Extreme Sailing Series is broken in favour of the competitor with the most Act wins. If that does not break the tie, the result in the last Act will be used. - Оценка в баллах. Победитель в акте получает 12 баллов, 2-й — 11 баллов, .... Любая ничья в общей оценке Extreme Sailing Series разрешается в пользу участника с наибольшим числом побед в актах. В случае равенства числа побед для разрешения используется последний акт. + Оценка в баллах. Победитель в акте получает 12 баллов, 2-й — 11 баллов, .... Любое равенство в общей оценке Extreme Sailing Series разрешается в пользу участника с наибольшим числом побед в актах. В случае равенства числа побед для разрешения используется последний акт. High Point System, Last Race Breaks Tie - Система высоких баллов, последняя гонка разрешает ничью + Система высоких баллов, равенство разрешается по последней гонке Low Point System, Winner Gets Zero - Система низких баллов, победитель получает ноль + Система низких баллов, победитель получает ноль High Point System, Winner Gets 5 Points - Система высоких баллов, победитель получает 5 баллов + Система высоких баллов, победитель получает 5 баллов High Point System, Winner Gets 6 Points - Система высоких баллов, победитель получает 6 баллов + Система высоких баллов, победитель получает 6 баллов High Point System, Winner Gets 8 Points - Система высоких баллов, победитель получает 8 баллов + Система высоких баллов, победитель получает 8 баллов High Point System, Winner 8 Points, Interpolated - Система высоких баллов, победитель 8 баллов, интерполяция + Система высоких баллов, победитель 8 баллов, интерполяция Low Point with Eliminations and Rounds, Winner Gets 0.7 Points - Низкий балл с турами на выбывание, победитель получает 0,7 балла + Низкий балл с турами на выбывание, победитель получает 0,7 балла A variant of the low-point scoring scheme, using eliminations consisting of rounds.\nEach round shall be modeled as a series in the regatta, and the fleet ordering counts backwards\nstarting with the highest number for the first round.\nIf a final and losers final take place, use 1 and 2 as their ordering. Start with 3 for the semi-final round. - Вариант схемы оценки с низкими баллами, использующий туры на выбывание.\nКаждый тур моделируется как серия в регате, и суда ранжируются в обратном порядке,\nначиная с наибольшего числа за первый раунд.\nВ финале и при определении 3-го места используйте ранги 1 и 2. Для полуфинального тура начните с 3. + Вариант схемы оценки с низкими баллами, использующий туры на выбывание.\nКаждый тур моделируется как серия в регате, и суда ранжируются в обратном порядке,\nначиная с наибольшего числа за первый раунд.\nВ финале и при определении 3-го места используйте ранги 1 и 2. Для полуфинального тура начните с 3. Low Point, ties broken by act point sum, then last act - Низкий балл, ничьи разрешаются по сумме актов, затем по последнему акту + Низкий балл, равенство разрешается по сумме актов, затем по последнему акту Instead of the usual count-back system used for low point schemes to break ties, competitors with equal point sums are compared by comparing the sum of the points they scored across all acts. If this still leaves a tie, the tie is broken by the last act. - Вместо обычной системы обратного отчета, используемой для разрешения ничьих в схемах низких баллов, участники с равными суммами баллов сравниваются по сумме баллов, полученных за все акты. Если равенство сохраняется, оно разрешается по последнему акту. + Вместо обычной системы обратного отчета, используемой для разрешения равенства в схемах низких баллов, участники с равными суммами баллов сравниваются по сумме баллов, полученных за все акты. Если равенство сохраняется, оно разрешается по последнему акту. High Point, winner gets 1 point - Высокий балл, победитель получает 1 балл + Высокий балл, победитель получает 1 балл High Point, winner gets 10 points - Высокий балл, победитель получает 10 баллов + Высокий балл, победитель получает 10 баллов High Point, winner gets 10 points (or 8) - Высокий балл, победитель получает 10 баллов (или 8) + Высокий балл, победитель получает 10 баллов (или 8) Score in points. Winner scores 10 points, 2nd - 9 points, .... A tie is broken in favour of the competitor having won most races, if tie remains then tie is broken by the rank in the last race scored. - Оценка в баллах. Победитель получает 10 баллов, 2-й — 9 баллов, .... Ничья разрешается в пользу участника, имеющего больше побед в гонках, если равенство сохраняется, оно разрешается по рангу в последней гонке. + Оценка в баллах. Победитель получает 10 баллов, 2-й — 9 баллов, .... Равенство разрешается в пользу участника, имеющего больше побед в гонках. Если равенство сохраняется, оно разрешается по рангу в последней гонке. High Point, winner gets 12 points (or 8) - Высокий балл, победитель получает 12 баллов (или 8) + Высокий балл, победитель получает 12 баллов (или 8) Score in points. Winner scores 12 points, 2nd - 11 points, .... A tie is broken in favour of the competitor having won most races, if tie remains then tie is broken by the rank in the last race scored. - Оценка в баллах. Победитель получает 12 баллов, 2-й — 11 баллов, .... Ничья разрешается в пользу участника, имеющего больше побед в гонках, если равенство сохраняется, оно разрешается по рангу в последней гонке. + Оценка в баллах. Победитель получает 12 баллов, 2-й — 11 баллов, .... Равенство разрешается в пользу участника, имеющего больше побед в гонках. Если равенство сохраняется, оно разрешается по рангу в последней гонке. Scoring system - Система оценок + Система оценок Create flexible leaderboard - Создать гибкую таблицу лидеров + Создать гибкую таблицу лидеров Create regatta leaderboard - Создать таблицу лидеров регаты + Создать таблицу лидеров регаты Create default regatta leaderboard - Создать таблицу лидеров регаты по умолчанию + Создать таблицу лидеров регаты по умолчанию Edit flexible leaderboard - Изменить гибкую таблицу лидеров + Изменить гибкую таблицу лидеров Edit regatta leaderboard - Изменить таблицу лидеров регаты + Изменить таблицу лидеров регаты Default regatta - Регата по умолчанию + Регата по умолчанию Use Overall leaderboard - Использовать итоговую таблицу лидеров + Использовать итоговую таблицу лидеров Overall Standings - Итоговые позиции + Итоговые позиции Type - Тип + Тип Add races - Добавить гонки + Добавить гонки races with name prefix - гонки с префиксом названия + гонки с префиксом названия Max SOG - Макс. SOG + Макс. SOG The competitor''s greatest speed over ground measured in the regatta. - Максимальная скорость участника относительно грунта в ходе данной регаты. + Максимальная скорость участника относительно грунта в ходе данной регаты. Total time downwind - Общее время по ветру + Общее время по ветру The total time the competitor sailed downwind in the regatta. - Общее время хода участника по ветру в данной регате. + Общее время хода участника по ветру в данной регате. Total time upwind - Общее время против ветра + Общее время против ветра The total time the competitor sailed upwind in the regatta. - Общее время хода участника против ветра в данной регате. + Общее время хода участника против ветра в данной регате. Total time reaching - Общее время полным ветром + Общее время полным ветром The total time the competitor sailed on reaching legs in the regatta. - Общее время хода участника полным ветром в данной регате. + Общее время хода участника полным ветром в данной регате. Total time - Общее время + Общее время The total time the competitor sailed in the regatta, counted from race start time. - Общее время хода участника в данной регате, отсчитываемое от времени старта регаты. + Общее время хода участника в данной регате, отсчитываемое от времени старта регаты. Total distance - Общее расстояние + Общее расстояние The total distance sailed in the regatta. - Общее пройденное расстояние в данной регате. + Общее пройденное расстояние в данной регате. ∅ SOG - ∅ SOG + ∅ SOG Average speed over ground across the regatta. - Средняя скорость относительно грунта в течение данной регаты. + Средняя скорость относительно грунта в течение данной регаты. hh:mm:ss - чч:мм:сс + чч:мм:сс Add wind data - Добавить данные о ветре + Добавить данные о ветре The value of ''{0}'' must be between {1} and {2}. - Значение ''{0}'' должно быть между {1} и {2}. + Значение ''{0}'' должно быть между {1} и {2}. The value of ''{0}'' must be greater than {1}. - Значение ''{0}'' должно быть больше {1}. + Значение ''{0}'' должно быть больше {1}. optional - необязательно + необязательно Please enter a value. - Введите значение. + Введите значение. Latitude - Широта + Широта Longitude - Долгота + Долгота Add Event - Добавить событие + Добавить событие Publication URL - URL-адрес публикации + URL-адрес публикации Suppress - Подавлять + Подавлять Is public - Общедоступно + Общедоступно Unsuppress - Отменить подавление + Отменить подавление Suppressed Competitors - Подавляемые участники + Подавляемые участники Audio & Video - Аудио и видео + Аудио и видео URL - URL-адрес + URL-адрес MIME Type - Тип MIME + Тип MIME Title - Название + Название Duration (h:mm:ss.ms) - Длительность (ч:мм:сс.мс) + Длительность (ч:мм:сс.мс) Add Audio/Video - Добавить аудио/видео + Добавить аудио/видео Add Youtube - Добавить Youtube + Добавить Youtube Youtube Id - Ид. Youtube + Ид. Youtube Do you really want to remove media track ''{0}''? - Действительно удалить объект мультимедиа ''{0}''? + Действительно удалить объект мультимедиа ''{0}''? Please select a series - Выберите серию + Выберите серию Please select a race - Выберите гонку + Выберите гонку Display groups in reverse order - Показывать группы в обратном порядке + Показывать группы в обратном порядке Configure URL - Настроить URL-адрес + Настроить URL-адрес Configure a leaderboard and obtain the resulting URL from the link - Настройте таблицу лидеров и получите разультирующий URL-адрес из ссылки + Настройте таблицу лидеров и получите разультирующий URL-адрес из ссылки Embedded - Внедрено + Внедрено Additional URL Settings - Дополнительные параметры URL-адреса + Дополнительные параметры URL-адреса Restrict to single selected race - Ограничить одной выбранной гонкой + Ограничить одной выбранной гонкой Expand selected race - Развернуть выбранную гонку + Развернуть выбранную гонку Allow for race details - Разрешить подробности гонки + Разрешить подробности гонки Number of races - Число гонок + Число гонок Please enter the number of last races to be displayed - Введите число последних гонок для просмотра + Введите число последних гонок для просмотра Choose the way you select races - Выберите способ выбора гонок + Выберите способ выбора гонок Select from all races - Выбрать из всех гонок + Выбрать из всех гонок Select a number of races - Выберите число гонок + Выберите число гонок Number of most current races to show - Число последних гонок для просмотра + Число последних гонок для просмотра Always expand last race shown - Всегда развертывать посленюю отображаемую гонку + Всегда развертывать посленюю отображаемую гонку Factor - Фактор + Фактор Error updating the medal race setting: {0} - Ошибка при обновлении параметра гонки на медали: {0} + Ошибка при обновлении параметра гонки на медали: {0} Maneuver loss - Потеря маневра + Потеря маневра ∅ Maneuver Loss - ∅ Потеря маневра + ∅ Потеря маневра The average distance loss during any mneuver. - Средняя потеря дистанции в ходе маневра. + Средняя потеря дистанции в ходе маневра. ∅ Tack Loss - ∅ Потеря на оверштаг + ∅ Потеря на оверштаг The average distance loss during tacks. - Средняя потеря дистанции при поворотах оверштаг. + Средняя потеря дистанции при поворотах оверштаг. ∅ Jibe Loss - ∅ Потеря на фордевинд + ∅ Потеря на фордевинд The average distance loss during jibes. - Средняя потеря дистанции при поворотах фордевинд. + Средняя потеря дистанции при поворотах фордевинд. Shape - Форма + Форма Pattern - Образец + Образец Legend - Условные знаки + Условные знаки live - активно + активно SwissTiming Archived Events - Архив событий SwissTiming + Архив событий SwissTiming Error fetching chart data: {0} - Ошибка при извлечении данных диаграммы: {0} + Ошибка при извлечении данных диаграммы: {0} Loading competitor data... - Загрузка данных участника... + Загрузка данных участника... {0} after race {1} - {0} после гонки {1} + {0} после гонки {1} Ranking - Ранг + Ранг Total points - Всего баллов + Общий балл Competitor''s total points in the race - Общий балл участника в данной гонке + Общий балл участника в данной гонке Competitor''s total points in the regatta - Общий балл участника в данной регате + Общий балл участника в данной регате Show rank chart - Показать диаграмму ранга + Показать диаграмму ранга Overall Leaderboard - Итоговая таблица лидеров + Итоговая таблица лидеров Error trying to obtain overall leaderboards: {0} - Ошибка при попытке получить итоговые таблицы лидеров: {0} + Ошибка при попытке получить итоговые таблицы лидеров: {0} finished - завершено + завершено upcoming - предстоит + предстоит Next - Далее + Далее in progress - выполняется + выполняется passed to {0} - передано {0} + передано {0} Gap Change - Изменение отставания + Изменение отставания The change of the gap to the leader since the last leg start.\nA negative number means that the gap was decreased;\na positive number means that it was increased. - Изменение отставания от лидера с момента начала последнего отрезка.\nОтрицательное число означает уменьшение отставания;\nположительное — увеличение. + Изменение отставания от лидера с момента начала последнего отрезка.\nОтрицательное число означает уменьшение отставания;\nположительное — увеличение. Mark rounded to - Обход отметки + Обход отметки Side to which the mark at leg start was rounded. - Сторона, с которой была обойдена отметка в начале отрезка. + Сторона, с которой была обойдена отметка в начале отрезка. Race {0} is live - Гонка {0} активна + Гонка {0} активна Races {0} are live - Гонки {0} активны + Гонки {0} активны Regatta {0} is live - Регата {0} активна + Регата {0} активна Regattas {0} are live - Регаты {0} активны + Регаты {0} активны kts - уз + уз {0,number,#0.0} kts - {0,number,#0.0} уз + {0,number,#0.0} уз {0,number,#0.0} - {1,number,#0.0} kts - {0,number,#0.0}–{1,number,#0.0} уз + {0,number,#0.0}–{1,number,#0.0} уз Generate Polar Sheet - Генерировать полярные данные + Генерировать полярные данные Polar Diagrams - Полярные диаграммы + Полярные диаграммы Polar Sheet - Полярные данные + Полярные данные Minimum datapoints per graph - Минимум точек данных на граф + Минимум точек данных на граф All graphs with less underlying datapoints are not shown visually.\nThis way there are no distracting diagrams shown for\nwindspeeds lacking a sufficient amount of data. - Все графы с меньшим объемом базовых данных не визуализируются.\nЭто устраняет отображение диаграмм\nпо скоростям ветра с недостаточным объемом данных. + Все графы с меньшим объемом базовых данных не визуализируются.\nЭто устраняет отображение диаграмм\nпо скоростям ветра с недостаточным объемом данных. Minimum datapoints per angle - Минимум точек данных на угол + Минимум точек данных на угол In each graph only data about angles with at least the given amount\nof underlying datapoints will be shown. Insignificant\nparts of the diagram can be hidden using this threshold. - В каждом графе будут показаны только данные об углах с объемом\nбазовых данных не меньше указанного. Этот порог\n позволяет скрыть незначимые части диаграммы. + В каждом графе будут показаны только данные об углах с объемом\nбазовых данных не меньше указанного. Этот порог\n позволяет скрыть незначимые части диаграммы. Minimum wind confidence value - Минимальное значение достоверности ветра + Минимальное значение достоверности ветра Only fixes with wind having a confidence over the given threshold are\nused. This confidence is between 0 and 1. - Учитываются только замеры ветра с достоверностью выше указанного порога.\nДостоверность изменяется от 0 до 1. + Учитываются только замеры ветра с достоверностью выше указанного порога.\nДостоверность изменяется от 0 до 1. Minimum confidence measure - Минимальный показатель достоверности + Минимальный показатель достоверности The minimum confidence measure for a point shown in the graph.\nIf it''s below the given threshold, the point won''t show.\nThe measure is between 0 and 1 and determined\nusing statistical measures. - Минимальный показатель достоверности для точки на графе.\nЕсли оно ниже указанного порога, точка не отображается.\nПоказатель изменяется от 0 до 1 и определяется\nпо статистическим показателям. + Минимальный показатель достоверности для точки на графе.\nЕсли оно ниже указанного порога, точка не отображается.\nПоказатель изменяется от 0 до 1 и определяется\nпо статистическим показателям. Use only wind gauge data for wind speed - Использовать для скорости ветра только данные анемометра + Использовать для скорости ветра только данные анемометра By taking only the speed data of wind gauges into account\nwe exclude guessed manually entered wind speeds, which can falsify the polar diagram. - Учитывая только данные скорости с датчиков ветра,\nмы исключаем скорости ветра, вводимые вручную на глаз, способные исказить полярную диаграмму. + Учитывая только данные скорости с датчиков ветра,\nмы исключаем скорости ветра, вводимые вручную на глаз, способные исказить полярную диаграмму. Use only estimation for wind direction - Использовать только расчетное направление ветра + Использовать только расчетное направление ветра By using the accurate wind estimation for the wind direction,\nwe fix polar diagrams that are rotated due to invalid wind\ngauge data. - Используя точный расчет направления ветра,\nмы исправляем полярные диаграммы, повернутые из-за неверных данных\nдатчика ветра. + Используя точный расчет направления ветра,\nмы исправляем полярные диаграммы, повернутые из-за неверных данных\nдатчика ветра. Remove outliers (Distance based approach) - Удалять выбросы (подход на базе дистанции) + Удалять выбросы (подход на базе дистанции) Outliers are removed by a distance based approach.\nThe parameters can be adjusted below. - Выбросы удаляются на основании дистанции.\nПараметры можно настроить ниже. + Выбросы удаляются на основании дистанции.\nПараметры можно настроить ниже. Outlier detection neighborhood radius - Радиус обнаружения выбросов + Радиус обнаружения выбросов The radius used in the distance based outlier\ndetection algorithm. - Радиус, используемый в алгоритме обнаружения\nвыбросов на базе дистанции. + Радиус, используемый в алгоритме обнаружения\nвыбросов на базе дистанции. Minimum outlier detection neighborhood percentage - Минимальный процент окрестности обнаружения выбросов + Минимальный процент окрестности обнаружения выбросов The percentage of neightborhood datapoints / overall datapoints\nneeds to be at less than the given value for the point to be considered an outlier. - Процент точек данных окрестности от общего числа точек данных\nдолжен быть не меньше указанного значения, чтобы считать точку выбросом. + Процент точек данных окрестности от общего числа точек данных\nдолжен быть не меньше указанного значения, чтобы считать точку выбросом. Number of histogram columns per wind range - Число столбцов гистограммы на диапазон ветра + Число столбцов гистограммы на диапазон ветра Wind stepping in knots - Скорость ветра в узлах + Скорость ветра в узлах Max. distance to wind level - Макс. дистанция до уровня ветра + Макс. дистанция до уровня ветра Sets a max radius for which fixes will be assigned to the wind levels. - Задает максимальный радиус, замеры для которого будут присваиваться уровням ветра. + Задает максимальный радиус, замеры для которого будут присваиваться уровням ветра. Split Windsources - Разделить источники ветра + Разделить источники ветра Splitting Windsources allows a better search for bad wind gauges.\nThe statistical correctness cannot be guaranteed. The same fix can\ncount into the resulting polar sheet multiple times (for different wind gauges). - Разделение источников ветра улучшает поиск поврежденных датчиков ветра.\nСтатистическая корректность не гарантируется. Один и тот же замер\nможет быть учтен в результирующих полярных данных несколько раз (для разных датчиков). + Разделение источников ветра улучшает поиск поврежденных датчиков ветра.\nСтатистическая корректность не гарантируется. Один и тот же замер\nможет быть учтен в результирующих полярных данных несколько раз (для разных датчиков). Apply minimum wind confidence - Применить минимальную достоверность ветра + Применить минимальную достоверность ветра If checked the value in the box above will be used as the minimum valid wind confidence during generation. - Если установлено, указанное выше значение будет использоваться как минимально допустимая достоверность ветра при генерировании. + Если установлено, указанное выше значение будет использоваться как минимально допустимая достоверность ветра при генерировании. Distribution in the wind range. - Распределение в диапазоне ветра. + Распределение в диапазоне ветра. Please hover over the options to view tooltips where available. - Наведите указатель на параметр, чтобы просмотреть всплывающую подсказку (если доступна). + Наведите указатель на параметр, чтобы просмотреть всплывающую подсказку (если доступна). Names of the members of this competitor. - Имена членов данного участника. + Имена членов данного участника. Shows the flag of the country and the sail number of the competitor. - Показывает флаг страны и хвостовой номер участника. + Показывает флаг страны и хвостовой номер участника. The overall rank of this competitor in the regatta. - Общий ранг данного участника в регате. + Общий ранг данного участника в регате. Points carried over from previous races. - Баллы, перенесенные из других гонок. + Баллы, перенесенные из других гонок. The total of all points of a competitor in the regatta.\nSorting based on rank, not points. - Общая сумма всех баллов участника в регате.\nСортировка по рангу, не по баллам. + Общая сумма всех баллов участника в регате.\nСортировка по рангу, не по баллам. Wind data - Данные о ветре + Данные о ветре GPS data - Данные GPS + Данные GPS Status - Статус + Статус No data found - Данные не найдены + Данные не найдены Display name - Отображаемое имя + Отображаемое имя Histogram - Гистограмма + Гистограмма Number of data points - Число точек данных + Число точек данных Angle: {0}; Total number of data-points: {1}; Coefficiant of Variation: {2}; Confidence Measure: {3} - Угол: {0}; общее число точек данных: {1}; коэффициент вариации: {2}; показатель достоверности: {3} + Угол: {0}; общее число точек данных: {1}; коэффициент вариации: {2}; показатель достоверности: {3} Buoy zone - Зона буя + Зона буя Radius (m) - Радиус (м) + Радиус (м) Filter regattas by name - Фильтровать регаты по названию + Фильтровать регаты по названию There are no regattas yet. - Регат еще нет. + Регат еще нет. Add course area - Добавить зону гонки + Добавить зону гонки Course areas - Зоны гонок + Зоны гонок Course area - Зона гонки + Зона гонки Please enter a course area. - Введите зону гонки. + Введите зону гонки. Please select a course area. - Выберите зону гонки. + Выберите зону гонки. Upper flag - Верхний флаг + Верхний флаг Lower flag - Нижний флаг + Нижний флаг This is a regatta leaderboard. You can only add races to this leaderboard directly in the regatta definition. - Это таблица лидеров регаты. Вы можете добавлять гонки в эту таблицу лидеров только непосредственно в определении регаты. + Это таблица лидеров регаты. Вы можете добавлять гонки в эту таблицу лидеров только непосредственно в определении регаты. Correct Score - Правильная оценка + Правильная оценка Correct score for competitor {0} in race {1} - Правильная оценка для участника {0} в гонке {1} + Правильная оценка для участника {0} в гонке {1} Penalty/Redress - Взыскание/компенсация + Взыскание/компенсация Net points - Точки сети + Точки сети Time point must be after start of tracking. - Отметка времени должна быть после начала отслеживания. + Отметка времени должна быть после начала отслеживания. Time point must be before end of tracking. - Отметка времени должна быть раньше окончания отслеживания. + Отметка времени должна быть раньше окончания отслеживания. Regatta used for the tracked race - Регата, используемая для отслеживаемой гонки + Регата, используемая для отслеживаемой гонки TracTrac Server Update URI - URI обновления сервера TracTrac + URI обновления сервера TracTrac Passing instructions - Передача инструкций + Передача инструкций Refresh now - Обновить сейчас + Обновить сейчас Stop updating - Прекратить обновление + Прекратить обновление Start updating - Начать обновление + Начать обновление Current time: - Текущее время: + Текущее время: TracTrac username - Имя пользователя TracTrac + Имя пользователя TracTrac TracTrac password - Пароль TracTrac + Пароль TracTrac Equals - Равно + Равно Not equals to - Не равно + Не равно Less than - Меньше + Меньше Less or equals - Меньше или равно + Меньше или равно Greater than - Больше + Больше Greater or equals - Больше или равно + Больше или равно Contains - Содержит + Содержит Not Contains - Не содержит + Не содержит Starts with - Начинается с + Начинается с Ends with - Заканчивается на + Заканчивается на Nationality - Гражданство + Гражданство The number must be greater than zero. - Число должна быть больше нуля. + Число должна быть больше нуля. Distance to line at Start - Дистанция до линии при старте + Дистанция до линии при старте Distance to line 5s before start - Дистанция до линии за 5 с до старта + Дистанция до линии за 5 с до старта SOG at Start - SOG при старте + SOG при старте SOG 5s before start - SOG за 5 с до старта + SOG за 5 с до старта SOG when Starting - SOG при старте + SOG при старте Distance to start line at race start time - Дистанция до линии старта во время старта гонки + Дистанция до линии старта во время старта гонки Distance to start line five seconds before the start - Дистанция до линии старта за пять секунд до старта + Дистанция до линии старта за пять секунд до старта Speed over ground at race start time - Скорость относительно грунта во время старта гонки + Скорость относительно грунта во время старта гонки Speed over ground five seconds before the start - Скорость относительно грунта за пять секунд до старта + Скорость относительно грунта за пять секунд до старта Speed over ground when the competitor passed the start - Скорость относительно грунта при прохождении старта участником + Скорость относительно грунта при прохождении старта участником Start Tack - Стартовый галс + Стартовый галс Tack on which the competitor started - Галс, на котором стартовал участник + Галс, на котором стартовал участник Port - Левый + Левый Starboard - Правый + Правый Port - Левый + Левый Starboard - Правый + Правый Please enter a number. - Введите число. + Введите число. Competitors filter - Фильтр участников + Фильтр участников Flag status - Статус флага + Статус флага Info - Инфо + Инфо Wind Fixes (first 3, last 3, true values) - Замеры ветра (первые 3, последние 3, истинные значения) + Замеры ветра (первые 3, последние 3, истинные значения) No Wind-Fixes available. - Нет доступных замеров ветра. + Нет доступных замеров ветра. Dist. Stb-Side of Line when Starting - До правой стороны линии при старте + До правой стороны линии при старте Distance to starboard side of start line or to the single start mark when the competitor passed the start - Дистанция до правой стороны линии старта или отметки старта при прохождении старта участником + Дистанция до правой стороны линии старта или отметки старта при прохождении старта участником Add filter - Добавить фильтр + Добавить фильтр Edit filter - Изменить фильтр + Изменить фильтр Available filters - Доступные фильтры + Доступные фильтры Create a filter set to filter the list of competitors. - Создание набора фильтров для фильтрации списка участников. + Создание набора фильтров для фильтрации списка участников. A filter with this name already exists. - Фильтр с таким названием уже существует. + Фильтр с таким названием уже существует. Add at least one filter criteria. - Добавьте минимум один критерий фильтра. + Добавьте минимум один критерий фильтра. Filter criteria - Критерии фильтра + Критерии фильтра Select a filter criteria - Выберите критерии фильтра + Выберите критерии фильтра Filter name - Название фильтра + Название фильтра operator - оператор + оператор value - значение + значение The nationality must be a two letter ISO code or a three letter IOC code. - Гражданство должно быть указан двузначным кодом ISO или трехзначным кодом IOC. + Гражданство должно быть указан двузначным кодом ISO или трехзначным кодом IOC. Race rank - Ранг гонки + Ранг гонки Waypoints - Путевые точки + Путевые точки Disable filter - Отключить фильтр + Отключить фильтр Enable filter - Включить фильтр + Включить фильтр Race Status - Статус гонки + Статус гонки Loading (on a slow internet connection this can take some minutes)... - Загрузка (при медленном подключении к Интернету может занять несколько минут)... + Загрузка (при медленном подключении к Интернету может занять несколько минут)... Races with state HIDDEN - Гонки с состоянием СКРЫТО + Гонки с состоянием СКРЫТО Invalid date format. Use: {0} - Недопустимый формат даты. Используйте формат: {0} + Недопустимый формат даты. Используйте формат: {0} Selected competitors - Выбранные участники + Выбранные участники Filter nothing - Не фильтровать + Не фильтровать Race Top {0} - Первые {0} в гонке + Первые {0} в гонке Total Top {0} - Первые {0} в общем зачете + Первые {0} в общем зачете Simulator - Симулятор + Симулятор Optionsbar - Панель параметров + Панель параметров Predict - Прогноз + Прогноз Setup - Настройка + Настройка Strength (in kn) - Сила (кН) + Сила (кН) Oscillation - Колебание + Колебание Oscillating - Колебания + Колебания Amplitude(in Grad) - Амплитуда (в град.) + Амплитуда (в град.) Frequency(in 1/h) - Частота (1/ч) + Частота (1/ч) Noise - Шум + Шум Sailing - Плавание + Плавание Optimality - Оптимум + Оптимум Maximum Turns - Максимум поворотов + Максимум поворотов Race Direction - Направление гонки + Направление гонки Upwind - Против ветра + Против ветра Downwind - По ветру + По ветру Strategies - Стратегии + Стратегии Omniscient - Всезнайка + Всезнайка Opportunist - Авантюрист + Авантюрист Mixed - Смешанная + Смешанная Maps - Карты + Карты Start-End - Начало-конец + Начало-конец Course - Курс + Курс Summary - Сводка + Сводка Replay - Воспроизвести + Воспроизвести Display - Просмотреть + Просмотреть Polar diagram - Полярная диаграмма + Полярная диаграмма Show - Показать + Показать hide - скрыть + скрыть Left wind - Ветер слева + Ветер слева Right wind - Ветер справа + Ветер справа strategyOption1 - strategyOption1 + strategyOption1 strategyOption2 - strategyOption2 + strategyOption2 strategyOption3 - strategyOption3 + strategyOption3 Leg - Этап + Этап Race - Гонка + Гонка Competitor - Участник + Участник Keep Bearings - Держать направление + Держать направление What-If Course - Подбирать курс + Подбирать курс Edit Regatta... - Изменить регату... + Изменить регату... Please select a sailing event... - Выбрать событие... + Выбрать событие... Please select a course area.. - Выбрать зону гонки... + Выбрать зону гонки... Area overview - Обзор зоны + Обзор зоны Course design overview - Обзор схемы курса + Обзор схемы курса Regatta overview - Обзор регаты + Обзор регаты Additional settings - Дополнительные параметры + Дополнительные параметры Show following course areas - Показать следующие зоны гонок + Показать следующие зоны гонок Show following regattas - Показать следующие регаты + Показать следующие регаты Show only races of same day - Показать только гонки того же дня + Показать только гонки того же дня Show currently running races - Показать текущие гонки + Показать текущие гонки Series defines result discarding rules - Серия определяет правила исключения результатов + Серия определяет правила исключения результатов Add series - Добавить серию + Добавить серию Regatta defines result discarding rules - Регата определяет правила исключения результатов + Регата определяет правила исключения результатов Series starts with zero score - Серия начинается с нулевым баллом + Серия начинается с нулевым баллом Master Data Import - Импорт мастер-данных + Импорт мастер-данных Success\nCreated {0} leaderboard group(s) and {1} leaderboard(s) and {2} event(s) and {3} regatta(s). - Спех\nСоздано групп таблиц лидеров: {0}, таблиц лидеров: {1}, событий: {2}, регат: {3}. + Успех\nСоздано групп таблиц лидеров — {0}, таблиц лидеров — {1}, событий — {2}, регат — {3}. Had to overwrite the following regattas, you will need to retrack their races: - Следующие регаты перезаписаны, потребуется повторное отслеживанием их гонок: + Следующие регаты перезаписаны, потребуется повторное отслеживанием их гонок: Regatta Overview Configuration - Конфигурация обзора регаты + Конфигурация обзора регаты Starts with non-discardable carry-forward - Начинается с неисключаемого переноса + Начинается с неисключаемого переноса Connect to replication master server - Подключиться к главному серверу репликации + Подключиться к главному серверу репликации Connect - Подключиться + Подключиться Connect to Master - Подключиться к главному серверу + Подключиться к главному серверу Messaging Hostname - Имя хоста обмена сообщениями + Имя хоста обмена сообщениями Hostname where the messaging queue is running on. - Имя хоста, на котором выполняется очередь сообщений. + Имя хоста, на котором выполняется очередь сообщений. Hostname where the replication master is running on. - Имя хоста, на котором выполняется главный сервер репликации. + Имя хоста, на котором выполняется главный сервер репликации. The name of the exchange queue. It must be the same as configured in master. - Имя очереди Exchange. Должно совпадать с указанным на главном сервере. + Имя очереди Exchange. Должно совпадать с указанным на главном сервере. The port of the messaging exchange queue. This is used for all data after initial load. - Порт очереди обмена сообщениями Exchange. Используется для всех данных после начальной загрузки. + Порт очереди обмена сообщениями Exchange. Используется для всех данных после начальной загрузки. Servlet port number that is used to connect to register this replica and receive initial load. - Номер порта сервлета, используемый для подключения для регистрации данной реплики и получения начальной загрузки. + Номер порта сервлета, используемый для подключения для регистрации данной реплики и получения начальной загрузки. Replicas registered (this server is master): - Зарегистрированные реплики (это главный сервер): + Зарегистрированные реплики (это главный сервер): Connections to a master (this server is a replica): - Подключение к главному серверу (это сервер реплики): + Подключение к главному серверу (это сервер реплики): No connections to a master server configured. Click the Connect to Master button to configure one. - Подключения к главному серверу не настроены. Нажмите кнопку "Подключиться к главному серверу", чтобы настроить подключение. + Подключения к главному серверу не настроены. Нажмите кнопку "Подключиться к главному серверу", чтобы настроить подключение. No connections from a replica found. You can connect replicas by using their AdminConsole. - Подключения от реплик не найдены. Реплики можно подключить с помощью их консоли администрирования. + Подключения от реплик не найдены. Реплики можно подключить с помощью их консоли администрирования. Stop Replication - Остановить репликацию + Остановить репликацию As link - Как ссылка + Как ссылка Pending - Ожидание + Ожидание Running - Выполнение + Выполнение Finishing - Завершение + Завершение Finished - Завершено + Завершено Scheduled - Запланировано + Запланировано Start phase - Стартовая фаза + Стартовая фаза General recall - Общий отзыв + Общий отзыв Postponed - Отложено + Отложено Abandoned - Отменено + Отменено Drop connection to all replicas (Dangerous!) - Сбросить подключение ко всем репликам (Опасно!) + Сбросить подключение ко всем репликам (Опасно!) ATTENTION: This server is configured to be a replica. Do NOT change anything or this will lead to strange effects! - ВНИМАНИЕ: этот сервер настроен как реплика. НЕ изменяйте ничего, иначе возможны непредвиденные последствия! + ВНИМАНИЕ: этот сервер настроен как реплика. НЕ изменяйте ничего, иначе возможны непредвиденные последствия! Drop connection - Сбросить подключение + Сбросить подключение Time Control - Управление временем + Управление временем The filter will show all competitors matching the criterias: - Фильтр покажет всех участников, отвечающих следующим критериям: + Фильтр покажет всех участников, отвечающих следующим критериям: Protest time - Время протеста + Время протеста ends - заканчивается + заканчивается ended - закончилось + закончилось Flags - Флаги + Флаги Last update - Последнее обновление + Последнее обновление No more racing today - Больше никаких гонок сегодня + Больше никаких гонок сегодня Further signals ashore - Дальнейшие сигналы на суше + Дальнейшие сигналы на суше Early starters - Рано стартовавшие + Рано стартовавшие Showing details for race {0} - Отображение подробностей для гонки {0} + Отображение подробностей для гонки {0} Start at - Старт в + Старт в Finish at - Финиш в + Финиш в Protest ends at - Протест заканчивается в + Протест заканчивается в Unknown - Неизвестно + Неизвестно Race log - Журнал гонки + Журнал гонки Regatta log - Журнал регаты + Журнал регаты Refresh race log - Обновить журнал гонки + Обновить журнал гонки Race log successfully reloaded from database - Журнал гонки успешно перезагружен из базы данных + Журнал гонки успешно перезагружен из базы данных Reload all race logs - Перезагрузить все журналы гонок + Перезагрузить все журналы гонок Export - Экспорт + Экспорт Sideline - Боковая линия + Боковая линия Flag displayed - eight minutes till start - the startprocedure is a gate start - Флаг установлен – восемь минут до старта – процедура старта через ворота + Флаг установлен – восемь минут до старта – процедура старта через ворота Flag displayed - five minutes till start - Флаг установлен – пять минут до старта + Флаг установлен – пять минут до старта Start mode flag - four minutes till start - Флаг стартового режима – четыре минуты до старта + Флаг стартового режима – четыре минуты до старта Start mode flag - disqualification for early starters - four minutes till start - Флаг стартового режима – дисквалификация для рано стартовавших – четыре минуты до старта + Флаг стартового режима – дисквалификация для рано стартовавших – четыре минуты до старта Start mode flag - penalties for early starters - four minutes till start - Флаг стартового режима – штрафы для рано стартовавших – четыре минуты до старта + Флаг стартового режима – штрафы для рано стартовавших – четыре минуты до старта Start mode flag - one minute till start - Флаг стартового режима – одна минута до старта + Флаг стартового режима – одна минута до старта Start mode flag - disqualification for early starters - one minute till start - Флаг стартового режима – дисквалификация для рано стартовавших – одна минута до старта + Флаг стартового режима – дисквалификация для рано стартовавших – одна минута до старта Start mode flag - start with penalties for early starters - one minute till start - Флаг стартового режима – старт со штрафами для рано стартовавших – одна минута до старта + Флаг стартового режима – старт со штрафами для рано стартовавших – одна минута до старта Flag removed - race starting signal - Флаг снят – сигнал о старте гонки + Флаг снят – сигнал о старте гонки Individual recall - had early starters - Индивидуальный отзыв – были рано стартовавшие + Индивидуальный отзыв – были рано стартовавшие General recall - had too many early starters - start procedure will be repeated - Общий отзыв – слишком много рано стартовавших – процедура старта будет повторена + Общий отзыв – слишком много рано стартовавших – процедура старта будет повторена Flag displayed - first competitor passed the finishing line - Флаг установлен – первый участник перешел финишную линию + Флаг установлен – первый участник перешел финишную линию Flag removed - last competitor passed the finishing line or the finish time limit is passed - Флаг снят – последний участник перешел финишную линию либо истек лимит времени финиширования + Флаг снят – последний участник перешел финишную линию либо истек лимит времени финиширования Flag removed - gate is closed - Флаг снят – ворота закрыты + Флаг снят – ворота закрыты Flag displayed - start postponed - no more racing today - Флаг установлен – старт отложен – больше никаких гонок сегодня + Флаг установлен – старт отложен – больше никаких гонок сегодня Flag displayed - start postponed - further signals ashore - Флаг установлен – старт отложен – дальнейшие сигналы на суше + Флаг установлен – старт отложен – дальнейшие сигналы на суше Flag displayed - start postponed - Флаг установлен – старт отложен + Флаг установлен – старт отложен Flag removed - start procedure starts in one minute - Флаг снят – запуск процедуры старта через одну минуту + Флаг снят – запуск процедуры старта через одну минуту Flag displayed - race abandoned - no more racing today - Флаг показан – гонка отменена – больше никаких гонок сегодня + Флаг показан – гонка отменена – больше никаких гонок сегодня Flag displayed - race abandoned - further signals ashore - Флаг установлен – гонка отменена – дальнейшие сигналы на суше + Флаг установлен – гонка отменена – дальнейшие сигналы на суше Flag displayed - race abandoned - Флаг установлен – гонка отменена + Флаг установлен – гонка отменена Protest time started - Время протеста началось + Время протеста началось Protest time ended - Время протеста закончилось + Время протеста закончилось Three minutes till start - Три минуты до старта + Три минуты до старта Two minutes till start - Две минуты до старта + Две минуты до старта One minute till start - Одна минута до старта + Одна минута до старта Race starting signal - Сигнал о старте гонки + Сигнал о старте гонки Data from before/after race - Данные из до/после гонки + Данные из до/после гонки All fixes (including outliers) - Все замеры (включая выбросы) + Все замеры (включая выбросы) Remote host: - Удаленный хост: + Удаленный хост: Fetch Leaderboard Group List - Извлечь список группы таблиц лидеров + Извлечь список групп таблиц лидеров Not a valid URL for fetching leaderboardgroups: {0} - Недействительный URL-адрес для извлечения групп таблиц лидеров: {0} + Недействительный URL-адрес для извлечения групп таблиц лидеров: {0} GET leaderboardgroups request failed for host {0} with error {1} - Сбой GET-запроса групп таблиц лидеров для хоста {0} с ошибкой {1} + Сбой GET-запроса групп таблиц лидеров для хоста {0} с ошибкой {1} No data was returned by the server - Сервер не вернул никаких данных + Сервер не вернул никаких данных GET leaderboardgroups request failed with Server error. Please see server logs for more information. - Сбой GET-запроса групп таблиц лидеров с ошибкой сервера. Дополнительные сведения см. в журналах сервера. + Сбой GET-запроса групп таблиц лидеров с ошибкой сервера. Дополнительные сведения см. в журналах сервера. Leaderboard Groups: - Группы таблиц лидеров: + Группы таблиц лидеров: Override existing data if names and ids match - Переопределять существующие данные при совпадении названий и идентификаторов + Переопределять существующие данные при совпадении названий и идентификаторов Import selected leaderboard groups - Импорт выбранных групп таблиц лидеров + Импорт выбранных групп таблиц лидеров You have to select at least one leaderboard group to import - Выберите не менее одной группы таблиц лидеров для импорта + Выберите не менее одной группы таблиц лидеров для импорта Deselect all - Отменить все + Отменить все Polar Diagram - Полярная диаграмма + Полярная диаграмма GPS Lag - Задержка GPS + Задержка GPS Ratio between time since last received GPS position and average sampling interval - Отношение между временем с момента последнего получения позиции GPS и средним интервалом дискретизации + Отношение между временем с момента последнего получения позиции GPS и средним интервалом дискретизации Tracking quality - Качество отслеживания + Качество отслеживания Leg Type - Тип отрезка + Тип отрезка Sail Number - Номер на парусе + Номер на парусе Series leaderboard - Таблица лидеров серии + Таблица лидеров серии Regatta leaderboards - Таблицы лидеров регаты + Таблицы лидеров регаты Clear Selection - Очистить выбор + Очистить выбор Running - Выполнение + Выполнение Run - Выполнить + Выполнить Done - Готово + Готово Last finished - Последний финиш + Последний финиш Run - Выполнить + Выполнить times - раз + раз Data amount - Объем данных + Объем данных ∅ Cleaned Server Time - ∅ Очищенное время сервера + ∅ Очищенное время сервера ∅ Cleaned Overall Time - ∅ Очищенное общее время + ∅ Очищенное общее время Data Mining Benchmark Results - Результаты сравнительного анализа добычи данных + Результаты сравнительного анализа добычи данных Server Time - Время сервера + Время сервера Confidence values need to be between 0 and 1. - Значения достоверности должны быть между 0 и 1 + Значения достоверности должны быть между 0 и 1 Minimum data size values cannot be negative. - Значения минимального размера данных не могут быть отрицательными. + Значения минимального размера данных не могут быть отрицательными. The number of histogram columns needs to be more than one. - Число столбцов гистограммы должно быть больше единицы. + Число столбцов гистограммы должно быть больше единицы. The outlier radius needs to be a positive number. - Радиус выбросов должен быть положительным числом. + Радиус выбросов должен быть положительным числом. The outlier neighborhood percentage needs to be positive and max. 1. - Процент окрестности выброса должен быть положительным и не более 1. + Процент окрестности выброса должен быть положительным и не более 1. Parameter fields cannot be empty. - Поля параметров не могут быть пустыми. + Поля параметров не могут быть пустыми. Error occured while trying to add\na new series to the chart. - Ошибка при попытке добавить\nновую серию в диаграмму. + Ошибка при попытке добавить\nновую серию в диаграмму. Generating Polar Sheet... - Генерирование полярных данных... + Генерирование полярных данных... Data Size - Размер данных + Размер данных Generation finished! - Генерирование завершено! + Генерирование завершено! Generating - Генерирование + Генерирование Remove all sheets - Удалить все листы + Удалить все листы Remove all - Удалить все + Удалить все Select sheet - Выбрать лист + Выбрать лист Cleaned Server Time - Очищенное время сервера + Очищенное время сервера Overall Time - Общее время + Общее время Cleaned Overall Time - Очищенное общее время + Очищенное общее время Data Mining Result - Результат добычи данных + Результат добычи данных Group by - Группировать по + Группировать по Calculate statistic - Рассчитать статистику + Рассчитать статистику Worked through {0} data entries in {1} seconds - Обработано {0} зап. данных за {1} с + Обработано {0} зап. данных за {1} с No query selected - Не выбран запрос + Не выбран запрос Run automatically - Выполнить автоматически + Выполнить автоматически Upload - Отправить + Отправить Import Wind from Expedition - Импортировать ветер из Expedition + Импортировать ветер из Expedition Boat Id: - Ид. лодки: + Ид. лодки: Do you really want to import wind into the selected {0} races?\nOnly races with applicable time window will be affected anyway. - Вы действительно хотите импортировать ветер в выбранные гонки ({0})?\nЗатронуты будут только гонки с применимым окном времени. + Вы действительно хотите импортировать ветер в выбранные гонки ({0})?\nЗатронуты будут только гонки с применимым окном времени. Do you really want to import wind into all races?\nOnly races with applicable time window will be affected anyway. - Вы действительно хотите импортировать ветер во все гонки ()?\nЗатронуты будут только гонки с применимым окном времени. + Вы действительно хотите импортировать ветер во все гонки ()?\nЗатронуты будут только гонки с применимым окном времени. Imported wind from: {0} to: {1}. {2} races affected. - Импортирован ветер из {0} в {1}. Затронуто гонок: {2}. + Импортирован ветер из {0} в {1}. Затронуто гонок: {2}. Race: {0} ({1}) - {2} fixes from: {3} to: {4} - Гонка: {0} ({1}) – {2} зам. из: {3} в: {4} + Гонка: {0} ({1}) – {2} зам. из: {3} в: {4} Error: {0} - Ошибка: {0} + Ошибка: {0} Toggles the visibility of the wind speed for all data sources - Переключает видимость скорости ветра для всех источников данных + Переключает видимость скорости ветра для всех источников данных Toggles the visibility of the wind direction for all data sources - Переключает видимость направления ветра для всех источников данных + Переключает видимость направления ветра для всех источников данных Combines all other wind sources to one data set - Объединяет все другие источники ветра в один набор данных + Объединяет все другие источники ветра в один набор данных A value based on the course layout (under the assumption, that the race starts with an upwind leg) - Значение, основанное на схеме курса (предполагается, что гонка начинается против ветра) + Значение, основанное на схеме курса (предполагается, что гонка начинается против ветра) Calculated data from the competitor tracks - Расчетные данные из отслеживаний участника + Расчетные данные из отслеживаний участника Data captured from mobile measuring systems - Данные из мобильных измеряющих систем + Данные из мобильных измеряющих систем Manually set data values - Вручную установить значения данных + Вручную установить значения данных Data manually set from the race officers - Данные, заданные вручную персоналом гонки + Данные, заданные вручную персоналом гонки Sheet name - Название листа + Название листа You can name your polar sheet using the text field.\nA name will be generated using the boat class\nif no name is provided. - В этом текстовом поле можно указать название для\nлиста полярных данных. Если имя не указано,\nоно будет сгенерировано на базе класса лодки. + В этом текстовом поле можно указать название для\nлиста полярных данных. Если имя не указано,\nоно будет сгенерировано на базе класса лодки. Time Downwind - Время по ветру + Время по ветру Time Reaching - Время полным ветром + Время полным ветром Time Upwind - Время против ветра + Время против ветра Total time traveled in this race, starting to measure when competitor passes start waypoint\n(not when the race starts) - Общее время хода в данной гонке, отсчитываемое с момента прохождения участником стартовой путевой точки\n(не от старта гонки) + Общее время хода в данной гонке, отсчитываемое с момента прохождения участником стартовой путевой точки\n(не от старта гонки) Total time traveled downwind in this race - Общее время хода по ветру в данной гонке + Общее время хода по ветру в данной гонке Total time traveled reaching in this race - Общее время хода полным ветром в данной гонке + Общее время хода полным ветром в данной гонке Total time traveled upwind in this race - Общее время хода против ветра в данной гонке + Общее время хода против ветра в данной гонке No statistic to calculate is selected - Не выбрана статистика для расчета + Не выбрана статистика для расчета The grouper script is empty - Скрипт группирования пуст + Скрипт группирования пуст No dimension to group by is selected - Не выбрано базовое измерение для группировки + Не выбрано базовое измерение для группировки No grouper type is selected - Не выбран тип группирования + Не выбран тип группирования No data retriever is selected - Не выбрано средство извлечения данных + Не выбрано средство извлечения данных No query possible, because - Запрос невозможен, так как + Запрос невозможен, так как Data Mining - Добыча данных + Добыча данных An error occured running the query - При выполнении запроса возникла ошибка + При выполнении запроса возникла ошибка Hide toolbar - Скрыть панель инструментов + Скрыть панель инструментов Show series leaderboards - Показать таблицы лидеров серии + Показать таблицы лидеров серии Show overall leaderboard - Показать итоговую таблицу лидеров + Показать итоговую таблицу лидеров length: {0}m\nangle to wind: {1}°\n{2} side has {3}m advantage - длина: {0} м\nугол к ветру: {1}°\n{2} сторона имеет преимущество {3} м + длина: {0} м\nугол к ветру: {1}°\n{2} сторона имеет преимущество {3} м Do you really want to remove the events? - Действительно удалить эти события? + Действительно удалить эти события? Do you really want to remove event {0}? - Действительно удалить событие {0}? + Действительно удалить событие {0}? Line - Линия + Линия Offset - Сдвиг + Сдвиг Do you really want to remove the selected competitors? - Действительно удалить выбранных участников? + Действительно удалить выбранных участников? Do you really want to remove competitor {0}? - Действительно удалить участника {0}? + Действительно удалить участника {0}? Filter competitors by name, boat class, sail number or ID - Фильтрация участников по названию, классу лодки, номеру на парусе или идентификатору + Фильтрация участников по названию, классу лодки, номеру на парусе или идентификатору Edit Competitor - Изменить участника + Изменить участника Please enter a sail number - Введите номер на парусе + Введите номер на парусе Successfully allowed reload of competitor(s) {0}. - Успешно разрешена перезагрузка участников {0}. + Успешно разрешена перезагрузка участников: {0}. ID - Ид. + Ид. Allow reload - Разрешить перезагрузку + Разрешить перезагрузку Compress - Сжать + Сжать Use only if exporting server instance is at least\nrunning with commit 0fbf6071dea125bec4a56dee55d61c99def4a62e. - Используйте только при выполнении экспортирующего экземпляра\nсервера с commit 0fbf6071dea125bec4a56dee55d61c99def4a62e. + Используйте только при выполнении экспортирующего экземпляра\nсервера с commit 0fbf6071dea125bec4a56dee55d61c99def4a62e. Query Runner - Выполнение запроса + Выполнение запроса Rerun query after refresh - Повторить запрос после обновления + Повторить запрос после обновления Refresh interval mustn''t be empty - Интервал обновления не должен быть пустым + Интервал обновления не должен быть пустым Selection Tables - Таблица выбора + Таблица выбора Add Configuration - Добавить конфигурацию + Добавить конфигурацию There are no configurations. - Нет конфигураций. + Нет конфигураций. Error retrieving configuration data from server. - Ошибка при получении данных конфигурации с сервера. + Ошибка при получении данных конфигурации с сервера. Allowed course areas - Разрешенные зоны гонок + Разрешенные зоны гонок Minimum rounds for Course - Минимум кругов для курса + Минимум кругов для курса Maximum rounds for Course - Максимум кругов для курса + Максимум кругов для курса Results mail recipient - Получатель результатов + Получатель результатов Device Configuration - Конфигурация устройства + Конфигурация устройства None - Нет + Нет Course designer - Дизайнер курса + Дизайнер курса Racing procedure - Процедура гонки + Процедура гонки Course names - Названия курса + Названия курса Generate - Генерировать + Генерировать Don''t overwrite - Не перезаписывать + Не перезаписывать Set regatta configuration - Настройка конфигурации регаты + Настройка конфигурации регаты Regatta configuration - Конфигурация регаты + Конфигурация регаты Activate individual recall - Активировать индивидуальный отзыв + Активировать индивидуальный отзыв Activate result entry - Активировать запись результата + Активировать запись результата RRS26 start - RRS26 старт + RRS26 старт Activate configuration - Активировать конфигурацию + Активировать конфигурацию Class flag - Флаг класса + Флаг класса Sets the class flag used for {0}. Choose ''Don''t Overwrite'' to activate the procedure''s default flag. - Установка флага класса для {0}. Выберите ''Не перезаписывать'', чтобы активировать соответствующий флаг процедуры по умолчанию. + Установка флага класса для {0}. Выберите ''Не перезаписывать'', чтобы активировать соответствующий флаг процедуры по умолчанию. When activated the Race Officer is able to call for individual recall. - При активации руководитель гонок может запросить индивидуальный отзыв. + При активации руководитель гонок может запросить индивидуальный отзыв. When activated the Race Officer is able to enter results during and after the finishing phase. - При активации руководитель гонок может вводить результаты во время и после фазы финиширования. + При активации руководитель гонок может вводить результаты во время и после фазы финиширования. Startmode flags - Флаги режима старта + Флаги режима старта Sets the available start mode flags. Choose none to enable the procedure''s default flags. - Установка доступных флагов режима старта. Выберите "Нет", чтобы активировать флаги процедуры по умолчанию. + Установка доступных флагов режима старта. Выберите "Нет", чтобы активировать флаги процедуры по умолчанию. Gate Start - Старт через ворота + Старт через ворота Activate Pathfinder - Активировать указатель курса + Активировать указатель курса When activated the Race Officer is able to set the Pathfinder boat. - При активации руководитель гонок может назначить лодку-указатель курса. + При активации руководитель гонок может назначить лодку-указатель курса. ESS Start - Старт ESS + Старт ESS Basic Start - Общий старт + Общий старт League Start - Старт лиги + Старт лиги Configured - Настроено + Настроено Activate regatta configuration - Активировать конфигурацию регаты + Активировать конфигурацию регаты Enabling this option will provide a base regatta configuration for all options unconfigured in the regatta-specific configuration. - Включение этого параметра предоставит базовую конфигурацию регаты для всех не настроенных параметров в конфигурации данной регаты. + Включение этого параметра предоставит базовую конфигурацию регаты для всех не настроенных параметров в конфигурации данной регаты. Set start time - Установить время старта + Установить время старта Current pass - Текущее прохождение + Текущее прохождение Select a new start time for the race. If you want to set the start time to a timepoint in the past then you have to enable the checkbox to update the pass count. Please be aware of the fact that changing the pass count will have you loose all information from the previous pass (like flag events). - Выберите новое время старта для гонки. Если требуется назначить старт на время в прошлом, установите флажок для обновления счетчика прохождений. Учтите, что при изменении числа прохождений будут потеряны все сведения из предыдущих прохождений (такие как события флагов). + Выберите новое время старта для гонки. Если требуется назначить старт на время в прошлом, установите флажок для обновления счетчика прохождений. Учтите, что при изменении числа прохождений будут потеряны все сведения из предыдущих прохождений (такие как события флагов). Set the start and end of tracking for the race. - Установка времени начала и окончания отслеживания гонки. + Установка времени начала и окончания отслеживания гонки. Activate additional golf down time - Активировать дополнительное время + Активировать дополнительное время Activate additional golf down time - Активировать дополнительное время + Активировать дополнительное время Invalid color: {0} - Недопустимый цвет: {0} + Недопустимый цвет: {0} Igtimi Accounts - Учетные записи Igtimi + Учетные записи Igtimi Error fetching all Igtimi accounts: {0} - Ошибка при извлечении всех учетных записей Igtimi: {0} + Ошибка при извлечении всех учетных записей Igtimi: {0} Add Igtimi Account - Добавить учетную запись Igtimi + Добавить учетную запись Igtimi Error authorizing access to Igtimi user {0}: {1} - Ошибка при авторизации доступа к пользователю Igtimi {0}: {1} + Ошибка при авторизации доступа к пользователю Igtimi {0}: {1} Successfully authorized access to Igtimi user {0} - Успешно авторизован доступа к пользователю Igtimi : {0} + Успешно авторизован доступа к пользователю Igtimi : {0} Could not authorize user {0}; invalid user name or password - Не удалось авторизовать пользователя {0}; недействительное имя пользователя и пароль + Не удалось авторизовать пользователя {0}; недействительное имя пользователя и пароль Error getting Igtimi authorization URL: {0} - Ошибка при получении URL-адреса авторизации Igtimi: {0} + Ошибка при получении URL-адреса авторизации Igtimi: {0} Add Igtimi User - Добавить пользователя Igtimi + Добавить пользователя Igtimi Do you really want to remove the leaderboards? - Действительно удалить эти таблицы лидеров? + Действительно удалить эти таблицы лидеров? Error trying to remove Igtimi account for user {0} - Ошибка при попытке удалить учетную запись Igtimi для пользователя {0} + Ошибка при попытке удалить учетную запись Igtimi для пользователя {0} Successfully removed Igtimi account for user {0} - Успешно удалена учетная запись Igtimi для пользователя {0} + Успешно удалена учетная запись Igtimi для пользователя {0} e-Mail address must not be empty - Адрес эл. почты не должен быть пустым + Адрес эл. почты не должен быть пустым e-mail address - адрес эл. почты + адрес эл. почты Password - Пароль + Пароль + + + Search tag + Тег поиска Do you really want to remove the leaderboard: ''{0}''? - Действительно удалить таблицу лидеров ''{0}''? + Действительно удалить таблицу лидеров ''{0}''? This is a meta leaderboard. It can''t be changed here. - Это метатаблица лидеров. Изменить ее здесь невозможно. + Это метатаблица лидеров. Изменить ее здесь невозможно. Failed to set new start time. This can happen when the same start time is already present in the race log or someone has added a start time that is newer (in terms of a pass id and logical time) than the one that you just added. - Не удалось установить новое время старта. Возможно, такое же время старта уже присутствует в журнале гонки или кто-то добавить более новое (ид. прохождения и логическое время) время старта, чем добавленное вами. + Не удалось установить новое время старта. Возможно, такое же время старта уже присутствует в журнале гонки или кто-то добавить более новое (ид. прохождения и логическое время) время старта, чем добавленное вами. Do you really want to remove Igtimi account granting access to data of user {0}? - Действительно удалить учетную запись Igtimi, предоставляющую доступ к данным пользователя {0}? + Действительно удалить учетную запись Igtimi, предоставляющую доступ к данным пользователя {0}? Import Wind from Igtimi - Импортировать ветер из Igtimi + Импортировать ветер из Igtimi See the ''Igtimi'' tab for account settings determining which data from Igtimi will be considered. - Параметры учетной записи, определяющие учитываемые данные из Igtimi см. на вкладке ''Igtimi''. + Параметры учетной записи, определяющие учитываемые данные из Igtimi, см. на вкладке ''Igtimi''. Import Wind from Igtimi - Импортировать ветер из Igtimi + Импортировать ветер из Igtimi Error importing wind data from Igtimi: {0} - Ошибка при импорте данных о ветре из Igtimi: {0} + Ошибка при импорте данных о ветре из Igtimi: {0} The following number of fixes were imported for the listed races: {0} - Для перечисленных гонок импортировано следующее число замеров: {0} + Для перечисленных гонок импортировано следующее число замеров: {0} {0} to start - {0} до старта + {0} до старта {0} since start - {0} от старта + {0} от старта Distance to start line - Дистанция до линии старта + Дистанция до линии старта The angle between the competitors direction and the wind - Угол между направлением участников и ветром + Угол между направлением участников и ветром Beat Angle - Угол удара + Угол удара You can also view the overall diagram for the available boat classes. - Также можно просмотреть общую диаграмму для доступных классов лодок. + Также можно просмотреть общую диаграмму для доступных классов лодок. Show Diagram - Показать диаграмму + Показать диаграмму Run the query automatically, after you changed e.g. the statistic or the grouping. - Автоматически выполнять запрос после изменения, например, статистики или группирования. + Автоматически выполнять запрос после изменения, например, статистики или группирования. Reruns the query after the tables have been refreshed. - Повторно выполняет запрос после обновления таблиц. + Повторно выполняет запрос после обновления таблиц. Query Definition Provider - Поставщик определения запроса + Поставщик определения запроса Statistic Provider - Поставщик статистики + Поставщик статистики Calculate the - Рассчитать + Рассчитать Grouping Provider - Поставщик группирования + Поставщик группирования News and Release History - Новости и история выпусков + Новости и история выпусков Split fleets scored contiguously - Разделенные флоты получили сходные оценки + Разделенные флоты получили сходные оценки Add RaceLog Tracker - Добавить средство отслеживания журнала гонки + Добавить средство отслеживания журнала гонки Add RaceLog Trackers - Добавить средства отслеживания журнала гонки + Добавить средства отслеживания журнала гонки Denote for RaceLog tracking - Отменить отслеживание журнала гонки + Отменить отслеживание журнала гонки Start RaceLog Tracking - Начать отслеживание журнала гонки + Начать отслеживание журнала гонки Filter by Leaderboard - Фильтровать по таблице лидеров + Фильтровать по таблице лидеров Remove Leaderboard filter - Удалить фильтр таблицы лидеров + Удалить фильтр таблицы лидеров Competitor Registrations - Регистрации участников + Регистрации участников Tracker Status - Статус средства отслеживания + Статус средства отслеживания + + + Course Status + Статус курса Can only be edited before starting tracking - Редактирование возможно только до начала отслеживания + Редактирование возможно только до начала отслеживания Define Course Layout - Определить схему курса + Определить схему курса Map Devices to Competitors and Marks - Сопоставить устройства с участниками и отметками + Сопоставить устройства с участниками и отметками Edit {0} - Изменить {0} + Изменить {0} Add {0} - Добавить {0} + Добавить {0} Remove {0} - Удалить {0} + Удалить {0} Please enter a {0} - Введите {0} + Введите {0} Ping {0} position - Проверить связь с позицией {0} + Проверить связь с позицией {0} Please enter a valid value for {0} ({1}) - Введите допустимое значение для {0} ({1}) + Введите допустимое значение для {0} ({1}) Copy course and competitors to other races - Копировать курс и участников для других гонок + Копировать курс и участников для других гонок Set tracking times - Установить время отслеживания + Установить время отслеживания Now select the race column and fleet to copy the course to. - Выберите столбец гонки и флот для копирования курса. + Выберите столбец гонки и флот для копирования курса. Select a single row in the table, to which to copy the course. - Выберите в таблице одну строку для копирования курса. + Выберите в таблице одну строку для копирования курса. Device ID - Ид. устройства + Ид. устройства Device Type - Тип устройства + Тип устройства Mapped to Type - Сопоставлено с типом + Сопоставлено с типом Mapped to - Сопоставлено с + Сопоставлено с Device Mappings - Сопоставления устройств + Сопоставления устройств At most one end of the mapping''s time range may be left open (left unset) - Открытым (не указанным) можно оставить не более одного конца диапазона времени сопоставления + Открытым (не указанным) можно оставить не более одного конца диапазона времени сопоставления Please select an item to map the device to - Выберите элемент для сопоставления с устройством + Выберите элемент для сопоставления с устройством Device - Устройство + Устройство Time Range - Диапазон времени + Диапазон времени Close open end of Time Range - Закрыть открытый конец диапазона времени + Закрыть открытый конец диапазона времени Set Timepoint that closes open time range - Установите время, закрывающее диапазон времени + Установите время, закрывающее диапазон времени The start of the time range must lie before its end - Начало диапазона времени должно быть раньше его окончания + Начало диапазона времени должно быть раньше его окончания Overall Progress - Общий ход выполнения + Общий ход выполнения Event Overview - Обзор события + Обзор события Start - Начать + Старт Show added scores for each entry in leaderboard - Показать добавленные баллы для каждой записи в таблице лидеров + Показать добавленные баллы для каждой записи в таблице лидеров ratio - отношение + отношение Import Wind - Импортировать ветер + Импортировать ветер Unchecking this option will drastically lower the amount of data transfered. - Снятие этого параметра существенно понизит объем передаваемых данных. + Снятие этого параметра существенно понизит объем передаваемых данных. Import Device Configurations - Импортировать конфигурации устройств + Импортировать конфигурации устройств Checking this will import ALL device configurations on the remote server. - Установка этого параметра импортирует ВСЕ конфигурации устройств на удаленный сервер. + Установка этого параметра импортирует ВСЕ конфигурации устройств на удаленный сервер. Author name - Имя автора + Имя автора Author priority - Приоритет автора + Приоритет автора Remove Denotation - Удалить обозначение + Удалить обозначение At least one of the selected races ({0}) is missing competitor registrations. Proceed anyway? - По крайней мере в одной из выбранных гонок ({0}) отсутствуют регистрации участников. Продолжить? + По крайней мере в одной из выбранных гонок ({0}) отсутствуют регистрации участников. Продолжить? Please select a control point - Выберите контрольную точку + Выберите контрольную точку 2-mark-controlpoint - 2-отметочная контрольная точка + 2-отметочная контрольная точка Registered Competitors - Зарегистрированные участники + Зарегистрированные участники Competitor Pool - Пул участников + Пул участников Select at least one - Выберите не менее одного + Выберите не менее одного Select the Races - Выберите гонки + Выберите гонки Import Fixes - Импортировать замеры + Импортировать замеры No remote server instances registered yet. - Удаленные инстанции сервера еще не зарегистрированы. + Удаленные экземпляры сервера еще не зарегистрированы. Registered remote server instances - Зарегистрированные удаленные инстанции сервера + Зарегистрированные удаленные экземпляры сервера Filter Events by name: - Фильтровать события по названию: + Фильтровать события по названию: Leaderboards (except those of selected group) - Таблицы лидеров (кроме выбранных групп) + Таблицы лидеров (кроме выбранных групп) Available Leaderboard Groups - Доступные группы таблиц лидеров + Доступные группы таблиц лидеров Leaderboard Groups Assigned to Event - Присвоенные событию группы таблиц лидеров + Присвоенные событию группы таблиц лидеров Add races - Добавить гонки + Добавить гонки with name prefix - с префиксом названия + с префиксом названия Please select a series first. - Сначала выберите серию. + Сначала выберите серию. in {0,number} days - Через {0,number} дн. + Через {0,number} дн. tomorrow - завтра + завтра today - сегодня + сегодня Share by mail - Поделиться по почте + Поделиться по почте Share on Twitter - Поделиться в Twitter + Поделиться в Twitter Share on Facebook - Поделиться в Facebook + Поделиться в Facebook {0} in {1} on {2} - {0} в {1} на {2} + {0} в {1} на {2} String eventSharingShortText(String eventName, String venue, String dateString); I''d like to share {0} in {1} on {2}. Details on: \n{3} - Хочу поделиться {0} в {1} на {2}. Подробно: \n{3} + Хочу поделиться {0} в {1} на {2}. Подробно: \n{3} String eventSharingLongText(String eventName, String venue, String dateString, String url); I''d like to share series {0}. Details: {1} - Хочу поделиться серией {0}. Подробно: \n{1} + Хочу поделиться серией {0}. Подробно: \n{1} String seriesSharingLongText(String seriesName, String url); {0} - {0} + {0} String seriesSharingShortText(String seriesName); Competitors Analytics - Анализ участников + Анализ участников There are no photos or videos available yet. - Фото или видео пока недоступны. + Фото или видео пока недоступны. Media - Мультимедиа + Мультимедиа Photos - Фото + Фото Videos - Видео + Видео Image - Изображение + Изображение Images - Изображения + Изображения Video - Видео + Видео Flag image URL (image renders with 18x12px) - URL-адрес изображения флага (визуализируется с разрешением 18x12 пикс.) + URL-адрес изображения флага (визуализируется с разрешением 18x12 пикс.) Image URLs - URL-адреса изображений + URL-адреса изображений Video URLs - URL-адреса видео + URL-адреса видео Sponsor Image URLs - URL-адреса изображений спонсоров + URL-адреса изображений спонсоров The current selection isn''t valid. - Текущий выбор недопустим. + Текущий выбор недопустим. Advance Pass ID - Ид. досрочного прохождения + Ид. досрочного прохождения Please select a scoring result... - Выберите результат оценки... + Выберите результат оценки... Time zone - Часовой пояс + Часовой пояс Official Website URL - URL-адрес официального веб-сайта + URL-адрес официального веб-сайта Sailors Info Website URL - URL-адрес веб-сайта сведений о мореходах + URL-адрес веб-сайта сведений о мореходах Event Overview URL - URL-адрес обзора события + URL-адрес обзора события Logo Image URL - URL-адрес изображения логотипа + URL-адрес изображения логотипа Show Mappings for pinged Marks - Показать сопоставления для проверенных отметок + Показать сопоставления для проверенных отметок Regatta name must not contain slashes - Название регаты не должно содержать символы косой черты + Название регаты не должно содержать символы косой черты Number of races scored - Число оцененных гонок + Число оцененных гонок Error fetching wind streamlet data: {0} - Ошибка при извлечении данных потока ветра: {0} + Ошибка при извлечении данных потока ветра: {0} Show wind streamlets overlay - Показать наложение потоков ветра + Показать наложение потоков ветра Color streamlets according to wind speed - Цвет потока соответствует скорости ветра + Цвет потока соответствует скорости ветра Show simulation overlay - Показать наложение симуляции + Показать наложение симуляции Time Comparison:\nRace is the leg duration measured from GPS, Omniscient is the course if one would know everything about the wind, Opportunist are courses if one decides to take a turn measuring the distance to the next mark, 1-Turners are the courses if one knows everything about the wind and just takes one turn - Сравнение времени:\nгонка — это продолжительность отрезка, измеренная по данным GPS, "Всезнайка" — курс при наличии всех сведений о ветре, "Авантюрист" — курсы при принятии решения сделать поворот для измерения дистанции до следующей отметки, "1 поворот" — курсы с одним поворотом при наличии всех сведений о ветре + Сравнение времени:\nгонка — это продолжительность отрезка, измеренная по данным GPS, "Всезнайка" — курс при наличии всех сведений о ветре, "Авантюрист" — курсы при принятии решения сделать поворот для измерения дистанции до следующей отметки, "1 поворот" — курсы с одним поворотом при наличии всех сведений о ветре Races scored - Оцененные гонки + Оцененные гонки Number of races the competitor has completed or gotten a score for in this leaderboard. - Число гонок, которые участник завершил или за которые имеет оценки в данной таблице лидеров. + Число гонок, которые участник завершил или за которые имеет оценки в данной таблице лидеров. Average number of operations per message - Среднее число операций на сообщение + Среднее число операций на сообщение Show uncorrected net points - Показать неисправленные точки сети + Показать неисправленные точки сети Set start time received - Установить полученное время старта + Установить полученное время старта This sets the startTimeReceived of the selected TrackedRace that isn''t persistent. This means that the new value will be forgotten after the server has been restarted. - Устанавливает полученное время старта для выбранной отслеживаемой гонки, которое не хранится постоянно. Это значит, что после перезагрузки сервера новое значение будет забыто. + Устанавливает полученное время старта для выбранной отслеживаемой гонки, которое не хранится постоянно. Это значит, что после перезагрузки сервера новое значение будет забыто. Last score correction time - Последнее время исправления оценки + Время последнего исправления оценки Last score correction comment - Комментарий последнего исправления оценки + Комментарий последнего исправления оценки Set time to ''now'' - Установить время "сейчас" + Установить время "сейчас" Use start time inference from start mark passings - Использовать вывод о времени старта из прохождений стартовой отметки + Использовать вывод о времени старта из прохождений стартовой отметки Number of queue messages sent - Число отправленных сообщений очереди + Число отправленных сообщений очереди Total Size - Общий размер + Общий размер Average size per message in bytes - Средний размер сообщения в байтах + Средний размер сообщения в байтах Total number of operations - Общее число операций + Общее число операций Current speed over ground in knots while leg is still running; average speed over ground when leg is completed - Текущая скорость относительно грунта в узлах во время прохождения отрезка; средняя скорость относительно грунта по завершении отрезка + Текущая скорость относительно грунта в узлах во время прохождения отрезка; средняя скорость относительно грунта по завершении отрезка SOG (∅ at end) - SOG (∅ в конце) + SOG (∅ в конце) Connectors - Соединители + Соединители Video - Видео + Видео Competitor Search Filter - Фильтр поиска участников + Фильтр поиска участников Search - Поиск + Поиск Go to event overview - Перейти к обзору события + Перейти к обзору события Go to the overview and see all Races in one Leaderboard - Переход к обзору и просмотр всех гонок в одной таблице лидеров + Переход к обзору и просмотр всех гонок в одной таблице лидеров No such event - Нет такого события + Нет такого события The leaderboard {0} is not contained in leaderboard group {1}. - Таблица лидеров {0} не входит в группу таблиц лидеров {1}. + Таблица лидеров {0} не входит в группу таблиц лидеров {1}. The leaderboard group {0} is not contained in event {1}. - Группа таблиц лидеров {0} не содержится в событии {1}. + Группа таблиц лидеров {0} не содержится в событии {1}. Race is Live - Гонка активна + Гонка активна Back to Live - Вернуться в оперативный режим + Вернуться в оперативный режим Wind Chart - Диаграмма ветра + Диаграмма ветра No Videos - Нет видео + Нет видео Show Video - Показать видео + Показать видео Click to show video: {0} - Щелкните, чтобы показать видео: {0} + Щелкните, чтобы показать видео: {0} Hide Video - Скрыть видео + Скрыть видео Click to hide video - Щелкните, чтобы скрыть видео + Щелкните, чтобы скрыть видео {0} Videos - {0} видео + {0} видео Click to select video - Щелкните, чтобы выбрать видео + Щелкните, чтобы выбрать видео Manage Media - Управление мультимедиа + Управление мультимедиа Configure audio and video clips, synchronize times - Настройка аудио- и видеоклипов, синхронизация времени + Настройка аудио- и видеоклипов, синхронизация времени Show all - Показать все + Показать все Visibility - Видимость + Видимость Enter carried points for competitor {0} - Введите перенесенные баллы для участника {0} + Введите перенесенные баллы для участника {0} Advanced - Дополнительно + Дополнительно based on - на базе + на базе Retrieve with - Получить с помощью + Получить с помощью Mapping Details - Подробности сопоставления + Подробности сопоставления If you are using the Tracking App, you can add the device mapping also by selecting a competitor/mark, setting the from and to times, and then scanning this QRCode. - Если используется приложение отслеживания, можно добавить также сопоставление устройства, выбрав участника/отметку, установив интервал времени, а затем отсканировав этот QR-код. + Если используется приложение отслеживания, можно добавить также сопоставление устройства, выбрав участника/отметку, установив интервал времени, а затем отсканировав этот QR-код. QRCode - QR-код + QR-код Enter course area name... - Ввести название зоны гонки... + Ввести название зоны гонки... Enter course name... - Ввести название курса... + Ввести название курса... Enter image URL... - Ввести URL-адрес изображения... + Ввести URL-адрес изображения... Enter video URL... - Ввести URL-адрес видео... + Ввести URL-адрес видео... Enter sponsor image URL... - Ввести URL-адрес изображения спонсора... + Ввести URL-адрес изображения спонсора... Enter race name... - Ввести название гонки... + Ввести название гонки... An error occurred while attempting to contact the server. Please check your network connection and try again. - При попытке обратиться к серверу возникла ошибка. Проверьте подключение к сети и повторите попытку. + При попытке обратиться к серверу возникла ошибка. Проверьте подключение к сети и повторите попытку. Remote Procedure Call - Удаленный вызов процедуры + Удаленный вызов процедуры Server replies - Ответы сервера + Ответы сервера Error communicating with server - Ошибка связи с сервером + Ошибка связи с сервером User Management - Управление пользователями + Управление пользователями Regatta Structure Import - Импорт структуры регаты + Импорт структуры регаты filtered by - отфильтровано по + отфильтровано по Edit Series - Изменить серию + Изменить серию Please select at least one regatta - Выберите не менее одной регаты + Выберите не менее одной регаты Please select an event - Выберите событие + Выберите событие Error trying to get events: {0} - Ошибка при попытке получить события: {0} + Ошибка при попытке получить события: {0} Show x-y-diagram - Показать диаграмму x-y + Показать диаграмму x-y x-y-Diagram - Диаграмма x-y + Диаграмма x-y Boat Speed - Скорость лодки + Скорость лодки Confidence - Достоверность + Достоверность Create default settings for all regattas... - Создать настройки по умолчанию для всех регат... + Создать настройки по умолчанию для всех регат... No overall event selected... - Не выбрано общее событие... + Не выбрано общее событие... Error trying to create new event {0}: {1} - Ошибка при попытке создать новое событие {0}: {1} + Ошибка при попытке создать новое событие {0}: {1} Create New Event - Создать новое событие + Создать новое событие No fleets defined. - Не определены флоты. + Не определены флоты. Successfully created regattas - Регаты успешно созданы + Регаты успешно созданы Error trying to register races {0} for tracking: {1}. Check live/stored URI syntax. - Ошибка при попытке зарегистрировать гонки {0} для отслеживания: {1}. проверьте синтаксис URI оперативных/хранимых данных. + Ошибка при попытке зарегистрировать гонки {0} для отслеживания: {1}. проверьте синтаксис URI оперативных/хранимых данных. Error determining availability of polar / VPP data for race {0}: {1} - Ошибка при определении доступности полярных/VPP данных для гонки {0}: {1} + Ошибка при определении доступности полярных/VPP данных для гонки {0}: {1} Error - Ошибка + Ошибка File Storage - Хранилище файлов + Хранилище файлов Active - Активно + Активно Score in points. Winner of an act scores 10 points, 2nd - 9 points, .... Any tie in the overall Extreme Sailing Series is broken in favour of the competitor with the most Act wins. If that does not break the tie, the result in the last Act will be used. - Оценка в баллах. Победитель в акте получает 10 баллов, 2-й — 9 баллов, .... Любая ничья в общей оценке Extreme Sailing Series разрешается в пользу участника с наибольшим числом побед в актах. В случае равенства числа побед для разрешения используется последний акт. + Оценка в баллах. Победитель в акте получает 10 баллов, 2-й — 9 баллов, .... Любое равенство в общей оценке Extreme Sailing Series разрешается в пользу участника с наибольшим числом побед в актах. В случае равенства числа побед для разрешения используется последний акт. Start delay - Задержка старта + Задержка старта Time between the start of the race and the time the competitor passed the start waypoint. Negative time suggests OCS. - Время между стартом гонки и временем, когда участник проходит путевую точку старта. Отрицательное время означает ранний старт (OCS). + Время между стартом гонки и временем, когда участник проходит путевую точку старта. Отрицательное время означает ранний старт (OCS). Use internal mark passing algorithm - Использовать внутренний алгоритм прохождения отметки + Использовать внутренний алгоритм прохождения отметки Edit Mark Passings - Изменить прохождения отметок + Изменить прохождения отметок Edit Mark Positions - Изменить позиции отметок + Изменить позиции отметок Remove fixed mark passing - Удалить прохождение фиксированной отметки + Удалить прохождение фиксированной отметки Set time as mark passing - Установить время как прохождение отметки + Установить время как прохождение отметки Suppress mark passing starting with current - Подавить прохождение отметки, начиная с текущей + Подавить прохождение отметки, начиная с текущей Remove suppressed mark passing - Удалить подавленное прохождение отметки + Удалить подавленное прохождение отметки Error removing fixed mark passing: {0} - Ошибка при удалении прохождения фиксированной отметки: {0} + Ошибка при удалении прохождения фиксированной отметки: {0} Error setting fixed mark passing: {0} - Ошибка при настройке прохождение фиксированной отметки: {0} + Ошибка при настройке прохождение фиксированной отметки: {0} Error suppressing mark passings: {0} - Ошибка при подавлении прохождений отметок: {0} + Ошибка при подавлении прохождений отметок: {0} Error removing suppressed mark passings: {0} - Ошибка при удалении подавленных прохождений отметок: {0} + Ошибка при удалении подавленных прохождений отметок: {0} Error trying to obtain mark passings: {0} - Ошибка при попытке получить прохождения отметок: {0} + Ошибка при попытке получить прохождения отметок: {0} Error trying to obtain race log mark passing data: {0} - Ошибка при попытке получить данные журнала гонки о прохождении отметок: {0} + Ошибка при попытке получить данные журнала гонки о прохождении отметок: {0} E-Mail - Эл. почта + Эл. почта Please enter an E-Mail. - Введите электронную почту. + Введите электронную почту. Invite competitors - Пригласить участников + Пригласить участников Invite selected competitors - Пригласить выбранных участников + Пригласить выбранных участников Not all competitors provide an E-Mail - Не все участники указали эл. почту + Не все участники указали эл. почту Sending Mails failed - Сбой отправки сообщений + Сбой отправки сообщений Successfully sent E-Mails - Сообщения эл. почты успешно отправлены + Сообщения эл. почты успешно отправлены Select Event for Invitation - Выберите событие для приглашения + Выберите событие для приглашения Image URL - URL-адрес изображения + URL-адрес изображения Upload - Отправить + Отправить Remove operation: {0} {1} - Удалить операцию: {0} {1} + Удалить операцию: {0} {1} Upload successful - Отправлено успешно + Отправлено успешно File upload: {0} {1}.\nIn case of error please check that your File Storage service is properly configured. - Отправка файла: {0} {1}.\nВ случае ошибки проверьте правильность конфигурации службы хранилища файлов. + Отправка файла: {0} {1}.\nВ случае ошибки проверьте правильность конфигурации службы хранилища файлов. Remove uploaded file - Удалить отправленный файл + Удалить отправленный файл Sail Id - Ид. паруса + Ид. паруса Competitor Full Name - Полное имя участника + Полное имя участника Loading dimension values - Загрузка значений измерения + Загрузка значений измерения Running Query - Выполнение запроса + Выполнение запроса Invite Buoy Tenders - Пригласить лоцманов + Пригласить лоцманов or multiple E-Mails separated by comma - или несколько адресов эл. почты через запятую + или несколько адресов эл. почты через запятую True course over ground in degrees - Истинный курс относительно грунта в градусах + Истинный курс относительно грунта в градусах COG - COG + COG Distance (w/ Gate Start) - Дистанция (при старте через ворота) + Дистанция (при старте через ворота) The distance traveled from the leg start to the end\nor to the current time point, if the leg isn''t finished.\nIf the leg includes a gate start, the distance from the pin end to the starting position is included\nto make competitors comparable even when started at different times. - Дистанция, пройденная от начала отрезка до конца\nили до текущего момента времени, если отрезок не завершен.\nЕсли отрезок включает старт через ворота, дистанция от отметки до стартовой позиции включается,\nчтобы обеспечить сравнение участников даже при старте в разное время. + Дистанция, пройденная от начала отрезка до конца\nили до текущего момента времени, если отрезок не завершен.\nЕсли отрезок включает старт через ворота, дистанция от отметки до стартовой позиции включается,\nчтобы обеспечить сравнение участников даже при старте в разное время. The distance traveled from the race start to the end\nor to the current time point, if the race isn''t finished. For a gate start, the distance from the pin end to the starting position is included\nto make competitors comparable even when started at different times. - Дистанция, пройденная от начала гонки до конца\nили до текущего момента времени, если гонка не завершена.\nДля старта через ворота включается дистанция от привязки до стартовой позиции,\nчтобы обеспечить сравнение участников даже при старте в разное время. + Дистанция, пройденная от начала гонки до конца\nили до текущего момента времени, если гонка не завершена.\nДля старта через ворота включается дистанция от привязки до стартовой позиции,\nчтобы обеспечить сравнение участников даже при старте в разное время. Results - Результаты + Результаты Group Name - Название группы + Название группы Value (Ascending) - Значение (по возрастанию) + Значение (по возрастанию) Value (Descending) - Значение (по убыванию) + Значение (по убыванию) Sort by - Сортировать по + Сортировать по Dashboard - Панель + Панель Wind Bot is not available. - WindBot недоступен. + WindBot недоступен. To receive live wind data from wind measurement units, ensure that the wind bot is switched on and connected to the SAP Sailing Analytics. - Чтобы получать оперативные данные от устройств измерения ветра, убедитесь, что WindBot включен и подключен к SAP Sailing Analytics. + Чтобы получать оперативные данные от устройств измерения ветра, убедитесь, что WindBot включен и подключен к SAP Sailing Analytics. Wind Bot - WindBot + WindBot TWS - TWS + TWS TWD - TWD + TWD kts - уз + уз deg - град. + град. live - активно + активно average - ср. + ср. ({0} minutes) - ({0} мин.) + ({0} мин.) Tap on chart to change interval - Коснитесь диаграммы, чтобы изменить интервал + Коснитесь диаграммы, чтобы изменить интервал Select Competitor - Выбор участника + Выбор участника Start Analyses - Анализы старта + Анализы старта New start analysis available - Доступен новый анализ старта + Доступен новый анализ старта No Start Analyses available. - Анализы старта недоступны. + Анализы старта недоступны. There is a Start Analysis created from each race start. Please stand by till racing has been started. - Анализ старта создается из каждого старта гонки. Дождитесь начала гонки. + Анализ старта создается из каждого старта гонки. Дождитесь начала гонки. No Startline Advantages available. - Преимущества на линии старта недоступны. + Преимущества на линии старта недоступны. For calculating the startline advantage there needs to be a course set by the race commitee. - Для расчета преимущества на линии старта необходим курс, установленный гоночным комитетом. + Для расчета преимущества на линии старта необходим курс, установленный гоночным комитетом. No Startline Advantage available. - Преимущество на линии старта недоступно. + Преимущество на линии старта недоступно. For calculating the startline advantage there needs to be a course set by the race commitee. - Для расчета преимущества на линии старта необходим курс, установленный гоночным комитетом + Для расчета преимущества на линии старта необходим курс, установленный гоночным комитетом Startline Advantages by Wind - Преимущества в ветре на линии старта + Преимущества в ветре на линии старта Startline Advantage by Geometry - Преимущества в геометрии на линии старта + Преимущества в геометрии на линии старта Rank at first mark - Ранг на первой отметке + Ранг на первой отметке Team - Команда + Команда Distance to line in m - Дистанция до линии в км + Дистанция до линии в км Speed in kts - Скорость в узлах + Скорость в узлах Advantage in m - Преимущество в м + Преимущество в м s - с + с Distance to rc boat in m - Дистанция до лодки ГК в км + Дистанция до лодки ГК в км Pin End - Привязка + Привязка RC Boat - Лодка ГК + Лодка ГК (fixed) - (фиксировано) + (фиксировано) (suppressed) - (подавляется) + (подавляется) Wind-Up (show wind from top of map) - На ветер (показать ветер сверху карты) + На ветер (показать ветер сверху карты) Filter by - Фильтровать по + Фильтровать по Current filter selection - Текущий выбор фильтра + Текущий выбор фильтра I''m not capable of generating a code for this identifier. - Я не могу сгенерировать код для этого идентификатора. + Я не могу сгенерировать код для этого идентификатора. Server URL - URL-адрес сервера + URL-адрес сервера Rotated {0} degrees from true north.\nTap/click to toggle between wind-up and north-up map display - Повернуто на {0} град. от истинного севера.\nКоснитесь/щелкните для переключения между отображением карты с ветром или севером вверху + Повернуто на {0} град. от истинного севера.\nКоснитесь/щелкните для переключения между отображением карты с ветром или севером вверху Start to first mark ({0}m) - От старта до первой отметки ({0} м) + От старта до первой отметки ({0} м) The Data-Mining components have been updated - Компоненты добычи данных обновлены. + Компоненты добычи данных обновлены. Click Reload to reload the components now. This will discard the current displayed data and run a default query.\nClick Close to do nothing. The Data-Mining won''t work correctly, until the components have been reloaded. - Щелкните "Перезагрузить", чтобы перезагрузить компоненты сейчас. Текущее отображение данных будет сброшено с выполнением запроса по умолчанию. Добыча данных не будет работать правильно до перезагрузки компонентов. + Щелкните "Перезагрузить", чтобы перезагрузить компоненты сейчас. Текущее отображение данных будет сброшено с выполнением запроса по умолчанию. Добыча данных не будет работать правильно до перезагрузки компонентов. There isn''t any data for the event yet. - Данных для данного события еще нет. + Данных для данного события еще нет. {0,number} countries - {0,number} стран + {0,number} стран {0,number} country - {0,number} страна + {0,number} страна {0,number} races - {0,number} гонок + {0,number} гонок {0,number} race - {0,number} гонка + {0,number} гонка {0,number} tracked races - {0,number} отслеживаемых гонок + {0,number} отслеживаемых гонок {0,number} tracked race - {0,number} отслеживаемая гонка + {0,number} отслеживаемая гонка {0,number} events - {0,number} событий + {0,number} событий {0,number} event - {0,number} событие + {0,number} событие {0,number} competitors - {0,number} участников + {0,number} участников Course area: {0} - Зона гонки: {0} + Зона гонки: {0} {0,number} competitor - {0,number} участник + {0,number} участник Leg {0,number}/{1,number} - Отрезок {0,number}/{1,number} + Отрезок {0,number}/{1,number} Clone - Клон + Клон Changed - Изменено + Изменено Matcher - Совпадение + Совпадение Any - Любое + Любое URL-based result providers - Поставщики результатов на базе URL-адресов + Поставщики результатов на базе URL-адресов Please select a URL-based result provider... - Выберите поставщика результатов на базе URL-адреса... + Выберите поставщика результатов на базе URL-адреса... Sample URL: {0} - Образец URL-адреса: {0} + Образец URL-адреса: {0} SAP Sports on - SAP Sports по адресу + SAP Sports в Follow SAP Sports - Отслеживать SAP Sports + Отслеживать SAP Sports Live now - Оперативные данные + Оперативные данные Analyze race - Анализировать гонку + Анализировать гонку Next race starts in - Следующая гонка начинается через + Следующая гонка начинается через {0} starts in - {0} начинается через + {0} начинается через Planned - Запланировано + Запланировано Abandoned - Отменено + Отменено Postponed - Отложено + Отложено Starts in {0,number} min - Начинается через {0,number} мин. + Начинается через {0,number} мин. Starting now - Начиная сейчас + Начиная сейчас Latest News - Последние новости + Последние новости Result update - Обновление результата + Обновление результата {0} won race - Выиграно гонок: {0} + Выиграно гонок: {0} Time on Time Factor - Фактор время-время (ToT) + Фактор время-время (ToT) Time on Distance Allowance - Фактор время-дистанция (ToD) + Фактор время-дистанция (ToD) Ranking - Ранг + Ранг One-Design - Монотип + Монотип ORC Performance Curve - Кривая эффективности ORC + Кривая эффективности ORC Time-on-Time/Time-on-Distance - Время-время/Время-дистанция + Время-время/Время-дистанция One-Design: First across the line wins;\nin-race ranks based on windward positions (upwind/downwind) or along-course position (reaching) - Монотип: первый пересекший линию побеждает;\nранги в гонке основаны на позиции к ветру (наветренная/подветренная) или позиции на курсе (полный ветер) + Монотип: первый пересекший линию побеждает;\nранги в гонке основаны на позиции к ветру (наветренная/подветренная) или позиции на курсе (полный ветер) ORC Performance Curve: Sophisticated handicapping scheme that awards time allowances based on a VPP/polar assuming pre-defined legs'' angles to the wind;\nbased on these allowances, an "implied wind" is calculated which is the basis for ranking.\nImplied wind can be calculated at any time in the race and is therefore the basis for in-race ranking. - Кривая эффективности ORC: сложная схема гандикапа, предоставляющая фору с учетом VPP/полярных данных для предварительно определенных углов отрезков к ветру;\nна основе этих допусков рассчитывается "подразумеваемый ветер", используемый как основа для ранжирования.\nПодразумеваемый ветер можно рассчитать в любое время в ходе гонки, поэтому он является основой для ранжирования в гонке. + Кривая эффективности ORC: сложная схема гандикапа, предоставляющая фору с учетом VPP/полярных данных для предварительно определенных углов отрезков к ветру;\nна основе этих допусков рассчитывается "подразумеваемый ветер", используемый как основа для ранжирования.\nПодразумеваемый ветер можно рассчитать в любое время в ходе гонки, поэтому он является основой для ранжирования в гонке. Time-on-Time / Time-on-Distance: Final ranking by calculated time;\ntime is calculated by optionally applying a factor to the actual time (defaulting to 1.0)\nand subtracting an allowance in seconds per nautical mile,\napplied to the course length.\nIn-race ranks based on expected arrival at fastest boat's current position\nassuming current leg's average VMG/VMC. - Время-время/Время-дистанция: окончательное ранжирование по расчетному времени;\nвремя рассчитывается путем необязательного применения коэффициента к фактическому времени (по умолчанию 1,0)\nи вычитания допуска в секундах на морскую милю\nк длине курса.\nРанги в гонке основаны на ожидаемом прибытии на текущую позицию самой быстрой лодки\nс учетом средних значений VMG/VMC текущего отрезка. + Время-время/Время-дистанция: окончательное ранжирование по расчетному времени;\nвремя рассчитывается путем необязательного применения коэффициента к фактическому времени (по умолчанию 1,0)\nи вычитания допуска в секундах на морскую милю\nк длине курса.\nРанги в гонке основаны на ожидаемом прибытии на текущую позицию самой быстрой лодки\nс учетом средних значений VMG/VMC текущего отрезка. ToT Factor - Фактор ToT + Фактор ToT ToD Allowance - Допуск ToD + Допуск ToD Time-on-Time factor; actual time is multiplied with this factor in calculated time formula - Фактор время-время; в формуле расчета времени фактическое время умножается на этот коэффициент + Фактор время-время; в формуле расчета времени фактическое время умножается на этот коэффициент Time-on-Distance allowance in seconds per nautical mile;\ndistance sailed is multiplied with this allowance, and the resulting seconds are subtracted from the actual time sailed\n(optionally calculated by applying the time-on-time factor) in the calculated time formula - Допуск время-дистанция в секундах на морскую милю;\nв формуле расчета времени пройденная дистанция умножается на этот допуск, и полученное число секунд вычитается из фактического времени хода\n(необязательно рассчитывается применением коэффициента время-время) + Допуск время-дистанция в секундах на морскую милю;\nв формуле расчета времени пройденная дистанция умножается на этот допуск, и полученное число секунд вычитается из фактического времени хода\n(необязательно рассчитывается применением коэффициента время-время) Calculated Time - Расчетное время + Расчетное время Calculated time according to competitor''s handicap\nbased on time since start of race and (windward) distance traveled - Расчетное время с учетом гандикапа участника\nна основе времени от старта гонки и пройденной (по ветру) дистанции + Расчетное время с учетом гандикапа участника\nна основе времени от старта гонки и пройденной (по ветру) дистанции Calc.Time @ Fastest - Расч. время самой быстрой лодки + Расч. время самой быстрой лодки Calculated time at estimated arrival at the competitor that is currently farthest ahead in the race;\nestimation based on average VMG in current leg and equal (calculated) performance to fastest boat in all subsequent legs - Расчетное время ожидаемого прибытия в позицию текущего лидера гонки;\nоценка основана на среднем значении VMG на текущем отрезке и (расчетной) эффективности, равной эффективности самой быстрой лодки, на всех последующих отрезках + Расчетное время ожидаемого прибытия в позицию текущего лидера гонки;\nоценка основана на среднем значении VMG на текущем отрезке и (расчетной) эффективности, равной эффективности самой быстрой лодки, на всех последующих отрезках Sailor Info - Сведения о мореходе + Сведения о мореходе Official Results, Protests,\nNotice Board, Sailor weather: - Официальные результаты, протесты,\nдоска уведомлений, погода: + Официальные результаты, протесты,\nдоска уведомлений, погода: Event Quickfinder - Быстрый поиск событий + Быстрый поиск событий Regatta Quickfinder - Быстрый поиск регат + Быстрый поиск регат Results Quickfinder - Быстрый поиск результатов + Быстрый поиск результатов Races Quickfinder - Быстрый поиск гонок + Быстрый поиск гонок Statistics - Статистика + Статистика Impressions - Впечатления + Впечатления This content is not available on mobile.\nSwitch to the desktop version? - Это содержимое недоступно на мобильных устройствах.\nПерейти на версию для рабочего стола? + Это содержимое недоступно на мобильных устройствах.\nПерейти на версию для рабочего стола? Go to desktop version - Перейти на версию для рабочего стола + Перейти на версию для рабочего стола Go back - Назад + Назад Show all {0} - Показать все {0} + Показать все {0} Collapse {0} - Свернуть {0} + Свернуть {0} GPS fixes - Замеры GPS + Замеры GPS Fastest - Самый быстрый + Самый быстрый Wind fixes - Замеры ветра + Замеры ветра Sailed miles - Пройдено миль + Пройдено миль {0,number,#0.0} mil - {0,number,#0.0} mil + {0,number,#0.0} mil {0,number,#0.0} bil - {0,number,#0.0} bil + {0,number,#0.0} bil There are no results available yet. - Результаты пока недоступны. + Результаты пока недоступны. There are no news available. - Новости пока недоступны. + Новости пока недоступны. {0,number,#0.00} - {0,number,#0.00} + {0,number,#0.00} Open Leaderboard - Открыть таблицу лидеров + Открыть таблицу лидеров Now - Сейчас + Сейчас {0} min ago - {0} мин. назад + {0} мин. назад {0} h ago - {0} ч. назад + {0} ч. назад Winner - Победитель + Победитель Duration - Продолжительность + Продолжительность Finished Races - Завершенные гонки + Завершенные гонки Please select one competitor - Выберите одного участника + Выберите одного участника All - Все + Все Points - Баллы + Баллы # Races - Гонки + Гонки Course Geometry - Геометрия курса + Геометрия курса {0}° to wind - {0}° к ветру + {0}° к ветру Shows the points along with the number of races per competitor - Показывает баллы и число гонок каждого участника + Показывает баллы и число гонок каждого участника {0,number}/{1,number} - {0,number}/{1,number} + {0,number}/{1,number} There are no finished races yet - Завершенных гонок еще нет + Завершенных гонок еще нет Races overview - Обзор гонок + Обзор гонок List format - Формат списка + Формат списка Competition format - Формат соревнования + Формат соревнования Empty - Пусто + Пусто Run a Query - Выполнить запрос + Выполнить запрос Latest Regatta Standings - Позиции в последней регате + Позиции в последней регате Plain Text - Простой текст + Простой текст Column Chart - Столбчатая диаграмма + Столбчатая диаграмма Choose Presentation - Выберите представление + Выберите представление Can''t display data of type {0} - Невозможно отобразить данные типа {0} + Невозможно отобразить данные типа {0} Shown Decimals - Отображаемые десятичные разряды + Отображаемые десятичные разряды Open fullscreen view - Открыть полноэкранный режим + Открыть полноэкранный режим Close fullscreen view - Закрыть полноэкранный режим + Закрыть полноэкранный режим Progress - Ход выполнения + Ход выполнения {0,number} videos - {0,number} видео + {0,number} видео {0,number} video - {0,number} видео + {0,number} видео {0,number} photos - {0,number} фото + {0,number} фото {0,number} photo - {0,number} фото + {0,number} фото {0} events have taken place - Имело место {0} событий + Имело место {0} событий One event has taken place - Имело место одно событие + Имело место одно событие Race Office - Служба гонки + Служба гонки Analyze - Анализировать + Анализировать Data Mining Settings - Параметры добычи данных + Параметры добычи данных Multi Results Presenter - Презентатор множества результатов + Презентатор множества результатов Plain Results Presenter - Презентатор простых результатов + Презентатор простых результатов Results Chart - Диаграмма результатов + Диаграмма результатов Tabbed Results Presenter - Презентатор результатов со вкладками + Презентатор результатов со вкладками Polar Results Presenter - Полярный презентатор результатов + Полярный презентатор результатов Data Retrieval - Извлечение данных + Извлечение данных Watch - Смотреть + Смотреть Analyze - Анализировать + Анализировать Shortcut to denote all races for racelog tracking - Ярлык для отмена отслеживания журналов всех гонок + Ярлык для отмены отслеживания журналов всех гонок Register Competitors - Регистрация участников + Регистрация участников Do you want to create a default Regatta Leaderboard? - Создать таблицу лидеров регаты по умолчанию? + Создать таблицу лидеров регаты по умолчанию? Copy Course - Копировать курс + Копировать курс Copy Competitors - Копировать участников + Копировать участников Smartphone Tracking - Отслеживание смартфона + Отслеживание смартфона {0,number} flights - {0,number} полетов + {0,number} полетов {0,number} flight - {0,number} полет + {0,number} полет View Query Definition - Просмотреть определение запроса + Просмотреть определение запроса Query Definition Viewer - Средство просмотра определения запроса + Средство просмотра определения запроса Group Average (Ascending) - Среднее группы (по возрастанию) + Среднее группы (по возрастанию) Group Average (Descending) - Среднее группы (по убыванию) + Среднее группы (по убыванию) Group Median (Ascending) - Медиана группы (по возрастанию) + Медиана группы (по возрастанию) Group Median (Descending) - Медиана группы (по убыванию) + Медиана группы (по убыванию) {0,number} results found for ''{1}'' - Найдено {0,number} результатов для ''{1}'' + Найдено {0,number} результатов для ''{1}'' {0,number} result found for ''{1}'' - Найден {0,number} результат для ''{1}'' + Найден {0,number} результат для ''{1}'' Run Predefined Query - Выполнить готовый запрос + Выполнить готовый запрос Select Predefined Query - Выбрать готовый запрос + Выбрать готовый запрос Predefined Query Runner - Средство выполнения готовых запросов + Средство выполнения готовых запросов Developer Options - Параметры разработчика + Параметры разработчика Copy to Clipboard - Копировать в буфер обмена + Копировать в буфер обмена Code - Код + Код Use Class.getName() for type names - Использовать Class.getName() для имен типов + Использовать Class.getName() для имен типов More robust against changes in the code base, but the code snippet can only be used in the scope where the classes are available. - Более надежно по сравнению с изменениями в основании кода, но фрагмент кода может быть использован только в области с доступными классами. + Более надежно по сравнению с изменениями в основании кода, но фрагмент кода может быть использован только в области с доступными классами. Use string literals for type names - Использовать строковые литералы для имен типов + Использовать строковые литералы для имен типов The code snippet can be used anywhere, but it will break if the code base changes. - Фрагмент кода может использоваться где угодно, но будет нарушен в случае изменения основания кода. + Фрагмент кода может использоваться где угодно, но будет нарушен в случае изменения основания кода. Error loading data. Trying again in a few moments. - Ошибка при загрузке данных. Повтор попытки через несколько секунд. + Ошибка при загрузке данных. Повтор попытки через несколько секунд. Add gallery photo - Добавить фото галереи + Добавить фото галереи Add stage image - Добавить изображение этапа + Добавить изображение этапа Add teaser image - Добавить изображение анонса + Добавить изображение анонса No images defined yet - Изображения еще не определены + Изображения еще не определены The size of the {0} image does not fit. The width should be between {1} and {2} px and the height between {3} and {4} px. - Размер изображения {0} не подходит. Ширина должна быть между {1} и {2} пикс., а высота между {3} и {4} пикс. + Размер изображения {0} не подходит. Ширина должна быть между {1} и {2} пикс., а высота между {3} и {4} пикс. The width and height of the image could not be retrieved yet. - Ширину и высоту изображения еще не удалось извлечь. + Ширину и высоту изображения еще не удалось извлечь. Please only upload content you have all usage rights for. Remember that content may be cropped. - Выкладывайте только контент, на использование которого у вас есть все права. Не забывайте о возможности кадрировать контент. + Выкладывайте только контент, на использование которого у вас есть все права. Не забывайте о возможности кадрировать контент. Register Competitors on Race (e.g. for split-fleet racing) - Зарегистрировать участников в гонке (например, для гонок с разделением флота) + Зарегистрировать участников в гонке (например, для гонок с разделением флота) Do you want to register competitors directly on the Race? - Зарегистрировать участников непосредственно в гонке? + Зарегистрировать участников непосредственно в гонке? Do you want to disable competitor registrations on the Race? - Отключить регистрации участников в гонке? + Отключить регистрации участников в гонке? Warning! This setting will lead to competitors for this race being registered on a per race basis. Competitors will not be automatically registered for this race, when registering them on the corresponding regatta. - Внимание! Этот параметр приведет к регистрации участников этой гонки только в данной гонке. При регистрации в соответствующей регате они не будут автоматически зарегистрированы в данной гонке. + Внимание! Этот параметр приведет к регистрации участников этой гонки только в данной гонке. При регистрации в соответствующей регате они не будут автоматически зарегистрированы в данной гонке. Warning! This will reactivate competior registrations on the regatta. All competitor registrations from the regatta will automatically be used as competitor registration for this race. - Внимание! Это приведет к повторной активации регистраций участника в регате. Все регистрации участника из регаты будут автоматически использованы как регистрация участника для данной гонки. + Внимание! Это приведет к повторной активации регистраций участника в регате. Все регистрации участника из регаты будут автоматически использованы как регистрация участника для данной гонки. Competitor Registrations are disabled for this race. Enable by checking the checkbox to the left. - Регистрации участника отключены для данной гонки. Чтобы включить, установите флажок слева. + Регистрации участника отключены для данной гонки. Чтобы включить, установите флажок слева. Add Mark to Regatta - Добавить отметку к регате + Добавить отметку к регате Select a leaderboard group... - Выбрать группу таблиц лидеров... + Выбрать группу таблиц лидеров... Please select - Выберите + Выберите This page requires a valid regatta, race column and fleet name to identify the race to show. - Эта страница определяет гонку для отображения по допустимым значениям регаты, столбца гонки и названия флота. + Эта страница определяет гонку для отображения по допустимым значениям регаты, столбца гонки и названия флота. Could not obtain a race with name {1} for fleet {2} for a regatta with name {0}: {3} - Не удалось получить гонку с названием {1} для флота {2} для регаты с названием {0}: {3} + Не удалось получить гонку с названием {1} для флота {2} для регаты с названием {0}: {3} Error trying to create the embedded map: {0} - Ошибка при попытке создать внедренную карту: {0} + Ошибка при попытке создать внедренную карту: {0} Top {0} - Первые {0} + Первые {0} Transparent buffer lines on hover of helplines - Прозрачные буферные линии при наведении на вспомогательные линии + Прозрачные буферные линии при наведении на вспомогательные линии Buffer line stroke weight - Вес штриха буферной линии + Вес штриха буферной линии Profile - Профиль + Профиль Show only competitors registered in Log - Показать только участников, зарегистрированных в журнале + Показать только участников, зарегистрированных в журнале This will reset your current, unsaved changes to the competitors.\nDo you really want to continue? - Это приведет к сбросу текущих, не сохраненных изменений участников.\nПродолжить? + Это приведет к сбросу текущих, не сохраненных изменений участников.\nПродолжить? Removal deavticated, marks are used in races {0} - Удаление деактивировано, отметки используются в гонках {0} + Удаление деактивировано, отметки используются в гонках {0} Would you like to create a default LeaderboardGroup for your event? - Создать группу таблиц лидеров по умолчанию для вашего события? + Создать группу таблиц лидеров по умолчанию для вашего события? Please create at least one mapping by first selecting a track on the left hand side and then selecting a mark or competitor on the right hand side - Создайте хотя бы одно сопоставление, выбрав отслеживание слева, а затем отметку или участника справа + Создайте хотя бы одно сопоставление, выбрав отслеживание слева, а затем отметку или участника справа Matcher type - Тип сопоставления + Тип сопоставления Matching devices - Соответствующие устройства + Соответствующие устройства Add Device - Добавить устройство + Добавить устройство Create Device Configuration - Создать конфигурацию устройства + Создать конфигурацию устройства Specify for which device the new configuration should apply - Укажите, для какого устройства следует применить новую конфигурацию + Укажите, для какого устройства следует применить новую конфигурацию Create - Создать + Создать There is already a configuration for this device. - Для этого устройства уже есть конфигурация. + Для этого устройства уже есть конфигурация. Enter an identifier name - Введите имя идентификатора + Введите имя идентификатора Administration - Администрирование + Администрирование Please select a leaderboard group - Выберите группу таблиц лидеров + Выберите группу таблиц лидеров Link Leaderboard to Leaderboard Group of Event - Связать таблицу лидеров с группой таблиц лидеров события + Связать таблицу лидеров с группой таблиц лидеров события Link leaderboard {0} to a leaderboard group of event {1} - Связать таблицу лидеров {0} с группой таблиц лидеров события {1} + Связать таблицу лидеров {0} с группой таблиц лидеров события {1} Failed to link leaderboard {0} to leaderboard group {1} - Не удалось связать таблицу лидеров {0} с группой таблиц лидеров {1} + Не удалось связать таблицу лидеров {0} с группой таблиц лидеров {1} Create default regatta - Создать регату по умолчанию + Создать регату по умолчанию Do you want to create a default Regatta? - Создать регату по умолчанию? + Создать регату по умолчанию? Default - По умолчанию + По умолчанию The default link is used unless a language specific overwrite exists. If no default exists, there will be no link for languages that do not explicitly have one set. - При отсутствии замен для конкретного языка используется ссылка по умолчанию. Если значения по умолчанию не существует, ссылок для языков, не имеющих одного явно заданного набора, не будет. + При отсутствии замен для конкретного языка используется ссылка по умолчанию. Если значения по умолчанию не существует, ссылок для языков, не имеющих одного явно заданного набора, не будет. QR-Sync - QR-синхронизация + QR-синхронизация There is no identifier set. - Не задан идентификатор. + Не задан идентификатор. Could not obtain access token for user {0} - Не удалось получить токен для пользователя {0} + Не удалось получить токен для пользователя {0} Device Name - Имя устройства + Имя устройства Please enter a valid Server URL - Введите действительный URL-адрес сервера + Введите действительный URL-адрес сервера Server URL empty - URL-адрес сервера пуст + URL-адрес сервера пуст Marks - Отметки + Отметки Add new fix - Добавить новый замер + Добавить новый замер Please select another timepoint. There already is a fix at the timepoint of the timeslider. - Выберите другой момент времени. На этой отметке шкалы времени уже есть замер. + Выберите другой момент времени. На этой отметке шкалы времени уже есть замер. Confirm move - Подтвердить движение + Подтвердить движение Confirm new fix - Подтвердить новый замер + Подтвердить новый замер Select a fix position by dragging the map. Then confirm or cancel with the buttons below. - Выберите позицию замера, перетащив карту. Затем подтвердите или отмените действие с помощью кнопок ниже. + Выберите позицию замера, перетащив карту. Затем подтвердите или отмените действие с помощью кнопок ниже. Please select a mark from the list on the left. - Выберите отметку из списка слева. + Выберите отметку из списка слева. Mark fixes - Замеры отметки + Замеры отметки utm_source=sapsailing&utm_medium=solutions&utm_content=2016.02&utm_campaign=solutions&pcampaignid=2016.sapsailing.solutions - utm_source=sapsailing&utm_medium=solutions&utm_content=2016.02&utm_campaign=solutions&pcampaignid=2016.sapsailing.solutions + utm_source=sapsailing&utm_medium=solutions&utm_content=2016.02&utm_campaign=solutions&pcampaignid=2016.sapsailing.solutions https://play.google.com/store/apps/details?id=com.sap.sailing.android.tracking.app - https://play.google.com/store/apps/details?id=com.sap.sailing.android.tracking.app + https://play.google.com/store/apps/details?id=com.sap.sailing.android.tracking.app https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app - https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app + https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app - https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app + https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app https://play.google.com/intl/en/badges/images/generic/en-play-badge.png - https://play.google.com/intl/en/badges/images/generic/en-play-badge.png + https://play.google.com/intl/en/badges/images/generic/en-play-badge.png http://appstore.com/sapsailinsight - http://appstore.com/sapsailinsight + http://appstore.com/sapsailinsight http://appstore.com/stgtrainingstagebuch - http://appstore.com/stgtrainingstagebuch + http://appstore.com/stgtrainingstagebuch _en - _en + _en Distance from average position - Дистанция от средней позиции + Дистанция от средней позиции Use a touch optimized UI to move the fix - Используйте сенсорный UI для перемещения замера + Используйте сенсорный UI для перемещения замера Delete fix - Удалить замер + Удалить замер The deletion of this fix is currently not possible. - Удалить этот замер сейчас невозможно. + Удалить этот замер сейчас невозможно. Loading mark fixes... - Загрузка замеров отметки... + Загрузка замеров отметки... Fix successfully added. - Замер успешно добавлен. + Замер успешно добавлен. Fix position successfully edited. - Позиция замера успешно изменена. + Позиция замера успешно изменена. Fix successfully removed. - Замер успешно удален. + Замер успешно удален. Drag to change position\nRight-click to get menu to delete - Позиция изменяется перетаскиванием\nДля удаления вызовите меню щелчком правой кнопкой мыши + Позиция изменяется перетаскиванием\nДля удаления вызовите меню щелчком правой кнопкой мыши Move fix - Переместить замер + Переместить замер + + + Manage2Sail Event + Событие Manage2Sail + + + There are no events yet. + Событий еще нет. + + + Please select a result import provider... + Выберите поставщика импорта результатов... + + + Build version + Версия сборки + + + Server Information + Информация о сервере + + + Server Configuration + Конфигурация сервера + + + Standalone server + Изолированный сервер + + + Send... + Отправить... + + + PassId + PassId + + + Created at + Создано в + + + Logical timepoint + Логическая отметка времени + + + Info + Инфо + + + Wind fixes + Замеры ветра + + + 2x Mark + 2-отмет. + + + {0,number,#,###}-{1,number,#,###} of {2,number,#,###} + {0,number,#,###}–{1,number,#,###} из {2,number,#,###} + + + {0,number,#,###}-{1,number,#,###} of over {2,number,#,###} + {0,number,#,###}–{1,number,#,###} из {2,number,#,###}
diff --git a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties.xlf b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties.xlf index 86eabbede7d..29668e4636b 100644 --- a/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties.xlf +++ b/XLF/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties.xlf @@ -1,12 +1,12 @@ - +
- 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 - +
@@ -1644,8 +1644,8 @@ 新标志 - Mark/Gate - 标志/门标 + Mark/Gate name + 标志/门标名称 Error updating race course: {0} @@ -3919,6 +3919,10 @@ Password 密码 + + Search tag + 搜索标记 + Do you really want to remove the leaderboard: ''{0}''? 是否确定移除积分榜: ''{0}''? @@ -4047,6 +4051,10 @@ Tracker Status 跟踪器状态 + + Course Status + 场地状态 + Can only be edited before starting tracking 只可以在开始跟踪前编辑 @@ -5987,6 +5995,70 @@ Move fix 移动修复 + + Manage2Sail Event + Manage2Sail 活动 + + + There are no events yet. + 暂无活动。 + + + Please select a result import provider... + 请选择结果导入提供方... + + + Build version + 内部版本 + + + Server Information + 服务器信息 + + + Server Configuration + 服务器配置 + + + Standalone server + 独立服务器 + + + Send... + 发送... + + + PassId + 场次编号 + + + Created at + 创建时间 + + + Logical timepoint + 逻辑时间点 + + + Info + 信息 + + + Wind fixes + 风力修复 + + + 2x Mark + 2 个标志 + + + {0,number,#,###}-{1,number,#,###} of {2,number,#,###} + {0,number,#,###}-{1,number,#,###}/{2,number,#,###} + + + {0,number,#,###}-{1,number,#,###} of over {2,number,#,###} + {0,number,#,###}-{1,number,#,###}/{2,number,#,###} 以上 +
\ No newline at end of file diff --git a/build.gradle b/build.gradle index 70b4cd10290..7413c75e2af 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,8 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.1.0-rc1' + classpath 'com.android.tools.build:gradle:2.1.0' + classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.5.0' } } diff --git a/ios/SAPTracker/src/xcode/SAPTracker/APIManager.swift b/ios/SAPTracker/src/xcode/SAPTracker/APIManager.swift index 853ef759d9b..ad3b99ccbe2 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/APIManager.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/APIManager.swift @@ -63,7 +63,7 @@ public class APIManager: NSObject { // set up reachability let operationQueue = manager!.operationQueue - manager!.reachabilityManager.setReachabilityStatusChangeBlock({(AFNetworkReachabilityStatus status) -> Void in + manager!.reachabilityManager.setReachabilityStatusChangeBlock({(status) -> Void in switch (status) { case AFNetworkReachabilityStatus.ReachableViaWWAN, AFNetworkReachabilityStatus.ReachableViaWiFi: operationQueue?.suspended = false @@ -106,7 +106,7 @@ public class APIManager: NSObject { } public func teamImage(competitorId: String!, result: (imageUrl: String?) -> Void) { - getTeam(competitorId, success: { (AFHTTPRequestOperation operation, AnyObject teamResponseObject) -> Void in + getTeam(competitorId, success: { (operation, teamResponseObject) -> Void in if let team = teamResponseObject as? [String: AnyObject], let imageUrl = team["imageUri"] as? String { @@ -115,7 +115,7 @@ public class APIManager: NSObject { result(imageUrl: nil) } - }, failure: { (AFHTTPRequestOperation operation, NSError error) -> Void in + }, failure: { (operation, error) -> Void in // No image & that's ok result(imageUrl: nil) }) diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Main.storyboard b/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Main.storyboard index bc5e07836d7..b93199fc460 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Main.storyboard +++ b/ios/SAPTracker/src/xcode/SAPTracker/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -213,8 +213,8 @@ - - + + @@ -803,9 +803,7 @@ diff --git a/ios/SAPTracker/src/xcode/SAPTracker/HomeViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/HomeViewController.swift index 16faace6a95..1fe17e1cf20 100755 --- a/ios/SAPTracker/src/xcode/SAPTracker/HomeViewController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/HomeViewController.swift @@ -9,7 +9,7 @@ import UIKit import CoreData -class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, UIActionSheetDelegate, NSFetchedResultsControllerDelegate, QRCodeManagerDelegate { +class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, NSFetchedResultsControllerDelegate, QRCodeManagerDelegate { enum AlertView: Int { case NoCameraAvailable @@ -157,25 +157,32 @@ class HomeViewController: UIViewController, UITableViewDataSource, UITableViewDe qrCodeManager!.parseUrl(url2) } - // MARK: - UIActionSheetDelegate - @IBAction func showActionSheet(sender: AnyObject) { - let actionSheet = UIActionSheet(title: nil, delegate: self, cancelButtonTitle: nil, destructiveButtonTitle: nil, otherButtonTitles: NSLocalizedString("Settings", comment: ""), NSLocalizedString("About", comment: ""), NSLocalizedString("Cancel", comment: "")) - actionSheet.cancelButtonIndex = 3 - actionSheet.showInView(view) - } - - func actionSheet(actionSheet: UIActionSheet, clickedButtonAtIndex buttonIndex: Int) { - switch buttonIndex{ - case 0: - performSegueWithIdentifier("Settings", sender: actionSheet) - break - case 1: - performSegueWithIdentifier("About", sender: actionSheet) - break - default: - break + + let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .ActionSheet) + if let popoverController = actionSheet.popoverPresentationController, + let barButtonItem = sender as? UIBarButtonItem { + popoverController.barButtonItem = barButtonItem } + + let aSettings = UIAlertAction(title: NSLocalizedString("Settings", comment: ""), style: .Default) { (action) -> Void in + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.6 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { () -> Void in + self.performSegueWithIdentifier("SettingsFromHome", sender: actionSheet) + } + } + actionSheet.addAction(aSettings) + + let aAbout = UIAlertAction(title: NSLocalizedString("About", comment: ""), style: .Default) { (action) -> Void in + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.6 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { () -> Void in + self.performSegueWithIdentifier("AboutFromHome", sender: actionSheet) + } + } + actionSheet.addAction(aAbout) + + let aCancel = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .Cancel, handler: nil) + actionSheet.addAction(aCancel) + + presentViewController(actionSheet, animated: true, completion: nil) } // MARK: - UITableViewDataSource diff --git a/ios/SAPTracker/src/xcode/SAPTracker/Info.plist b/ios/SAPTracker/src/xcode/SAPTracker/Info.plist index f98ed85acdf..78a2feb5690 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/Info.plist +++ b/ios/SAPTracker/src/xcode/SAPTracker/Info.plist @@ -30,7 +30,7 @@ CFBundleVersion - 30 + 37 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/ios/SAPTracker/src/xcode/SAPTracker/LaunchScreen.storyboard b/ios/SAPTracker/src/xcode/SAPTracker/LaunchScreen.storyboard index cbfb2d68981..f6bb9acf486 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/LaunchScreen.storyboard +++ b/ios/SAPTracker/src/xcode/SAPTracker/LaunchScreen.storyboard @@ -1,8 +1,8 @@ - + - + diff --git a/ios/SAPTracker/src/xcode/SAPTracker/QRCodeManager.swift b/ios/SAPTracker/src/xcode/SAPTracker/QRCodeManager.swift index 7402428ece2..671a4cb884e 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/QRCodeManager.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/QRCodeManager.swift @@ -96,17 +96,17 @@ class QRCodeManager : NSObject, UIAlertViewDelegate { // get event APIManager.sharedManager.getEvent(qrcodeData!.eventId, - success: { (AFHTTPRequestOperation operation, AnyObject eventResponseObject) -> Void in + success: { (operation, eventResponseObject) -> Void in self.eventDictionary = eventResponseObject as? [String: AnyObject] APIManager.sharedManager.getLeaderBoard(self.qrcodeData!.leaderBoardName, // get leader board - success: { (AFHTTPRequestOperation operation, AnyObject leaderBoardResponseObject) -> Void in + success: { (operation, leaderBoardResponseObject) -> Void in self.leaderBoardDictionary = leaderBoardResponseObject as? [String: AnyObject] APIManager.sharedManager.getCompetitor(self.qrcodeData!.competitorId, // get competitor - success: { (AFHTTPRequestOperation operation, AnyObject competitorResponseObject) -> Void in + success: { (operation, competitorResponseObject) -> Void in APIManager.sharedManager.teamImage(self.qrcodeData!.competitorId, result: { (imageUrl) -> Void in @@ -125,14 +125,14 @@ class QRCodeManager : NSObject, UIAlertViewDelegate { alertView.show() }) - }, failure: { (AFHTTPRequestOperation operation, NSError error) -> Void in + }, failure: { (operation, error) -> Void in self.delegate.activityIndicatorView?.stopAnimating() let title = String(format: NSLocalizedString("Couldn't get competitor %@", comment: ""), self.qrcodeData!.competitorId!) let alertView = UIAlertView(title: title, message: error.localizedDescription, delegate: self, cancelButtonTitle: NSLocalizedString("Cancel", comment: "")) alertView.tag = AlertView.ServerError.rawValue; alertView.show() - }) }, failure: { (AFHTTPRequestOperation operation, NSError error) -> Void in + }) }, failure: { (operation, error) -> Void in self.delegate.activityIndicatorView?.stopAnimating() let title = String(format: NSLocalizedString("Couldn't get leader board %@", comment: ""), self.qrcodeData!.leaderBoardName!) let alertView = UIAlertView(title: title, message: error.localizedDescription, delegate: self, cancelButtonTitle: NSLocalizedString("Cancel", comment: "")) @@ -140,7 +140,7 @@ class QRCodeManager : NSObject, UIAlertViewDelegate { alertView.show() }) - }, failure: { (AFHTTPRequestOperation operation, NSError error) -> Void in + }, failure: { (operation, error) -> Void in self.delegate.activityIndicatorView?.stopAnimating() let title = String(format: NSLocalizedString("Couldn't get event %@", comment: ""), self.qrcodeData!.eventId!) let alertView = UIAlertView(title: title, message: error.localizedDescription, delegate: self, cancelButtonTitle: NSLocalizedString("Cancel", comment: "")) @@ -185,7 +185,7 @@ class QRCodeManager : NSObject, UIAlertViewDelegate { checkIn.imageUrl = teamImageURL APIManager.sharedManager.checkIn(leaderBoardName, competitorId: competitorId, deviceUuid: DeviceUDIDManager.UDID, pushDeviceId: "", fromMillis: fromMillis, - success: { (AFHTTPRequestOperation operation, AnyObject eventResponseObject) -> Void in + success: { (operation, eventResponseObject) -> Void in // create core data objects let event = DataManager.sharedManager.newEvent(checkIn) @@ -197,7 +197,7 @@ class QRCodeManager : NSObject, UIAlertViewDelegate { DataManager.sharedManager.saveContext() self.delegate.qrCodeOK?() - }, failure: { (AFHTTPRequestOperation operation, NSError error) -> Void in + }, failure: { (operation, error) -> Void in self.delegate.activityIndicatorView?.stopAnimating() let title = String(format:NSLocalizedString("Couldn't check-in to %@", comment: ""), leaderBoardName); let alertView = UIAlertView(title: title, message: error.localizedDescription, delegate: self, cancelButtonTitle: NSLocalizedString("Cancel", comment: "")) diff --git a/ios/SAPTracker/src/xcode/SAPTracker/RegattaViewController.swift b/ios/SAPTracker/src/xcode/SAPTracker/RegattaViewController.swift index 154805fe2bd..c551eed0d66 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/RegattaViewController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/RegattaViewController.swift @@ -79,7 +79,7 @@ class RegattaViewController : UIViewController, UIActionSheetDelegate, UINavigat imageView.image = UIImage(data: DataManager.sharedManager.selectedCheckIn!.userImage!) self.yourTeamPhotoButton.hidden = true } else if DataManager.sharedManager.selectedCheckIn?.imageUrl != nil { - let imageUrl = NSURL(string: DataManager.sharedManager.selectedCheckIn!.imageUrl! + "sa") + let imageUrl = NSURL(string: DataManager.sharedManager.selectedCheckIn!.imageUrl!) let urlRequest = NSURLRequest(URL: imageUrl!) imageView.setImageWithURLRequest(urlRequest, placeholderImage: nil, @@ -224,7 +224,9 @@ class RegattaViewController : UIViewController, UIActionSheetDelegate, UINavigat case 0: showCheckOutAlertView() case 1: - performSegueWithIdentifier("Settings", sender: actionSheet) + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(0.6 * Double(NSEC_PER_SEC))), dispatch_get_main_queue()) { () -> Void in + self.performSegueWithIdentifier("Settings", sender: actionSheet) + } break case 2: showImageAlertView(actionSheet) @@ -279,7 +281,7 @@ class RegattaViewController : UIViewController, UIActionSheetDelegate, UINavigat DataManager.sharedManager.selectedCheckIn!.userImage = jpegData APIManager.sharedManager.postTeamImage(DataManager.sharedManager.selectedCheckIn!.competitorId, imageData: jpegData, - success: { (AnyObject responseObject) -> Void in + success: { (responseObject) -> Void in // http://wiki.sapsailing.com/wiki/tracking-app/api-v1#Competitor-Information-%28in-general%29 // "Additional Notes: Competitor profile image left out for now." let responseDictionary = responseObject as![String: AnyObject] @@ -289,7 +291,7 @@ class RegattaViewController : UIViewController, UIActionSheetDelegate, UINavigat NSUserDefaults.standardUserDefaults().setBool(false, forKey: self.uploadKey) NSUserDefaults.standardUserDefaults().synchronize() }, - failure: { (NSError error) -> Void in + failure: { (error) -> Void in let alertView = UIAlertView(title: NSLocalizedString("Failed to upload image", comment: ""), message: error.localizedDescription, delegate: self, cancelButtonTitle: NSLocalizedString("Cancel", comment: "")) alertView.tag = AlertView.UploadFailed.rawValue; alertView.show() @@ -323,9 +325,9 @@ class RegattaViewController : UIViewController, UIActionSheetDelegate, UINavigat competitorId: DataManager.sharedManager.selectedCheckIn!.competitorId, deviceUuid: DeviceUDIDManager.UDID, toMillis: toMillis, - success: { (AFHTTPRequestOperation operation, AnyObject competitorResponseObject) -> Void in + success: { (operation, competitorResponseObject) -> Void in }, - failure: { (AFHTTPRequestOperation operation, NSError error) -> Void in + failure: { (operation, error) -> Void in } ) DataManager.sharedManager.deleteCheckIn(DataManager.sharedManager.selectedCheckIn!) diff --git a/ios/SAPTracker/src/xcode/SAPTracker/SendGPSFixController.swift b/ios/SAPTracker/src/xcode/SAPTracker/SendGPSFixController.swift index 4df174933cc..44ff44974a9 100644 --- a/ios/SAPTracker/src/xcode/SAPTracker/SendGPSFixController.swift +++ b/ios/SAPTracker/src/xcode/SAPTracker/SendGPSFixController.swift @@ -80,7 +80,7 @@ class SendGPSFixController: NSObject { APIManager.sharedManager.initManager(serverUrl) if APIManager.sharedManager.networkAvailable { APIManager.sharedManager.postGPSFixes(DeviceUDIDManager.UDID, gpsFixes: gpsFixes, - success: { (AFHTTPRequestOperation operation, AnyObject competitorResponseObject) -> Void in + success: { (operation, competitorResponseObject) -> Void in print("sent GPS fixes") for gpsFix in gpsFixes { DataManager.sharedManager.managedObjectContext!.deleteObject(gpsFix) @@ -88,7 +88,7 @@ class SendGPSFixController: NSObject { let notification = NSNotification(name: NotificationType.gpsFixesSynced, object: self) NSNotificationQueue.defaultQueue().enqueueNotification(notification, postingStyle: NSPostingStyle.PostASAP) }, - failure: { (AFHTTPRequestOperation operation, NSError error) -> Void in + failure: { (operation, error) -> Void in print("error sending GPS fixes") }) } diff --git a/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java b/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java index 1df916f6e48..e461b040993 100644 --- a/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java +++ b/java/com.sap.sailing.autoload/src/com/sap/sailing/autoload/TracTrac.java @@ -112,7 +112,7 @@ public class TracTrac { raceRecords = getTracTracAdapter().getTracTracRaceRecords(new URL(eventJsonURL), /*loadClientParam*/ false); List result = new ArrayList(); for (RaceRecord raceRecord : raceRecords.getB()) { - if (listHiddenRaces == false && raceRecord.getRaceStatus().equals(TracTracConnectionConstants.HIDDEN_STATUS)) { + if (listHiddenRaces == false && raceRecord.getRaceVisibility().equals(TracTracConnectionConstants.HIDDEN_VISIBILITY)) { continue; } diff --git a/java/com.sap.sailing.dashboards.gwt/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sailing.dashboards.gwt/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..b5432fff458 --- /dev/null +++ b/java/com.sap.sailing.dashboards.gwt/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +eclipse.preferences.version=1 +encoding//src/main/java/com/sap/sailing/dashboards/gwt/client/i18n/StringMessages.properties=UTF-8 +encoding//src/main/java/com/sap/sailing/dashboards/gwt/client/i18n/StringMessages_de.properties=UTF-8 diff --git a/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF index 90005faa292..c058553927b 100644 --- a/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.common/META-INF/MANIFEST.MF @@ -11,6 +11,7 @@ Export-Package: com.sap.sailing.domain.common, com.sap.sailing.domain.common.confidence, com.sap.sailing.domain.common.confidence.impl, com.sap.sailing.domain.common.dto, + com.sap.sailing.domain.common.i18n, com.sap.sailing.domain.common.impl, com.sap.sailing.domain.common.media, com.sap.sailing.domain.common.quadtree, diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java index d6b57f62e8d..68cd340fb19 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/BoatClassMasterdata.java @@ -38,7 +38,7 @@ public enum BoatClassMasterdata { FARR_30 ("Farr 30", true, 9.42, 3.08, BoatHullType.MONOHULL, true, "F30", "F-30", "Farr-30"), FARR_280 ("Farr 280", true, 8.72, 2.87, BoatHullType.MONOHULL, true, "F280", "F-280", "Farr-280"), FINN ("Finn", true, 4.50, 1.51, BoatHullType.MONOHULL, false), - FLYING_DUTCHMAN ("Flying Dutchman", true, 6.10, 1.80, BoatHullType.MONOHULL, true), + FLYING_DUTCHMAN ("Flying Dutchman", true, 6.10, 1.80, BoatHullType.MONOHULL, true, "FD"), FLYING_PHANTOM ("Flying Phantom", true, 5.52, 3.00, BoatHullType.CATAMARAN, true), FOLKBOAT ("Folkboat", true, 7.68, 2.20, BoatHullType.MONOHULL, false, "Folke", "Folkeboot"), FUN ("FUN", true, 7.20, 2.45, BoatHullType.MONOHULL, true, "FUN O.D.", "FUN OD", "Open FUN"), diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DataImportProgress.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DataImportProgress.java index 330e3cd4589..546d0cb6c16 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DataImportProgress.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DataImportProgress.java @@ -4,14 +4,40 @@ import java.io.Serializable; import java.util.UUID; public interface DataImportProgress extends Serializable { - + + /** + * Helper for i18n of import sub progresses messages. A method according to the messageKey is required + * in com.sap.sailing.gwt.ui.masterdataimport.MasterDataImportSubProgressMessages interface. + */ + enum SubProgress { + CONNECTION_SETUP("connectionSetup"), + CONNECTION_ESTABLISH("connectionEstablish"), + TRANSFER_STARTED("transferStarted"), + TRANSFER_COMPLETED("transferCompleted"), + IMPORT_INIT("importInit"), + IMPORT_WAIT("importWait"), + IMPORT_LEADERBOARD_GROUPS("importLeaderboardGroups"), + IMPORT_WIND_TRACKS("importWindTracks"), + UPDATE_EVENT_LEADERBOARD_GROUP_LINKS("updateEventLeaderboardGroupLinks"); + + private final String messageKey; + + private SubProgress(String messageKey) { + this.messageKey = messageKey; + } + + public String getMessageKey() { + return messageKey; + } + } + double getOverallProgressPct(); void setOverAllProgressPct(double pct); - - String getNameOfCurrentSubProgress(); - - void setNameOfCurrentSubProgress(String name); + + DataImportSubProgress getCurrentSubProgress(); + + void setCurrentSubProgress(DataImportSubProgress subProgress); double getCurrentSubProgressPct(); diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DataImportSubProgress.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DataImportSubProgress.java new file mode 100644 index 00000000000..444b97bc7de --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/DataImportSubProgress.java @@ -0,0 +1,70 @@ +package com.sap.sailing.domain.common; + +import com.sap.sailing.domain.common.i18n.CommonStringMessages; + +public enum DataImportSubProgress { + + INIT { + @Override + public String getMessage(CommonStringMessages messages) { + return messages.init(); + } + }, + CONNECTION_SETUP { + @Override + public String getMessage(CommonStringMessages messages) { + return messages.connectionSetup(); + } + }, + CONNECTION_ESTABLISH { + @Override + public String getMessage(CommonStringMessages messages) { + return messages.connectionEstablish(); + } + }, + TRANSFER_STARTED { + @Override + public String getMessage(CommonStringMessages messages) { + return messages.transferStarted(); + } + }, + TRANSFER_COMPLETED { + @Override + public String getMessage(CommonStringMessages messages) { + return messages.transferCompleted(); + } + }, + IMPORT_INIT { + @Override + public String getMessage(CommonStringMessages messages) { + return messages.importInit(); + } + }, + IMPORT_WAIT { + @Override + public String getMessage(CommonStringMessages messages) { + return messages.importWait(); + } + }, + IMPORT_LEADERBOARD_GROUPS { + @Override + public String getMessage(CommonStringMessages messages) { + return messages.importLeaderboardGroups(); + } + }, + IMPORT_WIND_TRACKS { + @Override + public String getMessage(CommonStringMessages messages) { + return messages.importWindTracks(); + } + }, + UPDATE_EVENT_LEADERBOARD_GROUP_LINKS { + @Override + public String getMessage(CommonStringMessages messages) { + return messages.updateEventLeaderboardGroupLinks(); + } + }; + + public abstract String getMessage(CommonStringMessages messages); + +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PassingInstruction.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PassingInstruction.java index d8d11bf4472..5cabd2549b9 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PassingInstruction.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PassingInstruction.java @@ -14,20 +14,34 @@ package com.sap.sailing.domain.common; public enum PassingInstruction { None(1, 2), Port(1), - Single_Unknown(1), Starboard(1), + Single_Unknown(1), Gate(2), Line(2), Offset(2), FixedBearing(1); + /** + * Those values relevant for picking by a user when creating a waypoint; for example, this list + * does not include the {@link #None} or the {@link #Single_Unknown} literals. No guarantees + * made as to the ordering. The enum literals may change, be extended, have elements inserted or + * removed at any time! + */ public static PassingInstruction[] relevantValues() { - PassingInstruction[] uiValues = new PassingInstruction[PassingInstruction.values().length - 1]; - + final PassingInstruction[] uselessValues = new PassingInstruction[] { None, Single_Unknown, Offset, FixedBearing }; + PassingInstruction[] uiValues = new PassingInstruction[PassingInstruction.values().length - uselessValues.length]; int i = 0; for (PassingInstruction p : PassingInstruction.values()) { - if (p != PassingInstruction.None) + boolean useless = false; + for (PassingInstruction u : uselessValues) { + if (u == p) { + useless = true; + break; + } + } + if (!useless) { uiValues[i++] = p; + } } return uiValues; } diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/PathType.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PathType.java similarity index 90% rename from java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/PathType.java rename to java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PathType.java index 6ed4b730224..92486408659 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/PathType.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/PathType.java @@ -1,4 +1,4 @@ -package com.sap.sailing.simulator; +package com.sap.sailing.domain.common; public enum PathType { OMNISCIENT("1#Omniscient"), diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/i18n/CommonStringMessages.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/i18n/CommonStringMessages.java new file mode 100644 index 00000000000..932e8e12ae1 --- /dev/null +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/i18n/CommonStringMessages.java @@ -0,0 +1,17 @@ +package com.sap.sailing.domain.common.i18n; + +public interface CommonStringMessages { + + /* Master data import sub progress messages */ + String init(); + String connectionSetup(); + String connectionEstablish(); + String transferStarted(); + String transferCompleted(); + String importInit(); + String importWait(); + String importLeaderboardGroups(); + String importWindTracks(); + String updateEventLeaderboardGroupLinks(); + +} diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/DataImportProgressImpl.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/DataImportProgressImpl.java index e7260b714ca..495a482e866 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/DataImportProgressImpl.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/impl/DataImportProgressImpl.java @@ -3,6 +3,7 @@ package com.sap.sailing.domain.common.impl; import java.util.UUID; import com.sap.sailing.domain.common.DataImportProgress; +import com.sap.sailing.domain.common.DataImportSubProgress; import com.sap.sailing.domain.common.MasterDataImportObjectCreationCount; public class DataImportProgressImpl implements DataImportProgress { @@ -11,7 +12,7 @@ public class DataImportProgressImpl implements DataImportProgress { private UUID currentImportOperationId; private MasterDataImportObjectCreationCount result = null; private double overallProgressPct = 0; - private String currentSubProgressName = "Initializing Import"; + private DataImportSubProgress currentSubProgress = DataImportSubProgress.IMPORT_INIT; private double currentSubProgressPct = 0; private boolean failed = false; private String errorMessage; @@ -21,17 +22,16 @@ public class DataImportProgressImpl implements DataImportProgress { public DataImportProgressImpl(final UUID currentImportOperationId) { this.currentImportOperationId = currentImportOperationId; - } @Override public double getOverallProgressPct() { return overallProgressPct; } - + @Override - public String getNameOfCurrentSubProgress() { - return currentSubProgressName; + public DataImportSubProgress getCurrentSubProgress() { + return currentSubProgress; } @Override @@ -48,10 +48,10 @@ public class DataImportProgressImpl implements DataImportProgress { public void setOverAllProgressPct(double pct) { overallProgressPct = pct; } - + @Override - public void setNameOfCurrentSubProgress(String name) { - currentSubProgressName = name; + public void setCurrentSubProgress(DataImportSubProgress subProgress) { + currentSubProgress = subProgress; } @Override diff --git a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/DeviceMappingConstants.java b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/DeviceMappingConstants.java index 9ac739d6d27..743f6f1eb8a 100644 --- a/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/DeviceMappingConstants.java +++ b/java/com.sap.sailing.domain.common/src/com/sap/sailing/domain/common/racelog/tracking/DeviceMappingConstants.java @@ -1,6 +1,5 @@ package com.sap.sailing.domain.common.racelog.tracking; -import com.sap.sailing.domain.common.racelog.RaceLogServletConstants; import com.sap.sse.common.util.UrlHelper; /** @@ -39,44 +38,16 @@ public class DeviceMappingConstants { public static final String JSON_TO_MILLIS = "toMillis"; public static final String JSON_TEAM_IMAGE_URI = "teamImageUri"; - @Deprecated - static final String APK_PATH = "/apps/com.sap.sailing.android.tracking.app.apk"; - public static String getDeviceMappingForRegattaLogUrl(String serverUrlWithoutTrailingSlash, String eventId, String leaderboardName, String mappedItemType, String mappedItemId, UrlHelper helper) { - return helper.encodeUrl(serverUrlWithoutTrailingSlash + TRACKING_URL_BASE + "?" + URL_EVENT_ID + "=" + eventId + "&" - + URL_LEADERBOARD_NAME + "=" + leaderboardName + "&" + mappedItemType + "=" - + mappedItemId); + return serverUrlWithoutTrailingSlash + TRACKING_URL_BASE + "?" + URL_EVENT_ID + "=" + helper.encodeQueryString(eventId) + "&" + + URL_LEADERBOARD_NAME + "=" + helper.encodeQueryString(leaderboardName) + "&" + helper.encodeQueryString(mappedItemType) + "=" + + helper.encodeQueryString(mappedItemId); } public static String getBuoyTenderInvitationUrl(String serverUrlWithoutTrailingSlash, String leaderboardName, String eventId, UrlHelper helper) { - return helper.encodeUrl(serverUrlWithoutTrailingSlash + BUOY_TENDER_URL_BASE + "?" + URL_EVENT_ID + "=" - + eventId + "&" + URL_LEADERBOARD_NAME + "=" + leaderboardName); - } - - @Deprecated - public static String getDeviceMappingForRaceLogUrl(String serverUrlWithoutTrailingSlash, String leaderboardName, - String raceColumnName, String fleetName, String mappedItemType, String mappedItemId, Long fromMillis, - Long toMillis, UrlHelper helper) throws QRCodeURLCreationException { - - if (toMillis != null && fromMillis != null){ - if (fromMillis > toMillis) { - throw new QRCodeURLCreationException("from can't lie after to"); - } - } - - StringBuilder urlToEncode = new StringBuilder(); - - urlToEncode.append(serverUrlWithoutTrailingSlash + DeviceMappingConstants.APK_PATH + "?" - + RaceLogServletConstants.PARAMS_LEADERBOARD_NAME + "=" + leaderboardName + "&" - + RaceLogServletConstants.PARAMS_RACE_COLUMN_NAME + "=" + raceColumnName + "&" - + RaceLogServletConstants.PARAMS_RACE_FLEET_NAME + "=" + fleetName + "&" + mappedItemType + "=" - + mappedItemId); - - urlToEncode.append((fromMillis != null) ? "&" + DeviceMappingConstants.URL_FROM_MILLIS + "=" + fromMillis : ""); - urlToEncode.append((toMillis != null) ? "&" + DeviceMappingConstants.URL_TO_MILLIS + "=" + toMillis : ""); - - return helper.encodeUrl(urlToEncode.toString()); + return serverUrlWithoutTrailingSlash + BUOY_TENDER_URL_BASE + "?" + URL_EVENT_ID + "=" + + helper.encodeQueryString(eventId) + "&" + URL_LEADERBOARD_NAME + "=" + helper.encodeQueryString(leaderboardName); } } diff --git a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterImpl.java b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterImpl.java index 97368304af2..7df05a55997 100755 --- a/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterImpl.java +++ b/java/com.sap.sailing.domain.racelogtrackingadapter/src/com/sap/sailing/domain/racelogtracking/impl/RaceLogTrackingAdapterImpl.java @@ -391,14 +391,10 @@ public class RaceLogTrackingAdapterImpl implements RaceLogTrackingAdapter { @Override public void inviteBuoyTenderViaEmail(Event event, Leaderboard leaderboard, String serverUrlWithoutTrailingSlash, String emails, String iOSAppUrl, String androidAppUrl, Locale locale) throws MailException { - StringBuilder occuredExceptions = new StringBuilder(); - String[] emailArray = emails.split(","); String leaderboardName = leaderboard.getName(); - String eventId = event.getId().toString(); - String logoUrl = null; List imagesWithTag = event.findImagesWithTag(MediaTagConstants.LOGO); if (imagesWithTag != null && !imagesWithTag.isEmpty()) { @@ -417,7 +413,6 @@ public class RaceLogTrackingAdapterImpl implements RaceLogTrackingAdapter { occuredExceptions.append(e.getMessage() + "\r\n"); } } - if (!(occuredExceptions.length() == 0)) { throw new MailException(occuredExceptions.toString()); } diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java index fd072fb4fdb..f9deaa0607c 100644 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/LeaderboardScoringAndRankingTest.java @@ -1605,6 +1605,65 @@ public class LeaderboardScoringAndRankingTest extends AbstractLeaderboardTest { assertEquals(0., leaderboard1.getTotalPoints(c[0], beforeStartOfR1), 0.00000001); // not even the R1 scores apply before the start time of R1 } + /** + * When a DNS score correction is made then both, the MaxPointsReason and the score shall apply at the start of the race. + */ + @Test + public void testDNSScoreAppliesBeforeEndOfRace() throws NoWindException { + Competitor[] c = createCompetitors(4).toArray(new Competitor[0]); + final TimePoint endOfR1 = MillisecondsTimePoint.now(); + final TimePoint withinR1 = endOfR1.minus(1000); + final TimePoint startOfR1 = withinR1.minus(10000); + final TimePoint beforeStartOfR1 = startOfR1.minus(10000); + final TimePoint afterEndOfR1 = endOfR1.plus(1000); + FlexibleLeaderboard leaderboard1 = new FlexibleLeaderboardImpl("Leaderboard 1", new ThresholdBasedResultDiscardingRuleImpl(/* discarding thresholds */ new int[0]), + new LowPoint(), null); + leaderboard1.addRace(new MockedTrackedRaceWithStartTimeAndRanks(startOfR1, Arrays.asList(c)) { + private static final long serialVersionUID = 8705622361027154428L; + public TimePoint getEndOfRace() { + return endOfR1; + } + }, "R1", /* medalRace */false); + leaderboard1.getScoreCorrection().correctScore(c[0], leaderboard1.getRaceColumnByName("R1"), 123.); + leaderboard1.getScoreCorrection().setMaxPointsReason(c[0], leaderboard1.getRaceColumnByName("R1"), MaxPointsReason.DNS); + assertEquals(MaxPointsReason.NONE, leaderboard1.getMaxPointsReason(c[0], leaderboard1.getRaceColumnByName("R1"), beforeStartOfR1)); + assertEquals(MaxPointsReason.DNS, leaderboard1.getMaxPointsReason(c[0], leaderboard1.getRaceColumnByName("R1"), withinR1)); + assertEquals(MaxPointsReason.DNS, leaderboard1.getMaxPointsReason(c[0], leaderboard1.getRaceColumnByName("R1"), afterEndOfR1)); + assertNull(leaderboard1.getNetPoints(c[0], leaderboard1.getRaceColumnByName("R1"), beforeStartOfR1)); + assertEquals(123., leaderboard1.getNetPoints(c[0], leaderboard1.getRaceColumnByName("R1"), withinR1), 0.00000001); + assertEquals(123., leaderboard1.getNetPoints(c[0], leaderboard1.getRaceColumnByName("R1"), afterEndOfR1), 0.00000001); + } + + /** + * When a DNS score correction is made then both, the MaxPointsReason and the score shall apply at the start of the race. + */ + @Test + public void testDNFScoreDoesNotApplyBeforeEndOfRace() throws NoWindException { + Competitor[] c = createCompetitors(4).toArray(new Competitor[0]); + final TimePoint endOfR1 = MillisecondsTimePoint.now(); + final TimePoint withinR1 = endOfR1.minus(1000); + final TimePoint startOfR1 = withinR1.minus(10000); + final TimePoint beforeStartOfR1 = startOfR1.minus(10000); + final TimePoint afterEndOfR1 = endOfR1.plus(1000); + FlexibleLeaderboard leaderboard1 = new FlexibleLeaderboardImpl("Leaderboard 1", new ThresholdBasedResultDiscardingRuleImpl(/* discarding thresholds */ new int[0]), + new LowPoint(), null); + leaderboard1.addRace(new MockedTrackedRaceWithStartTimeAndRanks(startOfR1, Arrays.asList(c)) { + private static final long serialVersionUID = 8705622361027154428L; + public TimePoint getEndOfRace() { + return endOfR1; + } + }, "R1", /* medalRace */false); + leaderboard1.getScoreCorrection().correctScore(c[0], leaderboard1.getRaceColumnByName("R1"), 123.); + leaderboard1.getScoreCorrection().setMaxPointsReason(c[0], leaderboard1.getRaceColumnByName("R1"), MaxPointsReason.DNF); + assertEquals(MaxPointsReason.NONE, leaderboard1.getMaxPointsReason(c[0], leaderboard1.getRaceColumnByName("R1"), beforeStartOfR1)); + assertEquals(MaxPointsReason.NONE, leaderboard1.getMaxPointsReason(c[0], leaderboard1.getRaceColumnByName("R1"), withinR1)); + assertEquals(MaxPointsReason.DNF, leaderboard1.getMaxPointsReason(c[0], leaderboard1.getRaceColumnByName("R1"), afterEndOfR1)); + assertNull(leaderboard1.getNetPoints(c[0], leaderboard1.getRaceColumnByName("R1"), beforeStartOfR1)); + assertEquals(1.0, leaderboard1.getNetPoints(c[0], leaderboard1.getRaceColumnByName("R1"), withinR1), 0.00000001); // tracked rank is 1 + assertEquals(123., leaderboard1.getNetPoints(c[0], leaderboard1.getRaceColumnByName("R1"), afterEndOfR1), 0.00000001); + } + + /** * A test case for bug 1802. Competitors that didn't have a fleet (null fleet) were sorted to the end of the column even * if they had a score correction assigned. diff --git a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/MarkPassingCalculatorPerformanceTest.java b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/MarkPassingCalculatorPerformanceTest.java index 704f85bcd85..77b3f050896 100755 --- a/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/MarkPassingCalculatorPerformanceTest.java +++ b/java/com.sap.sailing.domain.test/src/com/sap/sailing/domain/test/markpassing/MarkPassingCalculatorPerformanceTest.java @@ -67,7 +67,7 @@ public class MarkPassingCalculatorPerformanceTest extends AbstractMockedRaceMark time = System.currentTimeMillis() - time; result.put("FinderPerformance", time); System.out.println(time); - Assert.assertTrue(time < 5000); + Assert.assertTrue("Time expected to be less than 5000ms but was "+time+"ms", time < 5000); } @Test diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie.java index ddaf258dfd0..ecaa46462a7 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie.java @@ -38,24 +38,19 @@ public abstract class AbstractHighPointFirstGetsFixedOr8AndLastBreaksTie extends @Override public int compareByBetterScore(Competitor o1, List> o1Scores, Competitor o2, List> o2Scores, boolean nullScoresAreBetter, TimePoint timePoint) { - int o1Wins = getWins(o1Scores); - int o2Wins = getWins(o2Scores); - int result = o2Wins - o1Wins; - return result; + Double o1Highest = getHighestScore(o1Scores); + Double o2Highest = getHighestScore(o2Scores); + return o2Highest.compareTo(o1Highest); } - /** - * Counts a competitor's wins by comparing the scores to {@link #MAX_POINTS} which is the score attributed to a race - * won - */ - private int getWins(List> scores) { - int wins = 0; + private double getHighestScore(List> scores) { + double highestScore = 0; for (com.sap.sse.common.Util.Pair score : scores) { - if (Math.abs(score.getB() - getScoreForRaceWinner() * score.getA().getFactor()) < 0.0000001) { - wins++; + if ((score.getB() * score.getA().getFactor()) > highestScore) { + highestScore = score.getB() * score.getA().getFactor(); } } - return wins; + return highestScore; } /** diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java index 76a200f12f6..32c16a2e8f3 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/AbstractSimpleLeaderboardImpl.java @@ -439,11 +439,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race @Override public Double getNetPoints(final Competitor competitor, final RaceColumn raceColumn, final TimePoint timePoint) { return getScoreCorrection().getCorrectedScore( - new Callable() { - public Integer call() throws NoWindException { - return getTrackedRank(competitor, raceColumn, timePoint); - } - }, competitor, + ()->getTrackedRank(competitor, raceColumn, timePoint), competitor, raceColumn, timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()).getCorrectedScore(); } @@ -684,21 +680,13 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race @Override public Entry getEntry(final Competitor competitor, final RaceColumn race, final TimePoint timePoint, Set discardedRaceColumns) throws NoWindException { - Callable trackedRankProvider = new Callable() { - public Integer call() throws NoWindException { - return getTrackedRank(competitor, race, timePoint); - } - }; + Callable trackedRankProvider = ()->getTrackedRank(competitor, race, timePoint); final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, race, timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); boolean discarded = isDiscarded(competitor, race, timePoint, discardedRaceColumns); final Double correctedScore = correctedResults.getCorrectedScore(); - return new EntryImpl(trackedRankProvider, correctedScore, new Callable() { - @Override - public Double call() { - return correctedResults.getUncorrectedScore(); - } - }, correctedResults.isCorrected(), discarded ? DOUBLE_0 : correctedScore == null ? null + return new EntryImpl(trackedRankProvider, correctedScore, ()->correctedResults.getUncorrectedScore(), + correctedResults.isCorrected(), discarded ? DOUBLE_0 : correctedScore == null ? null : Double.valueOf(correctedScore * race.getFactor()), correctedResults.getMaxPointsReason(), discarded, race.getFleetOfCompetitor(competitor)); } @@ -756,12 +744,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race Map> discardedRaces = new HashMap>(); for (final RaceColumn raceColumn : getRaceColumns()) { for (final Competitor competitor : getCompetitors()) { - Callable trackedRankProvider = new Callable() { - @Override - public Integer call() throws Exception { - return getTrackedRank(competitor, raceColumn, timePoint); - } - }; + Callable trackedRankProvider = ()->getTrackedRank(competitor, raceColumn, timePoint); final Result correctedResults = getScoreCorrection().getCorrectedScore(trackedRankProvider, competitor, raceColumn, timePoint, new NumberOfCompetitorsFetcherImpl(), getScoringScheme()); Set discardedRacesForCompetitor = discardedRaces.get(competitor); @@ -772,7 +755,7 @@ public abstract class AbstractSimpleLeaderboardImpl implements Leaderboard, Race boolean discarded = discardedRacesForCompetitor.contains(raceColumn); final Double correctedScore = correctedResults.getCorrectedScore(); Entry entry = new EntryImpl(trackedRankProvider, correctedScore, - new Callable() { @Override public Double call() { return correctedResults.getUncorrectedScore(); } }, + ()->correctedResults.getUncorrectedScore(), correctedResults.isCorrected(), discarded ? DOUBLE_0 : (correctedScore==null?null: Double.valueOf((correctedScore * raceColumn.getFactor()))), correctedResults.getMaxPointsReason(), diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/ScoreCorrectionImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/ScoreCorrectionImpl.java index ee0aff0bcfe..01035929d41 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/ScoreCorrectionImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/leaderboard/impl/ScoreCorrectionImpl.java @@ -324,11 +324,39 @@ public class ScoreCorrectionImpl implements SettableScoreCorrection { @Override public MaxPointsReason getMaxPointsReason(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { - MaxPointsReason result = maxPointsReasons.get(raceColumn.getKey(competitor)); - if (result == null || !isMaxPointsReasonApplicable(result, timePoint, raceColumn, competitor)) { - result = MaxPointsReason.NONE; + return getAnnotatedMaxPointsReason(competitor, raceColumn, timePoint).getMaxPointsReason(); + } + + private static class AnnotatedMaxPointsReason { + private final MaxPointsReason maxPointsReason; + private final boolean maxPointsReasonExistsButIsNotApplicableForTimePoint; + public AnnotatedMaxPointsReason(MaxPointsReason maxPointsReason, + boolean maxPointsReasonExistsButIsNotApplicableForTimePoint) { + super(); + this.maxPointsReason = maxPointsReason; + this.maxPointsReasonExistsButIsNotApplicableForTimePoint = maxPointsReasonExistsButIsNotApplicableForTimePoint; } - return result; + public MaxPointsReason getMaxPointsReason() { + return maxPointsReason; + } + public boolean isMaxPointsReasonExistsButIsNotApplicableForTimePoint() { + return maxPointsReasonExistsButIsNotApplicableForTimePoint; + } + } + + private AnnotatedMaxPointsReason getAnnotatedMaxPointsReason(Competitor competitor, RaceColumn raceColumn, TimePoint timePoint) { + MaxPointsReason maxPointsReason = maxPointsReasons.get(raceColumn.getKey(competitor)); + boolean maxPointsReasonExistsButIsNotApplicableForTimePoint; + if (maxPointsReason == null) { + maxPointsReason = MaxPointsReason.NONE; + maxPointsReasonExistsButIsNotApplicableForTimePoint = false; + } else if (!isMaxPointsReasonApplicable(maxPointsReason, timePoint, raceColumn, competitor)) { + maxPointsReason = MaxPointsReason.NONE; + maxPointsReasonExistsButIsNotApplicableForTimePoint = true; + } else { + maxPointsReasonExistsButIsNotApplicableForTimePoint = false; + } + return new AnnotatedMaxPointsReason(maxPointsReason, maxPointsReasonExistsButIsNotApplicableForTimePoint); } private boolean isMaxPointsReasonApplicable(MaxPointsReason maxPointsReason, TimePoint timePoint, RaceColumn raceColumn, Competitor competitor) { @@ -360,15 +388,21 @@ public class ScoreCorrectionImpl implements SettableScoreCorrection { final NumberOfCompetitorsInLeaderboardFetcher numberOfCompetitorsInLeaderboardFetcher, final ScoringScheme scoringScheme) { Double result; - final MaxPointsReason maxPointsReason = getMaxPointsReason(competitor, raceColumn, timePoint); - if (maxPointsReason == MaxPointsReason.NONE) { - result = getCorrectedNonMaxedScore(competitor, raceColumn, trackedRankProvider, scoringScheme, numberOfCompetitorsInLeaderboardFetcher, timePoint); + final AnnotatedMaxPointsReason maxPointsReason = getAnnotatedMaxPointsReason(competitor, raceColumn, timePoint); + if (maxPointsReason.getMaxPointsReason() == MaxPointsReason.NONE) { + // could be that there is a MaxPointsReason that just doesn't apply yet at timePoint; in this + // case ignore the score correction and deliver the uncorrected result + if (maxPointsReason.isMaxPointsReasonExistsButIsNotApplicableForTimePoint()) { + result = getUncorrectedScore(competitor, raceColumn, trackedRankProvider, scoringScheme, numberOfCompetitorsInLeaderboardFetcher, timePoint); + } else { + result = getCorrectedNonMaxedScore(competitor, raceColumn, trackedRankProvider, scoringScheme, numberOfCompetitorsInLeaderboardFetcher, timePoint); + } } else { // allow explicit override even when max points reason is specified; calculation may be wrong, // e.g., in case we have an untracked race and the number of competitors is estimated incorrectly Double correctedNonMaxedScore = correctedScores.get(raceColumn.getKey(competitor)); if (correctedNonMaxedScore == null) { - result = scoringScheme.getPenaltyScore(raceColumn, competitor, maxPointsReason, + result = scoringScheme.getPenaltyScore(raceColumn, competitor, maxPointsReason.getMaxPointsReason(), getNumberOfCompetitorsInRace(raceColumn, competitor, numberOfCompetitorsInLeaderboardFetcher), numberOfCompetitorsInLeaderboardFetcher); } else { @@ -379,7 +413,7 @@ public class ScoreCorrectionImpl implements SettableScoreCorrection { return new Result() { @Override public MaxPointsReason getMaxPointsReason() { - return maxPointsReason; + return maxPointsReason.getMaxPointsReason(); } @Override @@ -446,25 +480,30 @@ public class ScoreCorrectionImpl implements SettableScoreCorrection { Double correctedNonMaxedScore = correctedScores.get(raceColumn.getKey(competitor)); Double result; if (correctedNonMaxedScore == null || isCertainlyBeforeRaceFinish(timePoint, raceColumn, competitor)) { - try { - int trackedRank = trackedRankProvider.call(); - result = scoringScheme.getScoreForRank(leaderboard, raceColumn, competitor, - trackedRank, new Callable() { - @Override - public Integer call() { - return getNumberOfCompetitorsInRace(raceColumn, competitor, numberOfCompetitorsInLeaderboardFetcher); - } - }, numberOfCompetitorsInLeaderboardFetcher, timePoint); - } catch (Exception e) { - logger.log(Level.SEVERE, "Exception while computing corrected non maxed score", e); - throw new RuntimeException(e); - } + result = getUncorrectedScore(competitor, raceColumn, trackedRankProvider, scoringScheme, + numberOfCompetitorsInLeaderboardFetcher, timePoint); } else { result = correctedNonMaxedScore; } return result; } + private Double getUncorrectedScore(final Competitor competitor, final RaceColumn raceColumn, + Callable trackedRankProvider, ScoringScheme scoringScheme, + final NumberOfCompetitorsInLeaderboardFetcher numberOfCompetitorsInLeaderboardFetcher, TimePoint timePoint) { + final Double result; + try { + int trackedRank = trackedRankProvider.call(); + result = scoringScheme.getScoreForRank(leaderboard, raceColumn, competitor, + trackedRank, ()->getNumberOfCompetitorsInRace(raceColumn, competitor, numberOfCompetitorsInLeaderboardFetcher), + numberOfCompetitorsInLeaderboardFetcher, timePoint); + } catch (Exception e) { + logger.log(Level.SEVERE, "Exception while computing uncorrected score", e); + throw new RuntimeException(e); + } + return result; + } + @Override public Double getExplicitScoreCorrection(Competitor competitor, RaceColumn raceColumn) { return correctedScores.get(raceColumn.getKey(competitor)); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/CandidateFinder.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/CandidateFinder.java index 50199c8abe9..18c46ce1ac6 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/CandidateFinder.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/CandidateFinder.java @@ -29,7 +29,7 @@ public interface CandidateFinder { */ Util.Pair, Iterable> getAllCandidates(Competitor c); - Map, List>> updateWaypoints(Iterable addedWaypoints, Iterable removedWaypoints, Integer smallestIndex); + Map, List>> updateWaypoints(Iterable addedWaypoints, Iterable removedWaypoints, int smallestIndex); /** * @return The fixes for each Competitor that may have changed their status as a {@link Candidate} because of new diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java index f59626ef5a1..6e5837f52fc 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingCalculator.java @@ -28,6 +28,7 @@ import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.Util.Triple; +import com.sap.sse.common.util.IntHolder; import com.sap.sse.util.impl.ThreadFactoryWithPriority; /** @@ -106,15 +107,15 @@ public class MarkPassingCalculator { try { logger.info("MarkPassingCalculator is listening on race "+raceName); boolean finished = false; - Map> competitorFixes = new HashMap<>(); - Map> markFixes = new HashMap<>(); - List addedWaypoints = new ArrayList<>(); - List removedWaypoints = new ArrayList<>(); - Integer smallestChangedWaypointIndex = null; - List> fixedMarkPassings = new ArrayList<>(); - List> removedFixedMarkPassings = new ArrayList<>(); - List> suppressedMarkPassings = new ArrayList<>(); - List unsuppressedMarkPassings = new ArrayList<>(); + final Map> competitorFixes = new HashMap<>(); + final Map> markFixes = new HashMap<>(); + final List addedWaypoints = new ArrayList<>(); + final List removedWaypoints = new ArrayList<>(); + final IntHolder smallestChangedWaypointIndex = new IntHolder(-1); + final List> fixedMarkPassings = new ArrayList<>(); + final List> removedFixedMarkPassings = new ArrayList<>(); + final List> suppressedMarkPassings = new ArrayList<>(); + final List unsuppressedMarkPassings = new ArrayList<>(); while (!finished) { try { logger.finer("MPC for "+raceName+" is checking the queue"); @@ -139,9 +140,9 @@ public class MarkPassingCalculator { } } if (!finished && !suspended) { - if (smallestChangedWaypointIndex != null) { + if (smallestChangedWaypointIndex.value != -1) { Map, List>> candidateDeltas = finder - .updateWaypoints(addedWaypoints, removedWaypoints, smallestChangedWaypointIndex); + .updateWaypoints(addedWaypoints, removedWaypoints, smallestChangedWaypointIndex.value); chooser.removeWaypoints(removedWaypoints); chooser.addWaypoints(addedWaypoints); for (Entry, List>> entry : candidateDeltas @@ -157,7 +158,7 @@ public class MarkPassingCalculator { markFixes.clear(); addedWaypoints.clear(); removedWaypoints.clear(); - smallestChangedWaypointIndex = null; + smallestChangedWaypointIndex.value = -1; fixedMarkPassings.clear(); removedFixedMarkPassings.clear(); suppressedMarkPassings.clear(); @@ -202,7 +203,7 @@ public class MarkPassingCalculator { private void computeMarkPasses(Map> newCompetitorFixes, Map> newMarkFixes) { logger.finer("Calculating markpassings for race "+raceName+" with " + newCompetitorFixes.size() + " new competitor Fixes and " - + newMarkFixes.size() + "new mark fixes."); + + newMarkFixes.size() + " new mark fixes."); Map> combinedCompetitorFixes = new HashMap<>(); for (Entry> competitorEntry : newCompetitorFixes.entrySet()) { @@ -274,7 +275,7 @@ public class MarkPassingCalculator { @Override public void storePositionUpdate(Map> competitorFixes, Map> markFixes, List addedWaypoints, List removedWaypoints, - Integer smallestChangedWaypointIndex, + IntHolder smallestChangedWaypointIndex, List> fixedMarkPassings, List> removedMarkPassings, List> suppressedMarkPassings, List unSuppressedMarkPassings) { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingUpdateListener.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingUpdateListener.java index c35e2dd3d9c..fd08651b813 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingUpdateListener.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/MarkPassingUpdateListener.java @@ -17,6 +17,7 @@ import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.Util.Triple; +import com.sap.sse.common.util.IntHolder; /** * Listens for changes that might affect the MarkPassingCalculator: new Fixes of a Competitor or a Mark, updated @@ -36,7 +37,7 @@ public class MarkPassingUpdateListener extends AbstractRaceChangeListener { @Override public void storePositionUpdate(Map> competitorFixes, Map> markFixes, List addedWaypoints, List removedWaypoints, - Integer smallestChangedWaypointIndex, List> fixedMarkPassings, + IntHolder smallestChangedWaypointIndex, List> fixedMarkPassings, List> removedMarkPassings, List> suppressedMarkPassings, List unSuppressedMarkPassings) { } @@ -62,7 +63,7 @@ public class MarkPassingUpdateListener extends AbstractRaceChangeListener { @Override public void storePositionUpdate(Map> competitorFixes, Map> markFixes, List addedWaypoints, List removedWaypoints, - Integer smallestChangedWaypointIndex, + IntHolder smallestChangedWaypointIndex, List> fixedMarkPassings, List> removedMarkPassings, List> suppressedMarkPassings, List unSuppressedMarkPassings) { @@ -82,7 +83,7 @@ public class MarkPassingUpdateListener extends AbstractRaceChangeListener { @Override public void storePositionUpdate(Map> competitorFixes, Map> markFixes, List addedWaypoints, List removedWaypoints, - Integer smallestChangedWaypointIndex, + IntHolder smallestChangedWaypointIndex, List> fixedMarkPassings, List> removedMarkPassings, List> suppressedMarkPassings, List unSuppressedMarkPassings) { @@ -111,13 +112,13 @@ public class MarkPassingUpdateListener extends AbstractRaceChangeListener { @Override public void storePositionUpdate(Map> competitorFixes, Map> markFixes, List addedWaypoints, List removedWaypoints, - Integer smallestChangedWaypointIndex, + IntHolder smallestChangedWaypointIndex, List> fixedMarkPassings, List> removedMarkPassings, List> suppressedMarkPassings, List unSuppressedMarkPassings) { addedWaypoints.add(waypointThatGotAdded); - if (smallestChangedWaypointIndex == null || smallestChangedWaypointIndex > zeroBasedIndex) { - smallestChangedWaypointIndex = zeroBasedIndex; + if (smallestChangedWaypointIndex.value == -1|| smallestChangedWaypointIndex.value > zeroBasedIndex) { + smallestChangedWaypointIndex.value = zeroBasedIndex; } } }); @@ -129,13 +130,13 @@ public class MarkPassingUpdateListener extends AbstractRaceChangeListener { @Override public void storePositionUpdate(Map> competitorFixes, Map> markFixes, List addedWaypoints, List removedWaypoints, - Integer smallestChangedWaypointIndex, + IntHolder smallestChangedWaypointIndex, List> fixedMarkPassings, List> removedMarkPassings, List> suppressedMarkPassings, List unSuppressedMarkPassings) { removedWaypoints.add(waypointThatGotRemoved); - if (smallestChangedWaypointIndex == null || smallestChangedWaypointIndex > zeroBasedIndex) { - smallestChangedWaypointIndex = zeroBasedIndex; + if (smallestChangedWaypointIndex.value == -1 || smallestChangedWaypointIndex.value > zeroBasedIndex) { + smallestChangedWaypointIndex.value = zeroBasedIndex; } } }); @@ -147,7 +148,7 @@ public class MarkPassingUpdateListener extends AbstractRaceChangeListener { @Override public void storePositionUpdate(Map> competitorFixes, Map> markFixes, List addedWaypoints, List removedWaypoints, - Integer smallestChangedWaypointIndex, + IntHolder smallestChangedWaypointIndex, List> fixedMarkPassings, List> removedMarkPassings, List> suppressedMarkPassings, List unSuppressedMarkPassings) { @@ -162,7 +163,7 @@ public class MarkPassingUpdateListener extends AbstractRaceChangeListener { @Override public void storePositionUpdate(Map> competitorFixes, Map> markFixes, List addedWaypoints, List removedWaypoints, - Integer smallestChangedWaypointIndex, + IntHolder smallestChangedWaypointIndex, List> fixedMarkPassings, List> removedMarkPassings, List> suppressedMarkPassings, List unSuppressedMarkPassings) { @@ -177,7 +178,7 @@ public class MarkPassingUpdateListener extends AbstractRaceChangeListener { @Override public void storePositionUpdate(Map> competitorFixes, Map> markFixes, List addedWaypoints, List removedWaypoints, - Integer smallestChangedWaypointIndex, + IntHolder smallestChangedWaypointIndex, List> fixedMarkPassings, List> removedMarkPassings, List> suppressedMarkPassings, List unSuppressedMarkPassings) { @@ -192,7 +193,7 @@ public class MarkPassingUpdateListener extends AbstractRaceChangeListener { @Override public void storePositionUpdate(Map> competitorFixes, Map> markFixes, List addedWaypoints, List removedWaypoints, - Integer smallestChangedWaypointIndex, + IntHolder smallestChangedWaypointIndex, List> fixedMarkPassings, List> removedMarkPassings, List> suppressedMarkPassings, List unSuppressedMarkPassings) { diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/StorePositionUpdateStrategy.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/StorePositionUpdateStrategy.java index 956570a5e58..89e9ed55e29 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/StorePositionUpdateStrategy.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/StorePositionUpdateStrategy.java @@ -10,6 +10,7 @@ import com.sap.sailing.domain.common.tracking.GPSFix; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.Util.Triple; +import com.sap.sse.common.util.IntHolder; /** * Is queued between {@link MarkPassingCalculator} and {@link MarkPassingUpdateListener}. @@ -21,7 +22,7 @@ public interface StorePositionUpdateStrategy { * no fixes exist yet for that object, a new entry is created. */ void storePositionUpdate(Map> competitorFixes, Map> markFixes, - List addedWaypoints, List removedWaypoints, Integer smallestChangedWaypointIndex, + List addedWaypoints, List removedWaypoints, IntHolder smallestChangedWaypointIndex, List> fixedMarkPassings, List> removedMarkPassings, List> suppressedMarkPassings, List unSuppressedMarkPassings); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java index 3df53508ee6..8af8dd018e7 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/markpassingcalculation/impl/CandidateFinderImpl.java @@ -49,8 +49,19 @@ import com.sap.sse.common.Util.Pair; * */ public class CandidateFinderImpl implements CandidateFinder { - // The higher this is, the closer the fixes have to be to waypoint to become a Candidate - private final int STRICTNESS_OF_DISTANCE_BASED_PROBABILITY = 7; + /** + * The higher this is, the closer the fixes have to be to waypoint to become a Candidate. The formula basically + * works like this: + * {@code probability := adjacentLegLength / fixDistanceFromWaypoint / STRICTNESS_OF_DISTANCE_BASED_PROBABILITY}. In + * other words, the larger the distance, the less the probability; and this strictness factor is applied to the + * denominator. Therefore, approximately, if the fix distance from the waypoint is {@code adjacentLegLength / } + * {@link #STRICTNESS_OF_DISTANCE_BASED_PROBABILITY} then the probability based on that distance is 1. Twice + * that distance brings down the probability to 1/2. Lesser distances can't increase the probability beyond 1. + *

+ * + * (Effectively, the formula is a bit more complicated as tolerances for GPS HDOP are included.) + */ + private final int STRICTNESS_OF_DISTANCE_BASED_PROBABILITY = 10; // All of the penalties are multiplied onto the probability of a Candidate. A value of 0 excludes Candidates that do // not fit, a value of 1 imposes no penalty on each criteria @@ -247,7 +258,7 @@ public class CandidateFinderImpl implements CandidateFinder { @Override public Map, List>> updateWaypoints( - Iterable addedWaypoints, Iterable removedWaypoints, Integer smallestIndex) { + Iterable addedWaypoints, Iterable removedWaypoints, int smallestIndex) { Map> removedWaypointCandidates = removeWaypoints(removedWaypoints); Map, List>> newAndUpdatedCandidates = invalidateAfterCourseChange(smallestIndex); for (Entry> entry : removedWaypointCandidates.entrySet()) { @@ -349,7 +360,9 @@ public class CandidateFinderImpl implements CandidateFinder { } /** - * For each fix the distance to each waypoint is calculated. Then the fix is checked for being a candidate. + * For each fix the distance to each waypoint is calculated. Then the fix is checked for being a candidate by + * checking the distance of the fix before and after. If the distance at the fix is a local minimum and the + * probability exceeds {@link #penaltyForSkipping}, the fix is considered a candidate. */ private Util.Pair, List> checkForDistanceCandidateChanges(Competitor c, Iterable fixes, Iterable waypoints) { @@ -398,7 +411,6 @@ public class CandidateFinderImpl implements CandidateFinder { Boolean isCan = false; Candidate oldCan = null; Double probability = null; - double sidePenalty = 1; Distance distance = null; Double startProbabilityBasedOnOtherCompetitors = null; double onCorrectSideOfWaypoint = 0.8; @@ -442,7 +454,6 @@ public class CandidateFinderImpl implements CandidateFinder { && (probability == null || newProbability > probability)) { isCan = true; probability = newProbability; - sidePenalty = newOnCorrectSideOfWaypointPenalty; onCorrectSideOfWaypoint = newOnCorrectSideOfWaypointPenalty; distance = dis; startProbabilityBasedOnOtherCompetitors = newStartProbabilityBasedOnOtherCompetitors; @@ -458,10 +469,10 @@ public class CandidateFinderImpl implements CandidateFinder { } if (!wasCan && isCan) { Candidate newCan = new DistanceCandidateImpl(race.getRace().getCourse().getIndexOfWaypoint(w) + 1, - t, probability, startProbabilityBasedOnOtherCompetitors, w, sidePenalty, distance); + t, probability, startProbabilityBasedOnOtherCompetitors, w, onCorrectSideOfWaypoint, distance); getDistanceCandidates(c, w).put(fix, newCan); result.getA().add(newCan); - logger.finest("Added distance" + newCan.toString() + "for " + c); + logger.finest("Added distance candidate " + newCan.toString() + "for " + c); } else if (wasCan && !isCan) { getDistanceCandidates(c, w).remove(fix); result.getB().add(oldCan); @@ -470,7 +481,7 @@ public class CandidateFinderImpl implements CandidateFinder { t, probability, startProbabilityBasedOnOtherCompetitors, w, onCorrectSideOfWaypoint, distance); getDistanceCandidates(c, w).put(fix, newCan); result.getA().add(newCan); - logger.finest("Added distance" + newCan.toString() + "for " + c); + logger.finest("Added distance candidate " + newCan.toString() + "for " + c); result.getB().add(oldCan); } } @@ -573,14 +584,14 @@ public class CandidateFinderImpl implements CandidateFinder { if (xte == 0) { newCandidates.put(Arrays.asList(fix, fix), createCandidate(c, 0, 0, t, t, w, false)); } else { - if (fixAfter != null && xtesAfter != null) { + if (fixAfter != null && xtesAfter != null && xtesAfter.get(w).size() >= 2) { Double xteAfter = xtesAfter.get(w).get(1).getMeters(); if (xte < 0 != xteAfter <= 0) { newCandidates.put(Arrays.asList(fix, fixAfter), createCandidate(c, xte, xteAfter, t, tAfter, w, false)); } } - if (fixBefore != null) { + if (fixBefore != null && xtesBefore.get(w).size() >= 2) { Double xteBefore = xtesBefore.get(w).get(1).getMeters(); if (xte < 0 != xteBefore <= 0) { newCandidates.put(Arrays.asList(fixBefore, fix), @@ -1011,7 +1022,7 @@ public class CandidateFinderImpl implements CandidateFinder { result = 1 / (STRICTNESS_OF_DISTANCE_BASED_PROBABILITY/* Raising this will make it stricter */ // reduce distance by 2x the typical HDOP, accounting for the possibility that some distance from the mark // may have been caused by inaccurate GPS tracking - * Math.abs(Math.max(0.0, distance.add(GPSFix.TYPICAL_HDOP.scale(-2)).getMeters()) / legLength.getMeters()) + 1); + * Math.abs(Math.max(0.0, distance.add(GPSFix.TYPICAL_HDOP.scale(-2)).divide(legLength))) + 1); } else { result = null; } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java index 8b236db7afb..b692f921514 100644 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/tracking/impl/TrackedRaceImpl.java @@ -1398,7 +1398,15 @@ public abstract class TrackedRaceImpl extends TrackedRaceWithWindEssentials impl } @Override - public List getCompetitorsFromBestToWorst(TimePoint timePoint, WindLegTypeAndLegBearingCache cache) { + public List getCompetitorsFromBestToWorst(TimePoint unadjustedTimePoint, WindLegTypeAndLegBearingCache cache) { + final TimePoint timePoint; + // normalize the time point to get cache hits when asking for time points that are later than + // the last time point affected by any event received for this tracked race + if (Util.compareToWithNull(unadjustedTimePoint, getTimePointOfNewestEvent(), /* nullIsLess */ true) <= 0) { + timePoint = unadjustedTimePoint; + } else { + timePoint = getTimePointOfNewestEvent(); + } NamedReentrantReadWriteLock readWriteLock; synchronized (competitorRankingsLocks) { readWriteLock = competitorRankingsLocks.get(timePoint); diff --git a/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs index 9c4cb4b3974..df8834786fc 100755 --- a/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs +++ b/java/com.sap.sailing.gwt.ui/.settings/org.eclipse.core.resources.prefs @@ -1,6 +1,7 @@ eclipse.preferences.version=1 encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_de.properties=UTF-8 +encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_ja.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_ru.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_zh.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_zh_CN.properties=UTF-8 @@ -12,6 +13,7 @@ encoding//src/main/java/com/sap/sailing/gwt/ui/adminconsole/IgtimiAccountsPanel. encoding//src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java=utf-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties=UTF-8 +encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties=UTF-8 encoding//src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties=UTF-8 diff --git a/java/com.sap.sailing.gwt.ui/Simulator.css b/java/com.sap.sailing.gwt.ui/Simulator.css index ada77cd9fdf..14581efa50f 100644 --- a/java/com.sap.sailing.gwt.ui/Simulator.css +++ b/java/com.sap.sailing.gwt.ui/Simulator.css @@ -274,7 +274,7 @@ input.magnifier { } .sliderLabel { - width: 200px; + width: 210px; padding: 12px 0 14px 12px; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/SailingLocales.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/SailingLocales.gwt.xml index d0adb9336a9..b7e30a3a28e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/SailingLocales.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/SailingLocales.gwt.xml @@ -1,7 +1,7 @@ - - + + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_ja.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_ja.properties new file mode 100644 index 00000000000..c0d79a705e3 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_ja.properties @@ -0,0 +1,225 @@ +chartSelectionHint=ヒント: リーダーボード (リーダーボードタブ内) を使用して、チャートに表示する競技者を選択することもできます。 +chooseALanguage=言語を選択してください +events=イベント +home=ホーム +aboutUs=SAP について +contact=問い合わせ先 +sponsoring=スポンサー +solutions=ソリューション +blog=ブログ +german=ドイツ語 +english=英語 +fullEventSchedule=全イベントスケジュール +officalEventWebsite=公式イベント Web サイト +leg=レグ +races=レース +moreInfo=追加情報 +viewAnalysis=分析の表示 +overallLeaderboard=全体リーダーボード +pleaseEnterASearchTerm=検索語句を入力してください。 +pleaseTryAgain=再試行してください... +reloadPage=ページのリロード +errorMessageLoadingData=このページのデータのロードでエラーが発生しました。 +errorMessageNoSuchLeaderboard=この名称のリーダーボードを見つけることができませんでした。 +raceNotScheduled=スケジュールされていません。 +regattaDetails=レガッタ詳細 +regatta=レガッタ +competitorsAnalytics=競技者分析 +chooseChart=チャートタイプ選択 +series=シリーズ +regattaLeaderboards=レガッタリーダーボード +features=機能 +releaseNotes=リリースノート +whatsNew=最新情報 +overview=概要 +sapSailing=SAP Sailing +search=検索 +startSimulator=シミュレータ開始 +countdownStartingInDays=日 +countdownDays=日 +countdownHours=時間 +countdownMinutes=分 +countdownSeconds=秒 +countdownDay=曜日 +countdownHour=時 +countdownMinute=分 +countdownSecond=秒 +live=ライブ +finished=終了 +watchNow=現在視聴可能 +lastUpdate=最終更新 +leaderboard=リーダーボード +boxNewsUpdateMoreLink=レポートニュース +boxNewsUpdateTitle=最新ニュース +boxUpcomingRacesCountdownStarted1=スタート +boxUpcomingRacesCountdownStarted2=時間と +boxUpcomingRacesCountdownStarted3=分前 +boxUpcomingRacesCountdownStarting1=スタートまでの時間 +boxUpcomingRacesCountdownStarting2=時間と +boxUpcomingRacesCountdownStarting3=分 +boxUpcomingRacesMoreLink=イベントスケジュール +boxUpcomingRacesTitle=本日予定のレース +boxWeather=天候 +boxWeatherUnit=kt +defaultTitle=SAP Sailing Analytics +eventHeaderMedia=メディア +eventHeaderOverview=概要 +eventHeaderRaces=スケジュール +eventHeaderRegattas=レガッタ +seriesHeaderOverview=シリーズについて +seriesHeaderUpcoming=予定 +seriesHeaderRecent=最新イベント +seriesHeaderLeaderboard=リーダーボード +seriesOverviewHeaderRecent=最新シリーズイベント +seriesOverviewReadMore=その他 +eventsOverviewRecentEvents=イベント +eventsOverviewRecentCountries=国 +eventsOverviewRecentSailors=セーラー +eventsOverviewRecentTrackedRaces=追跡レース +eventResultsHeadline=勝者と結果 +eventResultsBoatClassesDropDown=すべての艇クラス +eventOverviewMoreEvents=その他イベント +eventOverviewRecentEvents=最新イベント +eventOverviewUpcomingDate=日付 +eventOverviewUpcomingEvent=イベント +eventOverviewUpcomingLocation=場所 +eventOverviewUpcomingSchedule=予定スケジュール +eventOverviewUpcomingSeries=シリーズ +eventOverviewUpcomingTeaser=その他イベントが近日公表されます +eventQuickfinderTitle=イベントクイックファインダ +eventRegattaHeaderLeaderboard=リーダーボード +eventRegattaHeaderRaceRaces=レース +eventRegattaListAllBoatClasses=すべての艇クラス +eventRegattaLeaderboardCompetitor=競技者 +eventRegattaLeaderboardExtendLeaderboard=リーダーボード拡張 +eventRegattaLeaderboardFinishedMessage=レースはすべて終了 +eventRegattaLeaderboardLastScoreUpdate=前回スコア更新 +eventRegattaLeaderboardName=名称 +eventRegattaLeaderboardRegattaRank=レガッタランク +eventRegattaLeaderboardTotalPoints=合計ポイント +eventRegattaRaceWatch=ライブ視聴 +eventRegattaRaceAnalyse=レース分析 +eventRegattaRaceNotTracked=未追跡 +eventRegattaRaceFinished=フィニッシュ時刻: +eventRegattaRaceStarted=スタート時刻: +eventRegattaRaceScheduled=スケジュール対象: +eventRegattaRaceRace=レース +eventRegattaRaceWind=風力: +eventRegattaRaceWinner=勝者 +eventRegattaRaceLeader=首位艇 +eventRegattaRacesLegendGps=追跡 +eventRegattaRacesLegendWind=風データ +eventRegattaRacesLegendVideo=ビデオ +eventRegattaRacesLegendAudio=音声 +eventSponsorsTitle=イベントスポンサーシップ +footerJobs=採用 +footerCopyright=© 2011-2016 SAP Sailing Analytics +footerLanguage=SAP Sailing - +footerLegal=利用規約 +footerPrivacy=個人情報保護 +footerFeedback=フィードバック +headerEvents=イベント +headerHome=ホーム +headerLogo=Sailing Analytics +headerSearch=検索 +headerSearchPlaceholder=検索 +headerSolutions=ソリューション +headerSponsoring=スポンサー +latestRaceResultsHeadline=公式レース最新結果 +latestRaceResultsIsNew=新規 +latestRaceResultsIsUpdated=更新済 +latestRaceResultsRaceCountUnit=レース +latestRaceResultsRefresh=リフレッシュ +leaderboardQuickAccessLabel=リーダーボードクイックアクセス +mainEventShowAll=全イベント表示 +mainEventTitle=Sailing World からの最新イベント +mainMediaSubtitle=お気に入りのセーリングイベントのすてきな最新画像を楽しんでください。 +mainMediaTitle=最新のセーリング画像 +mainMediaVideosSubtitle=最新イベントの動画にアクセスしてセーリングの世界に浸ってください。 +mainMediaVideosTitle=最新のセーリング動画 +mainSponsorsReadMore=続きを読む +mediaTeaserPhotosTitle=写真 +mediaTeaserVideosTitle=ビデオ +regattaCompetitors=競技者 +regattaRaces=レース +regattaTracked=追跡済 +regattaRegattaStarts=レガッタ開始予定 +regattaPts=ポイント +regattaCourse=コース +regattaArea=領域 +regattaFinishedAt=フィニッシュ時刻 +regattaScheduledAt=スケジュール時刻 +regattaStartedAt=スタート時刻 +regattasResultsTitle=勝者および結果 +searchResultHeaderButton=検索 +searchResultHeaderLabel=検索対象 +searchResultsFound=結果が見つかりました: +searchResultHeaderPlaceholder=検索キーを入力 +searchResultMore=詳細 +searchResultSearch=検索 +searchResultSearchAgain=再検索 +searchResultSortLabel=ソートキー +searchResultSortNewest=最新 +searchResultSortOldest=初期 +searchResultSortRelevance=関連性 +searchResultCount=約 2,450 の結果が ''Schnitzel'' に対して見つかりました +sharingEmail=電子メール送信 +sharingFacebook=Facebook でシェア +sharingTwitter=Twitter でツイート +socialFooterShare=これをシェアする場所: +socialFooterTitle=SAP Sailing Analytics を推奨 + +# SOLUTIONS + +solutionsSISHeadline=SAP in Sailing +solutionsSAPHeadline=SAP Sailing Analytics +solutionsRaceHeadline=SAP Sailing Race Manager +solutionsPostHeadline=レース後分析 +solutionsTrainingHeadline=トレーニング日記 +solutionsSimulatorHeadline=戦略シミュレータ + +sapInSailing=SAP in Sailing +contentSapInSailing1=セーリングは、SAP にとってソリューションを紹介するためのプラットフォームとしてうってつけのものであり、これまでになかったやり方でスポーツの運営を支援しています。SAP によるスポーツへの関与は、以下のことを行うツールを提供することによってセーリングから得られる体験を別次元のものに変えました。 +contentSapInSailing2=パフォーマンスの分析と戦略の最適化でセーラーを支援します +contentSapInSailing3=アクションをファンにとって身近なものにします +contentSapInSailing4=情報豊富なすばらしい解説を行うための情報とインサイトを報道関係者に提供します +contentSapInSailing5=SAP は、長年にわたりセーリングに関与しており、以下のようなチームおよびレガッタと関係を構築してきました。 +contentSapInSailing6=Travemünder Woche の公式テクノロジーパートナー +contentSapInSailing7=欧州セーリング連盟 (European Sailing Leagues) のテクノロジーパートナー +contentSapInSailing8=SAP Extreme Sailing Team のタイトルスポンサー +contentSapInSailing9=Extreme Sailing Series の公式テクニカルパートナー +contentSapInSailing10=ドイツ代表セーリングチームである Audi Sailing Team Germany のプレミアムパートナー +contentSapInSailing11=世界最大のレガッタである Kieler Woche (Kiel Week) のプレミアムパートナー +contentSapInSailing12=SAP 5O5 World Championships のタイトルスポンサー + +solutionsSAPText1=レースランキング、速力、操縦、ベアリングなどに関係するリアルタイム分析を提供することにより、SAP はセーリングという複雑なスポーツを平易かつ分かりやすく説明することをお手伝いしています。 +solutionsSAPText2=SAP Sailing Analytics は、クラウドおよびインメモリ技術を活用し、リアルタイムで GPS および風計測データを処理し、どこからでもアクセスできるさまざまなフロントエンドでコンテンツを視覚化することによってセーリングの世界にインサイトと透明性を提供します。 +solutionsRaceText=SAP Race Committee アプリは、セーリングレガッタ運営中にレース組織者にその手動での作業および連絡業務を自動化および簡略化することによって効率と統制を向上させます。Race Committee アプリは、レースイベントの運営および管理の業務を簡略化するのに役立ち、レース委員会がより円滑に運営されるよう支援します。 +solutionsPostText=セーリングイベントで収集されたデータへのアクセスにより、戦略の策定や帆走の向上を支援するためにセーラーがそのパフォーマンスを評価および分析するための機会が提供されます。
SAP Business Analytics ソリューションは、多くのレガッタおよびレースにわたって履歴データを照会および分析するツールをセーラー、コーチ、ファン、そして報道関係者に提供します。SAP HANA は、こうした人たちが自身のパフォーマンスを最適化し、予定されているレガッタのために成功を収める戦略を策定するために簡単、迅速、かつ効率的な方法でパフォーマンスのパターン、傾向、およびセーラーのプロファイルを知ることを可能にします。 +solutionsTrainingText=STG Training Diary は、アスリートの体調、ならびに水上トレーニング、身体的トレーニング、操艇、メンタル、理論、さらに教育や移動などのその他の活動の領域におけるトレーニングへの取り組みの日々の記録および分析をサポートします。 +solutionsSimulatorText=適切な戦略の選択は、レース中における大きな競争上の優位性をもたらすことができます。Strategy Simulator により、さまざまな風況および水流に対して最良のセーリング戦略を決定することが平易になります。風と水流をシミュレーションで直ちに変更することができるため、不確かな気象条件に対しても戦略の確かさを簡単に評価することができます。 + + +sapSailingAnalytics=SAP Sailing Analytics +contentSapSailingAnalytics1=レースランキング、速力、操縦、ベアリングなどに関係するリアルタイム分析を提供することによって、SAP はセーリングという複雑なスポーツを平易かつ分かりやすく説明することに役立っています。 +contentSapSailingAnalytics2=SAP Sailing Analytics は、クラウドおよびインメモリ技術を活用し、リアルタイムで GPS および風計測データを処理し、どこからでもアクセスできるさまざまなフロントエンドでコンテンツを視覚化することによってセーリングの世界にインサイトと透明性を提供します。 +sapSailingAnalyticsReadMore=続きを読む + +sapSailingRaceManager=SAP Sailing Race Manager +contentSapSailingRaceManager=SAP Sailing Race Manager は、セーリングレガッタ運営中にレース組織者にその手動での作業および連絡業務を自動化および簡略化することによって効率と統制を向上させます。Race Committee アプリは、レースイベントの運営および管理の業務を簡略化するのに役立ち、レース委員会がより円滑に運営されるよう支援します。 +sapSailingRaceManagerReadMore=詳細情報 + +postRaceAnalytics=レース後分析 +contentPostRaceAnalytics1=セーリングイベントで収集されたデータへのアクセスにより、戦略の策定や帆走の向上を支援するためにセーラーがそのパフォーマンスを評価および分析するための機会が提供されます。 +contentPostRaceAnalytics2=SAP Business Analytics ソリューションは、多くのレガッタおよびレースにわたって履歴データを照会および分析するツールをセーラー、コーチ、ファン、そして報道関係者に提供します。SAP HANA は、こうした人たちが自身のパフォーマンスを最適化し、予定されているレガッタのために成功を収める戦略を策定するために簡単、迅速、かつ効率的な方法でパフォーマンスのパターン、傾向、およびセーラーのプロファイルを知ることを可能にします。 + +i18nDeSolutionsTrainingHeadline=SAP Sail Better – STG Training Diary +i18nDeSolutionsTrainingText=STG Training Diary は、アスリートの体調、ならびに水上トレーニング、身体的トレーニング、操艇、メンタル、理論、さらに教育や移動などのその他の活動の領域におけるトレーニングへの取り組みの日々の記録および分析をサポートします。 + +strategySimulator=戦略シミュレータ +contentStrategySimulator=適切な戦略の選択は、レース中における大きな競争上の優位性をもたらすことができます。Strategy Simulator により、さまざまな風況および水流に対して最良のセーリング戦略を決定することが平易になります。風と水流をシミュレーションで直ちに変更することができるため、不確かな気象条件に対しても戦略の確かさを簡単に評価することができます。 + +strategySimulatorReadMore=詳細情報 + +impressum=刻印 + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_ru.properties index ef67df24cc6..491cd1ee293 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/TextMessages_ru.properties @@ -113,7 +113,7 @@ eventRegattaRacesLegendVideo=Видео eventRegattaRacesLegendAudio=Аудио eventSponsorsTitle=Спонсорская поддержка событий footerJobs=Работа -footerCopyright=\u00A9 SAP Sailing Analytics, 2011–2016. +footerCopyright=© SAP Sailing Analytics, 2011–2016. footerLanguage=SAP Sailing в footerLegal=Раскрытие юридической информации footerPrivacy=Конфиденциальность diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/XLF/TextMessages_ja.properties.xlf b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/XLF/TextMessages_ja.properties.xlf new file mode 100644 index 00000000000..3c315efa80f --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/XLF/TextMessages_ja.properties.xlf @@ -0,0 +1,861 @@ + + + +

+ + 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 + + + + + +
+ + + Hint: You can also use the leaderboard (on the LEADERBOARD tab) to select the competitors displayed in the chart. + ヒント: リーダーボード (リーダーボードタブ内) を使用して、チャートに表示する競技者を選択することもできます。 + + + Choose a language + 言語を選択してください + + + Events + イベント + + + Home + ホーム + + + About us + SAP について + + + Contact + 問い合わせ先 + + + Sponsoring + スポンサー + + + Solutions + ソリューション + + + Blog + ブログ + + + Deutsch + ドイツ語 + + + English + 英語 + + + Full event schedule + 全イベントスケジュール + + + Official event website + 公式イベント Web サイト + + + Leg + レグ + + + Races + レース + + + More info + 追加情報 + + + View analysis + 分析の表示 + + + Overall leaderboard + 全体リーダーボード + + + Please enter a search term. + 検索語句を入力してください。 + + + Please try again... + 再試行してください... + + + Reload page + ページのリロード + + + An error occured while loading the data for this page. + このページのデータのロードでエラーが発生しました。 + + + Could not find a leaderboard with this name. + この名称のリーダーボードを見つけることができませんでした。 + + + Not scheduled. + スケジュールされていません。 + + + Regatta details + レガッタ詳細 + + + Regatta + レガッタ + + + Competitors Analytics + 競技者分析 + + + Choose chart type + チャートタイプ選択 + + + Series + シリーズ + + + Regatta leaderboards + レガッタリーダーボード + + + Features + 機能 + + + Release notes + リリースノート + + + What''s new + 最新情報 + + + Overview + 概要 + + + SAP Sailing + SAP Sailing + + + Search + 検索 + + + Start Simulator + シミュレータ開始 + + + Days + 日 + + + Days + 日 + + + Hrs + 時間 + + + Min + 分 + + + Sec + 秒 + + + Day + 曜日 + + + Hour + 時 + + + Min + 分 + + + Sec + 秒 + + + Live + ライブ + + + Finished + 終了 + + + Watch now + 現在視聴可能 + + + Last Update + 最終更新 + + + Leaderboard + リーダーボード + + + Report news + レポートニュース + + + News Update + 最新ニュース + + + Started + スタート + + + hrs and + 時間と + + + minutes ago + 分前 + + + Starting in + スタートまでの時間 + + + hrs and + 時間と + + + minutes + 分 + + + Event Schedule + イベントスケジュール + + + Upcoming Races Today + 本日予定のレース + + + Weather + 天候 + + + KTS + kt + + + SAP Sailing Analytics + SAP Sailing Analytics + + + Media + メディア + + + Overview + 概要 + + + Schedule + スケジュール + + + Regattas + レガッタ + + + About the Series + シリーズについて + + + Upcoming + 予定 + + + Recent Events + 最新イベント + + + Leaderboard + リーダーボード + + + Latest Series Events + 最新シリーズイベント + + + More + その他 + + + Events + イベント + + + Countries + 国 + + + Sailors + セーラー + + + Tracked Races + 追跡レース + + + Winners and Results + 勝者と結果 + + + All Boat Classes + すべての艇クラス + + + More Events + その他イベント + + + Recent Events + 最新イベント + + + Date + 日付 + + + Event + イベント + + + Location + 場所 + + + Upcoming Schedule + 予定スケジュール + + + Series + シリーズ + + + More Events To Be Announced Soon! + その他イベントが近日公表されます + + + Event Quick-Finder + イベントクイックファインダ + + + Leaderboard + リーダーボード + + + Races + レース + + + All boat classes + すべての艇クラス + + + Competitor + 競技者 + + + Extend Leaderboard + リーダーボード拡張 + + + No more races + レースはすべて終了 + + + Last Score Update + 前回スコア更新 + + + Name + 名称 + + + Regatta Rank + レガッタランク + + + Total Points + 合計ポイント + + + Watch live + ライブ視聴 + + + Analyze Race + レース分析 + + + Not Tracked + 未追跡 + + + Finished at: + フィニッシュ時刻: + + + Started at: + スタート時刻: + + + Scheduled for: + スケジュール対象: + + + Race + レース + + + Wind Strength: + 風力: + + + Winner + 勝者 + + + Leader + 首位艇 + + + Tracking + 追跡 + + + Wind Data + 風データ + + + Video + ビデオ + + + Audio + 音声 + + + Event Sponsorships + イベントスポンサーシップ + + + Jobs + 採用 + + + © 2011-2016 SAP Sailing Analytics + © 2011-2016 SAP Sailing Analytics + + + SAP Sailing in + SAP Sailing - + + + Legal Disclosure + 利用規約 + + + Privacy + 個人情報保護 + + + Feedback + フィードバック + + + Events + イベント + + + Home + ホーム + + + + Search + 検索 + + + Search + 検索 + + + Solutions + ソリューション + + + Sponsoring + スポンサー + + + Latest Official Race Results + 公式レース最新結果 + + + New + 新規 + + + Updated + 更新済 + + + races + レース + + + refresh + リフレッシュ + + + Leaderboard Quick Access + リーダーボードクイックアクセス + + + Show All Events + 全イベント表示 + + + Latest Events from the Sailing World + Sailing World からの最新イベント + + + Get lost in the latest and most beautiful impressions from your favorite sailing event. + お気に入りのセーリングイベントのすてきな最新画像を楽しんでください。 + + + Photographers love Sailing + 最新のセーリング画像 + + + Immerse yourself in the world of sailing and check out our latest events for more videos. + 最新イベントの動画にアクセスしてセーリングの世界に浸ってください。 + + + Recent Video Entries + 最新のセーリング動画 + + + Read More + 続きを読む + + + Photos + 写真 + + + Videos + ビデオ + + + Competitors + 競技者 + + + Races + レース + + + Tracked + 追跡済 + + + Regatta will start + レガッタ開始予定 + + + Pts. + ポイント + + + Course + コース + + + Area + 領域 + + + Finished at + フィニッシュ時刻 + + + Scheduled at + スケジュール時刻 + + + Started at + スタート時刻 + + + Winner and Results + 勝者および結果 + + + Search + 検索 + + + Your Search + 検索対象 + + + result(s) found for + 結果が見つかりました: + + + Type in your searchkey + 検索キーを入力 + + + More + 詳細 + + + Search + 検索 + + + Search again + 再検索 + + + Sort by + ソートキー + + + Recent + 最新 + + + Oldest + 初期 + + + Relevance + 関連性 + + + About 2,450 results where found for ''Schnitzel'' + 約 2,450 の結果が ''Schnitzel'' に対して見つかりました + + + Send an email + 電子メール送信 + + + Share on Facebook + Facebook でシェア + + + Tweet about it + Twitter でツイート + + + Share this on + これをシェアする場所: + + + Recommend SAP Sailing Analytics + SAP Sailing Analytics を推奨 + + + SAP in Sailing + SAP in Sailing + SOLUTIONS + + + SAP Sailing Analytics + SAP Sailing Analytics + + + SAP Sailing Race Manager + SAP Sailing Race Manager + + + Post-Race Analytics + レース後分析 + + + Training Diary + トレーニング日記 + + + Strategy Simulator + 戦略シミュレータ + + + SAP in Sailing + SAP in Sailing + + + Sailing provides the perfect platform for SAP to showcase solutions and help the sport run like never before. SAP’s involvement in the sport has transformed the sailing experience by providing tools, which: + セーリングは、SAP にとってソリューションを紹介するためのプラットフォームとしてうってつけのものであり、これまでになかったやり方でスポーツの運営を支援しています。SAP によるスポーツへの関与は、以下のことを行うツールを提供することによってセーリングから得られる体験を別次元のものに変えました。 + + + Help sailors analyze performance and optimize strategy + パフォーマンスの分析と戦略の最適化でセーラーを支援します + + + Bring fans closer to the action + アクションをファンにとって身近なものにします + + + Provide the media with information and insights to deliver a greater informed commentary + 情報豊富なすばらしい解説を行うための情報とインサイトを報道関係者に提供します + + + SAP has a longstanding involvement with sailing and has established a portfolio spanning across teams and regattas, including: + SAP は、長年にわたりセーリングに関与しており、以下のようなチームおよびレガッタと関係を構築してきました。 + + + Official Technology Partner of Travemünder Woche + Travemünder Woche の公式テクノロジーパートナー + + + Technical Partner of the European Sailing Leagues + 欧州セーリング連盟 (European Sailing Leagues) のテクノロジーパートナー + + + Title sponsor of the SAP Extreme Sailing Team + SAP Extreme Sailing Team のタイトルスポンサー + + + Official Technical Partner of the Extreme Sailing Series + Extreme Sailing Series の公式テクニカルパートナー + + + Premium Partner of Germany’s national sailing team, Audi Sailing Team Germany + ドイツ代表セーリングチームである Audi Sailing Team Germany のプレミアムパートナー + + + Premium Partner of the world’s largest regatta, Kieler Woche (Kiel Week) + 世界最大のレガッタである Kieler Woche (Kiel Week) のプレミアムパートナー + + + Title sponsor of the SAP 5O5 World Championships + SAP 5O5 World Championships のタイトルスポンサー + + + By delivering real time analytics around race rankings, speeds, maneuvers, bearings etc., SAP is helping to simplify and demystify the complex sport of sailing. + レースランキング、速力、操縦、ベアリングなどに関係するリアルタイム分析を提供することにより、SAP はセーリングという複雑なスポーツを平易かつ分かりやすく説明することをお手伝いしています。 + + + SAP Sailing Analytics provide insights and transparency to the world of sailing by utilizing Cloud and In-Memory Technology, processing GPS and wind measurement data in real time and visualizing contents in various frontends accessible from anywhere. + SAP Sailing Analytics は、クラウドおよびインメモリ技術を活用し、リアルタイムで GPS および風計測データを処理し、どこからでもアクセスできるさまざまなフロントエンドでコンテンツを視覚化することによってセーリングの世界にインサイトと透明性を提供します。 + + + The SAP Race Committee App delivers greater efficiency and control to race organizers by automating and simplifying their manual tasks and communication during running sailing regattas. The Race Committee App helps simplify the operation of running and managing race events to help race committees operate smarter. + SAP Race Committee アプリは、セーリングレガッタ運営中にレース組織者にその手動での作業および連絡業務を自動化および簡略化することによって効率と統制を向上させます。Race Committee アプリは、レースイベントの運営および管理の業務を簡略化するのに役立ち、レース委員会がより円滑に運営されるよう支援します。 + + + Access to data collected at sailing events gives sailors the opportunity to assess and analyse their performances to help develop strategies and sail better.<br> SAP Business Analytics solutions provide sailors, coaches, fans and media with tools to browse and analyse historical data across many regattas and races. SAP HANA allows them to learn about performance patterns, tendencies and sailor profiles in a simple, fast and efficient way in order to optimize their own performance and develop successful strategies for future regattas. + セーリングイベントで収集されたデータへのアクセスにより、戦略の策定や帆走の向上を支援するためにセーラーがそのパフォーマンスを評価および分析するための機会が提供されます。<br>SAP Business Analytics ソリューションは、多くのレガッタおよびレースにわたって履歴データを照会および分析するツールをセーラー、コーチ、ファン、そして報道関係者に提供します。SAP HANA は、こうした人たちが自身のパフォーマンスを最適化し、予定されているレガッタのために成功を収める戦略を策定するために簡単、迅速、かつ効率的な方法でパフォーマンスのパターン、傾向、およびセーラーのプロファイルを知ることを可能にします。 + + + The STG Training Diary supports the daily documentation and analysis of an athlete''s physical condition and training efforts in the areas of water training, physical training, boat work, mental, theory as well as other activities like education or travel. + STG Training Diary は、アスリートの体調、ならびに水上トレーニング、身体的トレーニング、操艇、メンタル、理論、さらに教育や移動などのその他の活動の領域におけるトレーニングへの取り組みの日々の記録および分析をサポートします。 + + + Choosing the right strategy can provide great competitive advantage during a race. The Strategy Simulator simplifies determining the best sailing strategy for various wind conditions and water currents. Since wind and current can be changed in the simulation right away, the robustness of a strategy can be easily evaluated even for uncertain weather conditions. + 適切な戦略の選択は、レース中における大きな競争上の優位性をもたらすことができます。Strategy Simulator により、さまざまな風況および水流に対して最良のセーリング戦略を決定することが平易になります。風と水流をシミュレーションで直ちに変更することができるため、不確かな気象条件に対しても戦略の確かさを簡単に評価することができます。 + + + SAP Sailing Analytics + SAP Sailing Analytics + + + By delivering real time analytics around race rankings, speeds, maneuvers, bearings etc., SAP is helping to simplify and demystify the complex sport of sailing. + レースランキング、速力、操縦、ベアリングなどに関係するリアルタイム分析を提供することによって、SAP はセーリングという複雑なスポーツを平易かつ分かりやすく説明することに役立っています。 + + + SAP Sailing Analytics provide insights and transparency to the world of sailing by utilizing Cloud and In-Memory Technology, processing GPS and wind measurement data in real time and visualizing contents in various frontends accessible from anywhere. + SAP Sailing Analytics は、クラウドおよびインメモリ技術を活用し、リアルタイムで GPS および風計測データを処理し、どこからでもアクセスできるさまざまなフロントエンドでコンテンツを視覚化することによってセーリングの世界にインサイトと透明性を提供します。 + + + Read More + 続きを読む + + + SAP Sailing Race Manager + SAP Sailing Race Manager + + + The SAP Sailing Race Manager delivers greater efficiency and control to race organizers by automating and simplifying their manual tasks and communication during running sailing regattas. The Race Committee App helps simplify the operation of running and managing race events to help race committees operate smarter. + SAP Sailing Race Manager は、セーリングレガッタ運営中にレース組織者にその手動での作業および連絡業務を自動化および簡略化することによって効率と統制を向上させます。Race Committee アプリは、レースイベントの運営および管理の業務を簡略化するのに役立ち、レース委員会がより円滑に運営されるよう支援します。 + + + Learn More + 詳細情報 + + + Post-Race Analytics + レース後分析 + + + Access to data collected at sailing events gives sailors the opportunity to assess and analyse their performances to help develop strategies and sail better. + セーリングイベントで収集されたデータへのアクセスにより、戦略の策定や帆走の向上を支援するためにセーラーがそのパフォーマンスを評価および分析するための機会が提供されます。 + + + SAP Business Analytics solutions provide sailors, coaches, fans and media with tools to browse and analyse historical data across many regattas and races. SAP HANA allows them to learn about performance patterns, tendencies and sailor profiles in a simple, fast and efficient way in order to optimize their own performance and develop successful strategies for future regattas. + SAP Business Analytics ソリューションは、多くのレガッタおよびレースにわたって履歴データを照会および分析するツールをセーラー、コーチ、ファン、そして報道関係者に提供します。SAP HANA は、こうした人たちが自身のパフォーマンスを最適化し、予定されているレガッタのために成功を収める戦略を策定するために簡単、迅速、かつ効率的な方法でパフォーマンスのパターン、傾向、およびセーラーのプロファイルを知ることを可能にします。 + + + SAP Sail Better – STG Training Diary + SAP Sail Better – STG Training Diary + + + The STG Training Diary supports the daily documentation and analysis of an athlete’s physical condition and training efforts in the areas of water training, physical training, boat work, mental, theory as well as other activities like education or travel. + STG Training Diary は、アスリートの体調、ならびに水上トレーニング、身体的トレーニング、操艇、メンタル、理論、さらに教育や移動などのその他の活動の領域におけるトレーニングへの取り組みの日々の記録および分析をサポートします。 + + + Strategy Simulator + 戦略シミュレータ + + + Choosing the right strategy can provide great competitive advantage during a race. The Strategy Simulator simplifies determining the best sailing strategy for various wind conditions and water currents. Since wind and current can be changed in the simulation right away, the robustness of a strategy can be easily evaluated even for uncertain weather conditions. + 適切な戦略の選択は、レース中における大きな競争上の優位性をもたらすことができます。Strategy Simulator により、さまざまな風況および水流に対して最良のセーリング戦略を決定することが平易になります。風と水流をシミュレーションで直ちに変更することができるため、不確かな気象条件に対しても戦略の確かさを簡単に評価することができます。 + + + Learn More + 詳細情報 + + + Imprint + 刻印 + + +
+
\ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/XLF/TextMessages_ru.properties.xlf b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/XLF/TextMessages_ru.properties.xlf index c959dd9c6b4..619b00b9d85 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/XLF/TextMessages_ru.properties.xlf +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/XLF/TextMessages_ru.properties.xlf @@ -1,6 +1,6 @@ - +
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 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/XLF/TextMessages_zh_CN.properties.xlf b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/XLF/TextMessages_zh_CN.properties.xlf index 787f4589603..7ced312c930 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/XLF/TextMessages_zh_CN.properties.xlf +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/common/client/i18n/XLF/TextMessages_zh_CN.properties.xlf @@ -1,6 +1,6 @@ - +
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 @@ -13,7 +13,7 @@ Hint: You can also use the leaderboard (on the LEADERBOARD tab) to select the competitors displayed in the chart. - 提示:您还可以使用积分榜(在 LEADERBOARD 选项卡中)选择在记录中显示的参赛队。 + 提示:您还可以使用积分榜(在 LEADERBOARD 选项卡中)选择在记录中显示的参赛队。 Choose a language @@ -77,7 +77,7 @@ View analysis - 查看分析 + 查看分析 Overall leaderboard @@ -509,7 +509,7 @@ Search - 搜索 + 搜索 Solutions @@ -685,7 +685,7 @@ Share this on - 分享到 + 分享到 Recommend SAP Sailing Analytics diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/RaceContext.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/RaceContext.java index edc8421f2b9..4fe52c27d2e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/RaceContext.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/home/server/RaceContext.java @@ -429,6 +429,15 @@ public class RaceContext { return 0; } + private TimePoint getLiveTimePoint() { + return new MillisecondsTimePoint(getLiveTimePointInMillis()); + } + + private long getLiveTimePointInMillis() { + final Long liveDelay = leaderboard.getDelayToLiveInMillis(); + return System.currentTimeMillis() - (liveDelay == null ? 0l : liveDelay); + } + private SimpleCompetitorDTO getWinnerOrNull() { if (getLiveRaceViewState() != RaceViewState.FINISHED) { // We can't reliably calculate the winner for non finished races @@ -436,10 +445,7 @@ public class RaceContext { } // TODO do not calculate the winner if the blue flag is currently shown. try { - TimePoint finishTime = getFinishTime(); - if(finishTime == null) { - finishTime = HomeServiceUtil.getLiveTimePoint(); - } + TimePoint finishTime = getLiveTimePoint(); List competitors = leaderboard.getCompetitorsFromBestToWorst(raceColumn, finishTime); if (competitors == null || competitors.isEmpty()) { return null; @@ -542,7 +548,7 @@ public class RaceContext { if (finishTime != null && now.after(finishTime)) { return RaceViewState.FINISHED; } - if(raceLog != null) { + if (raceLog != null) { RaceLogFlagEvent abortingFlagEvent = checkForAbortFlagEvent(); if (abortingFlagEvent != null) { Flags upperFlag = abortingFlagEvent.getUpperFlag(); @@ -558,10 +564,10 @@ public class RaceContext { } } ScoreCorrection scoreCorrection = leaderboard.getScoreCorrection(); - if(trackedRace == null && scoreCorrection != null && scoreCorrection.hasCorrectionForNonTrackedFleet(raceColumn)) { + if (trackedRace == null && scoreCorrection != null && scoreCorrection.hasCorrectionForNonTrackedFleet(raceColumn)) { return RaceViewState.FINISHED; } - if(startTime != null) { + if (startTime != null) { return RaceViewState.RUNNING; } return RaceViewState.PLANNED; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/server/HomeServiceUtil.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/server/HomeServiceUtil.java index 9e81aa5626a..6ed4b53e7e8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/server/HomeServiceUtil.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/server/HomeServiceUtil.java @@ -449,16 +449,6 @@ public final class HomeServiceUtil { return result; } - public static TimePoint getLiveTimePoint() { - return new MillisecondsTimePoint(getLiveTimePointInMillis()); - } - - public static long getLiveTimePointInMillis() { - // TODO better solution - long livePlayDelayInMillis = 15_000; - return System.currentTimeMillis() - livePlayDelayInMillis; - } - public static String getCourseAreaNameForRegattaIdThereIsMoreThanOne(EventBase event, Leaderboard leaderboard) { /** The course area will not be shown if there is only one course area defined for the event */ if (Util.size(event.getVenue().getCourseAreas()) <= 1) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/AdminConsole.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/AdminConsole.gwt.xml index 2f84f572fae..833bd8dba52 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/AdminConsole.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/AdminConsole.gwt.xml @@ -26,7 +26,7 @@ - + diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java index 326b7efa3e5..527b8126211 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLeaderboardConfigPanel.java @@ -332,11 +332,8 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement } /** - * @param nameOfRaceColumnToSelect - * if not null, selects the first race column name with this name found in the leaderboard - * after the refresh has successfully completed. See {@link #selectRaceColumn(String)}. */ - public void loadAndRefreshLeaderboard(final String leaderboardName, final String nameOfRaceColumnToSelect) { + public void loadAndRefreshLeaderboard(final String leaderboardName) { sailingService.getLeaderboard(leaderboardName, new MarkedAsyncCallback( new AsyncCallback() { @Override @@ -350,9 +347,6 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement replaceLeaderboardInList(availableLeaderboardList, leaderboardName, leaderboard); filterLeaderboardPanel.updateAll(availableLeaderboardList); // also updates leaderboardList provider leaderboardSelectionModel.setSelected(leaderboard, true); - if (nameOfRaceColumnToSelect != null) { - selectRaceColumn(nameOfRaceColumnToSelect); - } leaderboardSelectionChanged(); getLeaderboardsRefresher().updateLeaderboards(leaderboardList.getList(), AbstractLeaderboardConfigPanel.this); } @@ -593,7 +587,7 @@ public abstract class AbstractLeaderboardConfigPanel extends FormPanel implement new ParallelExecutionHolder(callbacks.toArray(new ParallelExecutionCallback[0])) { @Override public void handleSuccess() { - loadAndRefreshLeaderboard(selectedLeaderboardName, result.getName()); + loadAndRefreshLeaderboard(selectedLeaderboardName); } @Override public void handleFailure(Throwable t) { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLogDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLogDialog.java index 483bf512ba5..b368215713d 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLogDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AbstractLogDialog.java @@ -22,6 +22,8 @@ import com.sap.sse.gwt.client.celltable.BaseCelltable; import com.sap.sse.gwt.client.dialog.DataEntryDialog; public abstract class AbstractLogDialog, E extends AbstractLogEventDTO> extends DataEntryDialog { + + protected final StringMessages stringMessages; private final R logDTO; private final SelectionModel selectionModel; @@ -30,6 +32,7 @@ public abstract class AbstractLogDialog, E extends A public AbstractLogDialog(final R logDTO, final StringMessages stringMessages, String logName, DialogCallback callback) { super(logName, null, stringMessages.ok(), stringMessages.cancel(), /* validator */ null, callback); + this.stringMessages = stringMessages; this.logDTO = logDTO; raceLogEventsList = new ListDataProvider(); @@ -146,11 +149,11 @@ public abstract class AbstractLogDialog, E extends A addFirstColumns(raceLogEventsTable, columnSortHandler); raceLogEventsTable.addColumn(logEventTypeColumn, stringMessages.type()); - raceLogEventsTable.addColumn(logEventCreatedColumn, "Created at"); - raceLogEventsTable.addColumn(logEventLogicalTimeColumn, "Logical timepoint"); - raceLogEventsTable.addColumn(logEventAuthorColumn, "Author"); - raceLogEventsTable.addColumn(logEventAuthorPriorityColumn, "Priority"); - raceLogEventsTable.addColumn(logEventInfoColumn, "Info"); + raceLogEventsTable.addColumn(logEventCreatedColumn, stringMessages.createdAt()); + raceLogEventsTable.addColumn(logEventLogicalTimeColumn, stringMessages.logicalTimepoint()); + raceLogEventsTable.addColumn(logEventAuthorColumn, stringMessages.authorName()); + raceLogEventsTable.addColumn(logEventAuthorPriorityColumn, stringMessages.authorPriority()); + raceLogEventsTable.addColumn(logEventInfoColumn, stringMessages.info()); addLastColumns(raceLogEventsTable, columnSortHandler); raceLogEventsList.addDataDisplay(raceLogEventsTable); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java index 983f752fb84..b02792ba839 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/AdminConsoleEntryPoint.java @@ -75,7 +75,6 @@ public class AdminConsoleEntryPoint extends AbstractSailingEntryPoint implements return createAdminConsolePanel(); } }); - headerPanel.setHeaderWidget(header); headerPanel.setContentWidget(authorizedContentDecorator); RootLayoutPanel rootPanel = RootLayoutPanel.get(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java index 8fa2cf96e96..1052cba267c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ControlPointTableWrapper.java @@ -36,7 +36,7 @@ public class ControlPointTableWrapper(leaderboardName, race.getA().getName(), race.getB().getName()); } - public void perform(String leaderboardName, RaceColumnDTOAndFleetDTOWithNameBasedEquality raceColumnDTOAndFleetDTO) { + public void perform(String leaderboardName, RaceColumnDTOAndFleetDTOWithNameBasedEquality raceColumnDTOAndFleetDTO, + final Runnable onSuccessCallback) { Triple fromTriple = toTriple(leaderboardName, raceColumnDTOAndFleetDTO); Set> toRacelogs = convertToRacelogs(leaderboardName); @@ -68,7 +69,9 @@ public class CourseAndCompetitorCopyOperation { @Override public void onSuccess(Void result) { - + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } } }); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java index 038b5027bd2..33051ab14d5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/EventListComposite.java @@ -184,7 +184,7 @@ public class EventListComposite extends Composite implements EventsRefresher, Le panel.add(filterTextbox); panel.add(eventTable); - noEventsLabel = new Label("No events defined yet."); + noEventsLabel = new Label(stringMessages.noEventsYet()); noEventsLabel.ensureDebugId("NoRegattasLabel"); noEventsLabel.setWordWrap(false); panel.add(noEventsLabel); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/InviteBuoyTenderDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/InviteBuoyTenderDialog.java index 54bcdb1753d..6ef843f92e7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/InviteBuoyTenderDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/InviteBuoyTenderDialog.java @@ -16,6 +16,9 @@ import com.sap.sse.gwt.client.controls.GenericListBox; import com.sap.sse.gwt.client.controls.GenericListBox.ValueBuilder; import com.sap.sse.gwt.client.dialog.DataEntryDialog; +/** + * The result of this dialog is a triple (event, serverUrlString, emailsString). + */ public class InviteBuoyTenderDialog extends DataEntryDialog> { private GenericListBox events; private TextBox serverUrl; @@ -25,7 +28,8 @@ public class InviteBuoyTenderDialog extends DataEntryDialog> callback) { + public InviteBuoyTenderDialog(final StringMessages stringMessages, SailingServiceAsync sailingService, + String leaderboardName, ErrorReporter errorReporter, DialogCallback> callback) { super(stringMessages.selectEventForInvitation(), null, stringMessages.inviteBuoyTenders(), stringMessages .cancel(), new Validator>() { @Override @@ -64,7 +68,6 @@ public class InviteBuoyTenderDialog extends DataEntryDialog() { @Override @@ -76,7 +79,6 @@ public class InviteBuoyTenderDialog extends DataEntryDialog>() { @Override diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java index 75e4b2735e5..74105e039f7 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LeaderboardConfigPanel.java @@ -548,7 +548,7 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { @Override public void onSuccess(Void arg0) { - loadAndRefreshLeaderboard(selectedLeaderboardName, /* raceColumnNameToSelect */ null); + loadAndRefreshLeaderboard(selectedLeaderboardName); } })); } @@ -572,7 +572,7 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { @Override public void onSuccess(Void result) { - loadAndRefreshLeaderboard(selectedLeaderboardName, selectedRaceColumnName); + loadAndRefreshLeaderboard(selectedLeaderboardName); } })); } @@ -596,7 +596,7 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { @Override public void onSuccess(Void result) { - loadAndRefreshLeaderboard(selectedLeaderboardName, selectedRaceColumnName); + loadAndRefreshLeaderboard(selectedLeaderboardName); } })); } @@ -671,7 +671,7 @@ TrackedRaceChangedListener, LeaderboardsDisplayer { @Override public void onSuccess(Void v) { - loadAndRefreshLeaderboard(leaderboardName, /* nameOfRaceColumnToSelect */ null); + loadAndRefreshLeaderboard(leaderboardName); } })); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ListBoxUtils.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ListBoxUtils.java index 28c370ab0bc..d6c174313d8 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ListBoxUtils.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/ListBoxUtils.java @@ -9,17 +9,18 @@ import com.sap.sailing.domain.common.racelog.RacingProcedureType; public final class ListBoxUtils { - public static void setupCourseDesignerModeListBox(ListBox box, CourseDesignerMode selectedCourseDesignerMode, String noItemText) { for (CourseDesignerMode mode : CourseDesignerMode.values()) { - if (mode == CourseDesignerMode.UNKNOWN) { - box.addItem(noItemText, mode.name()); - } else { - box.addItem(mode.toString(), mode.name()); - } - if (mode == selectedCourseDesignerMode) { - box.setSelectedIndex(box.getItemCount() - 1); + if (mode != CourseDesignerMode.ADMIN_CONSOLE) { + if (mode == CourseDesignerMode.UNKNOWN) { + box.addItem(noItemText, mode.name()); + } else { + box.addItem(mode.toString(), mode.name()); + } + if (mode == selectedCourseDesignerMode) { + box.setSelectedIndex(box.getItemCount() - 1); + } } } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LocalServerManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LocalServerManagementPanel.java index 0e0a7731295..fcbc8c2606e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LocalServerManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/LocalServerManagementPanel.java @@ -36,10 +36,10 @@ public class LocalServerManagementPanel extends SimplePanel { setWidget(mainPanel); mainPanel.setWidth("100%"); - serverInfoPanel = new CaptionPanel("Server Information"); + serverInfoPanel = new CaptionPanel(stringMessages.serverInformation()); mainPanel.add(serverInfoPanel); - serverConfigurationPanel = new CaptionPanel("Server Configuration"); + serverConfigurationPanel = new CaptionPanel(stringMessages.serverConfiguration()); mainPanel.add(serverConfigurationPanel); createServerInfoUI(); @@ -56,7 +56,7 @@ public class LocalServerManagementPanel extends SimplePanel { Grid grid = new Grid(2, 2); grid.setWidget(0, 0, new Label(stringMessages.name() + ":")); grid.setWidget(0, 1, serverNameLabel); - grid.setWidget(1, 0, new Label("Build version:")); + grid.setWidget(1, 0, new Label(stringMessages.buildVersion() + ":")); grid.setWidget(1, 1, buildVersionLabel); serverInfoContentPanel.add(grid); @@ -76,7 +76,7 @@ public class LocalServerManagementPanel extends SimplePanel { }); Grid grid = new Grid(2, 2); - grid.setWidget(0, 0, new Label("Standalone server:")); + grid.setWidget(0, 0, new Label(stringMessages.standaloneServer() + ":")); grid.setWidget(0, 1, isStandaloneServerCheckbox); serverConfigurationContentPanel.add(grid); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogDialog.java index fe6dab24da2..c3f663d3f6e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/RaceLogDialog.java @@ -12,11 +12,9 @@ import com.sap.sailing.gwt.ui.shared.RaceLogDTO; import com.sap.sailing.gwt.ui.shared.RaceLogEventDTO; public class RaceLogDialog extends AbstractLogDialog { - private final StringMessages stringMessages; public RaceLogDialog(final RaceLogDTO raceLogDTO, final StringMessages stringMessages, DialogCallback callback) { super(raceLogDTO, stringMessages, stringMessages.raceLog(), callback); - this.stringMessages = stringMessages; } @Override @@ -34,7 +32,7 @@ public class RaceLogDialog extends AbstractLogDialog regattaAndRaceIdentifiers) { - loadAndRefreshLeaderboard(getSelectedLeaderboard().name, getSelectedRaceColumnWithFleet().getA().getName()); + loadAndRefreshLeaderboard(getSelectedLeaderboard().name); } @Override public void racesRemoved(Iterable regattaAndRaceIdentifiers) { - loadAndRefreshLeaderboard(getSelectedLeaderboard().name, getSelectedRaceColumnWithFleet().getA().getName()); + loadAndRefreshLeaderboard(getSelectedLeaderboard().name); } }); } @@ -313,6 +313,7 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC new AsyncCallback() { @Override public void onSuccess(Void result) { + loadAndRefreshLeaderboard(leaderboardName); } @Override @@ -330,8 +331,8 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC leaderboardName, new DialogCallback() { @Override public void ok(CourseAndCompetitorCopyOperation operation) { - operation.perform(leaderboardName, raceColumnDTOAndFleetDTO); - loadAndRefreshLeaderboard(leaderboardName, raceColumnDTOAndFleetDTO.getA().getName()); + operation.perform(leaderboardName, raceColumnDTOAndFleetDTO, /* onSuccessCallback */ new Runnable() { + @Override public void run() { loadAndRefreshLeaderboard(leaderboardName); }}); } @Override @@ -419,10 +420,7 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC for (TrackedRaceChangedListener listener : trackedRacesListComposite.raceIsTrackedRaceChangeListener) { listener.racesStoppedTracking(racesToStopTracking); } - - for (RaceColumnDTOAndFleetDTOWithNameBasedEquality raceColumndDTOAndFleet : selectedSet) { - loadAndRefreshLeaderboard(getSelectedLeaderboard().name, raceColumndDTOAndFleet.getA().getName()); - } + loadAndRefreshLeaderboard(getSelectedLeaderboard().name); } })); } @@ -487,7 +485,6 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC raceColumnsAndFleets.add(new Triple(selectedLeaderboard.name, raceColumn.getName(), fleet.getName())); } } - sailingService.getTrackingTimes(raceColumnsAndFleets, new AsyncCallback, Pair>>() { @Override @@ -498,7 +495,6 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC @Override public void onSuccess(Map, Pair> result) { raceWithStartAndEndOfTrackingTime = result; - raceColumnTable.getDataProvider().getList().clear(); for (RaceColumnDTO raceColumn : selectedLeaderboard.getRaceList()) { for (FleetDTO fleet : raceColumn.getFleets()) { @@ -508,13 +504,11 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC } } }); - selectedLeaderBoardPanel.setVisible(true); selectedLeaderBoardPanel.setCaptionText("Details of leaderboard '" + selectedLeaderboard.name + "'"); if (!selectedLeaderboard.type.isMetaLeaderboard()) { trackedRacesCaptionPanel.setVisible(true); } - sailingService.doesRegattaLogContainCompetitors(((StrippedLeaderboardDTO) leaderboardSelectionModel.getSelectedSet().toArray()[0]).name, new RegattaLogCallBack()); } else { selectedLeaderBoardPanel.setVisible(false); @@ -543,7 +537,7 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC sailingService.denoteForRaceLogTracking(leaderboard.name, new AsyncCallback() { @Override public void onSuccess(Void result) { - loadAndRefreshLeaderboard(leaderboard.name, null); + loadAndRefreshLeaderboard(leaderboard.name); raceColumnTableSelectionModel.clear(); } @@ -559,7 +553,7 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC sailingService.denoteForRaceLogTracking(leaderboard.name, raceColumn.getName(), fleet.getName(), new AsyncCallback() { @Override public void onSuccess(Void result) { - loadAndRefreshLeaderboard(leaderboard.name, raceColumn.getName()); + loadAndRefreshLeaderboard(leaderboard.name); } @Override @@ -574,7 +568,7 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC sailingService.removeDenotationForRaceLogTracking(leaderboard.name, raceColumn.getName(), fleet.getName(), new AsyncCallback() { @Override public void onSuccess(Void result) { - loadAndRefreshLeaderboard(leaderboard.name, raceColumn.getName()); + loadAndRefreshLeaderboard(leaderboard.name); } @Override @@ -611,7 +605,7 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC new AsyncCallback() { @Override public void onSuccess(Void result) { - loadAndRefreshLeaderboard(leaderboard.name, getSelectedRaceColumnWithFleet().getA().getName()); + loadAndRefreshLeaderboard(leaderboard.name); trackedRacesListComposite.regattaRefresher.fillRegattas(); } @@ -690,11 +684,11 @@ public class SmartphoneTrackingEventManagementPanel extends AbstractLeaderboardC return LocaleInfo.getCurrentLocale().getLocaleName(); } - private void openChooseEventDialogAndSendMails(final String leaderBoardName) { - new InviteBuoyTenderDialog(stringMessages, sailingService, leaderBoardName, errorReporter, new DialogCallback>() { + private void openChooseEventDialogAndSendMails(final String leaderboardName) { + new InviteBuoyTenderDialog(stringMessages, sailingService, leaderboardName, errorReporter, new DialogCallback>() { @Override public void ok(Triple result) { - sailingService.inviteBuoyTenderViaEmail(result.getB(), result.getA(), leaderBoardName, result.getC(), + sailingService.inviteBuoyTenderViaEmail(result.getB(), result.getA(), leaderboardName, result.getC(), null, stringMessages.playStoreBuoypositioningApp(), getLocaleInfo(), new AsyncCallback() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java index 00061a94960..45a42de6f6c 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/StructureImportManagementPanel.java @@ -166,7 +166,7 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat }); Grid eventURLGrid = new Grid(3, 2); - eventURLGrid.setWidget(0, 0, new Label(stringMessages.manage2Sail() + " " + stringMessages.event() + " " + stringMessages.id() + ":")); + eventURLGrid.setWidget(0, 0, new Label(stringMessages.manage2SailEvent() + stringMessages.id() + ":")); eventURLGrid.setWidget(0, 1, eventIDTextBox); eventURLGrid.setWidget(1, 0, new Label(stringMessages.jsonUrl() + ":")); eventURLGrid.setWidget(1, 1, jsonURLTextBox); @@ -175,7 +175,7 @@ public class StructureImportManagementPanel extends SimplePanel implements Regat mainPanel.add(progressPanel); mainPanel.add(eventURLGrid); - regattaImportPanel = new CaptionPanel(stringMessages.manage2Sail() + " " + stringMessages.event()); + regattaImportPanel = new CaptionPanel(stringMessages.manage2SailEvent()); mainPanel.add(regattaImportPanel); VerticalPanel regattaImportContentPanel = new VerticalPanel(); regattaImportPanel.setContentWidget(regattaImportContentPanel); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java index 5079631c7dc..8ab67dbefbd 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/SwissTimingReplayConnectorPanel.java @@ -160,7 +160,7 @@ public class SwissTimingReplayConnectorPanel extends AbstractEventManagementPane // the regatta selection for a tracked race HorizontalPanel regattaPanel = new HorizontalPanel(); racesPanel.add(regattaPanel); - Label lblRegattas = new Label("Regatta used for the tracked race:"); + Label lblRegattas = new Label(stringMessages.regattaUsedForTheTrackedRace()); lblRegattas.setWordWrap(false); regattaPanel.setCellVerticalAlignment(lblRegattas, HasVerticalAlignment.ALIGN_MIDDLE); regattaPanel.setSpacing(5); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java index 65d7549a29a..db6848240d0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/TableWrapper.java @@ -7,7 +7,9 @@ import com.google.gwt.user.cellview.client.SimplePager; import com.google.gwt.user.client.ui.IsWidget; import com.google.gwt.user.client.ui.VerticalPanel; import com.google.gwt.user.client.ui.Widget; +import com.google.gwt.view.client.HasRows; import com.google.gwt.view.client.ListDataProvider; +import com.google.gwt.view.client.Range; import com.sap.sailing.gwt.ui.client.SailingServiceAsync; import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.controls.FlushableCellTable; @@ -45,7 +47,7 @@ public abstract class TableWrapper> im * {@link EntityIdentityComparator} to create a {@link RefreshableSelectionModel} */ - public TableWrapper(SailingServiceAsync sailingService, StringMessages stringMessages, ErrorReporter errorReporter, + public TableWrapper(SailingServiceAsync sailingService, final StringMessages stringMessages, ErrorReporter errorReporter, boolean multiSelection, boolean enablePager, EntityIdentityComparator entityIdentityComparator) { this.sailingService = sailingService; this.errorReporter = errorReporter; @@ -77,7 +79,19 @@ public abstract class TableWrapper> im mainPanel.add(table); if (enablePager) { table.setPageSize(8); - SimplePager pager = new SimplePager(); + SimplePager pager = new SimplePager() { + protected String createText() { + HasRows display = getDisplay(); + Range range = display.getVisibleRange(); + int pageStart = range.getStart() + 1; + int pageSize = range.getLength(); + int dataSize = display.getRowCount(); + int endIndex = Math.min(dataSize, pageStart + pageSize - 1); + endIndex = Math.max(pageStart, endIndex); + boolean exact = display.isRowCountExact(); + return stringMessages.pagerStateInfo(pageStart, endIndex, dataSize, exact); + } + }; pager.setDisplay(table); mainPanel.add(pager); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java index 3c81709fe46..a271f8ef68d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WaypointTableWrapper.java @@ -22,14 +22,14 @@ public class WaypointTableWrapper typeColumn = new TextColumn() { @Override public String getValue(WaypointDTO w) { ControlPointDTO d = w.controlPoint; if (d instanceof GateDTO) { - return "2x " + stringMessages.mark(); + return stringMessages.twoXMark(); } else if (d instanceof MarkDTO) { return stringMessages.mark(); } else { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java index 11289a9e458..6cada6cdd64 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/adminconsole/WindPanel.java @@ -165,7 +165,7 @@ public class WindPanel extends FormPanel implements RegattasDisplayer, WindShowe final VerticalPanel windSourcesPanel = new VerticalPanel(); windSourcesPanel.setSpacing(10); tabPanel.add(windSourcesPanel, stringMessages.windSourcesUsed()); - tabPanel.add(windFixesDisplayPanel, "Wind fixes"); + tabPanel.add(windFixesDisplayPanel, stringMessages.windFixes()); tabPanel.selectTab(0); raceIsKnownToStartUpwindBox = new CheckBox(stringMessages.raceIsKnownToStartUpwind()); @@ -240,10 +240,10 @@ public class WindPanel extends FormPanel implements RegattasDisplayer, WindShowe AdminConsoleTableResources tableRes = GWT.create(AdminConsoleTableResources.class); rawWindFixesTable = new BaseCelltable(/* pageSize */10000, tableRes); - rawWindFixesTable.addColumn(timeColumn, "Time"); - rawWindFixesTable.addColumn(speedInKnotsColumn, "Speed (kn)"); - rawWindFixesTable.addColumn(windDirectionInDegColumn, "From (deg)"); - rawWindFixesTable.addColumn(positionColumn, "Position"); + rawWindFixesTable.addColumn(timeColumn, stringMessages.time()); + rawWindFixesTable.addColumn(speedInKnotsColumn, stringMessages.speedInKnots()); + rawWindFixesTable.addColumn(windDirectionInDegColumn, stringMessages.fromDeg()); + rawWindFixesTable.addColumn(positionColumn, stringMessages.position()); rawWindFixesTable.addColumn(removeColumn, stringMessages.actions()); rawWindFixesDataProvider = new ListDataProvider(); rawWindFixesDataProvider.addDataDisplay(rawWindFixesTable); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/GwtUrlHelper.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/GwtUrlHelper.java index 58c0c4ce99d..e60d01853a3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/GwtUrlHelper.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/GwtUrlHelper.java @@ -19,4 +19,9 @@ public enum GwtUrlHelper implements UrlHelper { return URL.encode(decodedUrlString); } + @Override + public String encodeQueryString(String queryString) { + return URL.encodeQueryString(queryString); + } + } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java index e5446e7be83..50ac79933e6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/RaceTimePanel.java @@ -47,9 +47,9 @@ public class RaceTimePanel extends TimePanel implements R private final Duration initialTimeAfterRaceStartInReplayMode; public RaceTimePanel(UserService userService, Timer timer, TimeRangeWithZoomProvider timeRangeProvider, StringMessages stringMessages, - RaceTimesInfoProvider raceTimesInfoProvider, boolean canReplayWhileLiveIsPossible, boolean isScreenLargeEnoughToOfferChartSupport, + RaceTimesInfoProvider raceTimesInfoProvider, boolean canReplayWhileLiveIsPossible, boolean forcePaddingRightToAlignToCharts, RegattaAndRaceIdentifier selectedRaceIdentifier, Duration initialTimeAfterRaceStartInReplayMode) { - super(timer, timeRangeProvider, stringMessages, canReplayWhileLiveIsPossible, isScreenLargeEnoughToOfferChartSupport); + super(timer, timeRangeProvider, stringMessages, canReplayWhileLiveIsPossible, forcePaddingRightToAlignToCharts); this.userService = userService; this.raceTimesInfoProvider = raceTimesInfoProvider; selectedRace = null; diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java index fc6a57e698a..86fbc58fa9e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.java @@ -3,12 +3,14 @@ package com.sap.sailing.gwt.ui.client; import com.google.gwt.core.client.GWT; import com.google.gwt.i18n.client.LocalizableResource.DefaultLocale; import com.google.gwt.json.client.JSONValue; +import com.sap.sailing.domain.common.i18n.CommonStringMessages; import com.sap.sse.gwt.client.DefaultRule_0_1_2_n; import com.sap.sse.gwt.client.DefaultRule_0_1_n; import com.sap.sse.gwt.client.DefaultRule_1_0n; @DefaultLocale("en") -public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, com.sap.sse.gwt.adminconsole.StringMessages { +public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, + com.sap.sse.gwt.adminconsole.StringMessages, CommonStringMessages { public static final StringMessages INSTANCE = GWT.create(StringMessages.class); String manage2Sail(); @@ -1535,4 +1537,53 @@ public interface StringMessages extends com.sap.sse.gwt.client.StringMessages, c String fixSuccessfullyRemoved(); String dragToChangePosition(); String moveFix(); + String manage2SailEvent(); + String noEventsYet(); + String selectResultImportProvider(); + String buildVersion(); + String serverInformation(); + String serverConfiguration(); + String standaloneServer(); + String passId(); + String createdAt(); + String logicalTimepoint(); + String info(); + String windFixes(); + String twoXMark(); + String pagerStateInfo(int start , int end, int size, @Select boolean exact); + String strategySimulatorTitle(); + String controlSettings(); + String windDisplay(); + String windSetup(); + String sailingSetup(); + String chooseAWindPattern(); + String oscillations(); + String oscillationWithGusts(); + String gusts(); + String measured(); + String baseBearing(); + String raceCourseDiff(); + String baseSpeed(); + String probability(); + String gustSize(); + String averageSpeed(); + String speedVariance(); + String averageDirection(); + String speedLeftSide(); + String speedMiddle(); + String speedRightSide(); + String currentSpeed(); + String currentBearing(); + String percentUnit(); + String perHours(); + String pleaseSelectAValidWindPattern(); + String downwindLegsNotSupported(); + String raceLeader(); + String opportunistLeft(); + String opportunistRight(); + String oneTurnerLeft(); + String oneTurnerRight(); + String current(); + String simulationLegendAlgorithmTimedOutText(); + String simulationLegendMixedLegText(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties index 0696d841995..3787d15cee6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages.properties @@ -409,7 +409,7 @@ errorTryingToObtainLeaderboardContents=Error trying to obtain leaderboard conten lastScoreUpdate=Last Score Update warningBrowserUnsupported=This website is not optimized for your browser. Please switch to a newer one like Firefox or Chrome. newMark=New Mark -controlPoint=Mark/Gate +controlPoint=Mark/Gate name errorUpdatingRaceCourse=Error updating race course: {0} successfullyUpdatedCourse=Successfully updated race course selectTwoMarksForGate=Select two marks to create a gate @@ -683,8 +683,8 @@ setup=Setup strength=Strength (in kn) oscillation=Oscillation oscillating=Oscillating -amplitude=Amplitude(in Grad) -frequency=Frequency(in 1/h) +amplitude=Amplitude +frequency=Frequency noise=Noise sailing=Sailing optimality=Optimality @@ -910,7 +910,7 @@ line=Line offset=Offset doYouReallyWantToRemoveCompetitors=Do you really want to remove the selected competitors? doYouReallyWantToRemoveCompetitor=Do you really want to remove competitor {0}? -filterCompetitors=Filter competitors by name, boat class, sail number or ID +filterCompetitors=Filter competitors by name, boat class, sail number, search tag or ID editCompetitor=Edit Competitor pleaseEnterASailNumber=Please enter a sail number successfullyAllowedCompetitorReset=Successfully allowed reload of competitor(s) {0}. @@ -1500,4 +1500,65 @@ fixSuccessfullyAdded=Fix successfully added. fixPositionSuccessfullyEdited=Fix position successfully edited. fixSuccessfullyRemoved=Fix successfully removed. dragToChangePosition=Drag to change position\nRight-click to get menu to delete -moveFix=Move fix \ No newline at end of file +moveFix=Move fix +manage2SailEvent=Manage2Sail Event +noEventsYet=There are no events yet. +selectResultImportProvider=Please select a result import provider... +buildVersion=Build version +serverInformation=Server Information +serverConfiguration=Server Configuration +standaloneServer=Standalone server +send=Send... +passId=PassId +createdAt=Created at +logicalTimepoint=Logical timepoint +info=Info +windFixes=Wind fixes +twoXMark=2x Mark +pagerStateInfo={0,number,#,###}-{1,number,#,###} of {2,number,#,###} +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} of over {2,number,#,###} +init=Initializing... +connectionSetup=Setting up connection +connectionEstablish=Connecting... +transferStarted=Reading data +transferCompleted=Data-Transfer Complete, Initializing Import Operation +importInit=Initializing Import +importWait=Waiting for other data import operations to finish +importLeaderboardGroups=Importing leaderboard groups +importWindTracks=Importing wind tracks +updateEventLeaderboardGroupLinks=Updating Event-LeaderboardGroup links +strategySimulatorTitle=Strategy Simulator +controlSettings=Control settings +windDisplay=Wind display +windSetup=Wind setup +sailingSetup=Sailing setup +chooseAWindPattern=Choose a wind pattern +oscillations=Oscillations +oscillationWithGusts=Oscillation with Gusts +gusts=Gusts +measured=Measured +baseBearing=Base Bearing +raceCourseDiff=Race Course Diff +baseSpeed=Base Speed +probability=Probability +gustSize=Gust size +averageSpeed=Average Speed +speedVariance=Speed Variance +averageDirection=Average Direction +speedLeftSide=Speed Left Side +speedMiddle=Speed Middle +speedRightSide=Speed Right Side +currentSpeed=Current Speed +currentBearing=Current Bearing +percentUnit=% +perHours=per hr +pleaseSelectAValidWindPattern=Please select a valid wind pattern. +downwindLegsNotSupported=Downwind legs are NOT supported yet. Sorry about that :) +raceLeader=Race Leader +opportunistLeft=Opportunist Left +opportunistRight=Opportunist Right +oneTurnerLeft=1-Turner Left +oneTurnerRight=1-Turner Right +current=Current +simulationLegendAlgorithmTimedOutText = out-of-bounds +simulationLegendMixedLegText = ambiguous wind \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties index e9a77874f5a..cfb4a4181a5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_de.properties @@ -1,6 +1,7 @@ manage2Sail=Manage2Sail raceCommitteeApp=Race Manager App -createNewEvent=Create New EventtracTracEvents=TracTrac Events +createNewEvent=Create New Event +tracTracEvents=TracTrac Events swissTimingEvents=SwissTiming Events trackedBefore=Historie verfolgter Events general=Allgemein @@ -102,7 +103,7 @@ trackableRaces=Trackable Races trackWind=Track Wind regattaName=Name der Regatta eventName=Event-Name -event=Rennen +event=Veranstaltung startTime=Startzeit startOfTracking=Startzeit, die wo vom Tracking ist endOfTrackings=Ende des Trackings @@ -410,7 +411,7 @@ errorTryingToObtainLeaderboardContents=Fehler beim Berechnen der Ergebnisliste: lastScoreUpdate=Letzte Ergebnisänderung warningBrowserUnsupported=Diese Webseite ist für Ihren Browser nicht optimiert. Wir empfehlen den Firefox oder Chrome in der neuesten Version. newMark=Neue Marke -controlPoint=Einzelmarke/Gate +controlPoint=Einzelmarke/Gate Name errorUpdatingRaceCourse=Fehler beim Aktualisieren des neuen Kurses: {0} successfullyUpdatedCourse=Kurs erfolgreich aktualisiert selectTwoMarksForGate=Zwei Marken auswählen, um ein Gate zu erstellen @@ -468,9 +469,9 @@ latitude=Breite longitude=Länge actionAddEvent=Veranstaltung hinzufügen publicationUrl=URL für die Veröffentlichung -suppress=Entfernen +suppress=Ausblenden isPublic=Öffentlich zugänglich -unsuppress=Wieder hinzufügen +unsuppress=Wieder einblenden suppressedCompetitors=Unterdrückte Wettkampfteilnehmer mediaPanel=Audio & Video url=URL @@ -672,44 +673,44 @@ topNCompetitorsByRaceRank=Wettfahrt Top {0} topNCompetitorsByTotalRank=Gesamt Top {0} showHiddenRaces=Rennen im Status HIDDEN anzeigen simulator=Simulator -optionsBar=Optionsbar +optionsBar=Auswahlleiste simulateButton=Prognose -setup=Setup -strength=Strength(in kn) -oscillation=Oscillation -oscillating=Oscillating -amplitude=Amplitude(in Grad) -frequency=Frequency(in 1/h) -noise=Noise -sailing=Sailing -optimality=Optimality -maximumTurns=Maximum Turns -raceDirection=Race Direction -upWind=Upwind -downWind=Downwind -strategies=Strategies -omniscient=Omniscient +setup=Einstellungen +strength=Stärke (in kn) +oscillation=Oszillation +oscillating=Oszillierend +amplitude=Amplitude +frequency=Frequenz +noise=Rauschen +sailing=Segeln +optimality=Optimalität +maximumTurns=Maximum Wenden +raceDirection=Rennrichtung +upWind=Amwind +downWind=Vorwind +strategies=Strategien +omniscient=Allwissender opportunistic=Opportunist -mixed=Mixed -maps=Maps -startEnd=Start-End -racecourse=Course -summary=Summary -replay=Replay -display=Display +mixed=Gemischt +maps=Karten +startEnd=Start-Ende +racecourse=Kurs +summary=Zusammenfassung +replay=Wiedergabe +display=Anzeige simulatorPolarHeader = Polardiagramm show = zeigen hide = verbergen -leftWind=Left wind -rightWind=Right wind -strategyOption1=strategyOption1 -strategyOption2=strategyOption2 -strategyOption3=strategyOption3 +leftWind=Linker Wind +rightWind=Rechter Wind +strategyOption1=Strategie Option 1 +strategyOption2=Strategie Option 2 +strategyOption3=Strategie Option 3 legLabel=Leg -raceLabel=Race -competitorLabel=Competitor +raceLabel=Rennen +competitorLabel=Wettkampfteilnehmer freemode=Winkel erhalten -whatIfCourse=What-If Course +whatIfCourse=What-If Kurs editRegatta=Regatta bearbeiten... selectSailingEvent=Bitte Event auswählen... selectCourseArea=Bitte Bahn auswählen... @@ -902,7 +903,7 @@ line=Linie offset=Ablauftonne doYouReallyWantToRemoveCompetitors=Möchten Sie wirlich die ausgewählten Teilnehmer entfernen? doYouReallyWantToRemoveCompetitor=Möchten Sie wirklicn den Teilnehmer {0} entfernen? -filterCompetitors=Filtere Teilnehmer nach Name, Bootsklasse, Segelnummer oder ID +filterCompetitors=Filtere Teilnehmer nach Name, Bootsklasse, Segelnummer, Suchhilfe oder ID editCompetitor=Teilnehmer bearbeiten pleaseEnterASailNumber=Bitte Segelnummer eingeben successfullyAllowedCompetitorReset=Aktualisierung der Daten zu Teilnehmer(n) {0} erfolgreich ermöglicht.id=ID @@ -1484,4 +1485,65 @@ fixSuccessfullyAdded=Fix erfolgreich hinzugefügt. fixPositionSuccessfullyEdited=Fixposition erfolgreich editiert. fixSuccessfullyRemoved=Fix erfolgreich bewegt. dragToChangePosition=Ziehen um die Position zu verändern\nRechtsklick für Menü mit Löschfunktion -moveFix=Position durch Ziehen ändern \ No newline at end of file +moveFix=Position durch Ziehen ändern +manage2SailEvent=Manage2Sail Veranstaltung +noEventsYet=Es sind keine Veranstaltungen vorhanden. +selectResultImportProvider=Bitte wählen Sie einen Ergebnisdienst aus... +buildVersion=Build-Version +serverInformation=Server-Information +serverConfiguration=Server-Konfiguration +standaloneServer=Eigenständiger Server +send=Abschicken... +passId=Druchlauf-Nr. +createdAt=Erstellt am +logicalTimepoint=Logischer Zeitpunkt +info=Info +windFixes=Windwerte +twoXMark=2x Tonne +pagerStateInfo={0,number,#,###}-{1,number,#,###} von {2,number,#,###} +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} von über {2,number,#,###} +init=Initialisierung... +connectionSetup=Verbindungsaufbau +connectionEstablish=Verbinde... +transferStarted=Daten werden gelesen +transferCompleted=Daten-Übertragung abgeschlossen, Import wird initialisiert +importInit=Import wird initialisiert +importWait=Warten, bis andere Datenimport-Vorgänge angeschlossen sind +importLeaderboardGroups=Ranglisten-Gruppen werden importiert +importWindTracks=Windmessungen werden importiert +updateEventLeaderboardGroupLinks=Verbindungen zwischen Veranstaltungen und Ranglisten-Gruppen werden aktualisiert +strategySimulatorTitle=Strategie-Simulator +controlSettings=Einstellungen +windDisplay=Windanzeige +windSetup=Windeinstellungen +sailingSetup=Segel-Einstellungen +chooseAWindPattern=Ein Wind-Muster auswählen +oscillations=Oszillationen +oscillationWithGusts=Oszillationen mit Böen +gusts=Böen +measured=Gemessen +baseBearing=Mittlere Richtung +raceCourseDiff=Rennkurs Abweichung +baseSpeed=Mittlere Geschwindigkeit +probability=Wahrscheinlichkeit +gustSize=Böengröße +averageSpeed=Mittlere Geschwindigkeit +speedVariance=Geschwindigkeitsvarianz +averageDirection=Mittlere Richtung +speedLeftSide=Geschwindigkeit Linke Seite +speedMiddle=Geschwindigkeit Mitte +speedRightSide=Geschwindigkeit Rechte Seite +currentSpeed=Strömungsgeschwindigkeit +currentBearing=Strömungsrichtung +percentUnit=% +pleaseSelectAValidWindPattern=Bitte ein gültiges Wind-Muster auswählen. +downwindLegsNotSupported=Vorwind Legs werden aktuell leider NICHT unterstützt. +perHours=pro Stunde +raceLeader=Führender +opportunistLeft=Opportunist links +opportunistRight=Opportunist rechts +oneTurnerLeft=1-Wenden-Kurs links +oneTurnerRight=1-Wenden-Kurs rechts +current=Strömung +simulationLegendAlgorithmTimedOutText=außerhalb des zul. Bereichs +simulationLegendMixedLegText=mehrdeutiger Wind \ No newline at end of file diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties new file mode 100644 index 00000000000..2e85c231022 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ja.properties @@ -0,0 +1,1516 @@ +manage2Sail=Manage2Sail +raceCommitteeApp=レースマネージャアプリ +tracTracEvents=TracTrac イベント +swissTimingEvents=SwissTiming イベント +trackedBefore=追跡イベントの履歴 +general=一般 +listRaces=レース一覧 +listRegattas=レガッタ一覧 +wind=風 +maneuverType=マニューバー +windPanelLabel=これは風パネルであり、今のところ完全に空となっています。 +refresh=リフレッシュ +remove=削除 +windSource=風源 +dampeningInterval=制動間隔 +map=マップ +race=レース +races=レース +tracked=追跡済 +linkedRaces=リンクされているレース +time=計時 +timeTooltip=このレグを帆走した競技者を計時します。第 1 レグについては、競技者がスタートラインを通過したときにカウントされます。 +playSpeed=再生速度 +playSpeedHelp=通常の再生速度は "1"、2 倍速の再生は "2" と、以下同様に指定 +playModeLive=ライブ +playModeReplay=再生モード +timeDelay=遅延 +timeDelayHelp=任意の時間に再生する場合は空白のままにします。ライブデータフィード前の遅延を秒数で入力してください (例: "60" で 1 分間、"0" で遅延なし) +speedInKnots=速力 (kt) +fromDeg=開始 (度) +noRacesYet=レースがまだありません。 +total=Σ +carry=累積 +competitor=競技者 +competitors=競技者 +competitorsSelected={0}/{1} の競技者が選択されました。 +noSuchLeaderboard=そのようなリーダーボードはありません +leaderboardConfiguration=リーダーボード設定 +newDotDotDot=新規... +editScores=ポイント編集... +columnNamesInSelectedLeaderboard=選択されたリーダーボード上のレース名 +addDotDotDot=追加... +renameDotDotDot=名称変更... +trackedRaceConnectedToSelectedRaceName=選択されたレース名に結び付けられた追跡レース +linkToColumn=列にリンク +unlink=リンク解除 +leaderboardName=リーダーボード名 +cancel=取消 +pleaseEnterAName=名称を入力してください +pleaseEnterABoatClass=艇種を入力してください +discardRacesFromHowManyStartedRacesOn=レース除外 (スタート済レースから) +leaderboardWithThisNameAlreadyExists=この名称のリーダーボードは登録されています +discardThresholdsMustBeNumeric=破棄しきい値は数値でなければなりません +discardThresholdsMustBeAscending=破棄しきい値は昇順でなければなりません +discarding=破棄 +startingFromNumberOfRaces=レース数から開始 +renameLeaderboard=リーダーボード名称変更 +addColumnToLeaderboard=リーダーボードに列を追加 +pleaseEnterNameForNewRaceColumn=新規レース列の名称を入力してください +ok=OK +medalRace=メダルレース +renameRace=レース名称変更 +openSelectedLeaderboard=選択したリーダーボードを開く +rank=ランク +rankTooltip=レースにおける競技者のランク +regattaRank=レガッタランク +regattaRankTooltip=レガッタにおける競技者のランク +overallRank=全体ランク +overallRankTooltip=シリーズにおける競技者のランク +totalRegattaRank=レガッタランク +collapse=- +expand=+ +leg=L +distanceInMeters=距離 +averageSpeedInKnots=∅ 速力 +averageSpeedInKnotsTooltip=このレグ中の平均対地速力は、\n必要とした時間で移動距離を割ったもので計算されます。 +raceAverageSpeedInKnotsTooltip=このレース中の平均対地速力は、\n必要とした時間で移動距離を割ったもので計算されます。 +rankGain=ランクゲイン +rankGainTooltip=現在および前のログからのポジションの変化です。 +sapSailingAnalytics=Sailing Analytics +leaderboard=リーダーボード +leaderboards=リーダーボード +leaderboardSettings=リーダーボード設定 +settings=設定 +selectAtLeastOneLegDetail=レグ詳細を少なくとも 1 つ選択 +currentSpeedOverGroundInKnots=SOG +currentSpeedOverGroundInKnotsTooltip=現在の対地速力です。 +estimatedTimeToNextWaypointInSeconds=ETA +estimatedTimeToNextWaypointInSecondsTooltip=真風向を考慮して、競技者の現在の VMG (有効速度) に基づいて競技者が次の変針点に到達するために必要な推定時間です。 +gapToLeaderInSeconds=首位艇までの距離差 +gapToLeaderInSecondsTooltip=最も遠くの前方にいる艇の現在位置での (計算) 到着時刻を、\n現在レグの平均 VMG および後続レグに対するレース首位艇と同等の性能に基づいて推定します。\nこの距離差は、最も遠くの前方にいる艇の現在位置に到着が予測される時点においてレース首位艇と同じランクとなるために\n競技者が現在位置する場所までどれだけ以前にいなければならなかったかを示します。 +velocityMadeGoodInKnots=VMG +velocityMadeGoodInKnotsTooltip=アップウィンド/ダウンウィンドレグに対する風上/風下速度である VMG、またはリーチングレグに対するアロングトラック速度です。 +windwardDistanceToGoInMeters=ウィンワード残り距離 +windwardDistanceToGoInMetersTooltip=アップウィンド/ダウンウィンドレグに対する競技者の現在位置からレグの最後までのウィンワード/リーワード距離、またはリーチングレグのアロングトラック距離です。 +windwardDistanceToCompetitorFarthestAheadInMeters=距離差 +windwardDistanceToCompetitorFarthestAheadInMetersTooltip=アップウィンド/ダウンウィンドレグに対するレース内で最も遠くの前方にいる競技者までの競技者のウィンワード/リーワード距離、またはリーチングレグに対するアロングトラック距離です。\n最も遠くの前方にいる競技者は、ハンディキャップランキングが実施されている場合には必ずしもレースの首位艇ではないことに注意してください。 +name=名称 +leaveEmptyForDefault=デフォルトでは空のままにします +trackNewEvent=新規イベント追跡 +trackableRaces=追跡可能レース +trackWind=風の追跡 +regattaName=レガッタ名 +eventName=イベント名 +event=イベント +startTime=開始時間 +startOfTracking=追跡の開始 +endOfTracking=追跡の終了 +regatta=レガッタ +regattas=レガッタ +startTracking=追跡開始 +stopTracking=追跡停止 +connections=接続 +historyOfConnections=接続の履歴 +defineNewConnection=新規接続の定義 +trackedRaces=追跡レース +canSendRequests=要求を送信可能 +connectAndReadRaces=接続してレース読込 +ports=ポート +liveData=ライブデータ +storedData=格納データ +uris=URI +jsonUrl=JSON URL +livePort=ポートライブデータ +storedPort=ポート格納データ +liveUri=ライブ URI +storedUri=格納 URL +eventsConnectedTo=現在追跡中のイベント +hostname=ホスト名 +declinationCheckbox=偏角別の適切な風方位 +raceStartTrackingColumn=追跡開始 +pauseAutomaticRefresh=自動リフレッシュを中断 +autoRefresh=自動リフレッシュ +delayInSeconds=遅延 +delayBetweenAutoAdvances=自動更新間の遅延秒数: +delayMustBeNonNegative=遅延はゼロより大きい必要 +chooseUpdateIntervalOfAtLeastOneSecond=1 秒以上の更新間隔を選択 +errorSettingWindForRace=レースの風の設定でエラー発生 +errorFetchingWindInformationForRace=レースの風情報の取込でエラー発生 +errorWhileTryingToSetWindSourceForRace="アップウィンドの風で開始" フラグを設定しようとしてエラーが発生 +inEvent=イベント: +to=to +toSide=to +showEstimatedWind=推定された風の表示 +timing=タイミング +raceDetailsToShow=レース詳細 +overallDetailsToShow=全体詳細 +legDetailsToShow=レグ詳細 +columnMoveUp=上 +columnMoveDown=下 +port=ポート +raceStartTimeColumn=レーススタート済 +showOnlySelectedCompetitors=選択した競技者のみ表示 +showSelectedCompetitorsInfo=選択した競技者の情報ボックスを表示 +tailLength=テール長 +selectedRaces=どのレースを表示しますか +lengthInSeconds=長さ +stressTest=ストレステスト +numberOfManeuvers=マニューバー +numberOfManeuversTooltip=このレグで帆走したマニューバーの数です。 +numberOfManeuversInRaceTooltip=このレースで帆走したマニューバーの数です。 +tacks=タック +jibes=ジャイブ +penaltyCircles=ペナルティーサークル +medalRaceIsNull=メダルレース値は不可 +configuration=設定 +maneuverTypes=マニューバー +chooseChart=チャートの選択 +distanceTraveled=移動した距離 +distanceTraveledTooltip=レグ始点からレグ終点まで、またはレグが\nフィニッシュしていない場合は現時点までに移動した距離です。 +raceDistanceTraveledTooltip=レース開始からレース終了まで、またはレースが\nフィニッシュしていない場合は現時点までに移動した距離です。 +velocityMadeGoodLong=VMG (有効速度) +speedOverGroundLong=対地速力 +gapToLeaderLong=首位艇までの距離差 +pointsToLoad=ロードするポイント +speed=速力 +distance=距離 +in=in +secondsUnit=s +secondsPerNauticalMileUnit=s/NM +metersUnit=m +degreesUnit=° +close=閉じる +compareCompetitors=競技者比較 +description=内容説明 +sailNumber=セールナンバー +country=国 +no3LetterCodes=IOC 3 文字コードを検索できませんせした。 +add=追加 +delete=削除 +showCharts=チャート表示 +raceWithThisNameAlreadyExists=その名称のレースはすでに登録されています。 +raceWithThisNameAlreadyExistsInRegatta=その名称のレースはこのレガッタにすでに登録されています。 +headUp=ヘッドアップ +bearAway=ベアアウェイ +tack=タック +tackTooltip=このレースで帆走したタックの数です。 +jibe=ジャイブ +jibeTooltip=このレースで帆走したジャイブの数です。 +penaltyCircle=ペナルティーサークル +penaltyCircleTooltip=このレースで帆走したペナルティー回転の数です。 +markPassing=マーク通過 +mark=マーク +otherManeuver=不明のマニューバー +douglasPeuckerPoints=Douglas peucker ポイント +filterRacesByName=レースをフィルタリングする名称 +filterMediaByName=メディアをフィルタリングする名称 +trackSettings=設定追跡 +actionEdit=編集 +actionEditCompetitors=競技者編集 +actionEditScores=スコア編集 +actionOpenBrowser=ブラウザで開く +actionRemove=削除 +actionEditSeries=シリーズ編集 +actionRaceEdit=レース編集/名称変更 +actionRaceUnlink=レースのリンク解除 +actionRaceRemove=レース削除 +actionExportXML=XML としてエクスポート +actionOpenDashboard=コーチダッシュボードを開く +filterLeaderboardsByName=リーダーボードをフィルタリングする名称 +actions=アクション +islinked=リンク済 +yes=はい +no=いいえ +hoverOverAPoint=詳細についてはポイントをホバー +noSelection=未選択 +raceIsKnownToStartUpwind=アップウィンドレグでレーススタート +events=イベント +settingsForComponent={0} の設定 +noEventsFound=イベントが見つかりませんでした +noEventSelected=イベントが選択されていません +noLeaderboardsFound=リーダーボードが見つかりませんでした +searchEvents=イベントの検索 +location=場所 +from=開始 +until=終了 +onlyLiveEvents=ライブイベントのみ +locationNotAvailable=場所がありません +startDateNotAvailable=開始日付がありません +startDate=開始日付 +endDate=終了日付 +unknownManeuver=不明のマニューバー +tailLengthMustBePositive=テール長はゼロより大きい数でなければなりません。 +failedToLoadRaceInformation=レース情報のロードに失敗: {0} +competitorPassedMarkAtDate={0} が {2} に {1} を通過 +valueForCompetitorAt={1} での {0}: {2} +reallyRemoveRace=レース ''{0}'' を削除しますか。注記: このレースのレースログもすべて永続的に削除されます。 +showDetails=詳細表示 +hideLeaderboard=リーダーボード非表示 +hideLeaderboards=リーダーボード非表示 +showLeaderboard=リーダーボード表示 +hideRaces=レース非表示 +showMap=マップ表示 +hideMap=マップ非表示 +noLeaderboardSelected=リーダーボード未選択 +leaderboardGroups=リーダーボードグループ +filterLeaderboardGroupsByName=リーダーボードグループをフィルタリングする名称 +createNewLeaderboardGroup=リーダーボードグループ登録 +leaderboardGroupConfiguration=リーダーボードグループ設定 +legs=レグ +currentLeg=オンレグ +currentLegTooltip=競技者が現在競技しているレグの番号 (1 から開始) +leaderboardGroup=リーダーボードグループ +pleaseEnterNonEmptyDescription=空でない説明を入力してください +groupWithThisNameAlreadyExists=この名称のリーダーボードグループはすでに登録されています。 +detailsOfLeaderboardGroup=リーダーボードグループの詳細 +edit=編集 +save=保存 +abort=中止 +noLeaderboardGroupWithNameFound={0} という名称のリーダーボードグループは見つかりませんでした +overview=概要 +welcomeToSailingAnalytics=SAP Sailing Analytics へようこそ +welcomeToSailingAnalyticsBody=SAP は、水上で行われている動きをセーリングファンにとってかつてないほど身近なものにします。\nこの実況リーダーボードでランキング、艇速、艇差をリアルタイムに把握してファンとしての体験を充実したものにできます。SAP を使ってすべてのレースを追いかければ、追い抜きのマニューバーを見逃すことはありません。\nこれこそ、セーリング競技を生で簡単に視聴する方法なのです。 +home=ホーム +clickAndDragToZoomIn=拡大するプロット領域をクリックしてドラッグ +windSpeed=風速 (kt) +allTimesInUTC=時刻はすべて UTC +autoZoomTo=自動拡大: +autoZoomToBoats=艇 +autoZoomToBuoys=ブイ +autoZoomToWindSensors=風センサー +autoZoomOff=オフ +autoZoomToTails=テール +autoZoomSelectedCompetitors=選択した競技者にのみズーム +bearing=方位 +zoom=ズーム +stepSizeMustBeGreaterThanNull=ステップサイズは 0 より大きい必要 +stepSizeInSeconds=ステップサイズ秒数 +refreshInterval=リフレッシュ間隔 +refreshIntervalMustBeGreaterThanXSeconds=リフレッシュ間隔は {0} 秒より長いことが必要 +competitorCharts=競技者チャート +selectAtLeastOneCompetitor=競技者チャートを表示するには、少なくとも 1 つの競技者を選択してください +selectAtLeastOneCompetitorForInvitation=招待電子メールを送信するには、少なくとも 1 つの競技者を選択してください +directionChange=方向変更 +speedChange=艇速変更 +degreesShort=度 +untracked=未追跡 +delayForLiveMode=ライブモードの遅延: +notAvailable=利用不可 +details=詳細 +noGroupSelected=グループ未選択 +combinedWindSourceTypeName=複合 +courseBasedWindSourceTypeName=コースレイアウト +trackBasedEstimationWindSourceTypeName=推定 +windSensorWindSourceTypeName=計測 +webWindSourceTypeName=手動 +raceCommitteeWindSourceTypeName=レース委員会 +clickChartToSetTime=時刻を設定するチャートをクリック +position=位置 +windSourcesUsed=使用風源 +errorTryingToUpdateWindSourcesToExclude=レース {0} を除外するために風源を更新しようとしてエラーが発生: {1} +feedback=フィードバック +startStopPlaying=再生開始/停止 +backToLive=ライブ時点に戻る +slowPlaySpeedDown=再生速度を下げるか巻き戻し +speedPlaySpeedUp=再生速度を上げるか早送り +showHideComponent=表示/非表示 {0} +degreesBoatToTheWind=風とのアングル +racesInLeaderboard=リーダーボードにあるレース +leaderboardsInGroup=リーダーボードグループ +replication=複製 +errorFetchingReplicaData=複製データ取込でエラー発生: {0} +averageAbsoluteCrossTrackErrorInMeters=∅ XTE +averageAbsoluteCrossTrackErrorInMetersTooltip=レグ始点からレグ終点までの大圏航路 (円曲線) との間の平均クロストラックエラー +averageSignedCrossTrackErrorInMeters=∅ XTE +/- +averageSignedCrossTrackErrorInMetersTooltip=コース中央航路からの距離 (+/- 符号付) で表されたコース上の平均位置: 0 はコース中央航路を、マイナスの値は左側を、プラスの値は右側をそれぞれ意味します。 +raceAverageAbsoluteCrossTrackErrorInMetersTooltip=このレースのすべてのアップウィンドレグに対するレグ始点からレグ終点までの大圏航路 (円曲線) との間の平均絶対クロストラックエラー (競技者がコース中央航路のどちら側にいるかは無視) +raceAverageSignedCrossTrackErrorInMetersTooltip=このレースのすべてのアップウィンドレグに対するレグ始点からレグ終点までの大圏航路 (円曲線) との間の +/- 符号付の平均クロストラックエラー: マイナスの値はコース中央航路の左側、プラスの値は右側を意味します。 +enterMaster=マスタインスタンスのホスト名 +errorStartingReplication=ホスト {0} / エクスチェンジ {1} からの複製開始でエラー発生: {2} +helpLines=ヘルプライン +startLine=スタートライン +finishLine=フィニッシュライン +advantageLine=アドバンテージライン +courseMiddleLine=コース中央航路 +messagingPortNumber=メッセージングポート番号 (初期値は 0) +servletPortNumber=サーブレットポート番号 +registeredAt=登録時刻 {0} +replicatingFromMaster=マスタ {0}:{2}、メッセージングポート {3}:{1}、エクスチェンジ {4} から複製 +showWindSpeedSeries=風速データ表示 +showWindDirectionSeries=風向データ表示 +fleet=フリート +boatClass=艇種 +setDelayToLive=ライブに遅延を設定 +pleaseEnterNonEmptyVenue=開催地を入力してください。 +pleaseEnterStartAndEndDate=開始日付と終了日付を入力してください。 +startDateMustBeforeEndDate=開始日付は終了日付より前でなければなりません。 +eventWithThisNameAlreadyExists=この名称のイベントはすでに登録されています。 +venue=開催地 +pleaseSelectABoatClass=艇種を入力してください。 +regattaWithThisNameAlreadyExists=この名称のレガッタはすでに登録されています。 +seriesWithThisNameAlreadyExists=この名称のシリーズはすでに登録されています。 +fleetWithThisNameAlreadyExists=この名称のフリートはすでに登録されています。 +fleets=フリート +series=シリーズ +splitFleet=フリート分割 +color=色 +medalSeries=メダルシリーズ +noColor=色なし +pleaseSelectARegatta=レガッタを選択してください。 +addFleet=フリート追加 +doYouReallyWantToRemoveRegatta=レガッタ "{0}" を削除しますか。削除すると、このレガッタのためのリーダーボードもすべて削除されます。 +errorLoadingScoreCorrectionProviders=スコア修正プロバイダのロードでエラー発生: {0} +selectResultListToImportFrom=インポート元の結果一覧を選択 +of=of +errorObtainingScoreCorrections=プロバイダ {0} からのスコア修正および艇種 {2} のイベント {1} (公表日付 {3}) の取得でエラー発生: {4} +assignRaceNumbersToRaceColumns=公式結果のレース番号をリーダーボードのレース列に割り当ててください +importOfficialResults=公式結果をインポートしています... +selectAll=全選択 +apply=適用 +successfullyUpdatedScores=スコアが正常に更新されました +errorUpdatingScoresForLeaderboard=リーダーボード {0} のスコアを更新しようとしてエラーが発生: {1} +maneuverTypesToShowWhenCompetitorIsClicked=競技者をクリックしたときに表示するマニューバータイプ +simulateAsLiveRace=ライブレースとしてシミュレート +simulateWithOffset=スタート前のオフセット (分): +boatClassDoesNotMatchSelectedRegatta=選択したレースに、選択したレガッタの艇種 ''{0}'' と同じでない艇種が含まれています。レースはロードされません。 +regattaExistForSelectedBoatClass=選択した艇種に対して少なくとも 1 つのレガッタがあります。このレースに対してレガッタを初期登録しますか。 +reload=リロード +addRegatta=レガッタ追加... +importRegattas=レガッタのインポート... +exchangeName=エクスチェンジ名 +course=コース +courseLayout=コースレイアウト +buoy=ブイ +gate=ゲート +buoys=ブイ +waypoint=変針点 +showAllCompetitors=全競技者表示 +boatTails=ボートテール +startFirstMarkTriangle=最初のマークトライアングルへのスタートライン +resultImportUrls=結果インポート URL +errorRefreshingResultImportUrlList=結果インポート URL 一覧をリフレッシュしようとしてエラーが発生: {0} +successfullyUpdatedResultImportUrls=結果インポート URL 一覧を正常に更新 +errorRemovingResultImportUrls=結果インポート URL を削除しようとしてエラーが発生: {0} +pleaseEnterNonEmptyUrl=URL を入力してください。 +addResultImportUrl=結果インポート URL の追加 +errorAddingResultImportUrl=結果インポート URL を追加しようとしてエラーが発生: {0} +remoteServerInstances=リモートサーバインスタンス +localServer=ローカルサーバ +errorRefreshingSailingServers=SailingServer インスタンスをリフレッシュしようとしてエラーが発生: {0} +successfullyUpdatedSailingServers=SailingServer インスタンスを正常に更新 +errorRemovingSailingServers=SailingServer インスタンスを削除しようとしてエラーが発生: {0} +addSailingServer=SailingServer インスタンスの追加 +errorAddingSailingServer=SailingServer インスタンスを追加しようとしてエラーが発生: {0} +errorTryingToObtainLeaderboardContents=リーダーボード内容を取得しようとしてエラーが発生: {0} +lastScoreUpdate=最新スコア更新 +warningBrowserUnsupported=この Web サイトはご使用のブラウザには最適化されていません。Firefox や Chrome などの新しいブラウザに切り替えてください。 +newMark=新規マーク +controlPoint=マーク/ゲート +errorUpdatingRaceCourse=レースコースを更新しようとしてエラーが発生: {0} +successfullyUpdatedCourse=レースコースを正常に更新 +selectTwoMarksForGate=ゲートを登録するために 2 つのマークを選択 +errorTryingToObtainTheMarksOfTheRace=レースコースのマークを取得しようとしてエラーが発生: {0} +errorTryingToObtainRaceCourse=レースコースを取得しようとしてエラーが発生: {0} +insertWaypointBeforeSelected=選択前に変針点を挿入 +insertWaypointAfterSelected=選択後に変針点を挿入 +addGate=ゲート追加 +scoringSchemeLowPointSystem=低得点方式 +scoringSchemeHighPointSystem=高得点方式 +scoringSchemeHighPointEssOverall=高得点方式 ESS 全体 (10) +scoringSchemeHighPointEssOverall12=高得点方式 ESS 全体 (12) +scoringSchemeHighPointEssOverall12Description=得点数によるスコアです。アクトの勝者が 12 点、2 位が 11 点などと得点します。Extreme Sailing シリーズ全体で同点となった場合は、より多くのアクトで勝者となった競技者を上位とします。それでも同じ場合には、最終アクトの結果を用います。 +scoringSchemeHighPointLastBreaksTie=高得点方式、同点の場合は最終レースで順位決定 +scoringSchemeLowPointWinnerGetsZero=低得点方式、1 位が 0 点 +scoringSchemeWinnerGetsFive=高得点方式、1 位が 5 点 +scoringSchemeWinnerGetsSix=高得点方式、1 位が 6 点 +scoringSchemeWinnerGetsEight=高得点方式、1 位が 8 点 +scoringSchemeWinnerGetsEightAndInterpolation=高得点方式、1 位が 8 点、補間 +scoringSchemeLowPointWithEliminationsAndRoundsWinnerGets07=低得点方式、予選およびラウンドあり、1 位が 0.7 点 +scoringSchemeLowPointWithEliminationsAndRoundsWinnerGets07Description=ラウンドから構成される予選を用いた低得点方式のバリエーションです。\n各ラウンドがレガッタ内のシリーズとして形成されることになり、フリート順序は\n最初のラウンドで最も大きい数から開始してカウントダウンします。\n決勝や最下位決定戦が行われる場合は、その順序として 1 および 2 を使用します。準決勝ラウンドは 3 から始めます。 +scoringSchemeLowPointForLeagueOverallLeaderboard=低得点方式、アクト総得点、次に最終アクトによって順位を決定 +scoringSchemeLowPointForLeagueOverallLeaderboardDescription=順位決定のための低得点方式に使用される通常のカウントダウン方式ではなく、総得点が同点の競技者が全アクト共通で得点した総得点で比較されます。これでも同順位となる場合は、最終アクトによって順位を決定します。 +scoringSchemeHighPointFirstGetsOne=高得点方式、1 位が 1 点 +scoringSchemeHighPointFirstGetsTen=高得点方式、1 位が 10 点 +scoringSchemeHighPointFirstGetsTenOrEight=高得点方式、1 位が 10 点 (または 8 点) +scoringSchemeHighPointFirstGetsTenOrEightDescription=得点数によるスコアです。1 位が 10 点、2 位が 9 点などと得点します。同点となった場合は、より多くのレースで勝者となった競技者を上位とします。それでも同順位となる場合には、最終レースでの得点でのランクで順位を決定します。 +scoringSchemeHighPointFirstGetsTwelveOrEight=高得点方式、1 位が 12 点 (または 8 点) +scoringSchemeHighPointFirstGetsTwelveOrEightDescription=得点数によるスコアです。1 位が 12 点、2 位が 11 点などと得点します。同点となった場合は、より多くのレースで勝者となった競技者を上位とします。それでも同順位となる場合には、最終レースでの得点でのランクで順位を決定します。 +scoringSystem=得点方式 +createFlexibleLeaderboard=フレキシブルリーダーボード登録 +createRegattaLeaderboard=レガッタリーダーボード登録 +createDefaultRegattaLeaderboard=レガッタリーダーボード初期登録 +editFlexibleLeaderboard=フレキシブルリーダーボード編集 +editRegattaLeaderboard=レガッタリーダーボード編集 +defaultRegatta=レガッタ初期値 +useOverallLeaderboard=全体リーダーボード使用 +overallStandings=全体順位表 +type=タイプ +actionAddRaces=レース追加 +racesWithNamePrefix=レース - 名称接頭辞: +maximumSpeedOverGroundInKnots=最大 SOG +maximumSpeedOverGroundInKnotsTooltip=このレガッタで計測されたこの競技者の最大対地速力です。 +totalTimeSailedDownwindInSeconds=ダウンウィンド合計時間 +totalTimeSailedDownwindInSecondsTooltip=レガッタ内で競技者がダウンウィンドで帆走した合計時間です。 +totalTimeSailedUpwindInSeconds=アップウィンド合計時間 +totalTimeSailedUpwindInSecondsTooltip=レガッタ内で競技者がアップウィンドで帆走した合計時間です。 +totalTimeSailedReachingInSeconds=リーチング合計時間 +totalTimeSailedReachingInSecondsTooltip=レガッタ内で競技者がリーチングレグで帆走送した合計時間です。 +totalTimeSailedInSeconds=合計時間 +totalTimeSailedInSecondsTooltip=レーススタート時刻から計時した、レガッタ内で競技者が帆走した合計時間です。 +totalDistanceTraveled=合計距離 +totalDistanceTraveledTooltip=このレガッタで帆走した合計距離です。 +totalAverageSpeedOverGround=∅ SOG +totalAverageSpeedOverGroundTooltip=このレガッタ全体の平均対地速力です。 +hhmmssUnit=hh:mm:ss +actionAddWindData=風データ追加 +valueMustBeBetweenMinMax=''{0}'' の値は {1} と {2} の間になければなりません。 +valueMustBeGreaterThan=''{0}'' の値は {1} より大きくなければなりません。 +optional=オプション +pleaseEnterAValue=値を入力してください。 +latitude=緯度 +longitude=経度 +actionAddEvent=イベント追加 +publicationUrl=公開 URL +suppress=非表示 +isPublic=公開 +unsuppress=非表示解除 +suppressedCompetitors=非表示競技者 +mediaPanel=音声と動画 +url=URL +mimeType=MIME タイプ +title=表題 +duration=時間 (h:mm:ss.ms) +addMediaTrack=音声/動画追加 +addYoutubeTrack=YouTube 追加 +youtubeId=YouTube ID +reallyRemoveMediaTrack=メディアトラック ''{0}'' を削除しますか +pleaseSelectASeries=シリーズを選択してください +pleaseSelectARace=レースを選択してください +displayGroupsInReverseOrder=逆順序でグループを表示 +actionConfigureUrl=URL 設定 +configureLeaderboardUrlDescription=リーダーボードを設定し、リンクから結果 URL を取得 +embedded=埋込 +additionalUrlSettings=追加 URL 設定 +restrictToSingleSelectedRace=単一の選択レースに制限 +expandSelectedRace=選択レースを拡大 +showRaceDetails=レース詳細を許容 +numberOfRaces=レース数 +numberOfRacesMustBeNonNegativeNumber=表示する最終レース数を入力してください +chooseTheWayYouSelectRaces=レース選択方法の選択 +selectFromAllRaces=全レースから選択 +selectANumberOfRaces=レースの数を選択 +numberOfLastNRaces=表示する最新レース数 +expandLastRace=表示されている最後のレースを常に拡大 +factor=係数 +errorUpdatingIsMedalRace=メダルレース設定の更新でエラー発生: {0} +maneuverLoss=マニューバーロス +averageManeuverLossInMeters=∅ マニューバーロス +averageManeuverLossInMetersTooltip=マニューバー中の平均距離ロスです。 +averageTackLossInMeters=∅ タックロス +averageTackLossInMetersTooltip=タック中の平均距離ロスです。 +averageJibeLossInMeters=∅ ジャイブロス +averageJibeLossInMetersTooltip=ジャイブ中の平均距離ロスです。 +shape=シェイプ +pattern=パターン +legend=凡例 +live=ライブ +swissTimingArchiveConnector=SwissTiming アーカイブ済イベント +errorFetchingChartData=チャートデータの取込でエラー発生: {0} +loadingCompetitorData=競技者データをロードしています... +competitorRegattaDataAfterRaceN=レース {1} 後の {0} +rankChart=ランキング +totalPoints=総得点 +raceTotalPointsTooltip=当該レースの競技者総得点 +regattaTotalPointsTooltip=当該レガッタの競技者総得点 +showRankChart=ランクチャート表示 +overallLeaderboardSelection=全体リーダーボード +errorTryingToObtainOverallLeaderboards=全体リーダーボードを取得しようとしてエラーが発生: {0} +finished=フィニッシュ済 +upcoming=予定 +next=次 +inProgress=進行中 +passedTo={0} に受け渡し済 +gapChangeSinceLegStartInSeconds=距離差の変化 +gapChangeSinceLegStartInSecondsTooltip=最後のレグがスタートしてからの首位艇との距離差の変化です。\nマイナスの数字は距離差が減少したことを意味します。\nプラスの数字は距離差が増加したことを意味します。 +sideToWhichMarkAtLegStartWasRounded=マーク回航 +sideToWhichMarkAtLegStartWasRoundedTooltip=レグ始点でマークを回航する側です。 +raceIsLive=レース {0} はライブです +racesAreLive=レース {0} はライブです +regattaIsLive=レガッタ {0} はライブです +regattasAreLive=レガッタ {0} はライブです +knotsUnit=kt +knotsValue={0,number,#0.0} kt +knotsRange={0,number,#0.0} - {1,number,#0.0} kt +generatePolarSheet=ポーラーシート生成 +polars=ポーラーダイアグラム +polarSheetChart=ポーラーシート +polarSheetMinimumDataSizePerGraph=グラフ別の最小データポイント数 +polarSheetMinimumDataSizePerGraphTooltip=基礎となるデータポイントが少ないグラフはすべては表示されません。\nこのようにして、十分なデータ量を欠いている\n風速で注意を逸らすダイアグラムは表示されません。 +polarSheetMinimumDataSizePerAngle=アングル別の最小データポイント数 +polarSheetMinimumDataSizePerAngleTooltip=各グラフには、定められた数のデータポイントが少なくとも含まれている\nアングルに関するデータのみが表示されます。ダイアグラムの\n重要でない部分は、このしきい値を使用して非表示にすることができます。 +polarSheetMinimumWindConfidence=最小風信頼値 +polarSheetMinimumWindConfidenceTooltip=指定されたしきい値について信頼のある、風のフィックスのみが\n使用されます。この信頼は 0 から 1 までの範囲内です。 +polarSheetMinimumConfidenceMeasure=最小信頼測度 +polarSheetMinimumConfidenceMeasureTooltip=グラフに表示されたポイントの最小信頼測度です。\n指定されたしきい値より小さい場合には、ポイントは表示されません。\nこの測度は 0 から 1 までの範囲内で、\n統計的尺度を使用して決定されます。 +polarSheetUseOnlyWindGaugeData=風速に風速計データのみ使用 +polarSheetUseOnlyWindGaugeDataTooltip=風速計の風速データのみを考慮することにより、\nポーラーダイアグラムを誤らせる可能性のある、推量により手動で入力された風速が除外されます。 +polarSheetUseOnlyEstimationData=風向に推定値のみ使用 +polarSheetUseOnlyEstimationDataTooltip=風向に風の正確な推定値を使用することにより、\n無効な風速計データによって回転された\nポーラーダイアグラムを修正します。 +polarSheetRemoveOutliers=異常値の削除 (距離ベースのアプローチ) +polarSheetRemoveOutliersTooltip=異常値は、距離ベースのアプローチによって削除されます。\nパラメータを次のとおり調整することができます。 +polarSheetOutlierDetectionRadius=異常値検出近傍半径 +polarSheetOutlierDetectionRadiusTooltip=異常値検出アルゴリズムに基づく\n距離で使用される半径です。 +polarSheetOutlierDetectionMinimumPerc=最小異常値検出近傍率 +polarSheetOutlierDetectionMinimumPercTooltip=近傍データポイント/全体データポイントの比率は\n異常値と考慮されるには当該ポイントに対して指定された値よりも小さい必要があります。 +polarSheetNumberOfHistogramColumns=風範囲別のヒストグラム列の数 +polarSheetWindSteppingInKnots=ウィンドステッピング (kt) +polarSheetWindSteppingMaxDistance=風レベルへの最大距離 +polarSheetWindSteppingMaxDistanceTooltip=フィックスが風レベルに割り当てられる対象の最大半径を設定します。 +polarSheetSplitByWindGauges=風源の分割 +polarSheetSplitByWindGaugesTooltip=風源の分割により、不良な風速計に対してより良い検索を行うことが可能です。\n統計的な正確さは保証できません。同一フィックスを\n結果として得られるポーラーシートに複数回カウントすることができます (異なる風速計に対して)。 +applyMinimumWindConfidence=最小風信頼度の適用 +applyMinimumWindConfidenceTooltip=選択すると、上にあるボックスの値が、生成時に最小有効風信頼度として使用されます。 +windRangeDistribution=風範囲の分布 +pleaseSeeToolTips=ツールチップが利用できる場所でそれを表示するには、オプションの上をホバーしてください。 +competitorColumnTooltip=この競技者の乗員の名前です。 +sailIdColumnTooltip=競技者の国の国旗と競技者のセールナンバーを表示します。 +rankColumnTooltip=レガッタにおけるこの競技者の全体ランクです。 +carryColumnTooltip=以前のレースから持ち越された得点です。 +totalsColumnTooltip=レガッタにおける競技者のすべての得点の合計です。\nソートは得点ではなくランクに基づきます。 +windData=風データ +gpsData=GPS データ +status=ステータス +noDataFound=データが見つかりませんでした +displayName=名称表示 +histogram=ヒストグラム +numberOfDataPoints=データポイント数 +angleAndTotalNumberOfDataPointsAndCovAndCm=アングル: {0}、データポイントの合計数: {1}、変動係数: {2}、信頼基準: {3} +buoyZone=ブイゾーン +radiusInMeters=半径 (m) +filterRegattasByName=レガッタをフィルタリングする名称 +noRegattasYet=レガッタがまだありません。 +addCourseArea=コース領域追加 +courseAreas=コース領域 +courseArea=コース領域 +pleaseEnterNonEmptyCourseArea=コース領域を入力してください。 +pleaseSelectACourseArea=コース領域を選択してください。 +lastUpperFlag=上側の信号旗 +lastLowerFlag=下側の信号旗 +cannotAddRacesToRegattaLeaderboardButOnlyToRegatta=これはレガッタのリーダーボードです。レガッタ定義でこのリーダーボードに直接に追加できるのはレースのみです。 +correctScore=スコア修正 +correctScoreFor=レース {1} での競技者 {0} のスコアを修正 +penaltyOrRedress=ペナルティー/救済 +netScore=正味得点 +timePointMustBeAfterStartOfTracking=時点は追跡の開始後でなければなりません。 +timePointMustBeBeforeEndOfTracking=時点は追跡の終了前でなければなりません。 +regattaUsedForTheTrackedRace=追跡レースに使用されるレガッタ +tracTracUpdateUrl=TracTrac サーバ更新 URI +passingInstructions=通過指示書 +refreshNow=今すぐリフレッシュ +stopUpdating=更新停止 +startUpdating=更新開始 +currentTime=現在時刻: +tractracUsername=TracTrac ユーザ名 +tractracPassword=TracTrac パスワード +operatorEquals=等しい +operatorNotEqualTo=等しくない +operatorLessThan=未満 +operatorLessThanEquals=以下 +operatorGreaterThan=超過 +operatorGreaterThanEquals=以上 +operatorContains=包含 +operatorNotContains=非包含 +operatorStartsWith=開始: +operatorEndsWith=終了: +nationality=国籍 +numberMustBePositive=この数はゼロより大きい必要 +distanceToLineAtRaceStart=スタート時のラインまでの距離 +distanceToLineFiveSecondsBeforeStart=スタート 5 秒前のラインまでの距離 +speedOverGroundAtRaceStart=スタート時の SOG +speedOverGroundFiveSecondsBeforeStart=スタート 5 秒前の SOG +speedOverGroundWhenPassingStart=スタート時の SOG +distanceToLineAtRaceStartTooltip=レーススタート時刻のスタートラインまでの距離 +distanceToLineFiveSecondsBeforeStartTooltip=スタート 5 秒前のスタートラインまでの距離 +speedOverGroundAtRaceStartTooltip=レーススタート時刻の対地速力 +speedOverGroundFiveSecondsBeforeStartTooltip=スタート 5 秒前の対地速力 +speedOverGroundWhenPassingStartTooltip=競技者がスタートを通過したときの対地速力 +startTack=タック開始 +startTackTooltip=競技者が開始したタック +portTack=左舷 +starboardTack=右舷 +portSide=左舷 +starboardSide=右舷 +pleaseEnterANumber=番号を入力してください。 +competitorsFilter=競技者フィルタ +flagStatus=信号旗ステータス +additionalInformation=情報 +windFixListingDescription=風フィックス (当初 3、最終 3、真値) +noWindFixesAvailable=風フィックスが利用可能でありません。 +distanceToStarboardEndOfStartlineWhenPassingStart=スタート時のラインの右舷側距離 +distanceToStarboardEndOfStartlineWhenPassingStartTooltip=スタートラインの右舷側までの距離、または競技者がスタートを通過したときの単一スタートマークまでの距離 +actionAddFilter=フィルタ追加 +actionEditFilter=フィルタ編集 +availableFilters=利用可能フィルタ +createFilterHint=競技者の一覧をフィルタリングするフィルタセットを登録してください。 +filterThisNameAlreadyExists=この名称のフィルタはすでに登録されています。 +addAtLeastOneFilterCriteria=フィルタ基準を少なくとも 1 つ選択してください。 +filterCriteria=フィルタ基準 +selectAFilterCriteria=フィルタ基準の選択 +filterName=フィルタ名 +operator=演算子 +value=値 +nationalityMustBeISOorIOCcode=国籍は 2 文字の ISO コードまたは 3 文字の IOC コードでなければなりません。 +raceRank=レースランク +waypoints=変針点 +disableRaceFilter=フィルタ無効化 +enableRaceFilter=フィルタ有効化 +raceStatusColumn=レースステータス +loading=ロードしています (スピードの遅いインターネット接続では時間がかかる場合があります)... +racesWithHiddenState=非表示ステータスのレース +mediaDateFormatError=日付書式が無効です。使用: {0} +selectedCompetitors=選択された競技者 +filterNothing=フィルタなし +topNCompetitorsByRaceRank=レース上位 {0} +topNCompetitorsByTotalRank=合計上位 {0} +simulator=シミュレータ +optionsBar=オプションバー +simulateButton=予測 +setup=設定 +strength=風力 (kn) +oscillation=振動 +oscillating=振動中 +amplitude=振幅 (勾配) +frequency=周期 (1/h) +noise=雑音 +sailing=帆走 +optimality=最適性 +maximumTurns=最大ターン +raceDirection=レース方向 +upWind=アップウィンド +downWind=ダウンウィンド +strategies=戦略 +omniscient=全知 +opportunistic=オポチュニスト +mixed=混合 +maps=マップ +startEnd=開始/終了 +racecourse=コース +summary=サマリー +replay=リプレイ +display=表示 +simulatorPolarHeader=ポーラーダイアグラム +show=表示 +hide=非表示 +leftWind=レフトウィンド +rightWind=ライトウィンド +strategyOption1=strategyOption1 +strategyOption2=strategyOption2 +strategyOption3=strategyOption3 +legLabel=レグ +raceLabel=レース +competitorLabel=競技者 +freemode=方位を維持 +whatIfCourse=What-If コース +editRegatta=レガッタ編集... +selectSailingEvent=帆走イベントを選択してください... +selectCourseArea=コース領域を選択してください.. +courseAreaOverview=領域概要 +courseDesignOverview=コースデザイン概要 +regattaOverview=レガッタ概要 +additionalSettings=追加設定 +showFollowingCourseAreas=以下のコース領域を表示 +showFollowingRegattas=以下のレガッタを表示 +showOnlyRacesOfSameDay=同一のレースのみ表示 +showOnlyCurrentlyRunningRaces=現在競技中のレースのみ表示 +seriesDefinesResultDiscardingRule=シリーズにより結果取消ルールを定義 +addSeries=シリーズ追加 +regattaDefinesResultDiscardingRules=レガッタにより結果取消ルールを定義 +startsWithZeroScore=セロスコアでシリーズ開始 +masterDataImportPanel=マスタデータインポート +importSuccess=成功\n{0} リーダーボードグループ、{1} リーダーボード、{2} イベント、{3} レガッタを登録しました。 +importSuccessOverwriteInfo=以下のレガッタを上書きするには、以下のレースを再追跡する必要があります: +regattaOverviewConfiguration=レガッタ概要設定 +firstRaceIsNonDiscardableCarryForward=取消不可の持ち越しがあるスタート +addReplicationMaster=複製マスタサーバに接続 +connect=接続 +connectToMaster=マスタに接続 +exchangeHost=メッセージングホスト名 +explainExchangeHostName=メッセージキューを実行中のホスト名です。 +explainReplicationHostname=複製マスタを実行中のホスト名です。 +explainReplicationExchangeName=エクスチェンジキューの名称です。マスタで設定されたものと同じでなければなりません。 +explainReplicationExchangePort=メッセージングエクスチェンジキューのポートです。これは初期ロード後の全データに使用されます。 +explainReplicationServletPort=この複製登録し、初期ロードを受け取るための接続に使用されるサーブレットポート番号です。 +explainReplicasRegistered=登録された複製 (このサーバがマスタ): +explainConnectionsToMaster=マスタへの接続 (このサーバが複製): +explainNoConnectionsToMaster=マスタサーバへの接続が設定されていません。マスタへの接続ボタンをクリックして設定してください。 +explainNoConnectionsFromReplicas=複製からの接続が見つかりませんでした。複製の接続はその管理コンソールを使用して行うことができます。 +stopConnectionToMaster=複製の停止 +asLink=リンクとして +noStarttimeAnnouncedYet=保留 +raceIsRunning=実行中 +raceIsFinishing=フィニッシュ中 +raceIsFinished=フィニッシュ済 +raceIsScheduled=スケジュール済 +raceIsInStartphase=スタートフェーズ +generalRecall=リコール生成 +startPostponed=延期 +raceAbandoned=中止 +stopAllReplicas=すべての複製への接続をドロップ (危険です) +warningServerIsReplica=注意: このサーバは複製となるよう設定されています。何も変更しないでください。そうしないと、予想しない動作がもたらされます。 +dropReplicaConnection=接続のドロップ +timePanelName=時間管理 +competitorFilterExplanation=このフィルタは基準に一致するすべての競技者を表示します: +protestTime=抗議時間 +protestTimeFinishesAt=終了予定 +protestTimeFinishedAt=終了 +flags=信号旗 +lastUpdate=最終更新 +noMoreRacingToday=本日はもうレースはありません +furtherSignalsAshore=陸上の大会本部にさらに信号旗を掲揚 +earlyStarters=フライング艇 +showingDetailsOfRace=レース {0} の詳細表示 +startAt=スタート時刻 +finishAt=フィニッシュ時刻 +protestEndsAt=抗議最終時刻 +unknown=不明 +raceLog=レースログ +regattaLog=レガッタログ +refreshRaceLog=レースログのリフレッシュ +raceLogReloaded=レースログがデータベースから正常にリロード済 +reloadAllRaceLogs=全レースログのリロード +export=エクスポート +sideline=サイドライン +classAndGolfUp=信号旗の掲揚 - スタートまで 8 分 - スタート手順はゲートスタート +classDisplayed=信号旗の掲揚 - スタートまで 5 分 +papaDisplayed=スタート前の信号旗 - スタートまで 4 分 +blackDisplayed=スタート前の信号旗 - フライング艇の失格 - スタートまで 4 分 +zuluIndiaDisplayed=スタート前の信号旗 - フライング艇のペナルティー - スタートまで 4 分 +papaRemoved=スタート前の信号旗 - スタートまで 1 分 +blackRemoved=スタート前の信号旗 - フライング艇の失格 - スタートまで 1 分 +zuluIndiaRemoved=スタート前の信号旗 - フライング艇のペナルティーありのスタート - スタートまで 1 分 +classFlagRemoved=信号旗の降下 - レースのスタート信号 +xray=個別のリコール - フライング艇あり +firstSubstitute=ゼネラルリコール - フライング艇が多すぎる - スタート手順のやり直し +blueFlagDisplayed=信号旗の掲揚 - 先頭の競技者がフィニッシュラインを通過 +blueFlagRemoved=信号旗の降下 - 最終の競技者がフィニッシュラインを通過、またはフィニッシュ時間制限を超過 +golfRemoved=信号旗の降下 - ゲート閉鎖 +answeringPennantOverAlphaDisplayed=信号旗の掲揚 - スタート延期 - 本日のレースは終了 +answeringPennantOverHotelDisplayed=信号旗の掲揚 - スタート延期 - 陸上の大会本部にさらに信号旗を掲揚 +answeringPennantDisplayed=信号旗の掲揚 - スタート延期 +answeringPennantRemoved=信号旗の降下 - 1 分後にスタート手順が開始 +novemberOverAlphaDisplayed=信号旗の掲揚 - レース中止 - 本日のレースは終了 +novemberOverHotelDisplayed=信号旗の掲揚 - レース中止 - 陸上の大会本部にさらに信号旗を掲揚 +novemberDisplayed=信号旗の掲揚 - レース中止 +bravoDisplayed=抗議時間開始 +bravoRemoved=抗議時間終了 +essThreeDisplayed=スタートまで 3 分 +essTwoDisplayed=スタートまで 2 分 +essOneDisplayed=スタートまで 1 分 +essOneRemoved=レース開始信号 +dataBeforeAfter=レース前/後からのデータ +rawFixes=全フィックス (異常値を含む) +importRemoteHost=リモートホスト: +importFetchRemoteLgs=リーダーボードグループ一覧の取込 +importUrlInvalid=リーダーボードグループ取込に有効な URL でありません: {0} +importGetLeaderboardsFailed=リーダーボードグループ取得の要求がホスト {0} に対してエラー {1} で失敗しました +importNoDataReturned=データがサーバから戻されませんでした +importServerError=リーダーボード取得の要求がサーバエラーで失敗しました。詳細については、サーバログを参照してください。 +importLeaderboardGroups=リーダーボードグループ: +importOverrideSwitchLabel=名称と ID が一致する場合に既存データを上書き +importSelectedLeaderboardGroups=選択したリーダーボードグループのインポート +importSelectAtLeastOne=インポートするリーダーボードグループを少なくとも 1 つ選択する必要があります +deselectAll=全選択解除 +polarDiagramButton=ポーラーダイアグラム +ratioBetweenTimeSinceLastPositionFixAndAverageSamplingInterval=GPS ラグ +ratioBetweenTimeSinceLastPositionFixAndAverageSamplingIntervalTooltip=最後に GPS 位置を受信してからの時間と平均サンプリング間隔の間の比率 +trackingQuality=追跡品質 +legType=レグタイプ +sailID=セールナンバー +seriesLeaderboard=シリーズリーダーボード +regattaLeaderboards=レガッタリーダーボード +clearSelection=選択のクリア +running=実行中 +runAsSubstantive=実行 +done=実行済 +lastFinished=最終フィニッシュ +run=実行 +times=回 +dataAmount=データ量 +averageCleanedServerTime=∅ サーバ時間のクリア +averageCleanedOverallTime=∅ 全体時間のクリア +dataMiningBenchmarkResults=データマイニングベンチマーク結果 +serverTime=サーバ時間 +confidenceShouldBeBetween=信頼値は 0 から 1 までの間である必要があります。 +minDataValuesNeedToBeAtLeastZero=最小データサイズ値はマイナスにはできません。 +numberOfColumnsAtLeast2=ヒストグラム列の数は複数である必要があります。 +outlierRadiusNeedsToBePositiveString=半径の異常値はプラスの数である必要があります。 +outlierMinimumNeighborhoodPctString=近傍率はプラスで最大 1 である必要があります。 +oneOrMoreParametersAreEmpty=パラメータ項目は空白にはできません。 +errorWhileAddingSeriesToChart=新規シリーズをチャートに追加\nしようとしてエラーが発生しました。 +generatingPolarSheet=ポーラーシートを生成しています... +dataCount=データサイズ +generationFinished=生成が終了しました +generating=生成中 +removeAllSheets=全シート削除 +removeAll=全削除 +selectSheet=シート選択 +cleanedServerTime=サーバ時間のクリア +overallTime=全体時間 +cleanedOverallTime=全体時間のクリア +dataMiningResult=データマイニング結果 +groupBy=グループキー +statisticToCalculate=統計の計算 +queryResultsChartSubtitle={0} データエントリを {1} 秒で処理済 +noQuerySelected=クエリ未選択 +runAutomatically=自動的に実行 +windImport_Upload=アップロード +windImport_Title=Expedition から風をインポート +windImport_BoatId=艇 ID: +windImport_SelectedRacesWarning=選択した {0} レースに風をインポートしますか。\n影響を受けるのは適用可能な時間ウィンドウにあるレースのみです。 +windImport_AllRacesWarning=すべてのレースに風をインポートしますか。\n影響を受けるのは適用可能な時間ウィンドウにあるレースのみです。 +windImport_ResultHeader={0} から {1} に風をインポートしました。{2} レースが影響を受けます。 +windImport_ResultEntry=レース: {0} ({1}) - {2} フィックス 開始: {3} 終了: {4} +windImport_ResultError=エラー: {0} +showWindSpeedSeriesTooltip=すべてのデータソースに対する風速の表示を切替 +showWindDirectionSeriesTooltip=すべてのデータソースに対する風向の表示を切替 +combinedWindSourceTypeTooltip=他のすべての風源を 1 つのデータセットに結合 +courseBasedWindSourceTypeTooltip=コースレイアウトに基づく値 (アップウィンドレグでレースがスタートするという想定) +trackBasedEstimationWindSourceTypeTooltip=競技者追跡から計算されたデータ +windSensorWindSourceTypeTooltip=モバイル計測システムから取得されたデータ +webWindSourceTypeTooltip=手動で設定されたデータ値 +raceCommitteeWindSourceTypeTooltip=レースオフィサーにより手動で設定されたデータ +sheetName=シート名 +sheetNameTooltip=テキスト項目を使用してポーラーシートに名称を付ける\nことができます。名称が与えられていない場合は\n艇種を使用して生成されます。 +timeDownwind=ダウンウィンド時間 +timeReaching=リーチング時間 +timeUpwind=アップウィンド時間 +raceTimeTooltip=競技者がスタート変針店を通過したときに計測を開始した、このレースで移動した合計時間\n(レースのスタート時ではなく) +raceTimeDownwindTooltip=このレースでダウンウィンドで移動した合計時間 +raceTimeReachingTooltip=このレースでリーチングで移動した合計時間 +raceTimeUpwindTooltip=このレースでアップウィンドで移動した合計時間 +noStatisticSelectedError=計算する統計が選択されていません +noCustomGrouperScriptTextError=分類スクリプトが空です +noDimensionToGroupBySelectedError=グループ別にする次元が選択されていません +noGrouperSelectedError=分類タイプが選択されていません +noDataRetrieverChainDefinitonSelectedError=データリトリーバが選択されていません +queryNotValidBecause=クエリが可能でありません。理由: +dataMining=データマイニング +errorRunningDataMiningQuery=クエリ実行でエラーが発生しました +hideToolbar=ツールバー非表示 +showSeriesLeaderboards=シリーズリーダーボード表示 +showOverallLeaderboard=全体リーダーボード表示 +lineAngleToWindAndAdvantage=長さ: {0} m\n風とのアングル: {1}°\n{2} 側に {3} m のアドバンテージ +doYouReallyWantToRemoveEvents=イベントを削除しますか +doYouReallyWantToRemoveEvent=イベント {0} を削除しますか +line=ライン +offset=オフセット +doYouReallyWantToRemoveCompetitors=選択した競技者を削除しますか +doYouReallyWantToRemoveCompetitor=競技者 {0} を削除しますか +filterCompetitors=競技者をフィルタリングする名称、艇種、セールナンバー、または ID +editCompetitor=競技者編集 +pleaseEnterASailNumber=セールナンバーを入力してください +successfullyAllowedCompetitorReset=競技者 {0} のリロードが許可されました。 +id=ID +allowReload=リロード許可 +compress=圧縮 +compressTooltip=エクスポートサーバインスタンスが少なくとも\nコミット 0fbf6071dea125bec4a56dee55d61c99def4a62e で実行中の場合にのみ使用してください。 +queryRunner=クエリランナー +rerunQueryAfterRefresh=リフレッシュ後にクエリを再実行 +refreshIntervalMustntBeEmpty=リフレッシュ間隔は空であってはなりません +selectionTables=選択テーブル +addConfiguration=設定追加 +noConfigurations=設定がありません +errorRetrievingConfiguration=サーバからの設定データの取得でエラーが発生しました。 +allowedCourseAreas=許容コース領域 +minimumRoundsForCourse=コースの最小周回数 +maximumRoundsForCourse=コースの最大周回数 +resultsMailRecipient=結果メール受信者 +deviceConfiguration=デバイス設定 +none=なし +courseDesignerMode=コースデザイナー +racingProcedure=競技手順 +courseNames=コース名 +generate=生成 +dontoverwrite=上書きしない +setRacingProcedureConfiguration=レガッタ設定実行 +racingProcedureConfiguration=レガッタ設定 +activateIndividualRecall=個別リコール有効化 +activateResultEntry=結果エントリ有効化 +rrs26Start=RRS26 開始 +setConfiguration=設定有効化 +classFlag=クラス旗 +classFlagHelpText={0} に使用するクラス旗を選定します。手順にある初期値の旗を有効化するには、''上書きしない'' を選択してください。 +individualRecallHelpText=有効化されると、レースオフィサーが個別リコールをコールすることができます。 +resultEntryHelpText=有効化されると、レースオフィサーがフィニッシュフェーズ中およびその後に結果を入力することができます。 +startmodeFlags=スタート前の信号旗 +startmodeFlagsHelpText=利用可能なスタート前の信号旗を設定します。手順のデフォルト信号旗を有効化するには何も選択しないでください。 +gateStart=ゲートスタート +activatePathfinder=先導艇の有効化 +pathfinderHelpText=有効化されると、レースオフィサーが先導艇を設定することができます。 +essStart=ESS スタート +basicStart=基本スタート +leagueStart=リーグスタート +configured=設定済 +overwriteRacingProceduresConfiguration=レガッタ設定の有効化 +overwriteRacingProceduresConfigurationHelpText=このオプションを有効化すると、レガッタ固有の設定で未設定のすべてのオプションに対して基本レガッタ設定が与えられます。 +setStartTime=スタート時刻の設定 +currentPass=現在のパス +setStartTimeDescription=レースの新規スタート時刻を選択してください。スタート時刻を過去の時点に設定する場合は、パスカウントを更新するチェックボックスを有効化する必要があります。パスカウントを変更すると (信号旗イベントなどの) 以前のパスからの情報がすべて解放されるということに注意してください。 +setTrackingTimesDescription=レース追跡の開始および終了を設定してください。 +hasAdditionalGolfDownTime=追加ゴルフダウンタイムの有効化 +additionalGolfDownTimeHelpText=追加ゴルフダウンタイムの有効化 +invalidColor=無効な色: {0} +igtimiAccounts=Igtimi アカウント +errorFetchingIgtimiAccounts=全 Igtimi アカウントの取込でエラー発生: {0} +addIgtimiAccount=Igtimi アカウント追加 +errorAuthorizingAccessToIgtimiUser=Igtimi ユーザ {0} へのアクセス権限付与でエラー発生: {1} +successfullyAuthorizedAccessToIgtimiUser=Igtimi ユーザ {0} へのアクセス権限が付与されました +couldNotAuthorizedAccessToIgtimiUser=ユーザ {0} への権限付与ができませんでした (ユーザ名またはパスワードが無効) +errorGettingIgtimiAuthorizationUrl=Igtimi 権限 URL 取得でエラー発生: {0} +addIgtimiUser=Igtimi ユーザ追加 +doYouReallyWantToRemoveLeaderboards=リーダーボードを削除しますか +errorTryingToRemoveIgtimiAccount=ユーザ {0} の Igtimi アカウントを削除しようとしてエラーが発生 +successfullyRemoveIgtimiAccount=ユーザ {0} の Igtimi アカウントが削除されました +eMailMustNotBeEmpty=電子メールアドレスは空であってはなりません +emailAddress=電子メールアドレス +password=パスワード +searchTag=検索タグ +doYouReallyWantToRemoveLeaderboard=リーダーボード ''{0}'' を削除しますか。 +metaLeaderboardCannotBeChanged=これはメタリーダーボードです。ここで変更することはできません。 +failedToSetNewStartTime=新規スタート時刻の設定に失敗しました。これは、同じスタート時刻がすでにレースログに存在しているか、いま追加したものよりも (パス ID および論理時間の点で) 新しいスタート時間を誰かが追加している場合に発生することがあります。 +doYouReallyWantToRemoveIgtimiAccount=ユーザ {0} のデータへのアクセスを付与する Igtimi アカウントを削除しますか。 +igtimiWindImport=Igtimi から風をインポート +seeIgtimiTabForAccountSettings=''Igtimi'' タブで、Igtimi からのどのデータを考慮するかを指定するアカウント設定を参照してください。 +importWindFromIgtimi=Igtimi から風をインポート +errorImportingIgtimiWind=Igtimi からの風データのインポートでエラー発生: {0} +resultFromIgtimiWindImport=一覧されたレースに対して次の数のフィックスがインポートされました: {0} +timeToStart=スタートまで {0} +timeSinceStart=スタートから {0} +distanceToLine=スタートラインまでの距離 +beatAngleTooltip=競技者の方向と風の間のアングル +beatAngle=ビートアングル +showBoatClassChartsLabel=利用可能な艇種の全体図も表示することができます。 +showDiagram=ダイアグラム表示 +runAutomaticallyTooltip=統計やグルーピングなどを変更した後にクエリを自動的に実行します。 +rerunQueryAfterRefreshTooltip=テーブルがリフレッシュされた後にクエリを再実行します。 +queryDefinitionProvider=クエリ定義プロバイダ +statisticProvider=統計プロバイダ +calculateThe=計算: +groupingProvider=グルーピングプロバイダ +releaseNotes=新規およびリリース履歴 +hasSplitFleetContiguousScoring=連続的に得点するフリートの分割 +addRaceLogTracker=RaceLog トラッカー追加 +addRaceLogTrackers=RaceLog トラッカー追加 +denoteForRaceLogTracking=RaceLog 追跡の表示 +startRaceLogTracking=RaceLog 追跡の開始 +filterByLeaderboard=リーダーボードによるフィルタリング +removeLeaderboardFilter=リーダーボードフィルタ削除 +competitorRegistrations=競技者登録 +trackerStatus=トラッカーステータス +courseStatus=コースステータス +canOnlyBeEditedBeforeStartingTracking=追跡開始前にのみ編集可能 +defineCourse=コースレイアウト定義 +mapDevices=デバイスを競技者およびマークにマッピング +edit_what={0} を編集 +add_what={0} を追加 +remove_what={0} を削除 +pleaseEnterA={0} を入力してください +pingPosition=ping {0} 位置 +pleaseEnterAValidValueFor={0} の有効な値を入力してください ({1}) +copyCourseAndCompetitors=コースと競技者を他のレースにコピー +setTrackingTimes=追跡時間の設定 +nowSelectRaceColumnAndFleetToCopyTo=ここで、コースのコピー先のレース列およびフリートを選択してください。 +selectRowInTableToCopyCourse=コースのコピー先の、テーブル内の単一行を選択してください。 +deviceId=デバイス ID +deviceType=デバイスタイプ +mappedToType=マッピング先のタイプ +mappedTo=マッピング先 +deviceMappings=デバイスマッピング +atMostOneEndOfTheTimeRangeMayBeOpen=多くとも、マッピングの時間範囲の終了の 1 つは未指定のままにすることができます (左を未設定) +pleaseSelectAnItemToMapTo=デバイスのマッピング先のアイテムを選択してください +device=デバイス +timeRange=時間範囲 +closeTimeRange=時間範囲の未指定側を閉じる +setClosingTimePoint=未指定の時間範囲を閉じる時点の設定 +startOfTimeRangeMustLieBeforeEnd=時間範囲の開始はその終了より前でなければなりません +overallProgress=全体進行 +eventOverview=イベント概要 +start=スタート +showAddedScores=リーダーボードにあるエントリごとに追加されたスコアの表示 +ratio=率 +importWind=風のインポート +importWindTooltip=このオプションを指定解除すると、転送されるデータ量が著しく小さくなります。 +importDeviceConfigurations=デバイス設定のインポート +importDeviceConfigurationsTooltip=これを指定すると、リモートサーバ上のすべてのデバイス設定がインポートされます。 +authorName=作成者名 +authorPriority=作成者優先度 +removeDenotation=表示の削除 +competitorRegistrationsMissingProceed=選択したレース ({0}) のうち少なくとも 1 つで競技者登録が不足しています。続行しますか。 +pleaseSelectAControlPoint=コントロールポイントを選択してください +twoMarkControlPoint=2 マークコントロールポイント +registeredCompetitors=登録済競技者 +competitorPool=競技者プール +selectAtLeastOne=少なくとも 1 つ選択 +selectRaces=レースの選択 +importFixes=フィックスのインポート +noSailingServerInstancesYet=リモートサーバインスタンスがまだ登録されていません。 +registeredSailingServerInstances=登録済リモートサーバインスタンス +filterEventsByName=イベントをフィルタリングする名称 +leaderboardsExceptFromSelectedGroup=リーダーボード (選択したグループのものを除く) +availableLeaderboardGroups=利用可能リーダーボードグループ +leaderboardGroupsOfSelectedEvent=イベントに割当済のリーダーボードグループ +addRaces=レース追加 +withNamePrefix=- 名称接頭辞: +pleaseSelectASeriesFirst=最初にシリーズを選択してください +upcommingEventStartsInDays={0,number} 日間 +upcommingEventStartsInDays[one]=明日 +upcommingEventStartsInDays[none]=今日 +sharingMailTooltip=メールによるシェア +sharingTwitterTooltip=Twitter でシェア +sharingFacebookTooltip=Facebook でシェア +#String eventSharingShortText(String eventName, String venue, String dateString); +eventSharingShortText={1} の {0} を {2} で +#String eventSharingLongText(String eventName, String venue, String dateString, String url); +eventSharingLongText={1} の {0} を {2} でシェアします。詳細: \n{3} +#String seriesSharingLongText(String seriesName, String url); +seriesSharingLongText=シリーズ {0} をシェアします。詳細: {1} +#String seriesSharingShortText(String seriesName); +seriesSharingShortText={0} +competitorsAnalytics=競技者分析 +mediaNoContent=写真または動画がまだ利用可能でありません。 +media=メディア +photos=写真 +videos=動画 +image=画像 +images=画像 +video=動画 +flagImageURL=旗画像 URL (画像を 18x12px でレンダリング) +imageURLs=画像 URL +videoURLs=動画 URL +sponsorImageURLs=スポンサー画像 URL +invalidSelection=現在の選択は有効でありません。 +advancePassId=予選通過 ID +pleaseSelectAScoringResult=得点結果を選択してください... +timeZone=タイムゾーン +eventOfficialWebsiteURL=公式 Web サイト URL +eventSailorsInfoWebsiteURL=セーラー情報 Web サイト URL +eventOverviewURL=イベント概要 URL +eventLogoImageURL=ロゴ画像 URL +showPingMarkMappings=ping したマークのマッピング表示 +regattaNameMustNotContainSlashes=レガッタ名にスラッシュが含まれていてはなりません +showNumberOfRacesScored=スコアリングされたレース数 +errorFetchingWindStreamletData=風細流データの取込でエラー発生: {0} +showWindStreamletOverlay=風細流オーバーレイ表示 +showWindStreamletColors=風速に従った色細流 +showSimulationOverlay=シミュレーションオーバーレイ表示 +simulationLegendTooltip=時間比較:\nレースは GPS から計測されたレグ持続時間、全知は風についてすべてわかっている場合のコース、オポチュニストは次マークまでの距離を計測するためにターンする場合のコース、ワンターナーは風についてすべてわかっていて 1 ターンするだけの場合のコース +racesScored=スコアが記録されたレース +racesScoredTooltip=このリーダーボードにおいて競技者が完了したか、得点を獲得したレースの数 +averageNumberOfOperationsPerMessage=メッセージごとの操作の平均数 +showUncorrectedNetPoints=未修正正味得点表示 +setStartTimeReceived=設定済スタート時刻を受信 +setStartTimeReceivedDescription=これにより、永続的でない選択した TrackedRace の startTimeReceived が設定されます。したがって、サーバが再起動された後は新規の値は忘れられます。 +lastScoreCorrectionsTime=最終得点修正時刻 +lastScoreCorrectionsComment=最終得点修正コメント +setTimeToNow=時刻を ''現在'' に設定 +useStartTimeInference=スタートマーク通過からの推定スタート時刻を使用 +numberOfQueueMessagesSent=送信済キューメッセージ数 +totalSize=合計サイズ +averageMessageSize=メッセージごとの平均サイズ (バイト) +totalNumberOfOperations=操作の合計数 +currentOrAverageSpeedOverGroundInKnotsTooltip=レグをまだ競技中の現対地速力 (kt)、レグ完了時の平均対地速力 +currentOrAverageSpeedOverGroundInKnots=SOG (終了時 ∅) +connectors=コネクタ +videoComponentShortName=動画 +competitorSearchFilter=競技者検索フィルタ +searchCompetitorsBySailNumberOrName=検索 +goToEventOverview=イベント概要にジャンプ +goToOverviewAndSeeLeaderboard=概要にジャンプし、1 つのリーダーボードで全レースを参照 +noSuchEvent=そのようなイベントなし +leaderboardNotContainedInLeaderboardGroup=リーダーボード {0} はリーダーボードグループ {1} に含まれていません。 +leaderboardGroupNotContainedInEvent=リーダーボードグループ {0} はイベント {1} に含まれていません。 +raceIsInLiveTimePanelMode=レースがライブ中 +backToLiveTimePanelMode=ライブに戻る +windChart=風チャート +mediaNoVideosCaption=動画なし +mediaShowVideoCaption=動画表示 +mediaShowVideoTooltip=クリックして動画を表示: {0} +mediaHideVideoCaption=動画非表示 +mediaHideVideoTooltip=クリックして動画を非表示 +mediaSelectVideoCaption={0} 動画 +mediaSelectVideoTooltip=クリックして動画を選択 +mediaManageMediaCaption=メディア管理 +mediaManageMediaTooltip=音声および動画クリックを設定して、時間を同期 +showAll=全表示 +raceVisibilityColumn=可視性 +enterCarryValueFor=競技者 {0} の持ち越し得点を入力 +advanced=予選通過 +basedOn=基準 +retrieveWith=検索キー +mappingDetails=マッピング詳細 +deviceMappingQrCodeExplanation=追跡アプリを使用している場合は、競技者/マークを選択し、開始時刻と終了時刻を設定してから、この QRCode をスキャンすることによってデバイスマッピングも追加することができます。 +qrCode=QRCode +enterCourseAreaName=コース領域名を入力... +enterCourseName=コース名を入力... +enterImageURL=画像 URL を入力... +enterVideoURL=動画 URL を入力... +enterSponsorImageURL=スポンサー画像 URL を入力... +enterRaceName=レース名を入力... +serverError=サーバにアクセスしようとしてエラーが発生しました。ネットワーク接続を確認して再試行してください。 +remoteProcedureCall=リモートプロシージャコール +serverReplies=サーバ応答 +errorCommunicatingWithServer=サーバとの通信でエラー発生 +userManagement=ユーザ管理 +regattaStructureImport=レガッタ構成のインポート +filteredBy=フィルタキー +editSeries=シリーズ編集 +pleaseSelectAtLeastOneRegatta=レガッタを少なくとも 1 つ選択してください +pleaseSelectAnEvent=イベントを選択してください +errorTryingToGetEvents=イベント {0} を取得しようとしてエラーが発生 +showXYDiagram=x-y ダイアグラム表示 +xyDiagram=x-y ダイアグラム +boatSpeed=艇速 +confidence=信頼 +createDefaultSettingsForAllRegattas=全レガッタの初期設定を登録... +noOverallEventSelected=全体イベント未選択... +errorTryingToCreateNewEvent=新規イベント {0} を登録しようとしてエラーが発生: {1} +createNewEvent=新規イベント登録 +noFleetsDefined=フリートが定義されていません。 +successfullyCreatedRegattas=レガッタが登録されました +errorTryingToRegisterRacesForTracking=追跡するレース {0} を登録しようとしてエラーが発生: {1}。ライブ/格納 URI 構文をチェックしてください。 +errorDeterminingPolarAvailability=レース {0} のポーラー/VPP データの利用可能性の決定でエラー発生: {1} +error=エラー +fileStorage=ファイルストレージ +active=有効 +scoringSchemeHighPointEssOverallDescription=得点数によるスコアです。アクトの勝者が 10 点、2 位が 9 点などと得点します。Extreme Sailing シリーズ全体で同点となった場合は、より多くのアクトで勝者となった競技者を上位とします。それでも同じ場合には、最終アクトの結果を用います。 +timeBetweenRaceStartAndCompetitorStartInSeconds=スタート遅延 +timeBetweenRaceStartAndCompetitorStartInSecondsTooltip=レースのスタートと、競技者がスタート変針点を通過した時刻の間の時間です。マイナスの時間は OCS を意味します。 +useInternalAlgorithm=内部マーク通過アルゴリズムの使用 +editMarkPassings=マーク通過編集 +editMarkPositions=マーク位置編集 +removeFixedPassings=固定マーク通過を削除 +setFixedPassing=マーク通過の時刻を設定 +setSuppressedPassing=現在のもので開始したマーク通過を非表示 +removeSuppressedPassing=非表示のマーク通過を削除 +errorRemovingFixedPassing=固定マーク通過の削除でエラー発生: {0} +errorSettingFixedPassing=固定マーク通過の設定でエラー発生: {0} +errorSuppressingPassing=マーク通過の非表示でエラー発生: {0} +errorRemovingSupressedPassing=非表示のマーク通過の削除でエラー発生: {0} +errorTryingToObtainMarkPassing=マーク通過を取得しようとしてエラーが発生: {0} +errorTryingToObtainRaceLogMarkPassingData=レースログのマーク通過データを取得しようとしてエラーが発生: {0} +email=電子メール +pleaseEnterAnEmail=電子メールを入力してください +inviteCompetitors=競技者の招待 +inviteSelectedCompetitors=選択した競技者の招待 +notAllCompetitorsProvideEmail=一部の競技者が電子メールを未提供 +sendingMailsFailed=メール送信に失敗 +sendingMailsSuccessful=電子メールを正常に送信済 +selectEventForInvitation=招待のイベント選択 +imageURL=画像 URL +upload=アップロード +removeResult=操作の削除: {0} {1} +uploadSuccessful=正常にアップロード済 +fileUploadResult=ファイルアップロード: {0} {1}\nエラー発生の場合は、ファイルストレージサービスが適切に設定されていることを確認してください。 +removeUploadedFile=アップロード済ファイルの削除 +showCompetitorSailIdColumn=帆走 ID +showCompetitorFullNameColumn=競技者氏名 +loadingDimensionValues=次元値ロード中 +runningQuery=クエリ実行中 +inviteBuoyTenders=ブイ入札の招待 +orMultipleEmails=または複数の電子メールをカンマで区切り +courseOverGroundTrueDegreesTooltip=真の対地方位角度 (度) +courseOverGroundTrueDegrees=COG +distanceIncludingGateStartInMeters=距離 (ゲートスタートでの) +distanceTraveledIncludingGateStartTooltip=レグ始点からレグ終点まで、またはレグをフィニッシュ\nしていない場合は現時点までに移動した距離です。\nレグにゲートスタートが含まれる場合は、異なる時刻にスタートしたときでも\n競技者を比較するためにピンエンドからスタート位置までの距離が含まれます。 +raceDistanceTraveledIncludingGateStartTooltip=レース開始からレース終了まで、またはレースがフィニッシュ\nしていない場合は現時点までに移動した距離です。ゲートスタートの場合は、異なる時刻にスタートしたときでも\n競技者を比較するためにピンエンドからスタート位置までの距離が含まれます。 +results=結果 +groupName=グループ名 +valueAscending=値 (昇順) +valueDescending=値 (降順) +sortBy=ソートキー +dashboardHeader=ダッシュボード +dashboardNoWindBotAvailableHeader=ウィンドボットは利用できません。 +dashboardNoWindBotAvailableMessage=風計測ユニットからライブ風データを受信するには、ウィンドボットがオンになっていて、SAP Sailing Analytics に接続されていることを確認してください。 +dashboardWindBot=ウィンドボット +dashboardTrueWindSpeed=TWS +dashboardTrueWindDirection=TWD +dashboardTrueWindSpeedUnit=kt +dashboardTrueWindDirectionUnit=度 +dashboardLiveWind=ライブ +dashboardAverageWind=平均 +dashboardAverageWindMinutes=({0} 分) +dashboardWindChartHint=チャートをタップして間隔を変更 +dashboardSelectCompetitor=競技者選択 +dashboardStartAnalysesHeader=分析開始 +dashboardNewStartAnalysisAvailable=新規スタート分析が利用可能 +dashboardNoStartAnalysesAvailableHeader=利用可能なスタート分析がありません。 +dashboardNoStartAnalysesAvailableMessage=それぞれのレーススタートから作成されたスタート分析が存在しています。レースがスタートするまでお待ちください。 +dashboardNoStartlineAdvantagesByWindAvailableHeader=スタートラインアドバンテージは利用できません。 +dashboardNoStartlineAdvantagesByWindAvailableMessage=スタートラインアドバンテージを計算するには、レース委員会が設定したコースが必要です。 +dashboardNoStartlineAdvantagesByGeometryAvailableHeader=スタートラインアドバンテージは利用できません。 +dashboardNoStartlineAdvantagesByGeometryAvailableMessage=スタートラインアドバンテージを計算するには、レース委員会が設定したコースが必要です。 +dashboardStartlineAdvantagesByWindHeader=風によるスタートラインアドバンテージ +dashboardStartlineAdvantageByGeometryHeader=配置によるスタートラインアドバンテージ +dashboardRankAtFirstMark=第一マークでのランク +dashboardTeam=チーム +dashboardDistanceToLineInM=ラインまでの距離 (m) +dashboardSpeedInKts=速力 (kt) +dashboardAdvantageInSeconds=アドバンテージ (m) +dashboardSecondsAbreviation=s +dashboardDistanceToRCBoat=RC 艇までの距離 (m) +dashboardPinEnd=ピンエンド +dashboardRCBoat=RC 艇 +fixedMarkPassing=(固定) +suppressedMarkPassing=(非表示) +windUp=ウィンドアップ (マップの最上部から風を表示) +filterBy=フィルタキー +currentFilterSelection=現在のフィルタ選択 +notCapableOfGeneratingACodeForIdentifier=この ID のコードを生成することができません。 +serverUrl=サーバ URL +rotatedFromTrueNorthClickToToggleWindUp=真北から {0} 度を回転。\nタップ/クリックしてウィンドアップとノースアップの間のマップ表示を切替 +startLineToFirstMarkTriangle=スタートから第一マークまで ({0}m) +dataMiningComponentsHaveBeenUpdated=データマイニングコンポーネントが更新されました +dataMiningComponentsNeedReloadDialogMessage=リロードをクリックして、コンポーネントをリロードします。これにより、現在表示されているデータが破棄され、デフォルトクエリが実行されます。\n何も行わない場合は閉じるをクリックします。データマイニングは、コンポーネントがリロードされるまでは正しく機能しません。 +noDataForEvent=このイベントにまだデータが何もありません。 +countriesCount={0,number} カ国 +countriesCount[one]={0,number} カ国 +racesCount={0,number} レース +racesCount[one]={0,number} レース +trackedRacesCount={0,number} 追跡レース +trackedRacesCount[one]={0,number} 追跡レース +eventsCount={0,number} イベント +eventsCount[one]={0,number} イベント +competitorsCount={0,number} 競技者 +courseAreaName=コース領域: {0} +competitorsCount[one]={0,number} 競技者 +currentOfTotalLegs=レグ {0,number}/{1,number} +clone=クローン +changed=変更済 +matcher=一致 +any=任意 +urlProviders=URL ベースの結果プロバイダ +pleaseSelectAURLProvider=URL ベースの結果プロバイダを選択してください... +sampleURL=サンプル URL: {0} +sapSportsOn=SAP Sports: +followSapSports=SAP Sports をフォロー +liveNow=ライブ中 +analyseRace=レースを分析 +nextRaceStartingIn=次のレースのスタート: +startingIn={0} がスタート: +raceIsPlanned=計画済 +raceIsCanceled=中止 +raceIsPostponed=延期 +startingInMinutes=スタートまで {0,number} 分 +startingInMinutes[none]=スタートします +latestNews=最新ニュース +resultsUpdate=結果更新 +competitorWonRace={0} がレースに勝利 +showCompetitorTimeOnTimeFactorColumn=Time on Time 係数 +showCompetitorTimeOnDistanceAllowanceColumn=Time on Distance 修正時間 +rankingMetric=ランキング +oneDesignRankingMetric=ワンデザイン +orcPerformanceCurveRankingMetric=ORC 性能曲線 +timeOnTimeAndDistanceRankingMetric=Time on Time/Time on Distance +oneDesignRankingMetricDescription=ワンデザイン: ラインを最初に通過した艇が勝者、\nウィンワード位置 (アップウィンド/ダウンウィンド) またはアロングコース位置 (リーチング) に基づくレース内ランク +orcPerformanceCurveRankingMetricDescription=ORC 性能曲線: 風に対して事前定義したレグのアングルを仮定して VPP/ポーラーに基づいて修正時間を与える洗練されたハンディキャップ付与方式です。\nこれらの修正時間に基づいて、ランキングの基礎となる "風の推定" が計算されます。\n風の推定は、レース中にいつでも計算でき、したがってレース中ランキングの基礎となります。 +timeOnTimeAndDistanceRankingMetricDescription=Time on Time / Time on Distance: 計算された時間による最終ランキングです。\n時間は実際の時間にオプションで係数 (初期値は 1.0) を適用して、\nコース長に適用される、海里当りの修正時間 (秒単位) を\n減じて計算されます。\nレース中のランクは、現在のレグの平均 VMG/VMC を仮定して\n最速艇の現在の位置への到着の予想に基づきます。 +timeOnTimeFactor=ToT 係数 +timeOnDistanceAllowanceInSecondsPerNauticalMile=ToD 修正時間 +timeOnTimeFactorTooltip=Time on Time 係数、実際の時間は計算された時間公式をこの係数で乗算 +timeOnDistanceAllowanceInSecondsPerNauticalMileTooltip=海里ごとの Time on Distance 修正時間 (秒)、\n帆走した距離をこの修正時間で乗じ、結果として得られた秒数を、計算された時間公式において\n帆走した実際の時間から減算 (オプションで Time on Time 係数を適用して計算) +calculatedTimeTraveled=計算時間 +calculatedTimeTraveledTooltip=レースのスタートからの時間および移動した (ウィンワード) 距離に基づいて\n競技者のハンディキャップに従って計算された時間 +raceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSeconds=計算時間 @ 最速 +raceCalculatedTimeAtEstimatedArrivalAtCompetitorFarthestAheadInSecondsTooltip=レースにおいて現在最も遠くの前方にいる競技者の推定到着で計算された時間、\n推定は、現レグでの平均 VMG および後続のすべてのレグでの最速艇と同等の (計算された) 性能に依拠 +sailorInfo=帆走情報 +sailorInfoLongText=公式結果、抗議、\n掲示板、帆走中天候: +eventQuickfinder=イベントクイックファインダ +regattaQuickfinder=レガッタクイックファインダ +resultsQuickfinder=結果クイックファインダ +racesQuickfinder=レースクイックファインダ +statistics=統計 +impressions=印象 +notAvailableOnMobileMessage=この内容はモバイルでは利用できません。\nデスクトップバージョンに切り替えますか。 +gotoDesktop=デスクトップバージョンにジャンプ +goBack=戻る +showAllX=全 {0} を表示 +collapseX={0} を圧縮 + +numberOfGPSFixes=GPS フィックス +fastestSailor=最速 +numberWindFixes=風フィックス +sailedMiles=帆走マイル +millionValue={0,number,#0.0} 百万 +billionValue={0,number,#0.0} 十億 +noResults=結果はまだ利用可能でありません。 +noNews=利用可能なニュースがありません。 +pointsValue={0,number,#0.00} +openLeaderboard=リーダーボードを開く +now=現在 +minutesAgo={0} 分前 +hoursAgo={0} 時間前 +winner=勝者 +durationPlain=期間 +finishedRaces=フィニッシュしたレース +selectCompetitor=競技者を 1 つ選択してください +all=全 +points=ポイント +numberOfRacesShort=# レース +courseGeometry=コース配置 +degreesToWind=風と {0}° +miniLeeaderboardLegendText=競技者別にレース数とともに得点を表示 +currentOfTotal={0,number}/{1,number} +noFinishedRaces=フィニッシュしたレースがまだありません。 +racesOverview=レース概要 +listFormatLabel=レース一覧 +competitionFormatLabel=競技日 +empty=空 +runAQuery=クエリを実行 +latestRegattaStandings=レガッタ最新順位表 +plainText=プレーンテキスト +columnChart=縦棒グラフ +choosePresentation=プレゼンテーション選択 +cantDisplayDataOfType={0} タイプのデータは表示できません。 +shownDecimals=小数点表示 +openFullscreenView=全画面ビューを開く +closeFullscreenView=全画面ビューを閉じる +progress=進行 +videosCount={0,number} 動画 +videosCount[one]={0,number} 動画 +photosCount={0,number} 写真 +photosCount[one]={0,number} 写真 +eventsHaveTakenPlace={0} イベントが行われました +eventsHaveTakenPlace[one]=1 つのイベントが行われました +raceOffice=レース事務所 +analyze=分析 +dataMiningSettings=データマイニング設定 +multiResultsPresenter=複数結果表示 +plainResultsPresenter=プレーン結果表示ツール +resultsChart=結果チャート +tabbedResultsPresenter=タブ結果表示ツール +polarResultsPresenter=ポーラー結果表示ツール +dataMiningRetrieval=データ取得 +actionWatch=視聴 +actionAnalyze=分析 +denoteAllRacesForRaceLogTrackingShorctut=レースログ追跡の全レース表示へのショートカット +registerCompetitors=競技者の登録 +doYouWantToCreateADefaultRegattaLeaderboard=レガッタリーダーボードを初期登録しますか +copyCourse=コースのコピー +copyCompetitors=競技者のコピー +smartphoneTracking=スマートフォン追跡 +flightsCount={0,number} フライト +flightsCount[one]={0,number} フライト +viewQueryDefinition=クエリ定義の表示 +queryDefinitionViewer=クエリ定義ビューア +groupAverageAscending=グループ平均 (昇順) +groupAverageDescending=グループ平均 (降順) +groupMedianAscending=グループ中央値 (昇順) +groupMedianDescending=グループ中央値 (降順) +resultsFoundForSearch={0,number} 結果が ''{1}'' に見つかりました +resultsFoundForSearch[one]={0,number} 結果が ''{1}'' に見つかりました +runPredefinedQuery=事前定義クエリを実行 +selectPredefinedQuery=事前定義クエリを選択 +predefinedQueryRunner=事前定義クエリランナー +developerOptions=開発者オプション +copyToClipboard=クリップボードにコピー +code=コード +useClassGetName=タイプ名に class.getName() を使用 +useClassGetNameTooltip=コードベースでの変更に対してより堅牢ですが、コードスニペットはクラスが利用できる範囲でのみ使用できます。 +useStringLiterals=タイプ名に文字列リテラルを使用 +useStringLiteralsTooltip=このコードスニペットはどこでも使用できますが、コードベースが変更される場合は中断します。 +errorLoadingDataWithTryAgain=データのロードでエラーが発生しました。しばらくしてから再試行してください。 +addGalleryPhoto=ギャラリーフォト追加 +addStageImage=ステージ画像追加 +addTeaserImage=ティーザー画像追加 +addEventLogo=イベントロゴ追加 +noImagesDefinedYet=画像未定義 +imageSizeError={0} 画像のサイズが適合しません。幅は {1} と {2} px の間、高さは {3} と {4} px の間である必要があります。 +couldNotRetrieveImageSizeYet=画像の幅と高さをまだ取得することができません。 +pleaseOnlyUploadContentYouHaveAllUsageRightsFor=すべての使用権を持っているコンテンツのみをアップロードしてください。コンテンツは途中から切れてしまう場合があることを忘れないでください。 +registerCompetitorsOnRace=レースへの競技者の登録 (例: フリート分割レースに対して) +doYouWantToRegisterCompetitorsDirectlyOnTheRace=レースに競技者を直接に登録しますか +doYouWantToDisableCompetitorsRegistrationsOnTheRace=レースへの競技者登録を無効にしますか +warningDirectCompetitorRegistration=警告: この設定を行うと、このレースの競技者がレースごとに登録されることにつながります。競技者は、該当するレガッタに登録されるときに、このレースに対して自動的には登録されません。 +warningRegattaCompetitorRegistration=警告: これにより、レガッタへの競技者登録が再有効化されます。レガッタからのすべての競技者登録が、このレースに対する競技者登録として自動的に使用されます。 +competitorRegistrationsOnRaceDisabled=競技者登録はこのレースに対して無効化されています。左側のチェックボックスを選択して有効化します。 +addMarkToRegatta=マークをレガッタに追加 +selectALeaderboardGroup=リーダーボードグループを選択... +pleaseSelect=選択: +requiresValidRegatta=このページには、表示するレースを識別するために有効なレガッタ、レース列、およびフリート名が必要です。 +couldNotObtainRace=名称 {0} のレガッタに対してフリート {2} の名称 {1} でレースが取得できませんでした: {3} +errorTryingToCreateEmbeddedMap=埋込マップを登録しようとしてエラーが発生: {0} +topN=上位 {0} +transparentBufferLineOnHover=ヘルプ行のホバー上の透過バッファ行 +bufferLineStrokeWeight=バッファ行ストローク幅 +profile=プロファイル +showOnlyCompetitorsOfLog=ログに登録された競技者のみ表示 +confirmLosingCompetitorEditsWhenTogglingLogBasedView=これにより、現在の、競技者に対して未保存の変更内容がリセットされます。\n続行しますか。 +removalOfMarkDisabledMayBeUsedInRaces=削除は無効化され、マークはレース {0} で使用されます +createDefaultLeaderboardGroup=イベントにリーダーボードグループを初期登録しますか +pleaseCreateAtLeastOneMappingBy=左側にあるトラックを最初に選択してから右側にあるマークまたは競技者を選択してマッピングを少なくとも 1 つ登録してください +matcherType=一致タイプ +matchingDevices=一致デバイス +addDevice=デバイス追加 +createDeviceConfiguration=デバイス設定登録 +forWhichDeviceShouldConfigurationApply=新規設定を適用するデバイスを指定 +create=登録 +thereIsAlreadyAConfigurationForThisDevice=このデバイスの設定がすでに存在しています。 +enterDeviceIdentifierName=識別のための名称を入力してください +administration=管理 +pleaseSelectALeaderboardGroup=リーダーボードグループを選択してください +linkLeaderboardToLeaderboardGroupOfEvent=イベントのリーダーボードグループへのリーダーボードのリンク +doYouWantToLinkLeaderboardToLeaderboardGroupOfEvent=リーダーボード {0} をイベントのリーダーボードグループ {1} にリンクしてください +failedToLinkLeaderboardToLeaderboardGroup=リーダーボード {0} のリーダーボードグループ {1} へのリンクに失敗しました +createDefaultRegatta=レガッタ初期登録 +doYouWantToCreateADefaultRegatta=レガッタを初期登録しますか +defaultLocale=初期値 +defaultSailorsInfoLinkInfoText=言語依存の上書きがなければデフォルトのリンクが使用されます。デフォルト未登録の場合は、明示的に 1 セットも有していない言語に対するリンクはありません。 +qrSync=QR 同期 +thereIsNoIdentifierSet=ID が設定されていません。 +couldNotObtainAccessTokenForUser=ユーザ {0} のアクセストークンを取得できませんでした +raceManagerDeviceName=デバイス名 +serverURLInvalid=有効なサーバ URL を入力してください +serverURLEmpty=サーバ URL が空 +marks=マーク +addNewFix=新規フィックス追加 +pleaseSelectOtherTimepoint=別の時点を選択してください。タイムスライダの時点にフィックスがすでに存在します。 +confirmMove=移動を確認 +confirmNewFix=新規フィックス確認 +selectAFixPositionBy=マップをドラッグしてフィックス位置を選択してから、確認するか下記のボタンで取り消してください。 +pleaseSelectAMark=左側の一覧からマークを選択してください。 +markFixes=フィックスのマーク +playstoreCampaign=utm_source=sapsailing&utm_medium=solutions&utm_content=2016.02&utm_campaign=solutions&pcampaignid=2016.sapsailing.solutions +playstoreTrackingApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.tracking.app +playstoreRacecommitteeApp=https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app +playStoreBuoypositioningApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app +playstoreBadge=https://play.google.com/intl/en/badges/images/generic/en-play-badge.png +appstoreSapSailingInsight=http://appstore.com/sapsailinsight +appstoreStgTrainingstagebuch=http://appstore.com/stgtrainingstagebuch +appstoreBadgeSuffix=_en +distanceFromAveragePosition=平均位置からの距離 +useATouchOptimizedUI=タッチ最適化 UI を使用してフィックスを移動 +deleteFix=フィックス削除 +theDeletionOfThisFix=このフィックスの削除は現在は可能でありません。 +loadingMarkFixes=フィックスのマークをロードしています... +fixSuccessfullyAdded=フィックスが追加されました。 +fixPositionSuccessfullyEdited=フィックス位置が編集されました。 +fixSuccessfullyRemoved=フィックスが削除されました。 +dragToChangePosition=ドラッグして位置を変更\n右クリックして削除するメニューを取得 +moveFix=フィックスを移動 +manage2SailEvent=Manage2Sail イベント +noEventsYet=イベントがまだありません。 +selectResultImportProvider=結果インポートプロバイダを選択してください... +buildVersion=ビルドバージョン +serverInformation=サーバ情報 +serverConfiguration=サーバ設定 +standaloneServer=スタンドアロンサーバ +send=送信... +passId=パス ID +createdAt=登録時刻 +logicalTimepoint=論理タイムポイント +info=情報 +windFixes=風フィックス +twoXMark=2x マーク +pagerStateInfo={0,number,#,###}-{1,number,#,###} of {2,number,#,###} +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###} of over {2,number,#,###} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties index 03c2da16d74..67ac24c3d2c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_ru.properties @@ -1,5 +1,5 @@ manage2Sail=Manage2Sail -raceCommitteeApp=Приложение диспетчера гонок +raceCommitteeApp=Приложение Race Manager tracTracEvents=События TracTrac swissTimingEvents=События SwissTiming trackedBefore=История отслеживаемых событий @@ -29,7 +29,7 @@ timeDelayHelp=Оставьте пустым для воспроизведени speedInKnots=Скорость (уз) fromDeg=Источник (град.) noRacesYet=Гонок еще нет. -total=\u03A3 +total=Σ carry=Накоплено competitor=Участник competitors=Участники @@ -72,12 +72,12 @@ collapse=- expand=+ leg=Э distanceInMeters=Дистанция -averageSpeedInKnots=\u2205 Скорость +averageSpeedInKnots=∅ Скорость averageSpeedInKnotsTooltip=Средняя скорость относительно грунта на данном этапе:\nотношение пройденного расстояния к затраченному времени. raceAverageSpeedInKnotsTooltip=Средняя скорость в течение гонки,\nвычисляемая делением пройденного расстояния на затраченное время. rankGain=Повышение ранга rankGainTooltip=Разница в положении между текущим и предыдущим этапами. -sapSailingAnalytics=Аналитика плавания +sapSailingAnalytics=Sailing Analytics leaderboard=Таблица лидеров leaderboards=Таблицы лидеров leaderboardSettings=Параметры таблицы лидеров @@ -110,7 +110,7 @@ regatta=Регата regattas=Регаты startTracking=Начать отслеживание stopTracking=Прекратить отслеживание -connections=Подключение +connections=Подключения historyOfConnections=История подключений defineNewConnection=Определить новое подключение trackedRaces=Отслеживаемые гонки @@ -204,7 +204,7 @@ mark=Отметка otherManeuver=Неизвестный маневр douglasPeuckerPoints=Точки Дугласа – Пекера filterRacesByName=Фильтровать гонки по названию -filterMediaByName=Фильтровать носители по названию +filterMediaByName=Фильтровать по названию trackSettings=Параметры отслеживания actionEdit=Изменить actionEditCompetitors=Изменить участников @@ -226,7 +226,7 @@ hoverOverAPoint=Наведите указатель для просмотра п noSelection=Ничего не выбрано raceIsKnownToStartUpwind=Гонка начинается против ветра events=События -settingsForComponent=Параметры для {0} +settingsForComponent={0} — параметры noEventsFound=События не найдены noEventSelected=Не выбрано событие noLeaderboardsFound=Таблицы лидеров не найдены @@ -320,9 +320,9 @@ racesInLeaderboard=Гонки в таблице лидеров leaderboardsInGroup=Таблицы лидеров в группе replication=Репликация errorFetchingReplicaData=Ошибка извлечения данных реплики: {0} -averageAbsoluteCrossTrackErrorInMeters=\u2205 XTE +averageAbsoluteCrossTrackErrorInMeters=∅ XTE averageAbsoluteCrossTrackErrorInMetersTooltip=Среднее боковое уклонение к большому круговому сегменту, соединяющему начало отрезка с концом отрезка -averageSignedCrossTrackErrorInMeters=\u2205 XTE +/- +averageSignedCrossTrackErrorInMeters=∅ XTE +/- averageSignedCrossTrackErrorInMetersTooltip=Средняя позиция на курсе как расстояние (со знаком) от средней линии курса; отрицательные значения означают уклонение влево, положительные — вправо. raceAverageAbsoluteCrossTrackErrorInMetersTooltip=Среднее абсолютное боковое уклонение к большим круговым сегментам, соединяющим начало отрезка с концом отрезка, для всех наветренных отрезках данной гонки без учета стороны участника от средней линии курса raceAverageSignedCrossTrackErrorInMetersTooltip=Боковое уклонение (со знаком) к большим круговым сегментам, соединяющим начало отрезка с концом отрезка, для всех наветренных отрезков данной гонки; отрицательные значения означают уклонение влево от средней линии курса, положительные — вправо. @@ -407,21 +407,21 @@ errorTryingToObtainLeaderboardContents=Ошибка при попытке пол lastScoreUpdate=Последнее обновление оценок warningBrowserUnsupported=Этот веб-сайт не оптимизирован для вашего браузера. Перейдите на более новый, такой как Firefox или Chrome. newMark=Новая отметка -controlPoint=Отметка/ворота +controlPoint=Название отметки/ворот errorUpdatingRaceCourse=Ошибка при обновлении курса гонки: {0} successfullyUpdatedCourse=Курс гонки успешно обновлен selectTwoMarksForGate=Выберите две отметки, чтобы создать ворота errorTryingToObtainTheMarksOfTheRace=Ошибка при попытке получить отметки курса гонки: {0} errorTryingToObtainRaceCourse=Ошибка при попытке получить курс гонки: {0} -insertWaypointBeforeSelected=Вставка путевой точки перед выбранной -insertWaypointAfterSelected=Вставка путевой точки после выбранной +insertWaypointBeforeSelected=Вставить путевую точку перед выбранной +insertWaypointAfterSelected=Вставить путевую точку после выбранной addGate=Добавить ворота scoringSchemeLowPointSystem=Система низких баллов scoringSchemeHighPointSystem=Система высоких баллов scoringSchemeHighPointEssOverall=Итоговая оценка ESS по системе высоких баллов (10) scoringSchemeHighPointEssOverall12=Итоговая оценка ESS по системе высоких баллов (12) -scoringSchemeHighPointEssOverall12Description=Оценка в баллах. Победитель в акте получает 12 баллов, 2-й — 11 баллов, .... Любая ничья в общей оценке Extreme Sailing Series разрешается в пользу участника с наибольшим числом побед в актах. В случае равенства числа побед для разрешения используется последний акт. -scoringSchemeHighPointLastBreaksTie=Система высоких баллов, последняя гонка разрешает ничью +scoringSchemeHighPointEssOverall12Description=Оценка в баллах. Победитель в акте получает 12 баллов, 2-й — 11 баллов, .... Любое равенство в общей оценке Extreme Sailing Series разрешается в пользу участника с наибольшим числом побед в актах. В случае равенства числа побед для разрешения используется последний акт. +scoringSchemeHighPointLastBreaksTie=Система высоких баллов, равенство разрешается по последней гонке scoringSchemeLowPointWinnerGetsZero=Система низких баллов, победитель получает ноль scoringSchemeWinnerGetsFive=Система высоких баллов, победитель получает 5 баллов scoringSchemeWinnerGetsSix=Система высоких баллов, победитель получает 6 баллов @@ -429,14 +429,14 @@ scoringSchemeWinnerGetsEight=Система высоких баллов, поб scoringSchemeWinnerGetsEightAndInterpolation=Система высоких баллов, победитель 8 баллов, интерполяция scoringSchemeLowPointWithEliminationsAndRoundsWinnerGets07=Низкий балл с турами на выбывание, победитель получает 0,7 балла scoringSchemeLowPointWithEliminationsAndRoundsWinnerGets07Description=Вариант схемы оценки с низкими баллами, использующий туры на выбывание.\nКаждый тур моделируется как серия в регате, и суда ранжируются в обратном порядке,\nначиная с наибольшего числа за первый раунд.\nВ финале и при определении 3-го места используйте ранги 1 и 2. Для полуфинального тура начните с 3. -scoringSchemeLowPointForLeagueOverallLeaderboard=Низкий балл, ничьи разрешаются по сумме актов, затем по последнему акту -scoringSchemeLowPointForLeagueOverallLeaderboardDescription=Вместо обычной системы обратного отчета, используемой для разрешения ничьих в схемах низких баллов, участники с равными суммами баллов сравниваются по сумме баллов, полученных за все акты. Если равенство сохраняется, оно разрешается по последнему акту. +scoringSchemeLowPointForLeagueOverallLeaderboard=Низкий балл, равенство разрешается по сумме актов, затем по последнему акту +scoringSchemeLowPointForLeagueOverallLeaderboardDescription=Вместо обычной системы обратного отчета, используемой для разрешения равенства в схемах низких баллов, участники с равными суммами баллов сравниваются по сумме баллов, полученных за все акты. Если равенство сохраняется, оно разрешается по последнему акту. scoringSchemeHighPointFirstGetsOne=Высокий балл, победитель получает 1 балл scoringSchemeHighPointFirstGetsTen=Высокий балл, победитель получает 10 баллов scoringSchemeHighPointFirstGetsTenOrEight=Высокий балл, победитель получает 10 баллов (или 8) -scoringSchemeHighPointFirstGetsTenOrEightDescription=Оценка в баллах. Победитель получает 10 баллов, 2-й — 9 баллов, .... Ничья разрешается в пользу участника, имеющего больше побед в гонках, если равенство сохраняется, оно разрешается по рангу в последней гонке. +scoringSchemeHighPointFirstGetsTenOrEightDescription=Оценка в баллах. Победитель получает 10 баллов, 2-й — 9 баллов, .... Равенство разрешается в пользу участника, имеющего больше побед в гонках. Если равенство сохраняется, оно разрешается по рангу в последней гонке. scoringSchemeHighPointFirstGetsTwelveOrEight=Высокий балл, победитель получает 12 баллов (или 8) -scoringSchemeHighPointFirstGetsTwelveOrEightDescription=Оценка в баллах. Победитель получает 12 баллов, 2-й — 11 баллов, .... Ничья разрешается в пользу участника, имеющего больше побед в гонках, если равенство сохраняется, оно разрешается по рангу в последней гонке. +scoringSchemeHighPointFirstGetsTwelveOrEightDescription=Оценка в баллах. Победитель получает 12 баллов, 2-й — 11 баллов, .... Равенство разрешается в пользу участника, имеющего больше побед в гонках. Если равенство сохраняется, оно разрешается по рангу в последней гонке. scoringSystem=Система оценок createFlexibleLeaderboard=Создать гибкую таблицу лидеров createRegattaLeaderboard=Создать таблицу лидеров регаты @@ -461,7 +461,7 @@ totalTimeSailedInSeconds=Общее время totalTimeSailedInSecondsTooltip=Общее время хода участника в данной регате, отсчитываемое от времени старта регаты. totalDistanceTraveled=Общее расстояние totalDistanceTraveledTooltip=Общее пройденное расстояние в данной регате. -totalAverageSpeedOverGround=\u2205 SOG +totalAverageSpeedOverGround=∅ SOG totalAverageSpeedOverGroundTooltip=Средняя скорость относительно грунта в течение данной регаты. hhmmssUnit=чч:мм:сс actionAddWindData=Добавить данные о ветре @@ -506,11 +506,11 @@ expandLastRace=Всегда развертывать посленюю отобр factor=Фактор errorUpdatingIsMedalRace=Ошибка при обновлении параметра гонки на медали: {0} maneuverLoss=Потеря маневра -averageManeuverLossInMeters=\u2205 Потеря маневра +averageManeuverLossInMeters=∅ Потеря маневра averageManeuverLossInMetersTooltip=Средняя потеря дистанции в ходе маневра. -averageTackLossInMeters=\u2205 Потеря на оверштаг +averageTackLossInMeters=∅ Потеря на оверштаг averageTackLossInMetersTooltip=Средняя потеря дистанции при поворотах оверштаг. -averageJibeLossInMeters=\u2205 Потеря на фордевинд +averageJibeLossInMeters=∅ Потеря на фордевинд averageJibeLossInMetersTooltip=Средняя потеря дистанции при поворотах фордевинд. shape=Форма pattern=Образец @@ -521,7 +521,7 @@ errorFetchingChartData=Ошибка при извлечении данных д loadingCompetitorData=Загрузка данных участника... competitorRegattaDataAfterRaceN={0} после гонки {1} rankChart=Ранг -totalPoints=Всего баллов +totalPoints=Общий балл raceTotalPointsTooltip=Общий балл участника в данной гонке regattaTotalPointsTooltip=Общий балл участника в данной регате showRankChart=Показать диаграмму ранга @@ -681,8 +681,8 @@ setup=Настройка strength=Сила (кН) oscillation=Колебание oscillating=Колебания -amplitude=Амплитуда (в град.) -frequency=Частота (1/ч) +amplitude=Амплитуда +frequency=Частота noise=Шум sailing=Плавание optimality=Оптимум @@ -729,7 +729,7 @@ addSeries=Добавить серию regattaDefinesResultDiscardingRules=Регата определяет правила исключения результатов startsWithZeroScore=Серия начинается с нулевым баллом masterDataImportPanel=Импорт мастер-данных -importSuccess=Спех\nСоздано групп таблиц лидеров: {0}, таблиц лидеров: {1}, событий: {2}, регат: {3}. +importSuccess=Успех\nСоздано групп таблиц лидеров — {0}, таблиц лидеров — {1}, событий — {2}, регат — {3}. importSuccessOverwriteInfo=Следующие регаты перезаписаны, потребуется повторное отслеживанием их гонок: regattaOverviewConfiguration=Конфигурация обзора регаты firstRaceIsNonDiscardableCarryForward=Начинается с неисключаемого переноса @@ -812,7 +812,7 @@ essOneRemoved=Сигнал о старте гонки dataBeforeAfter=Данные из до/после гонки rawFixes=Все замеры (включая выбросы) importRemoteHost=Удаленный хост: -importFetchRemoteLgs=Извлечь список группы таблиц лидеров +importFetchRemoteLgs=Извлечь список групп таблиц лидеров importUrlInvalid=Недействительный URL-адрес для извлечения групп таблиц лидеров: {0} importGetLeaderboardsFailed=Сбой GET-запроса групп таблиц лидеров для хоста {0} с ошибкой {1} importNoDataReturned=Сервер не вернул никаких данных @@ -838,8 +838,8 @@ lastFinished=Последний финиш run=Выполнить times=раз dataAmount=Объем данных -averageCleanedServerTime=\u2205 Очищенное время сервера -averageCleanedOverallTime=\u2205 Очищенное общее время +averageCleanedServerTime=∅ Очищенное время сервера +averageCleanedOverallTime=∅ Очищенное общее время dataMiningBenchmarkResults=Результаты сравнительного анализа добычи данных serverTime=Время сервера confidenceShouldBeBetween=Значения достоверности должны быть между 0 и 1 @@ -911,7 +911,7 @@ doYouReallyWantToRemoveCompetitor=Действительно удалить уч filterCompetitors=Фильтрация участников по названию, классу лодки, номеру на парусе или идентификатору editCompetitor=Изменить участника pleaseEnterASailNumber=Введите номер на парусе -successfullyAllowedCompetitorReset=Успешно разрешена перезагрузка участников {0}. +successfullyAllowedCompetitorReset=Успешно разрешена перезагрузка участников: {0}. id=Ид. allowReload=Разрешить перезагрузку compress=Сжать @@ -976,12 +976,13 @@ successfullyRemoveIgtimiAccount=Успешно удалена учетная з eMailMustNotBeEmpty=Адрес эл. почты не должен быть пустым emailAddress=адрес эл. почты password=Пароль +searchTag=Тег поиска doYouReallyWantToRemoveLeaderboard=Действительно удалить таблицу лидеров ''{0}''? metaLeaderboardCannotBeChanged=Это метатаблица лидеров. Изменить ее здесь невозможно. failedToSetNewStartTime=Не удалось установить новое время старта. Возможно, такое же время старта уже присутствует в журнале гонки или кто-то добавить более новое (ид. прохождения и логическое время) время старта, чем добавленное вами. doYouReallyWantToRemoveIgtimiAccount=Действительно удалить учетную запись Igtimi, предоставляющую доступ к данным пользователя {0}? igtimiWindImport=Импортировать ветер из Igtimi -seeIgtimiTabForAccountSettings=Параметры учетной записи, определяющие учитываемые данные из Igtimi см. на вкладке ''Igtimi''. +seeIgtimiTabForAccountSettings=Параметры учетной записи, определяющие учитываемые данные из Igtimi, см. на вкладке ''Igtimi''. importWindFromIgtimi=Импортировать ветер из Igtimi errorImportingIgtimiWind=Ошибка при импорте данных о ветре из Igtimi: {0} resultFromIgtimiWindImport=Для перечисленных гонок импортировано следующее число замеров: {0} @@ -1008,6 +1009,7 @@ filterByLeaderboard=Фильтровать по таблице лидеров removeLeaderboardFilter=Удалить фильтр таблицы лидеров competitorRegistrations=Регистрации участников trackerStatus=Статус средства отслеживания +courseStatus=Статус курса canOnlyBeEditedBeforeStartingTracking=Редактирование возможно только до начала отслеживания defineCourse=Определить схему курса mapDevices=Сопоставить устройства с участниками и отметками @@ -1035,7 +1037,7 @@ setClosingTimePoint=Установите время, закрывающее ди startOfTimeRangeMustLieBeforeEnd=Начало диапазона времени должно быть раньше его окончания overallProgress=Общий ход выполнения eventOverview=Обзор события -start=Начать +start=Старт showAddedScores=Показать добавленные баллы для каждой записи в таблице лидеров ratio=отношение importWind=Импортировать ветер @@ -1053,8 +1055,8 @@ competitorPool=Пул участников selectAtLeastOne=Выберите не менее одного selectRaces=Выберите гонки importFixes=Импортировать замеры -noSailingServerInstancesYet=Удаленные инстанции сервера еще не зарегистрированы. -registeredSailingServerInstances=Зарегистрированные удаленные инстанции сервера +noSailingServerInstancesYet=Удаленные экземпляры сервера еще не зарегистрированы. +registeredSailingServerInstances=Зарегистрированные удаленные экземпляры сервера filterEventsByName=Фильтровать события по названию: leaderboardsExceptFromSelectedGroup=Таблицы лидеров (кроме выбранных групп) availableLeaderboardGroups=Доступные группы таблиц лидеров @@ -1110,7 +1112,7 @@ averageNumberOfOperationsPerMessage=Среднее число операций showUncorrectedNetPoints=Показать неисправленные точки сети setStartTimeReceived=Установить полученное время старта setStartTimeReceivedDescription=Устанавливает полученное время старта для выбранной отслеживаемой гонки, которое не хранится постоянно. Это значит, что после перезагрузки сервера новое значение будет забыто. -lastScoreCorrectionsTime=Последнее время исправления оценки +lastScoreCorrectionsTime=Время последнего исправления оценки lastScoreCorrectionsComment=Комментарий последнего исправления оценки setTimeToNow=Установить время "сейчас" useStartTimeInference=Использовать вывод о времени старта из прохождений стартовой отметки @@ -1119,7 +1121,7 @@ totalSize=Общий размер averageMessageSize=Средний размер сообщения в байтах totalNumberOfOperations=Общее число операций currentOrAverageSpeedOverGroundInKnotsTooltip=Текущая скорость относительно грунта в узлах во время прохождения отрезка; средняя скорость относительно грунта по завершении отрезка -currentOrAverageSpeedOverGroundInKnots=SOG (\u2205 в конце) +currentOrAverageSpeedOverGroundInKnots=SOG (∅ в конце) connectors=Соединители videoComponentShortName=Видео competitorSearchFilter=Фильтр поиска участников @@ -1182,7 +1184,7 @@ errorDeterminingPolarAvailability=Ошибка при определении д error=Ошибка fileStorage=Хранилище файлов active=Активно -scoringSchemeHighPointEssOverallDescription=Оценка в баллах. Победитель в акте получает 10 баллов, 2-й — 9 баллов, .... Любая ничья в общей оценке Extreme Sailing Series разрешается в пользу участника с наибольшим числом побед в актах. В случае равенства числа побед для разрешения используется последний акт. +scoringSchemeHighPointEssOverallDescription=Оценка в баллах. Победитель в акте получает 10 баллов, 2-й — 9 баллов, .... Любое равенство в общей оценке Extreme Sailing Series разрешается в пользу участника с наибольшим числом побед в актах. В случае равенства числа побед для разрешения используется последний акт. timeBetweenRaceStartAndCompetitorStartInSeconds=Задержка старта timeBetweenRaceStartAndCompetitorStartInSecondsTooltip=Время между стартом гонки и временем, когда участник проходит путевую точку старта. Отрицательное время означает ранний старт (OCS). useInternalAlgorithm=Использовать внутренний алгоритм прохождения отметки @@ -1291,7 +1293,7 @@ any=Любое urlProviders=Поставщики результатов на базе URL-адресов pleaseSelectAURLProvider=Выберите поставщика результатов на базе URL-адреса... sampleURL=Образец URL-адреса: {0} -sapSportsOn=SAP Sports по адресу +sapSportsOn=SAP Sports в followSapSports=Отслеживать SAP Sports liveNow=Оперативные данные analyseRace=Анализировать гонку @@ -1392,7 +1394,7 @@ polarResultsPresenter=Полярный презентатор результат dataMiningRetrieval=Извлечение данных actionWatch=Смотреть actionAnalyze=Анализировать -denoteAllRacesForRaceLogTrackingShorctut=Ярлык для отмена отслеживания журналов всех гонок +denoteAllRacesForRaceLogTrackingShorctut=Ярлык для отмены отслеживания журналов всех гонок registerCompetitors=Регистрация участников doYouWantToCreateADefaultRegattaLeaderboard=Создать таблицу лидеров регаты по умолчанию? copyCourse=Копировать курс @@ -1497,3 +1499,19 @@ fixPositionSuccessfullyEdited=Позиция замера успешно изм fixSuccessfullyRemoved=Замер успешно удален. dragToChangePosition=Позиция изменяется перетаскиванием\nДля удаления вызовите меню щелчком правой кнопкой мыши moveFix=Переместить замер +manage2SailEvent=Событие Manage2Sail +noEventsYet=Событий еще нет. +selectResultImportProvider=Выберите поставщика импорта результатов... +buildVersion=Версия сборки +serverInformation=Информация о сервере +serverConfiguration=Конфигурация сервера +standaloneServer=Изолированный сервер +send=Отправить... +passId=PassId +createdAt=Создано в +logicalTimepoint=Логическая отметка времени +info=Инфо +windFixes=Замеры ветра +twoXMark=2-отмет. +pagerStateInfo={0,number,#,###}–{1,number,#,###} из {2,number,#,###} +pagerStateInfo[false]={0,number,#,###}–{1,number,#,###} из {2,number,#,###} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties index 2eb21b55251..4754a13add1 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh.properties @@ -29,7 +29,7 @@ timeDelayHelp=要在任意时间重播,则留空;输入导入实况数据前 speedInKnots=速度 (kn) fromDeg=自(度) noRacesYet=暂无比赛轮次。 -total=\u03A3 +total=Σ carry=累积 competitor=参赛队 competitors=参赛队 @@ -72,7 +72,7 @@ collapse=- expand=+ leg=L distanceInMeters=距离 -averageSpeedInKnots=\u2205 速度 +averageSpeedInKnots=∅ 速度 averageSpeedInKnotsTooltip=此航段的平均实际速度,\n通过航行的距离除以所需的时间计算得出。 raceAverageSpeedInKnotsTooltip=此比赛轮次的平均实际速度,\n通过航行的距离除以所需的时间计算得出。 rankGain=名次进阶 @@ -320,9 +320,9 @@ racesInLeaderboard=积分榜中的比赛轮次 leaderboardsInGroup=分组中的积分榜 replication=复制 errorFetchingReplicaData=获取重复数据出错: {0} -averageAbsoluteCrossTrackErrorInMeters=\u2205 XTE +averageAbsoluteCrossTrackErrorInMeters=∅ XTE averageAbsoluteCrossTrackErrorInMetersTooltip=连接航段起点和终点的大圆弧的平均交叉轨道误差 -averageSignedCrossTrackErrorInMeters=\u2205 XTE +/- +averageSignedCrossTrackErrorInMeters=∅ XTE +/- averageSignedCrossTrackErrorInMetersTooltip=赛场中的平均位置 - 从场地中线开始计算的符号距离;0 代表场地中线;负数代表左侧,正数代表右侧。 raceAverageAbsoluteCrossTrackErrorInMetersTooltip=所有比赛轮次中迎风航段连接航段起点和终点的大圆弧的平均绝对交叉轨道误差,忽略参赛队所在的场地中线 raceAverageSignedCrossTrackErrorInMetersTooltip=所有比赛轮次中迎风航段连接航段起点和终点的大圆弧的符号平均交叉轨道误差;负数代表场地中线的左侧,正数代表右侧。 @@ -407,7 +407,7 @@ errorTryingToObtainLeaderboardContents=尝试获取积分榜内容出错: {0} lastScoreUpdate=上次得分更新 warningBrowserUnsupported=此网站未针对您的浏览器实施优化。请切换为更新浏览器,例如 Firefox 或 Chrome。 newMark=新标志 -controlPoint=标志/门标 +controlPoint=标志/门标名称 errorUpdatingRaceCourse=更新比赛轮次场地出错: {0} successfullyUpdatedCourse=已成功更新比赛轮次场地 selectTwoMarksForGate=选择两个标志,以创建门标 @@ -461,7 +461,7 @@ totalTimeSailedInSeconds=总时间 totalTimeSailedInSecondsTooltip=从比赛轮次开始时间算起,参赛队在比赛中航行的总时间。 totalDistanceTraveled=总距离 totalDistanceTraveledTooltip=比赛中航行的总距离。 -totalAverageSpeedOverGround=\u2205 实际速度 +totalAverageSpeedOverGround=∅ 实际速度 totalAverageSpeedOverGroundTooltip=比赛中的平均实际速度。 hhmmssUnit=hh:mm:ss actionAddWindData=添加风力数据 @@ -506,11 +506,11 @@ expandLastRace=始终展开显示的最后比赛轮次 factor=系数 errorUpdatingIsMedalRace=更新奖牌轮设置出错: {0} maneuverLoss=操作损失 -averageManeuverLossInMeters=\u2205 操作损失 +averageManeuverLossInMeters=∅ 操作损失 averageManeuverLossInMetersTooltip=任何操作中的平均距离损失。 -averageTackLossInMeters=\u2205 迎风转向损失 +averageTackLossInMeters=∅ 迎风转向损失 averageTackLossInMetersTooltip=迎风转向中的平均距离损失。 -averageJibeLossInMeters=\u2205 顺风转向损失 +averageJibeLossInMeters=∅ 顺风转向损失 averageJibeLossInMetersTooltip=顺风转向中的平均距离损失。 shape=形状 pattern=模式 @@ -611,7 +611,7 @@ passingInstructions=绕标说明 refreshNow=现在刷新 stopUpdating=停止更新 startUpdating=开始更新 -currentTime=当前时间: +currentTime=当前时间: tractracUsername=TracTrac 用户名 tractracPassword=TracTrac 密码 operatorEquals=等于 @@ -681,8 +681,8 @@ setup=设置 strength=强度 (kn) oscillation=振荡 oscillating=振荡 -amplitude=振幅 (Grad) -frequency=频率 (1/h) +amplitude=振幅 +frequency=频率 noise=噪声 sailing=航行 optimality=最优 @@ -838,8 +838,8 @@ lastFinished=最后完成 run=运行 times=次数 dataAmount=数据量 -averageCleanedServerTime=\u2205 清理服务器时间 -averageCleanedOverallTime=\u2205 清理总时间 +averageCleanedServerTime=∅ 清理服务器时间 +averageCleanedOverallTime=∅ 清理总时间 dataMiningBenchmarkResults=数据挖掘基准结果 serverTime=服务器时间 confidenceShouldBeBetween=置信度需要介于 0 和 1。 @@ -937,11 +937,13 @@ dontoverwrite=不要覆盖 setRacingProcedureConfiguration=设置比赛配置 racingProcedureConfiguration=比赛配置 activateIndividualRecall=激活个别召回 +activateResultEntry=激活结果条目 rrs26Start=RRS26 开始 setConfiguration=激活配置 classFlag=级别旗标 classFlagHelpText=设置用于{0}的级别旗标。 选择“不要覆盖”,激活程序的默认旗标。 individualRecallHelpText=激活后,裁判长能够执行个别召回。 +resultEntryHelpText=激活后,裁判长能够在结束阶段期间和之后输入结果。 startmodeFlags=起航模式旗标 startmodeFlagsHelpText=设置可用的起航模式旗标。选择无,启用程序的默认旗标。 gateStart=起航门标 @@ -974,7 +976,7 @@ successfullyRemoveIgtimiAccount=已成功移除用户{0}的 Igtimi 账户 eMailMustNotBeEmpty=电子邮件地址不得为空 emailAddress=电子邮件地址 password=密码 -searchTag=Search tag +searchTag=搜索标记 doYouReallyWantToRemoveLeaderboard=是否确定移除积分榜: ''{0}''? metaLeaderboardCannotBeChanged=此为元积分榜。无法在此处更改。 failedToSetNewStartTime=设置新开始时间失败。 相同的开始时间可能已存在于比赛轮次日志中或某人已添加(场次编号和逻辑时间)更新的开始时间。 @@ -1007,7 +1009,7 @@ filterByLeaderboard=按积分榜筛选 removeLeaderboardFilter=移除积分榜筛选器 competitorRegistrations=参赛队注册 trackerStatus=跟踪器状态 -courseStatus=Course Status +courseStatus=场地状态 canOnlyBeEditedBeforeStartingTracking=只可以在开始跟踪前编辑 defineCourse=定义场地设置 mapDevices=将设备映射到参赛队和标志 @@ -1119,7 +1121,7 @@ totalSize=总大小 averageMessageSize=各消息的平均大小(字节) totalNumberOfOperations=操作的总数 currentOrAverageSpeedOverGroundInKnotsTooltip=在航段航行时的当前实际速度(节);航段完成时的平均数实际速度 -currentOrAverageSpeedOverGroundInKnots=实际速度(\u2205 终点) +currentOrAverageSpeedOverGroundInKnots=实际速度(∅ 终点) connectors=连接器 videoComponentShortName=视频 competitorSearchFilter=参赛队搜索筛选器 @@ -1335,6 +1337,7 @@ gotoDesktop=进入桌面版 goBack=返回 showAllX=显示所有{0} collapseX=折叠{0} + numberOfGPSFixes=GPS 修复 fastestSailor=最快 numberWindFixes=风力修复 @@ -1468,8 +1471,6 @@ qrSync=QR-Sync thereIsNoIdentifierSet=未设置标识符。 couldNotObtainAccessTokenForUser=无法获取用户{0}的访问令牌 raceManagerDeviceName=设备名称 -playstoreBadge=https://play.google.com/intl/zh-cn/badges/images/generic/zh-cn-play-badge.png -appstoreBadgeSuffix=_zh serverURLInvalid=请输入有效的服务器 URL serverURLEmpty=服务器 URL 为空 marks=标志 @@ -1480,6 +1481,14 @@ confirmNewFix=确认新修复 selectAFixPositionBy=通过拖动地图选择修复位置。然后利用下方的按钮确认或取消。 pleaseSelectAMark=请从左侧的清单中选择一个标志。 markFixes=标志修复 +playstoreCampaign=utm_source=sapsailing&utm_medium=solutions&utm_content=2016.02&utm_campaign=solutions&pcampaignid=2016.sapsailing.solutions +playstoreTrackingApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.tracking.app +playstoreRacecommitteeApp=https://play.google.com/store/apps/details?id=com.sap.sailing.racecommittee.app +playStoreBuoypositioningApp=https://play.google.com/store/apps/details?id=com.sap.sailing.android.buoy.positioning.app +playstoreBadge=https://play.google.com/intl/en/badges/images/generic/en-play-badge.png +appstoreSapSailingInsight=http://appstore.com/sapsailinsight +appstoreStgTrainingstagebuch=http://appstore.com/stgtrainingstagebuch +appstoreBadgeSuffix=_en distanceFromAveragePosition=与平均位置的距离 useATouchOptimizedUI=使用触摸优化的 UI 移动修复 deleteFix=删除修复 @@ -1490,3 +1499,19 @@ fixPositionSuccessfullyEdited=已成功编辑修复位置。 fixSuccessfullyRemoved=已成功移除修复。 dragToChangePosition=拖动更改位置\n右键点击获取菜单以删除 moveFix=移动修复 +manage2SailEvent=Manage2Sail 活动 +noEventsYet=暂无活动。 +selectResultImportProvider=请选择结果导入提供方... +buildVersion=内部版本 +serverInformation=服务器信息 +serverConfiguration=服务器配置 +standaloneServer=独立服务器 +send=发送... +passId=场次编号 +createdAt=创建时间 +logicalTimepoint=逻辑时间点 +info=信息 +windFixes=风力修复 +twoXMark=2 个标志 +pagerStateInfo={0,number,#,###}-{1,number,#,###}/{2,number,#,###} +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###}/{2,number,#,###} 以上 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties index 3daacf366e9..4754a13add1 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/StringMessages_zh_CN.properties @@ -407,7 +407,7 @@ errorTryingToObtainLeaderboardContents=尝试获取积分榜内容出错: {0} lastScoreUpdate=上次得分更新 warningBrowserUnsupported=此网站未针对您的浏览器实施优化。请切换为更新浏览器,例如 Firefox 或 Chrome。 newMark=新标志 -controlPoint=标志/门标 +controlPoint=标志/门标名称 errorUpdatingRaceCourse=更新比赛轮次场地出错: {0} successfullyUpdatedCourse=已成功更新比赛轮次场地 selectTwoMarksForGate=选择两个标志,以创建门标 @@ -681,8 +681,8 @@ setup=设置 strength=强度 (kn) oscillation=振荡 oscillating=振荡 -amplitude=振幅 (Grad) -frequency=频率 (1/h) +amplitude=振幅 +frequency=频率 noise=噪声 sailing=航行 optimality=最优 @@ -976,7 +976,7 @@ successfullyRemoveIgtimiAccount=已成功移除用户{0}的 Igtimi 账户 eMailMustNotBeEmpty=电子邮件地址不得为空 emailAddress=电子邮件地址 password=密码 -searchTag=Search tag +searchTag=搜索标记 doYouReallyWantToRemoveLeaderboard=是否确定移除积分榜: ''{0}''? metaLeaderboardCannotBeChanged=此为元积分榜。无法在此处更改。 failedToSetNewStartTime=设置新开始时间失败。 相同的开始时间可能已存在于比赛轮次日志中或某人已添加(场次编号和逻辑时间)更新的开始时间。 @@ -1009,7 +1009,7 @@ filterByLeaderboard=按积分榜筛选 removeLeaderboardFilter=移除积分榜筛选器 competitorRegistrations=参赛队注册 trackerStatus=跟踪器状态 -courseStatus=Course Status +courseStatus=场地状态 canOnlyBeEditedBeforeStartingTracking=只可以在开始跟踪前编辑 defineCourse=定义场地设置 mapDevices=将设备映射到参赛队和标志 @@ -1499,3 +1499,19 @@ fixPositionSuccessfullyEdited=已成功编辑修复位置。 fixSuccessfullyRemoved=已成功移除修复。 dragToChangePosition=拖动更改位置\n右键点击获取菜单以删除 moveFix=移动修复 +manage2SailEvent=Manage2Sail 活动 +noEventsYet=暂无活动。 +selectResultImportProvider=请选择结果导入提供方... +buildVersion=内部版本 +serverInformation=服务器信息 +serverConfiguration=服务器配置 +standaloneServer=独立服务器 +send=发送... +passId=场次编号 +createdAt=创建时间 +logicalTimepoint=逻辑时间点 +info=信息 +windFixes=风力修复 +twoXMark=2 个标志 +pagerStateInfo={0,number,#,###}-{1,number,#,###}/{2,number,#,###} +pagerStateInfo[false]={0,number,#,###}-{1,number,#,###}/{2,number,#,###} 以上 diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/TimePanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/TimePanel.java index 4a9ba2b6dbf..026122860d9 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/TimePanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/client/TimePanel.java @@ -89,17 +89,20 @@ public class TimePanel extends SimplePanel implemen private static ClientResources resources = GWT.create(ClientResources.class); protected static TimePanelCss timePanelCss = TimePanelCssResources.INSTANCE.css(); + private final boolean forcePaddingRightToAlignToCharts; /** - * @param isScreenLargeEnoughToOfferChartSupport - * if true, the right padding will be set such that the time panel lines up with charts such - * as the competitor chart or the wind chart shown above it + * @param forcePaddingRightToAlignToCharts + * if true, the right padding will always be set such that the time panel lines up with + * charts such as the competitor chart or the wind chart shown above it, otherwise the padding depends + * on the flag set by {@link #setLiveGenerallyPossible(boolean)} */ public TimePanel(Timer timer, TimeRangeWithZoomProvider timeRangeProvider, StringMessages stringMessages, - boolean canReplayWhileLiveIsPossible, boolean isScreenLargeEnoughToOfferChartSupport) { + boolean canReplayWhileLiveIsPossible, boolean forcePaddingRightToAlignToCharts) { this.timer = timer; this.timeRangeProvider = timeRangeProvider; this.stringMessages = stringMessages; this.canReplayWhileLiveIsPossible = canReplayWhileLiveIsPossible; + this.forcePaddingRightToAlignToCharts = forcePaddingRightToAlignToCharts; timer.addTimeListener(this); timer.addPlayStateListener(this); timeRangeProvider.addTimeRangeChangeListener(this); @@ -111,9 +114,6 @@ public class TimePanel extends SimplePanel implemen timePanelSliderFlowWrapper = new FlowPanel(); timePanelSlider.setStyleName("timePanelSlider"); timePanelSlider.getElement().getStyle().setPaddingLeft(66, Unit.PX); - if (isScreenLargeEnoughToOfferChartSupport) { - timePanelSlider.getElement().getStyle().setPaddingRight(66, Unit.PX); - } timePanelSliderFlowWrapper.add(timePanelSlider); playSpeedImg = resources.timesliderPlaySpeedIcon(); @@ -555,8 +555,19 @@ public class TimePanel extends SimplePanel implemen */ protected void setLiveGenerallyPossible(boolean possible) { backToLivePlayButton.setVisible(possible); + updateTimeSliderPadding(possible); updatePlayPauseButtonsVisibility(timer.getPlayMode()); } + + private void updateTimeSliderPadding(boolean backToLivePlayButtonVisible) { + Style timePanelStyle = timePanelSlider.getElement().getStyle(); + if (backToLivePlayButtonVisible || forcePaddingRightToAlignToCharts) { + timePanelStyle.setPaddingRight(66, Unit.PX); + } else { + timePanelStyle.clearPaddingRight(); + } + timeSlider.onResize(); + } @SuppressWarnings("unchecked") public T getSettings() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/ResultSelectionAndApplyDialog.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/ResultSelectionAndApplyDialog.java index 3d77277f8ec..efb15971469 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/ResultSelectionAndApplyDialog.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/leaderboardedit/ResultSelectionAndApplyDialog.java @@ -78,7 +78,7 @@ public class ResultSelectionAndApplyDialog extends DataEntryDialog() { @Override public void onSuccess(RaceboardDataDTO result) { - // Determine if the screen is large enough to display charts such as the competitor chart or the wind chart. - // This decision is made once based on the initial screen height. Resizing the window afterwards will have - // no impact on the chart support, i.e. they are available/unavailable based on the initial decision. - boolean isScreenLargeEnoughToOfferChartSupport = Document.get().getClientHeight() >= 600; - checkUrlParameters(result, showMapControls, isScreenLargeEnoughToOfferChartSupport); + // Determine if the charts, such as the competitor chart or the wind chart, the edit marks + // panels, such as mark passing and mark position editors and manage media buttons should be shown. + // Automatic selection of attached video (if any) also depends on this flag. + // The decision is made once during initial page load based on the device type (mobile or not). + boolean showChartMarkEditMediaButtonsAndVideo = !DeviceDetector.isMobile(); + checkUrlParameters(result, showMapControls, showChartMarkEditMediaButtonsAndVideo); } @Override @@ -139,7 +140,7 @@ public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { } private void checkUrlParameters(RaceboardDataDTO raceboardData, boolean showMapControls, - boolean isScreenLargeEnoughToOfferChartSupport) { + boolean showChartMarkEditMediaButtonsAndVideo) { if (!raceboardData.isValidLeaderboard()) { createErrorPage(getStringMessages().noSuchLeaderboard()); return; @@ -170,7 +171,7 @@ public class RaceBoardEntryPoint extends AbstractSailingEntryPoint { RaceBoardPanel raceBoardPanel = new RaceBoardPanel(sailingService, mediaService, getUserService(), asyncActionsExecutor, raceboardData.getCompetitorAndTheirBoats(), timer, selectedRace.getRaceIdentifier(), leaderboardName, leaderboardGroupName, eventId, raceboardViewConfig, RaceBoardEntryPoint.this, getStringMessages(), userAgent, raceTimesInfoProvider, showMapControls, - isScreenLargeEnoughToOfferChartSupport); + showChartMarkEditMediaButtonsAndVideo); createRaceBoardInOneScreenMode(raceBoardPanel, raceboardViewConfig); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java index 0ea4c8f0b8c..bdbec06f3f0 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/raceboard/RaceBoardPanel.java @@ -37,7 +37,6 @@ import com.sap.sailing.gwt.ui.client.CompetitorColorProviderImpl; import com.sap.sailing.gwt.ui.client.CompetitorSelectionModel; import com.sap.sailing.gwt.ui.client.CompetitorSelectionProvider; import com.sap.sailing.gwt.ui.client.EntryPointLinkFactory; -import com.sap.sailing.gwt.ui.client.GlobalNavigationPanel; import com.sap.sailing.gwt.ui.client.LeaderboardUpdateListener; import com.sap.sailing.gwt.ui.client.MediaServiceAsync; import com.sap.sailing.gwt.ui.client.RaceTimePanel; @@ -133,17 +132,18 @@ public class RaceBoardPanel extends SimplePanel implements LeaderboardUpdateList * @param eventId * an optional event that can be used for "back"-navigation in case the race board shows a race in the * context of an event; may be null. - * @param isScreenLargeEnoughToOfferChartSupport - * if the screen is large enough to display charts such as the competitor chart or the wind chart, a - * padding is provided for the RaceTimePanel that aligns its right border with that of the charts, and - * the charts are created. This decision is made once on startup in the {@link RaceBoardEntryPoint} class. + * @param showChartMarkEditMediaButtonsAndVideo + * if true charts, such as the competitor chart or the wind chart, (as well as edit mark + * panels and manage media buttons) are shown and a padding is provided for the RaceTimePanel that + * aligns its right border with that of the chart. Otherwise those components will be hidden. + * This decision is made once on startup in the {@link RaceBoardEntryPoint} class. */ - public RaceBoardPanel(SailingServiceAsync sailingService, MediaServiceAsync mediaService, - UserService userService, AsyncActionsExecutor asyncActionsExecutor, Map competitorsAndTheirBoats, + public RaceBoardPanel(SailingServiceAsync sailingService, MediaServiceAsync mediaService, UserService userService, + AsyncActionsExecutor asyncActionsExecutor, Map competitorsAndTheirBoats, Timer timer, RegattaAndRaceIdentifier selectedRaceIdentifier, String leaderboardName, String leaderboardGroupName, UUID eventId, RaceBoardViewConfiguration raceboardViewConfiguration, - ErrorReporter errorReporter, final StringMessages stringMessages, - UserAgentDetails userAgent, RaceTimesInfoProvider raceTimesInfoProvider, boolean showMapControls, boolean isScreenLargeEnoughToOfferChartSupport) { + ErrorReporter errorReporter, final StringMessages stringMessages, UserAgentDetails userAgent, + RaceTimesInfoProvider raceTimesInfoProvider, boolean showMapControls, boolean showChartMarkEditMediaButtonsAndVideo) { this.sailingService = sailingService; this.mediaService = mediaService; this.stringMessages = stringMessages; @@ -220,7 +220,7 @@ public class RaceBoardPanel extends SimplePanel implements LeaderboardUpdateList leaderboardPanel.setTitle(stringMessages.leaderboard()); leaderboardPanel.getElement().getStyle().setMarginLeft(6, Unit.PX); leaderboardPanel.getElement().getStyle().setMarginTop(10, Unit.PX); - createOneScreenView(leaderboardName, leaderboardGroupName, eventId, mainPanel, showMapControls, isScreenLargeEnoughToOfferChartSupport, isScreenLargeEnoughToInitiallyDisplayLeaderboard, raceMap, userService); // initializes the raceMap field + createOneScreenView(leaderboardName, leaderboardGroupName, eventId, mainPanel, showMapControls, showChartMarkEditMediaButtonsAndVideo, isScreenLargeEnoughToInitiallyDisplayLeaderboard, raceMap, userService); // initializes the raceMap field leaderboardPanel.addLeaderboardUpdateListener(this); // in case the URL configuration contains the name of a competitors filter set we try to activate it // FIXME the competitorsFilterSets has now moved to CompetitorSearchTextBox (which should probably be renamed); pass on the parameters to the LeaderboardPanel and see what it does with it @@ -234,52 +234,52 @@ public class RaceBoardPanel extends SimplePanel implements LeaderboardUpdateList } } timePanel = new RaceTimePanel(userService, timer, timeRangeWithZoomModel, stringMessages, raceTimesInfoProvider, - false, isScreenLargeEnoughToOfferChartSupport, selectedRaceIdentifier, getConfiguration().getInitialDurationAfterRaceStartInReplay()); + false, showChartMarkEditMediaButtonsAndVideo, selectedRaceIdentifier, getConfiguration().getInitialDurationAfterRaceStartInReplay()); timeRangeWithZoomModel.addTimeZoomChangeListener(timePanel); raceTimesInfoProvider.addRaceTimesInfoProviderListener(timePanel); } - /** - * @param event - * an optional event; may be null or else can be used to show some context information in - * the {@link GlobalNavigationPanel}. - * @param isScreenLargeEnoughToOfferChartSupport - * if the screen is large enough to display charts such as the competitor chart or the wind chart, a - * padding is provided for the RaceTimePanel that aligns its right border with that of the charts, and - * the charts are created. - */ private void createOneScreenView(String leaderboardName, String leaderboardGroupName, UUID event, FlowPanel mainPanel, - boolean showMapControls, boolean isScreenLargeEnoughToOfferChartSupport, boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard, + boolean showMapControls, boolean showChartMarkEditMediaButtonsAndVideo, boolean isScreenLargeEnoughToInitiallyDisplayLeaderboard, RaceMap raceMap, UserService userService) { // create the default leaderboard and select the right race raceTimesInfoProvider.addRaceTimesInfoProviderListener(raceMap); List> components = new ArrayList>(); - if (isScreenLargeEnoughToOfferChartSupport) { - competitorChart = new MultiCompetitorRaceChart(sailingService, asyncActionsExecutor, competitorSelectionProvider, selectedRaceIdentifier, + if (showChartMarkEditMediaButtonsAndVideo) { + competitorChart = new MultiCompetitorRaceChart(sailingService, asyncActionsExecutor, competitorSelectionProvider, selectedRaceIdentifier, timer, timeRangeWithZoomModel, stringMessages, errorReporter, true, true, leaderboardGroupName, leaderboardName); competitorChart.getEntryWidget().setTitle(stringMessages.competitorCharts()); competitorChart.setVisible(false); components.add(competitorChart); - windChart = new WindChart(sailingService, selectedRaceIdentifier, timer, timeRangeWithZoomModel, new WindChartSettings(), + + windChart = new WindChart(sailingService, selectedRaceIdentifier, timer, timeRangeWithZoomModel, new WindChartSettings(), stringMessages, asyncActionsExecutor, errorReporter, /* compactChart */ true); windChart.setVisible(false); windChart.getEntryWidget().setTitle(stringMessages.windChart()); components.add(windChart); } + editMarkPassingPanel = new EditMarkPassingsPanel(sailingService, selectedRaceIdentifier, stringMessages, competitorSelectionProvider, errorReporter, timer); - editMarkPassingPanel.setLeaderboard(leaderboardPanel.getLeaderboard()); - editMarkPassingPanel.getEntryWidget().setTitle(stringMessages.editMarkPassings()); - components.add(editMarkPassingPanel); - editMarkPositionPanel = new EditMarkPositionPanel(raceMap, leaderboardPanel, selectedRaceIdentifier, leaderboardName, stringMessages, sailingService, timer, timeRangeWithZoomModel, - asyncActionsExecutor, errorReporter); - editMarkPositionPanel.setLeaderboard(leaderboardPanel.getLeaderboard()); - components.add(editMarkPositionPanel); - boolean autoSelectMedia = getConfiguration().isAutoSelectMedia(); + if (showChartMarkEditMediaButtonsAndVideo) { + editMarkPassingPanel.setLeaderboard(leaderboardPanel.getLeaderboard()); + editMarkPassingPanel.getEntryWidget().setTitle(stringMessages.editMarkPassings()); + components.add(editMarkPassingPanel); + } + + editMarkPositionPanel = new EditMarkPositionPanel(raceMap, leaderboardPanel, selectedRaceIdentifier, leaderboardName, + stringMessages, sailingService, timer, timeRangeWithZoomModel, asyncActionsExecutor, errorReporter); + if (showChartMarkEditMediaButtonsAndVideo) { + editMarkPositionPanel.setLeaderboard(leaderboardPanel.getLeaderboard()); + components.add(editMarkPositionPanel); + } + + boolean autoSelectMedia = getConfiguration().isAutoSelectMedia() && showChartMarkEditMediaButtonsAndVideo; MediaPlayerManagerComponent mediaPlayerManagerComponent = new MediaPlayerManagerComponent( selectedRaceIdentifier, raceTimesInfoProvider, timer, mediaService, userService, stringMessages, errorReporter, userAgent, this, autoSelectMedia); - leaderboardAndMapViewer = new SideBySideComponentViewer(leaderboardPanel, raceMap, mediaPlayerManagerComponent, components, stringMessages, userService, editMarkPassingPanel, editMarkPositionPanel); + leaderboardAndMapViewer = new SideBySideComponentViewer(leaderboardPanel, raceMap, mediaPlayerManagerComponent, + components, stringMessages, userService, editMarkPassingPanel, editMarkPositionPanel); this.setupUserManagementControlPanel(userService); componentViewers.add(leaderboardAndMapViewer); for (ComponentViewer componentViewer : componentViewers) { @@ -287,7 +287,7 @@ public class RaceBoardPanel extends SimplePanel implements LeaderboardUpdateList } boolean showLeaderboard = getConfiguration().isShowLeaderboard() && isScreenLargeEnoughToInitiallyDisplayLeaderboard; setLeaderboardVisible(showLeaderboard); - if (isScreenLargeEnoughToOfferChartSupport) { + if (showChartMarkEditMediaButtonsAndVideo) { setWindChartVisible(getConfiguration().isShowWindChart()); setCompetitorChartVisible(getConfiguration().isShowCompetitorsChart()); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 6cbd5d97fb3..d218e96760d 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -146,6 +146,7 @@ import com.sap.sailing.domain.base.impl.TeamImpl; import com.sap.sailing.domain.common.Bearing; import com.sap.sailing.domain.common.CourseDesignerMode; import com.sap.sailing.domain.common.DataImportProgress; +import com.sap.sailing.domain.common.DataImportSubProgress; import com.sap.sailing.domain.common.DetailType; import com.sap.sailing.domain.common.Distance; import com.sap.sailing.domain.common.LeaderboardNameConstants; @@ -157,6 +158,7 @@ import com.sap.sailing.domain.common.MaxPointsReason; import com.sap.sailing.domain.common.NoWindException; import com.sap.sailing.domain.common.NotFoundException; import com.sap.sailing.domain.common.PassingInstruction; +import com.sap.sailing.domain.common.PathType; import com.sap.sailing.domain.common.PolarSheetsXYDiagramData; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.RaceFetcher; @@ -429,7 +431,6 @@ import com.sap.sailing.server.operationaltransformation.UpdateServerConfiguratio import com.sap.sailing.server.operationaltransformation.UpdateSpecificRegatta; import com.sap.sailing.server.simulation.SimulationService; import com.sap.sailing.simulator.Path; -import com.sap.sailing.simulator.PathType; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationResults; import com.sap.sailing.simulator.TimedPositionWithSpeed; @@ -1017,7 +1018,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S raceRecords = getTracTracAdapter().getTracTracRaceRecords(new URL(eventJsonURL), /*loadClientParam*/ false); List result = new ArrayList(); for (RaceRecord raceRecord : raceRecords.getB()) { - if (listHiddenRaces == false && raceRecord.getRaceStatus().equals(TracTracConnectionConstants.HIDDEN_STATUS)) { + if (listHiddenRaces == false && raceRecord.getRaceVisibility().equals(TracTracConnectionConstants.HIDDEN_VISIBILITY)) { continue; } @@ -1493,7 +1494,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S PathDTO[] pathDTOs = new PathDTO[noOfPaths]; int index = noOfPaths - 1; for (Entry entry : paths.entrySet()) { - pathDTOs[index] = new PathDTO(entry.getKey().getTxtId()); + pathDTOs[index] = new PathDTO(entry.getKey()); // fill pathDTO with path points where speed is true wind speed List wList = new ArrayList(); for (TimedPositionWithSpeed p : entry.getValue().getPathPoints()) { @@ -4596,7 +4597,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S public UUID importMasterData(final String urlAsString, final String[] groupNames, final boolean override, final boolean compress, final boolean exportWind, final boolean exportDeviceConfigurations) { final UUID importOperationId = UUID.randomUUID(); - getService().createOrUpdateDataImportProgressWithReplication(importOperationId, 0.0, "Initializing", 0.0); + getService().createOrUpdateDataImportProgressWithReplication(importOperationId, 0.0, DataImportSubProgress.INIT, 0.0); // Create a progress indicator for as long as the server gets data from the other server. // As soon as the server starts the import operation, a progress object will be built on every server Runnable masterDataImportTask = new Runnable() { @@ -4604,7 +4605,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S public void run() { long startTime = System.currentTimeMillis(); getService().createOrUpdateDataImportProgressWithReplication(importOperationId, 0.01, - "Setting up connection", 0.5); + DataImportSubProgress.CONNECTION_SETUP, 0.5); String query; try { query = createLeaderboardQuery(groupNames, compress, exportWind, exportDeviceConfigurations); @@ -4620,7 +4621,8 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S String path = "/sailingserver/spi/v1/masterdata/leaderboardgroups"; serverAddress = createUrl(base, path, query); connection = HttpUrlConnectionHelper.redirectConnection(serverAddress); - getService().createOrUpdateDataImportProgressWithReplication(importOperationId, 0.02, "Connecting", 0.5); + getService().createOrUpdateDataImportProgressWithReplication(importOperationId, 0.02, + DataImportSubProgress.CONNECTION_ESTABLISH, 0.5); if (compress) { InputStream timeoutExtendingInputStream = new TimeoutExtendingInputStream( connection.getInputStream(), connection); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java index d80af7aaee2..44332496f8e 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SimulatorServiceImpl.java @@ -19,6 +19,7 @@ import java.util.logging.Logger; import com.google.gwt.user.server.rpc.RemoteServiceServlet; import com.sap.sailing.domain.common.Bearing; import com.sap.sailing.domain.common.Distance; +import com.sap.sailing.domain.common.PathType; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.Speed; import com.sap.sailing.domain.common.SpeedWithBearing; @@ -61,7 +62,6 @@ import com.sap.sailing.server.simulation.SimulationServiceFactory; import com.sap.sailing.simulator.BoatClassProperties; import com.sap.sailing.simulator.Grid; import com.sap.sailing.simulator.Path; -import com.sap.sailing.simulator.PathType; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationParameters; import com.sap.sailing.simulator.Simulator; @@ -949,7 +949,7 @@ public class SimulatorServiceImpl extends RemoteServiceServlet implements Simula // NOTE: pathName convention is: sort-digit + "#" + path-name // pathsAndNames is TreeMap which ensures sorting - pathDTOs[index] = new PathDTO(entry.getKey().getTxtId()); + pathDTOs[index] = new PathDTO(entry.getKey()); // fill pathDTO with path points where speed is true wind speed List wList = new ArrayList(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/PathDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/PathDTO.java index 74282eb6d3f..94968dcdcea 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/PathDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/PathDTO.java @@ -2,19 +2,31 @@ package com.sap.sailing.gwt.ui.shared; import java.util.List; +import com.sap.sailing.domain.common.PathType; import com.sap.sailing.domain.common.dto.NamedDTO; public class PathDTO extends NamedDTO { - private static final long serialVersionUID = 2381814072763342186L; + private static final long serialVersionUID = 4926185583314563898L; private List points; private boolean algorithmTimedOut; private boolean mixedLeg; + private PathType pathType; - public PathDTO() { + protected PathDTO() { + } + + public PathDTO(PathType pathType) { + super(pathType.getTxtId()); + this.pathType = pathType; } public PathDTO(final String name) { super(name); + this.pathType = null; + } + + public PathType getPathType() { + return pathType; } public List getPoints() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/WindPatternDTO.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/WindPatternDTO.java index 6a6c74ad7f3..efc7d479912 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/WindPatternDTO.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/WindPatternDTO.java @@ -1,21 +1,24 @@ package com.sap.sailing.gwt.ui.shared; import com.sap.sailing.domain.common.dto.NamedDTO; +import com.sap.sailing.gwt.ui.simulator.windpattern.WindPattern; public class WindPatternDTO extends NamedDTO { - private static final long serialVersionUID = 2765611781878430790L; - private String displayName; + + private static final long serialVersionUID = 7533083343024427310L; + + private WindPattern pattern; public WindPatternDTO() { } + public WindPatternDTO(WindPattern pattern) { + super(pattern.name()); + this.pattern = pattern; + } - public WindPatternDTO(String name, String displayName) { - super(name); - this.displayName = displayName; - } - - public String getDisplayName() { - return displayName; + public WindPattern getPattern() { + return pattern; } + } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/PathNameFormatter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/PathNameFormatter.java new file mode 100644 index 00000000000..a08e751f583 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/PathNameFormatter.java @@ -0,0 +1,26 @@ +package com.sap.sailing.gwt.ui.shared.racemap; + +import com.sap.sailing.gwt.ui.client.StringMessages; +import com.sap.sailing.gwt.ui.shared.PathDTO; + +public class PathNameFormatter { + + private final StringMessages stringMessages; + + public PathNameFormatter(StringMessages stringMessages) { + this.stringMessages = stringMessages; + } + + public String format(PathDTO path) { + switch (path.getPathType()) { + case OMNISCIENT: return stringMessages.omniscient(); + case OPPORTUNIST_LEFT: return stringMessages.opportunistLeft(); + case OPPORTUNIST_RIGHT: return stringMessages.opportunistRight(); + case ONE_TURNER_LEFT: return stringMessages.oneTurnerLeft(); + case ONE_TURNER_RIGHT: return stringMessages.oneTurnerRight(); + } + // fallback, if no path type is specified + return path.getName(); + } + +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/RaceSimulationOverlay.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/RaceSimulationOverlay.java index 786a2b794c5..ed2ea9b35c6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/RaceSimulationOverlay.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/shared/racemap/RaceSimulationOverlay.java @@ -56,6 +56,7 @@ public class RaceSimulationOverlay extends FullCanvasOverlay { private final SailingServiceAsync sailingService; private final AsyncActionsExecutor asyncActionsExecutor; private final ColorPalette colors; + private final PathNameFormatter pathNameFormatter; private SimulatorResultsDTO simulationResult; private Boolean[] visiblePaths; private PathDTO racePath; @@ -70,6 +71,7 @@ public class RaceSimulationOverlay extends FullCanvasOverlay { this.stringMessages = stringMessages; this.asyncActionsExecutor = asyncActionsExecutor; this.colors = new ColorPaletteGenerator(); + this.pathNameFormatter = new PathNameFormatter(stringMessages); } public void updateLeg(int newLeg, boolean clearCanvas, long newVersion) { @@ -225,7 +227,7 @@ public class RaceSimulationOverlay extends FullCanvasOverlay { double timewidth = txtmet.getWidth(); double txtmaxwidth = 0.0; if (racePath != null) { - txtmet = context2d.measureText(racePath.getName().split("#")[1]); + txtmet = context2d.measureText(stringMessages.raceLeader()); txtmaxwidth = Math.max(txtmaxwidth, txtmet.getWidth()); } boolean containsTimeOut = false; @@ -238,7 +240,7 @@ public class RaceSimulationOverlay extends FullCanvasOverlay { if (path.getMixedLeg()) { containsMixedLeg = true; } - txtmet = context2d.measureText(path.getName().split("#")[1]); + txtmet = context2d.measureText(pathNameFormatter.format(path)); txtmaxwidth = Math.max(txtmaxwidth, txtmet.getWidth()); } double newwidth = 0; @@ -268,13 +270,13 @@ public class RaceSimulationOverlay extends FullCanvasOverlay { int canvasHeight = (int)Math.ceil(yOffset + rectHeight*(paths.length + (racePath == null? 0 : 1))); setCanvasSize(canvas, canvasWidth, canvasHeight); if (racePath != null) { - drawRectangleWithText(context2d, xOffset, yOffset, null, - racePath.getName().split("#")[1], getFormattedTime(racePath.getPathTime()), txtmaxwidth, timewidth, deltaTime, true); + drawRectangleWithText(context2d, xOffset, yOffset, null, stringMessages.raceLeader(), + getFormattedTime(racePath.getPathTime()), txtmaxwidth, timewidth, deltaTime, true); } for (PathDTO path : paths) { String timeText = (path.getMixedLeg() ? mixedLegText : (path.getAlgorithmTimedOut() ? algorithmTimedOutText : getFormattedTime(path.getPathTime()))); drawRectangleWithText(context2d, xOffset, yOffset + (paths.length-index-(racePath==null?1:0)) * rectHeight, this.colors.getColor(paths.length-1-index), - path.getName().split("#")[1], timeText, txtmaxwidth, timewidth, (path.getMixedLeg()?deltaMixedLeg:(path.getAlgorithmTimedOut()?deltaTimeOut:deltaTime)), visiblePaths[paths.length-1-index]); + pathNameFormatter.format(path), timeText, txtmaxwidth, timewidth, (path.getMixedLeg()?deltaMixedLeg:(path.getAlgorithmTimedOut()?deltaTimeOut:deltaTime)), visiblePaths[paths.length-1-index]); index++; } } @@ -350,12 +352,11 @@ public class RaceSimulationOverlay extends FullCanvasOverlay { simulationResult = result; PathDTO[] paths = result.getPaths(); if (result.getLegDuration() > 0) { - racePath = new PathDTO(); + racePath = new PathDTO("0#Race Leader"); List racePathPoints = new ArrayList(); racePathPoints.add(new SimulatorWindDTO(null, 0, 0, paths[0].getPoints().get(0).timepoint)); racePathPoints.add(new SimulatorWindDTO(null, 0, 0, paths[0].getPoints().get(0).timepoint + result.getLegDuration())); racePath.setPoints(racePathPoints); - racePath.setName("0#Race Leader"); } else { racePath = null; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/PathLegendCanvasOverlay.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/PathLegendCanvasOverlay.java index 43b0aa7834d..baa5898a268 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/PathLegendCanvasOverlay.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/PathLegendCanvasOverlay.java @@ -11,6 +11,7 @@ import com.google.gwt.i18n.client.TimeZone; import com.google.gwt.maps.client.MapWidget; import com.sap.sailing.domain.common.AbstractBearing; import com.sap.sailing.domain.common.impl.DegreeBearingImpl; +import com.sap.sailing.gwt.ui.client.StringMessages; import com.sap.sailing.gwt.ui.client.shared.racemap.CoordinateSystem; import com.sap.sailing.gwt.ui.simulator.racemap.FullCanvasOverlay; import com.sap.sailing.simulator.util.SailingSimulatorConstants; @@ -22,6 +23,8 @@ import com.sap.sailing.simulator.util.SailingSimulatorConstants; * */ public class PathLegendCanvasOverlay extends FullCanvasOverlay { + + private final StringMessages stringMessages; private List pathOverlays; @@ -43,13 +46,17 @@ public class PathLegendCanvasOverlay extends FullCanvasOverlay { private String algorithmTimedOutText = "out-of-bounds"; private String mixedLegText = "ambiguous wind"; - public PathLegendCanvasOverlay(MapWidget map, int zIndex, char mode, CoordinateSystem coordinateSystem) { + public PathLegendCanvasOverlay(MapWidget map, int zIndex, char mode, CoordinateSystem coordinateSystem, + StringMessages stringMessages) { super(map, zIndex, coordinateSystem); + this.stringMessages = stringMessages; this.mode = mode; if (this.mode == SailingSimulatorConstants.ModeEvent) { yOffset = 170; } setPathOverlays(null); + this.algorithmTimedOutText = stringMessages.simulationLegendAlgorithmTimedOutText(); + this.mixedLegText = stringMessages.simulationLegendMixedLegText(); } @Override @@ -138,7 +145,7 @@ public class PathLegendCanvasOverlay extends FullCanvasOverlay { double cY = yOffset + (pathOverlays.size()-1) * rectHeight + 75.0; context2d.setGlobalAlpha(0.80); context2d.setFillStyle("white"); - double bgWidth = 100.0; + double bgWidth = 120.0; double bgHeight = 70.0; if (this.curSpeed == 0.0) { bgHeight = 20.0; @@ -148,7 +155,7 @@ public class PathLegendCanvasOverlay extends FullCanvasOverlay { context2d.setFont(textFont); context2d.setFillStyle(textColor); //TextMetrics txtmet; - String cText = "Current: " + SimulatorMainPanel.formatSliderValue(curSpeed) + "kn"; + String cText = stringMessages.current() + ": " + stringMessages.knotsValue(curSpeed); txtmet = context2d.measureText(cText); double txtwidth = txtmet.getWidth(); context2d.fillText(cText, cX-(txtwidth/2.0), cY-25); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorEntryPoint.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorEntryPoint.java index 5128e0248f2..9c1cdcf21e6 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorEntryPoint.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorEntryPoint.java @@ -20,8 +20,6 @@ import com.sap.sse.gwt.resources.Highcharts; public class SimulatorEntryPoint extends AbstractSailingEntryPoint { - private final String titleName = "Strategy Simulator"; - private final SimulatorServiceAsync simulatorService = GWT.create(SimulatorService.class); private int xRes = 40; private int yRes = 20; @@ -184,7 +182,8 @@ public class SimulatorEntryPoint extends AbstractSailingEntryPoint { } private FlowPanel createLogoAndTitlePanel(SimulatorMainPanel simulatorPanel) { - LogoAndTitlePanel logoAndTitlePanel = new LogoAndTitlePanel(titleName, null, getStringMessages(), this, getUserService()); + LogoAndTitlePanel logoAndTitlePanel = new LogoAndTitlePanel(getStringMessages().strategySimulatorTitle(), null, + getStringMessages(), this, getUserService()); /*{ @Override public void onResize() { diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java index a6723a5f6a5..cd53e58fd14 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMainPanel.java @@ -54,7 +54,9 @@ import com.sap.sailing.gwt.ui.shared.SimulatorUISelectionDTO; import com.sap.sailing.gwt.ui.shared.WindFieldGenParamsDTO; import com.sap.sailing.gwt.ui.shared.WindPatternDTO; import com.sap.sailing.gwt.ui.simulator.windpattern.WindPatternDisplay; +import com.sap.sailing.gwt.ui.simulator.windpattern.WindPatternFormatter; import com.sap.sailing.gwt.ui.simulator.windpattern.WindPatternSetting; +import com.sap.sailing.gwt.ui.simulator.windpattern.WindPatternSetting.SettingName; import com.sap.sailing.simulator.util.SailingSimulatorConstants; import com.sap.sse.common.Duration; import com.sap.sse.common.impl.MillisecondsDurationImpl; @@ -132,6 +134,8 @@ public class SimulatorMainPanel extends SimplePanel { private Button polarDiagramDialogCloseButton; private VerticalPanel polarDiv; private BoatClassDTO[] boatClasses = new BoatClassDTO[0]; + + private final WindPatternFormatter formatter; private class WindControlCapture implements ValueChangeHandler { @@ -149,14 +153,14 @@ public class SimulatorMainPanel extends SimplePanel { sliderBar.setTitle(SimulatorMainPanel.formatSliderValue(sliderBar.getCurrentValue())); logger.info("Slider value : " + arg0.getValue()); setting.setValue(arg0.getValue()); - if (setting.getDisplayName().equals("Base Bearing (Degrees)")) { + if (setting.getSettingName() == SettingName.BASE_BEARING_IN_DEGREES) { simulatorMap.clearOverlays(); //System.out.println("Wind Base Bearing: "+setting.getValue()); simulatorMap.getRegattaAreaCanvasOverlay().updateRaceCourse(1, (Double) setting.getValue()); simulatorMap.getRaceCourseCanvasOverlay().draw(); simulatorMap.getWindNeedleCanvasOverlay().draw(); } - if (setting.getDisplayName().equals("Race Course Diff (Degrees)")) { + if (setting.getSettingName() == SettingName.RACE_COURSE_DIFF_IN_DEGREES) { simulatorMap.clearOverlays(); //System.out.println("Wind Base Bearing: "+setting.getValue()); simulatorMap.getRegattaAreaCanvasOverlay().updateRaceCourse(2, (Double) setting.getValue()); @@ -234,6 +238,7 @@ public class SimulatorMainPanel extends SimplePanel { char event, boolean showGrid, boolean showLines, char seedLines, boolean showArrows, boolean showLineGuides, boolean showStreamlets, boolean showMapControls) { super(); + this.formatter = new WindPatternFormatter(stringMessages); this.macroWeather = streamletPars.macroWeather; this.simulatorSvc = svc; this.stringMessages = stringMessages; @@ -393,7 +398,7 @@ public class SimulatorMainPanel extends SimplePanel { HorizontalPanel optionsPanel = new HorizontalPanel(); optionsPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); - optionsPanel.setTitle("Optionsbar"); + optionsPanel.setTitle(stringMessages.optionsBar()); optionsPanel.getElement().setClassName("optionsPanel"); Label options = new Label(stringMessages.optionsBar()); options.getElement().setClassName("sectorHeadline"); @@ -418,7 +423,7 @@ public class SimulatorMainPanel extends SimplePanel { FlowPanel controlPanel = new FlowPanel(); FlowPanel controlPanelInnerWrapper = new FlowPanel(); controlPanelInnerWrapper.getElement().setClassName("controlPanelInnerWrapper"); - controlPanel.setTitle("Control Settings"); + controlPanel.setTitle(stringMessages.controlSettings()); controlPanel.getElement().setClassName("controlPanel"); controlPanel.getElement().setId("masterPanelLeft"); controlPanel.add(controlPanelInnerWrapper); @@ -433,7 +438,7 @@ public class SimulatorMainPanel extends SimplePanel { controlPanel.add(windPanel); windPanel.getElement().setClassName("windPanel"); - String windSetup = stringMessages.wind() + " " + stringMessages.setup(); + String windSetup = stringMessages.windSetup(); Label windSetupLabel = new Label(windSetup); windSetupLabel.getElement().setClassName("innerHeadline"); windPanel.add(windSetupLabel); @@ -454,8 +459,9 @@ public class SimulatorMainPanel extends SimplePanel { public void onSuccess(List patterns) { for (WindPatternDTO p : patterns) { if ((mode != SailingSimulatorConstants.ModeFreestyle)||(!p.getName().equals("MEASURED"))) { - patternSelector.addItem(p.getDisplayName()); - patternNameDTOMap.put(p.getDisplayName(), p); + String displayName = formatter.formatPattern(p.getPattern()); + patternSelector.addItem(displayName); + patternNameDTOMap.put(displayName, p); } } if (mode == SailingSimulatorConstants.ModeMeasured) { @@ -508,7 +514,7 @@ public class SimulatorMainPanel extends SimplePanel { private Panel getSliderPanel(Panel parentPanel, WindPatternSetting s) { - String labelName = s.getDisplayName(); + String labelName = formatter.formatSetting(s.getSettingName()); double minValue = (Double) s.getMin(); double maxValue = (Double) s.getMax(); double defaultValue = (Double) s.getDefault(); @@ -560,7 +566,7 @@ public class SimulatorMainPanel extends SimplePanel { HorizontalPanel mapOptions = new HorizontalPanel(); mapOptions.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE); mapOptions.setSize("100%", "45px"); - mapOptions.setTitle("Maps"); + mapOptions.setTitle(stringMessages.maps()); mapOptions.getElement().setClassName("mapOptions"); Label mapsLabel = new Label(stringMessages.maps()); @@ -701,7 +707,7 @@ public class SimulatorMainPanel extends SimplePanel { VerticalPanel sailingPanel = new VerticalPanel(); controlPanel.add(sailingPanel); sailingPanel.getElement().setClassName("sailingPanel"); - String sailingSetup = stringMessages.sailing() + " " + stringMessages.setup(); + String sailingSetup = stringMessages.sailingSetup(); Label sailingSetupLabel = new Label(sailingSetup); sailingSetupLabel.getElement().setClassName("innerHeadline"); @@ -897,7 +903,7 @@ public class SimulatorMainPanel extends SimplePanel { if (mode == SailingSimulatorConstants.ModeMeasured) { if (selectedLegIndex % 2 != 0) { - errorReporter.reportError("Downwind legs are NOT supported yet. Sorry about that :)"); + errorReporter.reportError(stringMessages.downwindLegsNotSupported()); } else { simulatorMap.removePolyline(); showTimePanel(false); @@ -968,7 +974,7 @@ public class SimulatorMainPanel extends SimplePanel { } }); - this.windDisplayButton = new RadioButton("Map Display Options", stringMessages.wind() + " " + stringMessages.display()); + this.windDisplayButton = new RadioButton("Map Display Options", stringMessages.windDisplay()); this.windDisplayButton.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent arg0) { @@ -1207,7 +1213,8 @@ public class SimulatorMainPanel extends SimplePanel { int index = 0; for (String legName : response) { - legSelector.addItem(legName + ((index % 2 == 0) ? " (upwind)" : " (downwind)")); + legSelector.addItem(legName + ((index % 2 == 0) ? " (" + stringMessages.upWind() + ")" + : " (" + stringMessages.downWind() + ")")); index++; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMap.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMap.java index b32bf914d2d..8e5e6f18c06 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMap.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/SimulatorMap.java @@ -50,6 +50,7 @@ import com.sap.sailing.gwt.ui.shared.WindFieldDTO; import com.sap.sailing.gwt.ui.shared.WindFieldGenParamsDTO; import com.sap.sailing.gwt.ui.shared.racemap.GoogleMapAPIKey; import com.sap.sailing.gwt.ui.shared.racemap.GoogleMapStyleHelper; +import com.sap.sailing.gwt.ui.shared.racemap.PathNameFormatter; import com.sap.sailing.gwt.ui.simulator.util.ColorPalette; import com.sap.sailing.gwt.ui.simulator.util.ColorPaletteGenerator; import com.sap.sailing.gwt.ui.simulator.util.SimulatorResources; @@ -110,7 +111,8 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ private static Logger LOGGER = Logger.getLogger(SimulatorMap.class.getName()); private static boolean SHOW_ONLY_PATH_POLYLINE = false; private char raceCourseDirection; - private final CoordinateSystem coordinateSystem; + private final CoordinateSystem coordinateSystem; + private final PathNameFormatter pathNameFormatter; public enum ViewName { SUMMARY, REPLAY, WINDDISPLAY @@ -182,7 +184,7 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ } ReplayPathCanvasOverlay replayPathCanvasOverlay = new ReplayPathCanvasOverlay(map, SimulatorMapOverlaysZIndexes.PATH_ZINDEX, - pathName.split("#")[1], timer, windParams, algorithmTimedOut, currentPath.getMixedLeg(), coordinateSystem); + pathNameFormatter.format(currentPath), timer, windParams, algorithmTimedOut, currentPath.getMixedLeg(), coordinateSystem); replayPathCanvasOverlays.add(replayPathCanvasOverlay); replayPathCanvasOverlay.setPathColor(colorPalette.getColor(Integer.parseInt(pathName.split("#")[0])-1)); @@ -337,6 +339,7 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ TimePanel timePanel, WindFieldGenParamsDTO windParams, SimpleBusyIndicator busyIndicator, char mode, SimulatorMainPanel parent, boolean showMapControls, CoordinateSystem coordinateSystem) { this.coordinateSystem = coordinateSystem; + this.pathNameFormatter = new PathNameFormatter(stringMessages); this.simulatorService = simulatorSvc; this.stringMessages = stringMessages; this.errorReporter = errorReporter; @@ -595,7 +598,7 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ windLineCanvasOverlay = new WindLineCanvasOverlay(map, SimulatorMapOverlaysZIndexes.WINDLINE_ZINDEX, timer, coordinateSystem); } replayPathCanvasOverlays = new ArrayList(); - legendCanvasOverlay = new PathLegendCanvasOverlay(map, SimulatorMapOverlaysZIndexes.PATHLEGEND_ZINDEX, mode, coordinateSystem); + legendCanvasOverlay = new PathLegendCanvasOverlay(map, SimulatorMapOverlaysZIndexes.PATHLEGEND_ZINDEX, mode, coordinateSystem, stringMessages); Window.addResizeHandler(new ResizeHandler() { @Override public void onResize(ResizeEvent event) { @@ -626,7 +629,7 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ public void generateWindField(final WindPatternDisplay windPatternDisplay, final boolean removeOverlays) { LOGGER.info("In generateWindField"); if (windPatternDisplay == null) { - errorReporter.reportError("Please select a valid wind pattern."); + errorReporter.reportError(stringMessages.pleaseSelectAValidWindPattern()); return; } @@ -745,7 +748,7 @@ public class SimulatorMap extends AbsolutePanel implements RequiresDataInitializ LOGGER.info("In generatePath"); if (windPatternDisplay == null) { - errorReporter.reportError("Please select a valid wind pattern."); + errorReporter.reportError(stringMessages.pleaseSelectAValidWindPattern()); return; } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/WindPatternFormatter.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/WindPatternFormatter.java new file mode 100644 index 00000000000..3b38aba5976 --- /dev/null +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/WindPatternFormatter.java @@ -0,0 +1,74 @@ +package com.sap.sailing.gwt.ui.simulator.windpattern; + +import com.sap.sailing.gwt.ui.client.StringMessages; + +public class WindPatternFormatter { + + private final StringMessages stringMessages; + + public WindPatternFormatter(StringMessages stringMessages) { + this.stringMessages = stringMessages; + } + + public String formatPattern(WindPattern pattern) { + switch (pattern) { + case BLASTS: return stringMessages.gusts(); + case MEASURED: return stringMessages.measured(); + case OSCILLATIONS: return stringMessages.oscillations(); + case OSCILLATION_WITH_BLASTS: return stringMessages.oscillationWithGusts(); + case NONE: return stringMessages.chooseAWindPattern(); + } + return null; + } + + public String formatSetting(WindPatternSetting.SettingName settingName) { + String unit = formatSettingUnit(settingName); + return formatSettingName(settingName) + (unit == null ? "" : (" (" + unit + ")")); + } + + private String formatSettingName(WindPatternSetting.SettingName settingName) { + switch (settingName) { + case BASE_BEARING_IN_DEGREES: return stringMessages.baseBearing(); + case RACE_COURSE_DIFF_IN_DEGREES: return stringMessages.raceCourseDiff(); + case BASE_SPEED_IN_KNOTS: return stringMessages.baseSpeed(); + case PROPABILITY_IN_PERCENT: return stringMessages.probability(); + case GUST_SIZE: return stringMessages.gustSize(); + case AVERAGE_SPEED_IN_PERCENT: return stringMessages.averageSpeed(); + case SPEED_VARIANCE_IN_PERCENT: return stringMessages.speedVariance(); + case AVERAGE_DIRECTION_IN_DEGREES: return stringMessages.averageDirection(); + case SPEED_LEFT_SIDE_IN_PERCENT: return stringMessages.speedLeftSide(); + case SPEED_MIDDLE_IN_PERCENT: return stringMessages.speedMiddle(); + case SPEED_RIGHT_SIDE_IN_PERECENT: return stringMessages.speedRightSide(); + case FREQUENCY_PER_HOURS: return stringMessages.frequency(); + case AMPLITUDE_IN_DEGREES: return stringMessages.amplitude(); + case CURRENT_SPEED_IN_KNOTS: return stringMessages.currentSpeed(); + case CURRENT_BEARING_IN_DEGREES: return stringMessages.currentBearing(); + } + return null; + } + + private String formatSettingUnit(WindPatternSetting.SettingName settingName) { + switch (settingName) { + case FREQUENCY_PER_HOURS: + return stringMessages.perHours(); + case BASE_SPEED_IN_KNOTS: + case CURRENT_SPEED_IN_KNOTS: + return stringMessages.knotsUnit(); + case BASE_BEARING_IN_DEGREES: + case RACE_COURSE_DIFF_IN_DEGREES: + case AVERAGE_DIRECTION_IN_DEGREES: + case AMPLITUDE_IN_DEGREES: + case CURRENT_BEARING_IN_DEGREES: + return stringMessages.degreesShort(); + case PROPABILITY_IN_PERCENT: + case AVERAGE_SPEED_IN_PERCENT: + case SPEED_VARIANCE_IN_PERCENT: + case SPEED_LEFT_SIDE_IN_PERCENT: + case SPEED_MIDDLE_IN_PERCENT: + case SPEED_RIGHT_SIDE_IN_PERECENT: + return stringMessages.percentUnit(); + case GUST_SIZE: + } + return null; + } +} diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/WindPatternSetting.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/WindPatternSetting.java index cc144bc25fd..ed6978443c5 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/WindPatternSetting.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/WindPatternSetting.java @@ -12,6 +12,13 @@ public interface WindPatternSetting extends Named { LISTBOX } + public enum SettingName { + BASE_BEARING_IN_DEGREES, RACE_COURSE_DIFF_IN_DEGREES, BASE_SPEED_IN_KNOTS, PROPABILITY_IN_PERCENT, GUST_SIZE, + AVERAGE_SPEED_IN_PERCENT, SPEED_VARIANCE_IN_PERCENT, AVERAGE_DIRECTION_IN_DEGREES, SPEED_LEFT_SIDE_IN_PERCENT, + SPEED_MIDDLE_IN_PERCENT, SPEED_RIGHT_SIDE_IN_PERECENT, FREQUENCY_PER_HOURS, AMPLITUDE_IN_DEGREES, + CURRENT_SPEED_IN_KNOTS, CURRENT_BEARING_IN_DEGREES; + } + public DisplayWidgetType getDisplayWidgetType(); public SettingsType getMin(); @@ -32,6 +39,6 @@ public interface WindPatternSetting extends Named { public SettingsType getValue(); - public String getDisplayName(); + public SettingName getSettingName(); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternDisplayManagerImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternDisplayManagerImpl.java index 82450651445..fddd696fae7 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternDisplayManagerImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternDisplayManagerImpl.java @@ -8,6 +8,7 @@ import com.sap.sailing.gwt.ui.simulator.windpattern.WindPattern; import com.sap.sailing.gwt.ui.simulator.windpattern.WindPatternDisplay; import com.sap.sailing.gwt.ui.simulator.windpattern.WindPatternDisplayManager; import com.sap.sailing.gwt.ui.simulator.windpattern.WindPatternSetting; +import com.sap.sailing.gwt.ui.simulator.windpattern.WindPatternSetting.SettingName; import com.sap.sailing.simulator.util.SailingSimulatorConstants; public class WindPatternDisplayManagerImpl implements WindPatternDisplayManager { @@ -30,7 +31,7 @@ public class WindPatternDisplayManagerImpl implements WindPatternDisplayManager if (((w == WindPattern.MEASURED)||(w == WindPattern.NONE))&&(mode == SailingSimulatorConstants.ModeEvent)) { continue; } - list.add(new WindPatternDTO(w.name(), w.getDisplayName())); + list.add(new WindPatternDTO(w)); } return list; } @@ -62,69 +63,69 @@ public class WindPatternDisplayManagerImpl implements WindPatternDisplayManager private void addBlastParameters(WindPatternDisplay display) { if (mode == SailingSimulatorConstants.ModeEvent) { WindPatternSetting windBearingSetting = new WindPatternSettingSliderBar("windBearing", - "Base Bearing (Degrees)", 0, 360, 5, 0, 36); + SettingName.BASE_BEARING_IN_DEGREES, 0, 360, 5, 0, 36); display.addSetting(windBearingSetting); WindPatternSetting baseWindBearing = new WindPatternSettingSliderBar("baseWindBearing", - "Race Course Diff (Degrees)", -20, 20, 1, 0, 10); + SettingName.RACE_COURSE_DIFF_IN_DEGREES, -20, 20, 1, 0, 10); display.addSetting(baseWindBearing); } WindPatternSetting windSpeedSetting = new WindPatternSettingSliderBar("baseWindSpeed", - "Base Speed (kn)", 2, 22, 0.5, 12, 10); + SettingName.BASE_SPEED_IN_KNOTS, 2, 22, 0.5, 12, 10); display.addSetting(windSpeedSetting); WindPatternSetting blastProbability = new WindPatternSettingSliderBar("blastProbability", - "Probability (%)", 0, 50, 5, 25, 10); + SettingName.PROPABILITY_IN_PERCENT, 0, 50, 5, 25, 10); display.addSetting(blastProbability); - WindPatternSetting maxBlastSize = new WindPatternSettingSliderBar("maxBlastSize", "Gust Size", 1, 10, - 1, 1, 10); + WindPatternSetting maxBlastSize = new WindPatternSettingSliderBar("maxBlastSize", + SettingName.GUST_SIZE, 1, 10, 1, 1, 10); display.addSetting(maxBlastSize); WindPatternSetting blastWindSpeed = new WindPatternSettingSliderBar("blastWindSpeed", - "Average Speed (%)", 0, 200, 5, 120, 10); + SettingName.AVERAGE_SPEED_IN_PERCENT, 0, 200, 5, 120, 10); display.addSetting(blastWindSpeed); WindPatternSetting blastWindSpeedVar = new WindPatternSettingSliderBar("blastWindSpeedVar", - "Speed Variance (%)", 1e-4, 100, 5, 10, 10); + SettingName.SPEED_VARIANCE_IN_PERCENT, 1e-4, 100, 5, 10, 10); display.addSetting(blastWindSpeedVar); } private void addOscillationParameters(WindPatternDisplay display) { if (mode == SailingSimulatorConstants.ModeEvent) { WindPatternSetting windBearingSetting = new WindPatternSettingSliderBar("windBearing", - "Base Bearing (Degrees)", 0, 360, 5, 0, 36); + SettingName.BASE_BEARING_IN_DEGREES, 0, 360, 5, 0, 36); display.addSetting(windBearingSetting); WindPatternSetting baseWindBearing = new WindPatternSettingSliderBar("baseWindBearing", - "Race Course Diff (Degrees)", -20, 20, 1, 0, 10); + SettingName.RACE_COURSE_DIFF_IN_DEGREES, -20, 20, 1, 0, 10); display.addSetting(baseWindBearing); WindPatternSetting windSpeedSetting = new WindPatternSettingSliderBar("baseWindSpeed", - "Base Speed (kn)", 2, 22, 1, 12, 10); + SettingName.BASE_SPEED_IN_KNOTS, 2, 22, 1, 12, 10); display.addSetting(windSpeedSetting); } else { WindPatternSetting windSpeedSetting = new WindPatternSettingSliderBar("baseWindSpeed", - "Base Speed (kn)", 2, 22, 1, 12, 10); + SettingName.BASE_SPEED_IN_KNOTS, 2, 22, 1, 12, 10); display.addSetting(windSpeedSetting); WindPatternSetting baseWindBearing = new WindPatternSettingSliderBar("baseWindBearing", - "Average Direction (Degrees)", -20, 20, 1, 0, 10); + SettingName.AVERAGE_DIRECTION_IN_DEGREES, -20, 20, 1, 0, 10); display.addSetting(baseWindBearing); } WindPatternSetting leftWindSpeed = new WindPatternSettingSliderBar("leftWindSpeed", - "Speed Left Side (%)", 0, 200, 5, 100, 10); + SettingName.SPEED_LEFT_SIDE_IN_PERCENT, 0, 200, 5, 100, 10); display.addSetting(leftWindSpeed); WindPatternSetting middleWindSpeed = new WindPatternSettingSliderBar("middleWindSpeed", - "Speed Middle (%)", 0, 200, 5, 100, 10); + SettingName.SPEED_MIDDLE_IN_PERCENT, 0, 200, 5, 100, 10); display.addSetting(middleWindSpeed); WindPatternSetting rightWindSpeed = new WindPatternSettingSliderBar("rightWindSpeed", - "Speed Right Side (%)", 0, 200, 5, 100, 10); + SettingName.SPEED_RIGHT_SIDE_IN_PERECENT, 0, 200, 5, 100, 10); display.addSetting(rightWindSpeed); WindPatternSetting frequency = new WindPatternSettingSliderBar("frequency", - "Frequency (per hr)", 0, 20, 0.5, 6, 10); + SettingName.FREQUENCY_PER_HOURS, 0, 20, 0.5, 6, 10); display.addSetting(frequency); WindPatternSetting amplitude = new WindPatternSettingSliderBar("amplitude", - "Amplitude (Degrees)", -30, 30, 1, 0, 10); + SettingName.AMPLITUDE_IN_DEGREES, -30, 30, 1, 0, 10); display.addSetting(amplitude); WindPatternSetting curSpeed = new WindPatternSettingSliderBar("curSpeed", - "Current Speed (kn)", 0, 2, 0.1, 0, 10); + SettingName.CURRENT_SPEED_IN_KNOTS, 0, 2, 0.1, 0, 10); display.addSetting(curSpeed); WindPatternSetting curBearing = new WindPatternSettingSliderBar("curBearing", - "Current Bearing (Degrees)", 0, 360, 5, 180, 36); + SettingName.CURRENT_BEARING_IN_DEGREES, 0, 360, 5, 180, 36); display.addSetting(curBearing); } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternSettingListBox.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternSettingListBox.java index 94ba7956d61..abd6116c048 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternSettingListBox.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternSettingListBox.java @@ -16,7 +16,7 @@ public class WindPatternSettingListBox implements WindPatternSetting, Is * name should match the corresponding field name from @WindControlParameters for which this control is being set */ private String name; - private String displayName; + private SettingName settingName; private String currentValue; private List values; @@ -27,9 +27,9 @@ public class WindPatternSettingListBox implements WindPatternSetting, Is } - public WindPatternSettingListBox(String name, String displayName) { + public WindPatternSettingListBox(String name, SettingName settingName) { this.name = name; - this.displayName = displayName; + this.settingName = settingName; this.values = new ArrayList(); } @@ -102,7 +102,7 @@ public class WindPatternSettingListBox implements WindPatternSetting, Is } @Override - public String getDisplayName() { - return displayName; + public SettingName getSettingName() { + return settingName; } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternSettingSliderBar.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternSettingSliderBar.java index 0be18ef3937..220b09e851f 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternSettingSliderBar.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/simulator/windpattern/impl/WindPatternSettingSliderBar.java @@ -15,7 +15,7 @@ public class WindPatternSettingSliderBar implements WindPatternSetting, * name should match the corresponding field name from @WindControlParameters for which this control is being set */ private String name; - private String displayName; + private SettingName settingName; private double currentValue; private double min; private double max; @@ -30,9 +30,10 @@ public class WindPatternSettingSliderBar implements WindPatternSetting, } - public WindPatternSettingSliderBar(String name, String displayName, double min, double max, double res, double defaultValue, int steps) { + public WindPatternSettingSliderBar(String name, SettingName settingName, double min, double max, double res, + double defaultValue, int steps) { this.name = name; - this.displayName = displayName; + this.settingName = settingName; this.min = min; this.max = max; this.res = res; @@ -108,7 +109,7 @@ public class WindPatternSettingSliderBar implements WindPatternSetting, } @Override - public String getDisplayName() { - return displayName; + public SettingName getSettingName() { + return settingName; } } diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/DataMining.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/DataMining.gwt.xml index 45a9266d71a..c27792efff1 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/DataMining.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/DataMining.gwt.xml @@ -27,9 +27,7 @@ - - - + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/EmbeddedMapAndWindChart.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/EmbeddedMapAndWindChart.gwt.xml index 8085c4deb08..d29023d89b3 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/EmbeddedMapAndWindChart.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/EmbeddedMapAndWindChart.gwt.xml @@ -25,10 +25,7 @@ - - - - + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Leaderboard.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Leaderboard.gwt.xml index 457ccb02bb7..dc3971ece1c 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Leaderboard.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Leaderboard.gwt.xml @@ -27,10 +27,7 @@ - - - - + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/RaceBoard.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/RaceBoard.gwt.xml index c5f537d6977..d5cefc995ab 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/RaceBoard.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/RaceBoard.gwt.xml @@ -25,9 +25,7 @@ - - - + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Simulator.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Simulator.gwt.xml index 7fe26ecafdf..68b20b2b961 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Simulator.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Simulator.gwt.xml @@ -23,10 +23,7 @@ - - - - + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Spectator.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Spectator.gwt.xml index c623e10ec66..abbf1f2b41b 100644 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Spectator.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/Spectator.gwt.xml @@ -25,9 +25,7 @@ - - - + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/VideoPopup.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/VideoPopup.gwt.xml index 198ef0c4d6d..b6c216cf123 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/VideoPopup.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/VideoPopup.gwt.xml @@ -23,7 +23,7 @@ - + diff --git a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/YoutubePopup.gwt.xml b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/YoutubePopup.gwt.xml index 2dc0c034b42..cd338aad587 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/YoutubePopup.gwt.xml +++ b/java/com.sap.sailing.gwt.ui/src/main/resources/com/sap/sailing/gwt/ui/YoutubePopup.gwt.xml @@ -21,10 +21,9 @@ - - + + - diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java index bfafde9194f..34e86e8a935 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java @@ -18,6 +18,7 @@ import com.sap.sailing.domain.abstractlog.AbstractLogEventAuthor; import com.sap.sailing.domain.abstractlog.race.RaceLog; import com.sap.sailing.domain.abstractlog.race.RaceLogStartTimeEvent; import com.sap.sailing.domain.abstractlog.race.analyzing.impl.RaceLogResolver; +import com.sap.sailing.domain.abstractlog.regatta.RegattaLog; import com.sap.sailing.domain.base.Competitor; import com.sap.sailing.domain.base.CompetitorStore; import com.sap.sailing.domain.base.CourseArea; @@ -40,6 +41,7 @@ import com.sap.sailing.domain.base.configuration.DeviceConfigurationIdentifier; import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher; import com.sap.sailing.domain.base.configuration.RegattaConfiguration; import com.sap.sailing.domain.common.DataImportProgress; +import com.sap.sailing.domain.common.DataImportSubProgress; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.RaceFetcher; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; @@ -548,12 +550,10 @@ public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetch DataImportLockWithProgress getDataImportLock(); DataImportProgress createOrUpdateDataImportProgressWithReplication(UUID importOperationId, - double overallProgressPct, - String subProgressName, double subProgressPct); + double overallProgressPct, DataImportSubProgress subProgress, double subProgressPct); DataImportProgress createOrUpdateDataImportProgressWithoutReplication(UUID importOperationId, - double overallProgressPct, - String subProgressName, double subProgressPct); + double overallProgressPct, DataImportSubProgress subProgress, double subProgressPct); void setDataImportFailedWithReplication(UUID importOperationId, String errorMessage); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java index 1de83e5335d..d8ff0a5aee6 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java @@ -91,6 +91,7 @@ import com.sap.sailing.domain.base.impl.EventImpl; import com.sap.sailing.domain.base.impl.RegattaImpl; import com.sap.sailing.domain.base.impl.RemoteSailingServerReferenceImpl; import com.sap.sailing.domain.common.DataImportProgress; +import com.sap.sailing.domain.common.DataImportSubProgress; import com.sap.sailing.domain.common.Distance; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; @@ -3034,19 +3035,19 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes @Override public DataImportProgress createOrUpdateDataImportProgressWithReplication(UUID importOperationId, - double overallProgressPct, String subProgressName, double subProgressPct) { + double overallProgressPct, DataImportSubProgress subProgress, double subProgressPct) { // Create/Update locally DataImportProgress progress = createOrUpdateDataImportProgressWithoutReplication(importOperationId, - overallProgressPct, subProgressName, subProgressPct); + overallProgressPct, subProgress, subProgressPct); // Create/Update on replicas - replicate(new CreateOrUpdateDataImportProgress(importOperationId, overallProgressPct, subProgressName, + replicate(new CreateOrUpdateDataImportProgress(importOperationId, overallProgressPct, subProgress, subProgressPct)); return progress; } @Override public DataImportProgress createOrUpdateDataImportProgressWithoutReplication(UUID importOperationId, - double overallProgressPct, String subProgressName, double subProgressPct) { + double overallProgressPct, DataImportSubProgress subProgress, double subProgressPct) { DataImportProgress progress = dataImportLock.getProgress(importOperationId); boolean newObject = false; if (progress == null) { @@ -3054,7 +3055,7 @@ public class RacingEventServiceImpl implements RacingEventService, ClearStateTes newObject = true; } progress.setOverAllProgressPct(overallProgressPct); - progress.setNameOfCurrentSubProgress(subProgressName); + progress.setCurrentSubProgress(subProgress); progress.setCurrentSubProgressPct(subProgressPct); if (newObject) { dataImportLock.addProgress(importOperationId, progress); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/masterdata/MasterDataImporter.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/masterdata/MasterDataImporter.java index c7da99ec747..c52613e5058 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/masterdata/MasterDataImporter.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/masterdata/MasterDataImporter.java @@ -12,6 +12,7 @@ import com.sap.sailing.domain.base.DomainFactory; import com.sap.sailing.domain.base.Regatta; import com.sap.sailing.domain.base.impl.MasterDataImportInformation; import com.sap.sailing.domain.base.impl.RegattaImpl; +import com.sap.sailing.domain.common.DataImportSubProgress; import com.sap.sailing.domain.common.MasterDataImportObjectCreationCount; import com.sap.sailing.domain.common.impl.MasterDataImportObjectCreationCountImpl; import com.sap.sailing.domain.masterdataimport.TopLevelMasterData; @@ -35,8 +36,8 @@ public class MasterDataImporter { ClassNotFoundException { ObjectInputStreamResolvingAgainstCache objectInputStream = racingEventService.getBaseDomainFactory() .createObjectInputStreamResolvingAgainstThisFactory(inputStream); - racingEventService - .createOrUpdateDataImportProgressWithReplication(importOperationId, 0.03, "Reading Data", 0.5); + racingEventService.createOrUpdateDataImportProgressWithReplication(importOperationId, 0.03, + DataImportSubProgress.TRANSFER_STARTED, 0.5); RaceLogStore raceLogStore = MongoRaceLogStoreFactory.INSTANCE.getMongoRaceLogStore( racingEventService.getMongoObjectFactory(), racingEventService.getDomainObjectFactory()); @@ -65,7 +66,7 @@ public class MasterDataImporter { } racingEventService.createOrUpdateDataImportProgressWithReplication(importOperationId, 0.3, - "Data-Transfer Complete, Initializing Import Operation", 0.5); + DataImportSubProgress.TRANSFER_COMPLETED, 0.5); applyMasterDataImportOperation(topLevelMasterData, importOperationId, override); } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateOrUpdateDataImportProgress.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateOrUpdateDataImportProgress.java index 9c0e037a209..9060f858d5c 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateOrUpdateDataImportProgress.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/CreateOrUpdateDataImportProgress.java @@ -2,30 +2,31 @@ package com.sap.sailing.server.operationaltransformation; import java.util.UUID; +import com.sap.sailing.domain.common.DataImportSubProgress; import com.sap.sailing.server.RacingEventService; import com.sap.sailing.server.RacingEventServiceOperation; public class CreateOrUpdateDataImportProgress extends AbstractRacingEventServiceOperation { - private static final long serialVersionUID = 7619972832594152077L; + private static final long serialVersionUID = 1980532937717924118L; private UUID importOperationId; private double overallProgressPct; - private String subProgressName; + private DataImportSubProgress subProgress; private double subProgressPct; - public CreateOrUpdateDataImportProgress(UUID importOperationId, double overallProgressPct, String subProgressName, - double subProgressPct) { + public CreateOrUpdateDataImportProgress(UUID importOperationId, double overallProgressPct, + DataImportSubProgress subProgress, double subProgressPct) { this.importOperationId = importOperationId; this.overallProgressPct = overallProgressPct; - this.subProgressName = subProgressName; + this.subProgress = subProgress; this.subProgressPct = subProgressPct; } @Override public Void internalApplyTo(RacingEventService toState) throws Exception { toState.createOrUpdateDataImportProgressWithoutReplication(importOperationId, overallProgressPct, - subProgressName, subProgressPct); + subProgress, subProgressPct); return null; } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/ImportMasterDataOperation.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/ImportMasterDataOperation.java index 533a37782af..f4c26059041 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/ImportMasterDataOperation.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/operationaltransformation/ImportMasterDataOperation.java @@ -26,6 +26,7 @@ import com.sap.sailing.domain.base.configuration.DeviceConfiguration; import com.sap.sailing.domain.base.configuration.DeviceConfigurationMatcher; import com.sap.sailing.domain.base.impl.RegattaImpl; import com.sap.sailing.domain.common.DataImportProgress; +import com.sap.sailing.domain.common.DataImportSubProgress; import com.sap.sailing.domain.common.RaceIdentifier; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; import com.sap.sailing.domain.common.Wind; @@ -96,10 +97,10 @@ public class ImportMasterDataOperation extends public MasterDataImportObjectCreationCountImpl internalApplyTo(RacingEventService toState) throws Exception { final DataImportLockWithProgress dataImportLock = toState.getDataImportLock(); this.progress = dataImportLock.getProgress(importOperationId); - progress.setNameOfCurrentSubProgress("Waiting for other data import operations to finish"); + progress.setCurrentSubProgress(DataImportSubProgress.IMPORT_WAIT); LockUtil.lockForWrite(dataImportLock); try { - progress.setNameOfCurrentSubProgress("Importing leaderboard groups"); + progress.setCurrentSubProgress(DataImportSubProgress.IMPORT_LEADERBOARD_GROUPS); progress.setCurrentSubProgressPct(0); int numOfGroupsToImport = masterData.getLeaderboardGroups().size(); int i = 0; @@ -108,7 +109,7 @@ public class ImportMasterDataOperation extends i++; progress.setCurrentSubProgressPct((double) i / numOfGroupsToImport); } - progress.setNameOfCurrentSubProgress("Updating Event-LeaderboardGroup links"); + progress.setCurrentSubProgress(DataImportSubProgress.UPDATE_EVENT_LEADERBOARD_GROUP_LINKS); progress.setOverAllProgressPct(0.4); progress.setCurrentSubProgressPct(0); final Iterable allEvents = masterData.getAllEvents(); @@ -119,7 +120,7 @@ public class ImportMasterDataOperation extends eventCounter++; progress.setCurrentSubProgressPct((double) eventCounter / numOfEventsToHandle); } - progress.setNameOfCurrentSubProgress("Importing wind tracks"); + progress.setCurrentSubProgress(DataImportSubProgress.IMPORT_WIND_TRACKS); progress.setOverAllProgressPct(0.5); progress.setCurrentSubProgressPct(0); createWindTracks(toState); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationService.java index 29bc4cafb08..2cb465addda 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationService.java @@ -4,8 +4,8 @@ import java.util.Map; import java.util.concurrent.ExecutionException; import com.sap.sailing.domain.common.LegIdentifier; +import com.sap.sailing.domain.common.PathType; import com.sap.sailing.simulator.Path; -import com.sap.sailing.simulator.PathType; import com.sap.sailing.simulator.SimulationParameters; import com.sap.sailing.simulator.SimulationResults; diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceImpl.java index 11b9da2822f..7946320eb4b 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/simulation/SimulationServiceImpl.java @@ -26,6 +26,7 @@ import com.sap.sailing.domain.common.LegIdentifier; import com.sap.sailing.domain.common.LegIdentifierImpl; import com.sap.sailing.domain.common.LegType; import com.sap.sailing.domain.common.NoWindException; +import com.sap.sailing.domain.common.PathType; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.RaceIdentifier; import com.sap.sailing.domain.common.RegattaAndRaceIdentifier; @@ -42,7 +43,6 @@ import com.sap.sailing.domain.tracking.TrackedRace; import com.sap.sailing.domain.tracking.impl.AbstractRaceChangeListener; import com.sap.sailing.server.RacingEventService; import com.sap.sailing.simulator.Path; -import com.sap.sailing.simulator.PathType; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationParameters; import com.sap.sailing.simulator.SimulationResults; diff --git a/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/SimulatorTest.java b/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/SimulatorTest.java index 5e126420003..43b08ca6c32 100644 --- a/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/SimulatorTest.java +++ b/java/com.sap.sailing.simulator.test/src/com/sap/sailing/simulator/test/SimulatorTest.java @@ -11,10 +11,10 @@ import junit.framework.Assert; import org.junit.Test; import com.sap.sailing.domain.common.Bearing; +import com.sap.sailing.domain.common.PathType; import com.sap.sailing.domain.common.Position; import com.sap.sailing.domain.common.impl.DegreePosition; import com.sap.sailing.simulator.Path; -import com.sap.sailing.simulator.PathType; import com.sap.sailing.simulator.PolarDiagram; import com.sap.sailing.simulator.SimulationParameters; import com.sap.sailing.simulator.impl.PolarDiagram49STG; diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/SimulationResults.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/SimulationResults.java index e26a93f4603..a22dc4a7034 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/SimulationResults.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/SimulationResults.java @@ -3,6 +3,7 @@ package com.sap.sailing.simulator; import java.util.Date; import java.util.Map; +import com.sap.sailing.domain.common.PathType; import com.sap.sailing.domain.common.Position; import com.sap.sse.common.TimePoint; diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/Simulator.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/Simulator.java index 03dfcd9043b..2b8744ef29f 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/Simulator.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/Simulator.java @@ -2,6 +2,7 @@ package com.sap.sailing.simulator; import java.util.List; +import com.sap.sailing.domain.common.PathType; import com.sap.sailing.simulator.impl.SparseSimulationDataException; diff --git a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SimulatorImpl.java b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SimulatorImpl.java index d993a4bd300..a4d76d0807d 100644 --- a/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SimulatorImpl.java +++ b/java/com.sap.sailing.simulator/src/com/sap/sailing/simulator/impl/SimulatorImpl.java @@ -4,8 +4,8 @@ import java.util.ArrayList; import java.util.List; import java.util.logging.Logger; +import com.sap.sailing.domain.common.PathType; import com.sap.sailing.simulator.Path; -import com.sap.sailing.simulator.PathType; import com.sap.sailing.simulator.RaceProperties; import com.sap.sailing.simulator.SimulationParameters; import com.sap.sailing.simulator.Simulator; diff --git a/java/com.sap.sailing.xrr.structureimport/src/com/sap/sailing/xrr/structureimport/StructureImporter.java b/java/com.sap.sailing.xrr.structureimport/src/com/sap/sailing/xrr/structureimport/StructureImporter.java index f132381a169..486b9a5b82f 100755 --- a/java/com.sap.sailing.xrr.structureimport/src/com/sap/sailing/xrr/structureimport/StructureImporter.java +++ b/java/com.sap.sailing.xrr.structureimport/src/com/sap/sailing/xrr/structureimport/StructureImporter.java @@ -31,6 +31,7 @@ import com.sap.sailing.domain.base.impl.RegattaImpl; import com.sap.sailing.domain.base.impl.SeriesImpl; import com.sap.sailing.domain.base.impl.TeamImpl; import com.sap.sailing.domain.common.FleetColors; +import com.sap.sailing.domain.common.LeaderboardNameConstants; import com.sap.sailing.domain.common.ScoringSchemeType; import com.sap.sailing.domain.ranking.OneDesignRankingMetric; import com.sap.sailing.xrr.resultimport.ParserFactory; @@ -161,7 +162,7 @@ public class StructureImporter { GuessFleetOrderingStrategy fleetOrderingStrategy = new GuessFleetOrderingFromFleetName(); String fleetColor = ""; if (fleets.size() <= 1) { - fleetColor = "Default"; + fleetColor = LeaderboardNameConstants.DEFAULT_FLEET_NAME; FleetImpl fleetImpl = new FleetImpl(fleetColor, 0, getColorFromString(fleetColor)); fleetsImpl.add(fleetImpl); } else { @@ -185,9 +186,6 @@ public class StructureImporter { if (result == null) { result = AbstractColor.getColorByLowercaseNameStatic(colorString.toLowerCase()); } - if (result == null) { - result = Color.BLACK; - } return result; } diff --git a/java/com.sap.sse.common.test/src/com/sap/sse/common/test/DummyTest.java b/java/com.sap.sse.common.test/src/com/sap/sse/common/test/DummyTest.java deleted file mode 100644 index e2720a64f54..00000000000 --- a/java/com.sap.sse.common.test/src/com/sap/sse/common/test/DummyTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.sap.sse.common.test; - -import static org.junit.Assert.assertEquals; - -import org.junit.Test; - -public class DummyTest { - @Test - public void testOneBeaufortLow() { - assertEquals(1, 1); - } -} diff --git a/java/com.sap.sse.common.test/src/com/sap/sse/common/test/TestUrlEncoding.java b/java/com.sap.sse.common.test/src/com/sap/sse/common/test/TestUrlEncoding.java new file mode 100644 index 00000000000..7cd3199e79c --- /dev/null +++ b/java/com.sap.sse.common.test/src/com/sap/sse/common/test/TestUrlEncoding.java @@ -0,0 +1,18 @@ +package com.sap.sse.common.test; + +import static org.junit.Assert.assertEquals; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.net.URLEncoder; + +import org.junit.Test; + +public class TestUrlEncoding { + @Test + public void testUrlQueryStringEncoding() throws UnsupportedEncodingException { + final String s = "Test & Test"; + assertEquals("Test+%26+Test", URLEncoder.encode(s, "UTF-8")); + assertEquals(s, URLDecoder.decode(URLEncoder.encode(s, "UTF-8"), "UTF-8")); + } +} diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/util/DoubleHolder.java b/java/com.sap.sse.common/src/com/sap/sse/common/util/DoubleHolder.java new file mode 100644 index 00000000000..485dc4a2b98 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/util/DoubleHolder.java @@ -0,0 +1,10 @@ +package com.sap.sse.common.util; + +public class DoubleHolder { + public double value; + + public DoubleHolder(double value) { + super(); + this.value = value; + } +} \ No newline at end of file diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/util/IntHolder.java b/java/com.sap.sse.common/src/com/sap/sse/common/util/IntHolder.java new file mode 100644 index 00000000000..15f8fa92158 --- /dev/null +++ b/java/com.sap.sse.common/src/com/sap/sse/common/util/IntHolder.java @@ -0,0 +1,10 @@ +package com.sap.sse.common.util; + +public class IntHolder { + public int value; + + public IntHolder(int value) { + super(); + this.value = value; + } +} \ No newline at end of file diff --git a/java/com.sap.sse.common/src/com/sap/sse/common/util/UrlHelper.java b/java/com.sap.sse.common/src/com/sap/sse/common/util/UrlHelper.java index 0b55cf0da1c..5dc875a3e8f 100644 --- a/java/com.sap.sse.common/src/com/sap/sse/common/util/UrlHelper.java +++ b/java/com.sap.sse.common/src/com/sap/sse/common/util/UrlHelper.java @@ -6,4 +6,6 @@ package com.sap.sse.common.util; */ public interface UrlHelper { String encodeUrl(String decodedUrlString); + + String encodeQueryString(String queryString); } diff --git a/java/com.sap.sse.datamining/META-INF/MANIFEST.MF b/java/com.sap.sse.datamining/META-INF/MANIFEST.MF index 9b3ee531aa3..d4a9e064d7b 100644 --- a/java/com.sap.sse.datamining/META-INF/MANIFEST.MF +++ b/java/com.sap.sse.datamining/META-INF/MANIFEST.MF @@ -24,6 +24,7 @@ Require-Bundle: com.sap.sse.datamining.shared;bundle-version="1.0.0", com.sap.sse.common;bundle-version="1.0.0", org.eclipse.osgi, com.sap.sse, + com.sap.sse.common, com.sap.sse.datamining.annotations;bundle-version="1.0.0" Bundle-Activator: com.sap.sse.datamining.impl.DataMiningFrameworkActivator Bundle-ActivationPolicy: lazy diff --git a/java/com.sap.sse.datamining/src/com/sap/sse/datamining/impl/components/aggregators/ParallelGroupedNumberDataAverageAggregationProcessor.java b/java/com.sap.sse.datamining/src/com/sap/sse/datamining/impl/components/aggregators/ParallelGroupedNumberDataAverageAggregationProcessor.java index 01a1f697f7c..5cc2d03ad6d 100644 --- a/java/com.sap.sse.datamining/src/com/sap/sse/datamining/impl/components/aggregators/ParallelGroupedNumberDataAverageAggregationProcessor.java +++ b/java/com.sap.sse.datamining/src/com/sap/sse/datamining/impl/components/aggregators/ParallelGroupedNumberDataAverageAggregationProcessor.java @@ -6,6 +6,9 @@ import java.util.Map; import java.util.Map.Entry; import java.util.concurrent.ExecutorService; +import org.omg.CORBA.DoubleHolder; +import org.omg.CORBA.IntHolder; + import com.sap.sse.datamining.components.AggregationProcessorDefinition; import com.sap.sse.datamining.components.Processor; import com.sap.sse.datamining.impl.components.GroupedDataEntry; @@ -14,24 +17,6 @@ import com.sap.sse.datamining.shared.GroupKey; public class ParallelGroupedNumberDataAverageAggregationProcessor extends AbstractParallelGroupedDataStoringAggregationProcessor { - private static class IntHolder { - public int value; - - public IntHolder(int value) { - super(); - this.value = value; - } - } - - private static class DoubleHolder { - public double value; - - public DoubleHolder(double value) { - super(); - this.value = value; - } - } - private static final AggregationProcessorDefinition DEFINITION = new SimpleAggregationProcessorDefinition<>(Number.class, Number.class, "Average", ParallelGroupedNumberDataAverageAggregationProcessor.class); diff --git a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/StringMessages.java b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/StringMessages.java index 1d9e16fbe5b..2d8abd2fa16 100755 --- a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/StringMessages.java +++ b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/StringMessages.java @@ -16,4 +16,6 @@ public interface StringMessages extends Messages { String removeUploadedFile(); String pleaseOnlyUploadContentYouHaveAllUsageRightsFor(); + + String send(); } diff --git a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/URLFieldWithFileUpload.java b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/URLFieldWithFileUpload.java index 5a48dfc4461..e01c3ee807e 100755 --- a/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/URLFieldWithFileUpload.java +++ b/java/com.sap.sse.gwt.adminconsole/src/com/sap/sse/gwt/adminconsole/URLFieldWithFileUpload.java @@ -97,7 +97,7 @@ public class URLFieldWithFileUpload extends Composite implements HasValue extends MultiSelectionModel implements RefreshableSelectionModel { final EntityIdentityComparator comp; - private boolean dontcheckSelectionState = false; + private boolean dontCheckSelectionState = false; private final ListDataProvider listDataProvider; /** @@ -72,7 +72,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel */ @Override public void setSelected(T item, boolean selected) { - if (comp == null || dontcheckSelectionState || item == null || getSelectedSet().isEmpty()) { + if (comp == null || dontCheckSelectionState || item == null || getSelectedSet().isEmpty()) { super.setSelected(item, selected); } else { T wasSelectedBefore = null; @@ -94,9 +94,9 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel } /** - * Refreshes the {@link RefreshableMultiSelectionModel} with the newObjects.All objects from the + * Refreshes the {@link RefreshableMultiSelectionModel} with the newObjects. All objects from the * current selection that {@link EntityIdentityComparator#representSameEntity(Object, Object) represent the same - * entity} as an object from newObjects will be reselected. All others are de-selected. That means a + * entity} as an object from newObjects will be reselected. All others are de-selected. That means if a * selected object is not contained in newObjects the object wouldn't be selected anymore. If this * selection model has no {@link EntityIdentityComparator} set, this method will use the {@link #equals(Object)} * method to compare. If an object is reselected it will be replaced with the new version of it. @@ -111,8 +111,8 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel */ @Override public void refreshSelectionModel(Iterable newObjects) { - if (!dontcheckSelectionState) { // avoid endless recursions - dontcheckSelectionState = true; + if (!dontCheckSelectionState) { // avoid endless recursions + dontCheckSelectionState = true; try { final Set selectedSet = getSelectedSet(); // gets the selected set as a non-live copy, so later // changes to the selection won't change this set anymore @@ -141,7 +141,7 @@ public class RefreshableMultiSelectionModel extends MultiSelectionModel SelectionChangeEvent.fire(this); } } finally { - dontcheckSelectionState = false; + dontCheckSelectionState = false; } } } diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_ja.properties b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_ja.properties new file mode 100644 index 00000000000..f2c627cd908 --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/StringMessages_ja.properties @@ -0,0 +1,27 @@ +save=保存 +remove=削除 +ok=OK +cancel=取消 +add=追加 +edit=編集 +serverError=サーバにアクセスしようとしてエラーが発生しました。ネットワーク接続を確認して、再試行してください。 +close=終了 +remoteProcedureCall=リモートプロシージャコール +serverReplies=サーバ応答 +errorCommunicatingWithServer=サーバとの通信でエラー発生 +active=有効 +setAsActive=有効に設定 +couldNotTestProperties=プロパティをテストできません +couldNotSetProperties=プロパティを設定できません +couldNotSetActiveService=有効なサービスを設定できません +couldNotLoadActiveService=有効なサービスをロードできません +couldNotLoadAvailableServices=利用可能なサービスをロードできません +refresh=リフレッシュ +name=名称 +value=値 +description=内容説明 +error=エラー +configuration=設定 +settings=設定 +settingsForComponent={0} の設定 +pleaseSelect=選択してください: diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ja.properties.xlf b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ja.properties.xlf new file mode 100644 index 00000000000..abd201c84fe --- /dev/null +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ja.properties.xlf @@ -0,0 +1,124 @@ + + + +
+ + H4sIAAAAAAAAAH1SzU4jMQy+5yn8BIgXyKEa2MuuAFEEZ5O4naiZZOR4WiG0746TtLNod+Hi/2/8eb4UPJLdqjFMU9b4sTmTD/b+p3GYHEU7NGfQe7vx3pAPYm/VmEJ8JL5lzmw3CagGkJ1bmMnDaQyRAEVomiWkPUgGl5OgE5CRoKOv4CESFgI3kjvAW14YEskp86FOJ3IScgJMHoTfAPcY0pVxMReyQ7WNudADZ0d+YRowxnaGEKxFqNUz30eaY6Bity0D7qlp7Ic8TUsKDivhlyBjH7LtRuXzqQsnbZ+PMHpT0L+3aU73yKb0WLcIYIE+YFxeor/L8kRFlNxMLJXKUMuQsv4YbcC8dlaAfua/87rqi/G+v/IPjv5CdDaNvDZX1K+M/itY1N63uCOGiK/xAi3/YC8DF3hR5XZMZVS1mjcJJ7J3aswR40L2uVrjqTgOc30G9uZP3AXryiiTtAv7hbFNDZ+zqkbVqwreg7Xyowk+50RJ1i7sVOn3699mbu9yS1GfoD0/0tIy8wFPN7vhNwMAAA== + + + + + +
+ + + Save + 保存 + + + Remove + 削除 + + + OK + OK + + + Cancel + 取消 + + + Add + 追加 + + + Edit + 編集 + + + An error occurred while attempting to contact the server. Please check your network connection and try again. + サーバにアクセスしようとしてエラーが発生しました。ネットワーク接続を確認して、再試行してください。 + + + Close + 終了 + + + Remote Procedure Call + リモートプロシージャコール + + + Server replies + サーバ応答 + + + Error communicating with server + サーバとの通信でエラー発生 + + + Active + 有効 + + + Set as active + 有効に設定 + + + Could not test properties + プロパティをテストできません + + + Could not set properties + プロパティを設定できません + + + Could not set active service + 有効なサービスを設定できません + + + Could not load active service + 有効なサービスをロードできません + + + Could not load available services + 利用可能なサービスをロードできません + + + Refresh + リフレッシュ + + + Name + 名称 + + + Value + 値 + + + Description + 内容説明 + + + Error + エラー + + + Configuration + 設定 + + + Settings + 設定 + + + Settings for {0} + {0} の設定 + + + Please select + 選択してください: + + +
+
\ No newline at end of file diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ru.properties.xlf b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ru.properties.xlf index 38633f9b8cb..3fc65e1cf81 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ru.properties.xlf +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_ru.properties.xlf @@ -1,6 +1,6 @@ - +
H4sIAAAAAAAAAH1SzU4jMQy+5yn8BIgXyKEa2MuuAFEEZ5O4naiZZOR4WiG0746TtLNod+Hi/2/8eb4UPJLdqjFMU9b4sTmTD/b+p3GYHEU7NGfQe7vx3pAPYm/VmEJ8JL5lzmw3CagGkJ1bmMnDaQyRAEVomiWkPUgGl5OgE5CRoKOv4CESFgI3kjvAW14YEskp86FOJ3IScgJMHoTfAPcY0pVxMReyQ7WNudADZ0d+YRowxnaGEKxFqNUz30eaY6Bity0D7qlp7Ic8TUsKDivhlyBjH7LtRuXzqQsnbZ+PMHpT0L+3aU73yKb0WLcIYIE+YFxeor/L8kRFlNxMLJXKUMuQsv4YbcC8dlaAfua/87rqi/G+v/IPjv5CdDaNvDZX1K+M/itY1N63uCOGiK/xAi3/YC8DF3hR5XZMZVS1mjcJJ7J3aswR40L2uVrjqTgOc30G9uZP3AXryiiTtAv7hbFNDZ+zqkbVqwreg7Xyowk+50RJ1i7sVOn3699mbu9yS1GfoD0/0tIy8wFPN7vhNwMAAA== diff --git a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_zh_CN.properties.xlf b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_zh_CN.properties.xlf index a011fb40119..81cbe1dffc3 100644 --- a/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_zh_CN.properties.xlf +++ b/java/com.sap.sse.gwt/src/com/sap/sse/gwt/client/controls/filestorage/XLF/StringMessages_zh_CN.properties.xlf @@ -1,6 +1,6 @@ - +
H4sIAAAAAAAAAH1SzU4jMQy+5yn8BIgXyKEa2MuuAFEEZ5O4naiZZOR4WiG0746TtLNod+Hi/2/8eb4UPJLdqjFMU9b4sTmTD/b+p3GYHEU7NGfQe7vx3pAPYm/VmEJ8JL5lzmw3CagGkJ1bmMnDaQyRAEVomiWkPUgGl5OgE5CRoKOv4CESFgI3kjvAW14YEskp86FOJ3IScgJMHoTfAPcY0pVxMReyQ7WNudADZ0d+YRowxnaGEKxFqNUz30eaY6Bity0D7qlp7Ic8TUsKDivhlyBjH7LtRuXzqQsnbZ+PMHpT0L+3aU73yKb0WLcIYIE+YFxeor/L8kRFlNxMLJXKUMuQsv4YbcC8dlaAfua/87rqi/G+v/IPjv5CdDaNvDZX1K+M/itY1N63uCOGiK/xAi3/YC8DF3hR5XZMZVS1mjcJJ7J3aswR40L2uVrjqTgOc30G9uZP3AXryiiTtAv7hbFNDZ+zqkbVqwreg7Xyowk+50RJ1i7sVOn3699mbu9yS1GfoD0/0tIy8wFPN7vhNwMAAA== @@ -13,111 +13,111 @@ Save - 保存 + 保存 Remove - 删除 + 删除 OK - 确定 + 确定 Cancel - 取消 + 取消 Add - 添加 + 添加 Edit - 编辑 + 编辑 An error occurred while attempting to contact the server. Please check your network connection and try again. - 尝试联系服务器时出错。请检查网络连接,并重试。 + 尝试联系服务器时出错。请检查网络连接,并重试。 Close - 关闭 + 关闭 Remote Procedure Call - 远程过程调用 + 远程过程调用 Server replies - 服务器应答 + 服务器应答 Error communicating with server - 与服务器通信时发生错误 + 与服务器通信时发生错误 Active - 已激活 + 已激活 Set as active - 设置为已激活 + 设置为已激活 Could not test properties - 无法测试属性 + 无法测试属性 Could not set properties - 无法设置属性 + 无法设置属性 Could not set active service - 无法设置已激活的服务 + 无法设置已激活的服务 Could not load active service - 无法加载已激活的服务 + 无法加载已激活的服务 Could not load available services - 无法加载可用的服务 + 无法加载可用的服务 Refresh - 刷新 + 刷新 Name - 名称 + 名称 Value - 值 + 值 Description - 描述 + 描述 Error - 错误 + 错误 Configuration - 配置 + 配置 Settings - 设置 + 设置 Settings for {0} - {0}设置 + {0}设置 Please select - 请选择 + 请选择 diff --git a/java/com.sap.sse.security.ui/.settings/org.eclipse.core.resources.prefs b/java/com.sap.sse.security.ui/.settings/org.eclipse.core.resources.prefs index 6a62863ba5d..985d2d5e1f6 100755 --- a/java/com.sap.sse.security.ui/.settings/org.eclipse.core.resources.prefs +++ b/java/com.sap.sse.security.ui/.settings/org.eclipse.core.resources.prefs @@ -2,6 +2,8 @@ eclipse.preferences.version=1 encoding//src/main/java/com/sap/sse/security/ui/client/UserService.java=utf-8 encoding//src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages.properties=UTF-8 encoding//src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_de.properties=UTF-8 +encoding//src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ja.properties=UTF-8 +encoding//src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties=UTF-8 encoding//src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh.properties=UTF-8 encoding//src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_zh_CN.properties=UTF-8 encoding//src/main/java/com/sap/sse/security/ui/login/LoginView.java=utf-8 diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ja.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ja.properties new file mode 100644 index 00000000000..5095f9318e6 --- /dev/null +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ja.properties @@ -0,0 +1,96 @@ +name=名前 +company=会社 +password=パスワード +passwordRepeat=パスワード繰返 +loggedIn=ユーザ {0} としてログインしました +signIn=サインイン +signUp=サインアップ +signOut=サインアウト +invalidUsername=無効なユーザ名 +couldNotSignOut=サインアウトできませんでした: {0} +createUser=ユーザ登録 +settings=設定 +filterUsers=ユーザのフィルタリング... +welcome={0} さん、ようこそ +tryingToVerifyUser=ユーザ照合を試行中です... +fetchingUserInformation=ユーザ情報を取り込んでいます... +signedInAs={0} としてサインイン +signedInAsTitle=サインインしたユーザ: +close=閉じる +loading=ロードしています... +user=ユーザ +users=ユーザ +addURLFilter=URL フィルタ追加 +ok=OK +cancel=取消 +username=ユーザ名 +email=電子メール +usernameMustHaveAtLeastNCharacters=少なくとも {0} 文字あるユーザ名を入力してください +passwordMustHaveAtLeastNCharacters=少なくとも {0} 文字あるパスワードを入力してください +passwordsDontMatch=パスワードが次と一致しなければなりません: +couldNotCreateUser=ユーザを登録できませんでした: {0} +changePassword=パスワード変更 +userDetails=ユーザ詳細 +deleteUser=ユーザ削除 +deleteUserQuestion=ユーザを削除しますか +doYouReallyWantToDeleteUser=ユーザ {0} を削除しますか +errorDeletingUser=ユーザの削除中にエラーが発生しました +account={0} アカウント +save=保存 +remove=削除 +add=追加 +edit=編集 +errorUpdatingRoles=ユーザ {0} のロール更新でエラー発生: {1} +errorUpdatingPermissions=ユーザ {0} の権限更新でエラー発生: {1} +enterRoleName=ロール名を入力... +enterPermissionName=権限名を入力... +errorCreatingUser=ユーザ名 {0} でのサインアップでエラー発生: {1} +signedUpSuccessfully=ユーザ {0} が正常にサインアップされました。電子メールをチェックして、電子メールアカウントを確認してください。 +unknownErrorCreatingUser=名前 {0} でユーザをサインアップしようとして未知のエラーが発生しました。その名前で別のユーザがすでに登録されている可能性があります。 +userAlreadyExists=名前 {0} のユーザはすでに登録されています +currentPassword=現在のパスワード +passwordSuccessfullyChanged=パスワードが変更されました +passwordDoesNotMeetRequirements=パスワードが要件を満たしていません +errorChangingPassword=パスワード変更でエラー発生: {0} +invalidCredentials=信用証明書が無効 +editEmail=電子メールアドレス編集 +errorUpdatingEmail=電子メールアドレス更新でエラー発生: {0} +successfullyUpdatedEmail=ユーザ {0} の電子メールアドレスが更新されました。\n入力チェックが {1} に送信されました。 +errorUpdatingUserProperties=ユーザプロパティ更新でエラー発生: {0} +successfullyUpdatedUserProperties=ユーザ {0} のプロパティが更新されました。 +forgotPassword=パスワードを忘れました +errorDuringPasswordReset=パスワードリセット中にエラー発生: {0} +cannotResetPasswordWithoutValidatedEmail=ユーザ {0} に確認済の電子メールアドレスがないためパスワードをリセットできません +passwordResetLinkSent=パスワードをリセットするためのリンクが {0} の確認済の電子メールアドレスに送信されました +errorValidatingEmail=ユーザ {0} の電子メールアドレス確認でエラー発生: {1} +emailValidatedSuccessfully=ユーザ {0} の電子メールアドレスが確認されました +errorResettingPassword=ユーザ {0} のパスワードリセットでエラー発生: {1} +successfullyResetPassword=ユーザ {0} のパスワードがリセットされました。電子メールが送信されました。 +failedToSignIn=サインインに失敗しました。信用証明書を確認してください。 +emailValidationUnsuccessful=ユーザ {0} の電子メールアドレスの確認に失敗しました。最新のチェックコードを使用しましたか。 +roles=ロール +permissions=権限 +signInWith=サインインのユーザ... +generalWelcome=ようこそ +newUser=初めての利用ですか +pleaseSignIn=サインインしてください +enterUsernameOrEmail=ユーザ名または電子メールアドレスを入力してください +serverError=サーバにアクセスしようとしてエラーが発生しました。ネットワーク接続を確認して、再試行してください。 +remoteProcedureCall=リモートプロシージャコール +serverReplies=サーバ応答 +errorCommunicatingWithServer=サーバとの通信でエラー発生 +editProfile=ユーザプロファイル編集 +allPermissions=全権限 (ロールから推定されるものを含む) +configuration=設定 +settingsForComponent={0} の設定 +pleaseSelect=選択してください: +oldPassword=現在のパスワード +newPasswordPlaceholder=新しいパスワードを入力してください +oldPasswordPlaceholder=現在のパスワードを入力してください +passwordRepeatPlaceholder=新しいパスワードを確認してください +resetPassword=パスワードのリセット +youAreNotSignedIn=現在はサインインしていません。 +youDontHaveRequiredPermission=必要な権限がありません。 +accountConfirmation=アカウント情報 +error=未知のエラー +refresh=リフレッシュ diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties index ebefc56379f..fe466b43f1c 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/StringMessages_ru.properties @@ -30,7 +30,7 @@ passwordMustHaveAtLeastNCharacters=Используйте пароль длин passwordsDontMatch=Пароли должны совпадать couldNotCreateUser=Не удалось создать пользователя: {0} changePassword=Изменить пароль -userDetails=Сведения пользователя +userDetails=Данные пользователя deleteUser=Удалить пользователя deleteUserQuestion=Удалить пользователя? doYouReallyWantToDeleteUser=Действительно удалить пользователя {0}? diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ja.properties.xlf b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ja.properties.xlf new file mode 100644 index 00000000000..8dc7a273a95 --- /dev/null +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ja.properties.xlf @@ -0,0 +1,400 @@ + + + +
+ + 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 + + + + + +
+ + + Name + 名前 + + + Company + 会社 + + + Password + パスワード + + + Repeat password + パスワード繰返 + + + Logged in as user {0}! + ユーザ {0} としてログインしました + + + Sign in + サインイン + + + Sign up + サインアップ + + + Sign out + サインアウト + + + Invalid username + 無効なユーザ名 + + + Could not sign out: {0} + サインアウトできませんでした: {0} + + + Create user + ユーザ登録 + + + Settings + 設定 + + + Filter users... + ユーザのフィルタリング... + + + Welcome, {0}! + {0} さん、ようこそ + + + Trying to verify user... + ユーザ照合を試行中です... + + + Fetching user information... + ユーザ情報を取り込んでいます... + + + Signed in as {0} + {0} としてサインイン + + + Signed in as: + サインインしたユーザ: + + + Close + 閉じる + + + Loading... + ロードしています... + + + User + ユーザ + + + Users + ユーザ + + + Add URL filter + URL フィルタ追加 + + + OK + OK + + + Cancel + 取消 + + + Username + ユーザ名 + + + e-mail + 電子メール + + + Please enter a username with at least {0} characters + 少なくとも {0} 文字あるユーザ名を入力してください + + + Please use a password that has at least {0} characters + 少なくとも {0} 文字あるパスワードを入力してください + + + Passwords must match + パスワードが次と一致しなければなりません: + + + Could not create user: {0} + ユーザを登録できませんでした: {0} + + + Change password + パスワード変更 + + + User details + ユーザ詳細 + + + Delete user + ユーザ削除 + + + Delete user? + ユーザを削除しますか + + + Do you really want to delete user {0}? + ユーザ {0} を削除しますか + + + An error happened while deleting the user + ユーザの削除中にエラーが発生しました + + + {0}-Account + {0} アカウント + + + Save + 保存 + + + Remove + 削除 + + + Add + 追加 + + + Edit + 編集 + + + Error updating roles for user {0}: {1} + ユーザ {0} のロール更新でエラー発生: {1} + + + Error updating permissions for user {0}: {1} + ユーザ {0} の権限更新でエラー発生: {1} + + + Enter role name... + ロール名を入力... + + + Enter permission name... + 権限名を入力... + + + Error signing up with username {0}: {1} + ユーザ名 {0} でのサインアップでエラー発生: {1} + + + Successfully signed up user {0}. Please check you e-mails to validate your e-mail account. + ユーザ {0} が正常にサインアップされました。電子メールをチェックして、電子メールアカウントを確認してください。 + + + Unknown error trying to sign up user with name {0}. Perhaps another user with that name already exists. + 名前 {0} でユーザをサインアップしようとして未知のエラーが発生しました。その名前で別のユーザがすでに登録されている可能性があります。 + + + A user with name {0} already exists + 名前 {0} のユーザはすでに登録されています + + + Current password + 現在のパスワード + + + Password successfully changed + パスワードが変更されました + + + Password does not meet requirements + パスワードが要件を満たしていません + + + Error changing password: {0} + パスワード変更でエラー発生: {0} + + + Invalid credentials + 信用証明書が無効 + + + Edit e-mail address + 電子メールアドレス編集 + + + Error updating e-mail address: {0} + 電子メールアドレス更新でエラー発生: {0} + + + Successfully updated e-mail address for user {0}.\nValidation check sent to {1}. + ユーザ {0} の電子メールアドレスが更新されました。\n入力チェックが {1} に送信されました。 + + + Error updating user properties: {0} + ユーザプロパティ更新でエラー発生: {0} + + + Successfully updated properties for user {0}. + ユーザ {0} のプロパティが更新されました。 + + + Forgot password + パスワードを忘れました + + + Error during password reset: {0} + パスワードリセット中にエラー発生: {0} + + + Cannot reset password because user {0} does not have a validated e-mail address + ユーザ {0} に確認済の電子メールアドレスがないためパスワードをリセットできません + + + A link for resetting the password has been sent to {0}''s validated e-mail address + パスワードをリセットするためのリンクが {0} の確認済の電子メールアドレスに送信されました + + + Error validating e-mail address of user {0}: {1} + ユーザ {0} の電子メールアドレス確認でエラー発生: {1} + + + e-mail address of user {0} validated successfully + ユーザ {0} の電子メールアドレスが確認されました + + + Error resetting password for user {0}: {1} + ユーザ {0} のパスワードリセットでエラー発生: {1} + + + Successfully reset password for user {0}. e-Mail has been sent. + ユーザ {0} のパスワードがリセットされました。電子メールが送信されました。 + + + Failed to sign in. Check your credentials. + サインインに失敗しました。信用証明書を確認してください。 + + + Validation of e-mail address for user {0} failed. Did you really use the latest validation code? + ユーザ {0} の電子メールアドレスの確認に失敗しました。最新のチェックコードを使用しましたか。 + + + Roles + ロール + + + Permissions + 権限 + + + Sign in with... + サインインのユーザ... + + + Welcome! + ようこそ + + + New here? + 初めての利用ですか + + + Please sign in. + サインインしてください + + + Enter user name or e-mail address + ユーザ名または電子メールアドレスを入力してください + + + An error occurred while attempting to contact the server. Please check your network connection and try again. + サーバにアクセスしようとしてエラーが発生しました。ネットワーク接続を確認して、再試行してください。 + + + Remote Procedure Call + リモートプロシージャコール + + + Server replies + サーバ応答 + + + Error communicating with server + サーバとの通信でエラー発生 + + + Edit user profile + ユーザプロファイル編集 + + + All permissions (including inferred from roles) + 全権限 (ロールから推定されるものを含む) + + + Configuration + 設定 + + + Settings for {0} + {0} の設定 + + + Please select + 選択してください: + + + Old password + 現在のパスワード + + + Type in a new password + 新しいパスワードを入力してください + + + Type in the old password + 現在のパスワードを入力してください + + + Repeat the new password + 新しいパスワードを確認してください + + + Reset password + パスワードのリセット + + + You are currently not signed in. + 現在はサインインしていません。 + + + You don''t have the required permission. + 必要な権限がありません。 + + + Account confirmation + アカウント情報 + + + Unknown error + 未知のエラー + + + Refresh + リフレッシュ + + +
+
\ No newline at end of file diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ru.properties.xlf b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ru.properties.xlf index bc8995542d0..bba0cbfc83a 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ru.properties.xlf +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_ru.properties.xlf @@ -1,6 +1,6 @@ - +
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 @@ -101,7 +101,7 @@ Add URL filter - Добавить фильтр URL-адреса + Добавить фильтр URL-адреса OK @@ -141,7 +141,7 @@ User details - Сведения пользователя + Данные пользователя Delete user diff --git a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_zh_CN.properties.xlf b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_zh_CN.properties.xlf index 9d7d6c91133..0a637913d25 100644 --- a/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_zh_CN.properties.xlf +++ b/java/com.sap.sse.security.ui/src/main/java/com/sap/sse/security/ui/client/i18n/XLF/StringMessages_zh_CN.properties.xlf @@ -1,6 +1,6 @@ - +
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 @@ -13,387 +13,387 @@ Name - 名称 + 名称 Company - 公司 + 公司 Password - 密码 + 密码 Repeat password - 重复密码 + 重复密码 Logged in as user {0}! - 以用户{0}登录! + 以用户{0}登录! Sign in - 登录 + 登录 Sign up - 注册 + 注册 Sign out - 注销 + 注销 Invalid username - 用户名无效 + 用户名无效 Could not sign out: {0} - 无法注销: {0} + 无法注销: {0} Create user - 创建用户 + 创建用户 Settings - 设置 + 设置 Filter users... - 筛选用户... + 筛选用户... Welcome, {0}! - {0}欢迎使用! + {0}欢迎使用! Trying to verify user... - 正在尝试验证用户... + 正在尝试验证用户... Fetching user information... - 正在获取用户信息... + 正在获取用户信息... Signed in as {0} - 以{0}登录 + 以{0}登录 Signed in as: - 登录身份: + 登录身份: Close - 关闭 + 关闭 Loading... - 正在加载... + 正在加载... User - 用户 + 用户 Users - 用户 + 用户 Add URL filter - 添加 URL 筛选器 + 添加 URL 筛选器 OK - 确定 + 确定 Cancel - 取消 + 取消 Username - 用户名 + 用户名 e-mail - 电子邮件 + 电子邮件 Please enter a username with at least {0} characters - 输入至少有 {0} 个字符的用户名 + 输入至少有 {0} 个字符的用户名 Please use a password that has at least {0} characters - 请使用至少有 {0} 个字符的密码 + 请使用至少有 {0} 个字符的密码 Passwords must match - 密码必须匹配 + 密码必须匹配 Could not create user: {0} - 无法创建用户: {0} + 无法创建用户: {0} Change password - 更改密码 + 更改密码 User details - 用户详细信息 + 用户详细信息 Delete user - 删除用户 + 删除用户 Delete user? - 是否删除用户? + 是否删除用户? Do you really want to delete user {0}? - 是否确定删除用户{0}? + 是否确定删除用户{0}? An error happened while deleting the user - 删除用户时出错 + 删除用户时出错 {0}-Account - {0} 账户 + {0} 账户 Save - 保存 + 保存 Remove - 移除 + 移除 Add - 添加 + 添加 Edit - 编辑 + 编辑 Error updating roles for user {0}: {1} - 更新用户{0}的角色出错: {1} + 更新用户{0}的角色出错: {1} Error updating permissions for user {0}: {1} - 更新用户{0}的权限出错: {1} + 更新用户{0}的权限出错: {1} Enter role name... - 输入角色名称... + 输入角色名称... Enter permission name... - 输入权限名称... + 输入权限名称... Error signing up with username {0}: {1} - 使用用户名{0}注册出错: {1} + 使用用户名{0}注册出错: {1} Successfully signed up user {0}. Please check you e-mails to validate your e-mail account. - 已成功注册用户{0}。 请检查电子邮件,验证您的电子邮件账户。 + 已成功注册用户{0}。 请检查电子邮件,验证您的电子邮件账户。 Unknown error trying to sign up user with name {0}. Perhaps another user with that name already exists. - 尝试使用名称{0}注册用户时发生未知错误。 可能已存在含此名称的用户。 + 尝试使用名称{0}注册用户时发生未知错误。 可能已存在含此名称的用户。 A user with name {0} already exists - 名称为{0}的用户已存在 + 名称为{0}的用户已存在 Current password - 当前密码 + 当前密码 Password successfully changed - 已成功更改密码 + 已成功更改密码 Password does not meet requirements - 密码不满足要求 + 密码不满足要求 Error changing password: {0} - 更改密码出错: {0} + 更改密码出错: {0} Invalid credentials - 凭据无效 + 凭据无效 Edit e-mail address - 编辑电子邮件地址 + 编辑电子邮件地址 Error updating e-mail address: {0} - 更新电子邮件地址出错: {0} + 更新电子邮件地址出错: {0} Successfully updated e-mail address for user {0}.\nValidation check sent to {1}. - 已成功更新用户{0}的电子邮件地址。\n验证检查已发送到{1}。 + 已成功更新用户{0}的电子邮件地址。\n验证检查已发送到{1}。 Error updating user properties: {0} - 更新用户属性出错: {0} + 更新用户属性出错: {0} Successfully updated properties for user {0}. - 已成功更新用户{0}的属性。 + 已成功更新用户{0}的属性。 Forgot password - 忘记密码 + 忘记密码 Error during password reset: {0} - 密码重置期间出错: {0} + 密码重置期间出错: {0} Cannot reset password because user {0} does not have a validated e-mail address - 用户{0}没有验证的电子邮件地址,无法重置密码 + 用户{0}没有验证的电子邮件地址,无法重置密码 A link for resetting the password has been sent to {0}''s validated e-mail address - 重置密码的链接已发送到{0}的验证的电子邮件地址 + 重置密码的链接已发送到{0}的验证的电子邮件地址 Error validating e-mail address of user {0}: {1} - 验证用户{0}的电子邮件地址出错: {1} + 验证用户{0}的电子邮件地址出错: {1} e-mail address of user {0} validated successfully - 用户{0}的电子邮件地址已成功验证 + 用户{0}的电子邮件地址已成功验证 Error resetting password for user {0}: {1} - 重置用户{0}的密码出错: {1} + 重置用户{0}的密码出错: {1} Successfully reset password for user {0}. e-Mail has been sent. - 已成功重置用户{0}的密码。已发送电子邮件。 + 已成功重置用户{0}的密码。已发送电子邮件。 Failed to sign in. Check your credentials. - 登录失败。请检查凭据。 + 登录失败。请检查凭据。 Validation of e-mail address for user {0} failed. Did you really use the latest validation code? - 用户{0}的电子邮件地址验证失败。 是否确定使用最新的验证码? + 用户{0}的电子邮件地址验证失败。 是否确定使用最新的验证码? Roles - 角色 + 角色 Permissions - 权限 + 权限 Sign in with... - 登录信息... + 登录信息... Welcome! - 欢迎使用! + 欢迎使用! New here? - 新内容? + 新内容? Please sign in. - 请登录。 + 请登录。 Enter user name or e-mail address - 输入用户名或电子邮件地址 + 输入用户名或电子邮件地址 An error occurred while attempting to contact the server. Please check your network connection and try again. - 尝试联系服务器时出错。请检查网络连接,并重试。 + 尝试联系服务器时出错。请检查网络连接,并重试。 Remote Procedure Call - 远程过程调用 + 远程过程调用 Server replies - 服务器应答 + 服务器应答 Error communicating with server - 与服务器通信时发生错误 + 与服务器通信时发生错误 Edit user profile - 编辑用户参数文件 + 编辑用户参数文件 All permissions (including inferred from roles) - 所有权限(包括从角色推断的权限) + 所有权限(包括从角色推断的权限) Configuration - 配置 + 配置 Settings for {0} - {0}设置 + {0}设置 Please select - 请选择 + 请选择 Old password - 旧密码 + 旧密码 Type in a new password - 输入新密码 + 输入新密码 Type in the old password - 输入旧密码 + 输入旧密码 Repeat the new password - 重复新密码 + 重复新密码 Reset password - 重置密码 + 重置密码 You are currently not signed in. - 您目前未登录。 + 您目前未登录。 You don''t have the required permission. - 您没有所需的权限。 + 您没有所需的权限。 Account confirmation - 账户确认 + 账户确认 Unknown error - 未知错误 + 未知错误 Refresh - 刷新 + 刷新 diff --git a/java/com.sap.sse.shared.android/src/com/sap/sse/util/impl/NonGwtUrlHelper.java b/java/com.sap.sse.shared.android/src/com/sap/sse/util/impl/NonGwtUrlHelper.java index 5a19f9d7fd8..b67b7f04b0a 100644 --- a/java/com.sap.sse.shared.android/src/com/sap/sse/util/impl/NonGwtUrlHelper.java +++ b/java/com.sap.sse.shared.android/src/com/sap/sse/util/impl/NonGwtUrlHelper.java @@ -1,15 +1,21 @@ package com.sap.sse.util.impl; +import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.net.URLEncoder; +import java.util.logging.Level; +import java.util.logging.Logger; import com.sap.sse.common.util.UrlHelper; public enum NonGwtUrlHelper implements UrlHelper { INSTANCE; + private static final Logger logger = Logger.getLogger(NonGwtUrlHelper.class.getName()); + public String encodeUrl(String decodedUrlString) { try { URL url = new URL(decodedUrlString); @@ -17,6 +23,17 @@ public enum NonGwtUrlHelper implements UrlHelper { url.getQuery(), url.getRef()); return uri.toString(); } catch (MalformedURLException | URISyntaxException f) { + logger.log(Level.WARNING, "problem encoding URL string "+decodedUrlString, f); + return null; + } + } + + @Override + public String encodeQueryString(String queryString) { + try { + return URLEncoder.encode(queryString, "UTF-8"); + } catch (UnsupportedEncodingException e) { + logger.log(Level.WARNING, "problem encoding URL query string "+queryString, e); return null; } } diff --git a/mobile/.gitignore b/mobile/.gitignore index eaa5cd3cc18..850c7ad7ed7 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -1,3 +1,4 @@ com.sap.sailing.android.tracking.app.test-test/bin/ /target -build.info \ No newline at end of file +build.info +assets_gen \ No newline at end of file diff --git a/mobile/com.sap.sailing.android.shared/.classpath b/mobile/com.sap.sailing.android.shared/.classpath index a0a969274af..e89fb998e31 100755 --- a/mobile/com.sap.sailing.android.shared/.classpath +++ b/mobile/com.sap.sailing.android.shared/.classpath @@ -1,16 +1,17 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/mobile/com.sap.sailing.android.shared/build.gradle b/mobile/com.sap.sailing.android.shared/build.gradle index 26ee1c69d88..82321d8bf32 100644 --- a/mobile/com.sap.sailing.android.shared/build.gradle +++ b/mobile/com.sap.sailing.android.shared/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.library' +apply plugin: 'com.getkeepsafe.dexcount' android { buildToolsVersion rootProject.ext.buildTools diff --git a/mobile/com.sap.sailing.android.shared/project.properties b/mobile/com.sap.sailing.android.shared/project.properties index fac622301eb..708e26d206c 100755 --- a/mobile/com.sap.sailing.android.shared/project.properties +++ b/mobile/com.sap.sailing.android.shared/project.properties @@ -16,3 +16,5 @@ android.library=true android.library.reference.1=../google-android-support-v7-appcompat_lib android.library.reference.2=../licensesdialog_lib/de.psdev.licensesdialog +android.library.reference.3=../google-android-support-v4 + diff --git a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/util/AppUtils.java b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/util/AppUtils.java index 64bff031398..8b51c6e4084 100644 --- a/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/util/AppUtils.java +++ b/mobile/com.sap.sailing.android.shared/src/com/sap/sailing/android/shared/util/AppUtils.java @@ -76,13 +76,21 @@ public class AppUtils { /** * Read the build.info from the asset folder, which is written at build time with git information * - * @return content of the build.info file or an empty string, if no file was found + * @return content of the build.info (or build_gradle.info) file or an empty string, if no file was found */ public String getBuildInfo() { String buildInfo = ""; BufferedReader reader = null; try { - reader = new BufferedReader(new InputStreamReader(mContext.getAssets().open("build.info"))); + String file = "build.info"; + String[] files = mContext.getAssets().list(""); + for (String item : files) { + if ("build_gradle.info".equals(item)) { + file = item; + break; + } + } + reader = new BufferedReader(new InputStreamReader(mContext.getAssets().open(file))); buildInfo = reader.readLine(); } catch (IOException e) { Log.d(TAG, "Can't open file with build info", e); diff --git a/mobile/com.sap.sailing.android.tracking.app/AndroidManifest.xml b/mobile/com.sap.sailing.android.tracking.app/AndroidManifest.xml index 9c8d7231f91..712306f5355 100644 --- a/mobile/com.sap.sailing.android.tracking.app/AndroidManifest.xml +++ b/mobile/com.sap.sailing.android.tracking.app/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="10" + android:versionName="1.8"> + android:versionCode="10" + android:versionName="1.8"> - - - - - - - - - - - - + + + + + + + + + + + + + + diff --git a/mobile/com.sap.sailing.racecommittee.app/AndroidManifest.xml b/mobile/com.sap.sailing.racecommittee.app/AndroidManifest.xml index d7d344dc0a8..452baff7a2c 100644 --- a/mobile/com.sap.sailing.racecommittee.app/AndroidManifest.xml +++ b/mobile/com.sap.sailing.racecommittee.app/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="16" + android:versionName="1.8"> com.h6ah4i.android.widget.advrecyclerview advrecyclerview - 0.6.2 + 0.7.4 aar diff --git a/mobile/com.sap.sailing.racecommittee.app/project.properties b/mobile/com.sap.sailing.racecommittee.app/project.properties index 5748d82c066..c16fb912f8d 100755 --- a/mobile/com.sap.sailing.racecommittee.app/project.properties +++ b/mobile/com.sap.sailing.racecommittee.app/project.properties @@ -12,8 +12,9 @@ # Project target. target=Google Inc.:Google APIs:22 -android.library.reference.1=../google-play-services_lib -android.library.reference.2=../com.sap.sailing.android.shared -android.library.reference.3=../google-android-support-v7-recyclerview_lib -android.library.reference.4=../github-advrecyclerview_lib -android.library.reference.5=../licensesdialog_lib/de.psdev.licensesdialog +android.library.reference.1=..\\google-play-services_lib +android.library.reference.2=..\\com.sap.sailing.android.shared +android.library.reference.3=..\\google-android-support-v7-recyclerview_lib +android.library.reference.4=..\\github-advrecyclerview_lib +android.library.reference.5=..\\licensesdialog_lib\\de.psdev.licensesdialog +android.library.reference.6=../google-android-support-v13 diff --git a/mobile/com.sap.sailing.racecommittee.app/res/values-de/strings.xml b/mobile/com.sap.sailing.racecommittee.app/res/values-de/strings.xml index 2394e54f3a8..d996574e796 100644 --- a/mobile/com.sap.sailing.racecommittee.app/res/values-de/strings.xml +++ b/mobile/com.sap.sailing.racecommittee.app/res/values-de/strings.xml @@ -78,13 +78,14 @@ Wenderichtung auswählen Strafe auswählen - - Backbord - Steuerbord - Gate - Linie - Ablauf - + Leer + Backbord + Steuerbord + Einzel, unbekannt + Gate + Linie + Ablauf + Feste Peilung Veröffentlichen Zurücknehmen diff --git a/mobile/com.sap.sailing.racecommittee.app/res/values/strings.xml b/mobile/com.sap.sailing.racecommittee.app/res/values/strings.xml index 56c8a122899..9e26b4bb2ea 100644 --- a/mobile/com.sap.sailing.racecommittee.app/res/values/strings.xml +++ b/mobile/com.sap.sailing.racecommittee.app/res/values/strings.xml @@ -91,13 +91,14 @@ Pick a rounding direction Select penalty reason - - Port - Starboard - Gate - Line - Offset - + None + Port + Starboard + Single, Unknown + Gate + Line + Offset + Fixed Bearing Publish Unpublish diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/BaseDraggableSwipeViewHolder.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/BaseDraggableSwipeViewHolder.java deleted file mode 100644 index b9ed2bb06cd..00000000000 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/BaseDraggableSwipeViewHolder.java +++ /dev/null @@ -1,72 +0,0 @@ -package com.sap.sailing.racecommittee.app.ui.adapters; - -import android.support.v7.widget.RecyclerView; -import android.view.View; - -import com.h6ah4i.android.widget.advrecyclerview.draggable.DraggableItemViewHolder; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemViewHolder; - -public abstract class BaseDraggableSwipeViewHolder extends RecyclerView.ViewHolder - implements DraggableItemViewHolder, SwipeableItemViewHolder { - - private int mDragStateFlags; - private int mSwipeStateFlags; - private int mSwipeResult = RecyclerViewSwipeManager.RESULT_NONE; - private int mAfterSwipeReaction = RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT; - private float mSwipeAmount; - - public BaseDraggableSwipeViewHolder(View itemView) { - super(itemView); - } - - @Override - public void setDragStateFlags(int flags) { - mDragStateFlags = flags; - } - - @Override - public int getDragStateFlags() { - return mDragStateFlags; - } - - @Override - public void setSwipeStateFlags(int flags) { - mSwipeStateFlags = flags; - } - - @Override - public int getSwipeStateFlags() { - return mSwipeStateFlags; - } - - @Override - public void setSwipeResult(int result) { - mSwipeResult = result; - } - - @Override - public int getSwipeResult() { - return mSwipeResult; - } - - @Override - public void setAfterSwipeReaction(int reaction) { - mAfterSwipeReaction = reaction; - } - - @Override - public int getAfterSwipeReaction() { - return mAfterSwipeReaction; - } - - @Override - public void setSwipeItemSlideAmount(float amount) { - mSwipeAmount = amount; - } - - @Override - public float getSwipeItemSlideAmount() { - return mSwipeAmount; - } -} diff --git a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/FinishListAdapter.java b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/FinishListAdapter.java index 4793a87b657..09625c7da3b 100644 --- a/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/FinishListAdapter.java +++ b/mobile/com.sap.sailing.racecommittee.app/src/com/sap/sailing/racecommittee/app/ui/adapters/FinishListAdapter.java @@ -2,6 +2,17 @@ package com.sap.sailing.racecommittee.app.ui.adapters; import java.util.List; +import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange; +import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; +import com.h6ah4i.android.widget.advrecyclerview.utils.AbstractDraggableSwipeableItemViewHolder; +import com.sap.sailing.android.shared.util.BitmapHelper; +import com.sap.sailing.android.shared.util.ViewHelper; +import com.sap.sailing.domain.common.MaxPointsReason; +import com.sap.sailing.racecommittee.app.R; +import com.sap.sailing.racecommittee.app.domain.impl.CompetitorResultWithIdImpl; +import com.sap.sailing.racecommittee.app.utils.ThemeHelper; + import android.content.Context; import android.graphics.drawable.Drawable; import android.support.v4.view.ViewCompat; @@ -10,16 +21,6 @@ import android.view.View; import android.view.ViewGroup; import android.widget.TextView; -import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange; -import com.h6ah4i.android.widget.advrecyclerview.draggable.RecyclerViewDragDropManager; -import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; -import com.sap.sailing.android.shared.util.BitmapHelper; -import com.sap.sailing.android.shared.util.ViewHelper; -import com.sap.sailing.domain.common.MaxPointsReason; -import com.sap.sailing.racecommittee.app.R; -import com.sap.sailing.racecommittee.app.domain.impl.CompetitorResultWithIdImpl; -import com.sap.sailing.racecommittee.app.utils.ThemeHelper; - public class FinishListAdapter extends BaseDraggableSwipeAdapter { private Context mContext; @@ -80,7 +81,7 @@ public class FinishListAdapter extends BaseDraggableSwipeAdapter { private final static String TAG = CourseElementAdapter.class.getName(); @@ -195,7 +195,7 @@ public class CourseElementAdapter extends BaseDraggableSwipeAdapter - - - - - - - - + + + + + + + + + + + + diff --git a/mobile/github-advrecyclerview_lib/AndroidManifest.xml b/mobile/github-advrecyclerview_lib/AndroidManifest.xml index 80ca8cddac4..7e6be5e490b 100644 --- a/mobile/github-advrecyclerview_lib/AndroidManifest.xml +++ b/mobile/github-advrecyclerview_lib/AndroidManifest.xml @@ -1,3 +1,4 @@ + + package="com.h6ah4i.android.widget.advrecyclerview" + android:versionCode="9" + android:versionName="0.7.4" > - - - - - + + + \ No newline at end of file diff --git a/mobile/github-advrecyclerview_lib/README b/mobile/github-advrecyclerview_lib/README new file mode 100755 index 00000000000..2d375229d0a --- /dev/null +++ b/mobile/github-advrecyclerview_lib/README @@ -0,0 +1,2 @@ +advrecyclerview-0.7.4 ist the last version that depends on Android 22.2.1 which is what we're currently using. +See http://jcenter.bintray.com/com/h6ah4i/android/widget/advrecyclerview/advrecyclerview/0.7.4/. \ No newline at end of file diff --git a/mobile/github-advrecyclerview_lib/project.properties b/mobile/github-advrecyclerview_lib/project.properties index eba516190a6..362a0a3082a 100644 --- a/mobile/github-advrecyclerview_lib/project.properties +++ b/mobile/github-advrecyclerview_lib/project.properties @@ -12,6 +12,4 @@ # Project target. target=android-22 -android.library.reference.1=../google-android-support-v7-recyclerview_lib android.library=true -android.library.reference.2=../google-android-support-v7-appcompat_lib diff --git a/mobile/github-advrecyclerview_lib/src/META-INF/MANIFEST.MF b/mobile/github-advrecyclerview_lib/src/META-INF/MANIFEST.MF new file mode 100755 index 00000000000..58630c02ef4 --- /dev/null +++ b/mobile/github-advrecyclerview_lib/src/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/BaseItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/BaseItemAnimator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java old mode 100644 new mode 100755 index f6f3aad8e2e..8d25e41ef1e --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/GeneralItemAnimator.java @@ -36,7 +36,7 @@ public abstract class GeneralItemAnimator extends BaseItemAnimator { private ItemChangeAnimationManager mChangeAnimationsManager; private ItemMoveAnimationManager mMoveAnimationsManager; - GeneralItemAnimator() { + protected GeneralItemAnimator() { setup(); } @@ -65,7 +65,7 @@ public abstract class GeneralItemAnimator extends BaseItemAnimator { @Override public boolean animateRemove(final RecyclerView.ViewHolder holder) { if (mDebug) { - Log.d(TAG, "animateRemove(id = " + holder.getItemId() + ", position = " + holder.getPosition() + ")"); + Log.d(TAG, "animateRemove(id = " + holder.getItemId() + ", position = " + holder.getLayoutPosition() + ")"); } return mRemoveAnimationManager.addPendingAnimation(holder); @@ -74,7 +74,7 @@ public abstract class GeneralItemAnimator extends BaseItemAnimator { @Override public boolean animateAdd(final RecyclerView.ViewHolder holder) { if (mDebug) { - Log.d(TAG, "animateAdd(id = " + holder.getItemId() + ", position = " + holder.getPosition() + ")"); + Log.d(TAG, "animateAdd(id = " + holder.getItemId() + ", position = " + holder.getLayoutPosition() + ")"); } return mAddAnimationsManager.addPendingAnimation(holder); @@ -83,7 +83,7 @@ public abstract class GeneralItemAnimator extends BaseItemAnimator { @Override public boolean animateMove(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { if (mDebug) { - Log.d(TAG, "animateMove(id = " + holder.getItemId() + ", position = " + holder.getPosition() + ", fromX = " + fromX + ", fromY = " + fromY + ", toX = " + toX + ", toY = " + toY + ")"); + Log.d(TAG, "animateMove(id = " + holder.getItemId() + ", position = " + holder.getLayoutPosition() + ", fromX = " + fromX + ", fromY = " + fromY + ", toX = " + toX + ", toY = " + toY + ")"); } return mMoveAnimationsManager.addPendingAnimation(holder, fromX, fromY, toX, toY); @@ -94,9 +94,9 @@ public abstract class GeneralItemAnimator extends BaseItemAnimator { RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) { if (mDebug) { final String oldId = (oldHolder != null) ? Long.toString(oldHolder.getItemId()) : "-"; - final String oldPosition = (oldHolder != null) ? Long.toString(oldHolder.getPosition()) : "-"; + final String oldPosition = (oldHolder != null) ? Long.toString(oldHolder.getLayoutPosition()) : "-"; final String newId = (newHolder != null) ? Long.toString(newHolder.getItemId()) : "-"; - final String newPosition = (newHolder != null) ? Long.toString(newHolder.getPosition()) : "-"; + final String newPosition = (newHolder != null) ? Long.toString(newHolder.getLayoutPosition()) : "-"; Log.d(TAG, "animateChange(old.id = " + oldId + ", old.position = " + oldPosition + ", new.id = " + newId + ", new.position = " + newPosition + ", fromX = " + fromX + ", fromY = " + fromY + ", toX = " + toX + ", toY = " + toY + ")"); diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java old mode 100644 new mode 100755 index fe6d8dbe6ca..ff74aa686a9 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/RefactoredDefaultItemAnimator.java @@ -48,7 +48,7 @@ public class RefactoredDefaultItemAnimator extends GeneralItemAnimator { /** * Item Animation manager for ADD operation (Same behavior as DefaultItemAnimator class) */ - private static class DefaultItemAddAnimationManager extends ItemAddAnimationManager { + protected static class DefaultItemAddAnimationManager extends ItemAddAnimationManager { public DefaultItemAddAnimationManager(BaseItemAnimator itemAnimator) { super(itemAnimator); @@ -93,7 +93,7 @@ public class RefactoredDefaultItemAnimator extends GeneralItemAnimator { /** * Item Animation manager for REMOVE operation (Same behavior as DefaultItemAnimator class) */ - private static class DefaultItemRemoveAnimationManager extends ItemRemoveAnimationManager { + protected static class DefaultItemRemoveAnimationManager extends ItemRemoveAnimationManager { public DefaultItemRemoveAnimationManager(BaseItemAnimator itemAnimator) { super(itemAnimator); @@ -137,7 +137,7 @@ public class RefactoredDefaultItemAnimator extends GeneralItemAnimator { /** * Item Animation manager for CHANGE operation (Same behavior as DefaultItemAnimator class) */ - private static class DefaultItemChangeAnimationManager extends ItemChangeAnimationManager { + protected static class DefaultItemChangeAnimationManager extends ItemChangeAnimationManager { public DefaultItemChangeAnimationManager(BaseItemAnimator itemAnimator) { super(itemAnimator); } @@ -220,7 +220,7 @@ public class RefactoredDefaultItemAnimator extends GeneralItemAnimator { /** * Item Animation manager for MOVE operation (Same behavior as DefaultItemAnimator class) */ - private static class DefaultItemMoveAnimationManager extends ItemMoveAnimationManager { + protected static class DefaultItemMoveAnimationManager extends ItemMoveAnimationManager { public DefaultItemMoveAnimationManager(BaseItemAnimator itemAnimator) { super(itemAnimator); diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/SwipeDismissItemAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/SwipeDismissItemAnimator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/AddAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/AddAnimationInfo.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/BaseItemAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/BaseItemAnimationManager.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ChangeAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ChangeAnimationInfo.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAddAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAddAnimationManager.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemAnimationInfo.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemChangeAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemChangeAnimationManager.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemMoveAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemMoveAnimationManager.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemRemoveAnimationManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/ItemRemoveAnimationManager.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/MoveAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/MoveAnimationInfo.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/RemoveAnimationInfo.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/animator/impl/RemoveAnimationInfo.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java old mode 100644 new mode 100755 index d638bf6f3f1..0c7e8fb4303 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/ItemShadowDecorator.java @@ -78,8 +78,6 @@ public class ItemShadowDecorator extends RecyclerView.ItemDecoration { } c.restoreToCount(savedCount); - - ViewCompat.postInvalidateOnAnimation(parent); } private static boolean shouldDrawDropShadow(View child) { diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/SimpleListDividerDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/decoration/SimpleListDividerDecorator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BaseDraggableItemDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BaseDraggableItemDecorator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BasicSwapTargetTranslationInterpolator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/BasicSwapTargetTranslationInterpolator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java old mode 100644 new mode 100755 index bf257edb9ca..89aa001f27c --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemAdapter.java @@ -24,22 +24,24 @@ public interface DraggableItemAdapter { * Called when user is attempt to drag the item. * * @param holder The ViewHolder which is associated to item user is attempt to start dragging. + * @param position The position of the item within the adapter's data set. * @param x Touched X position. Relative from the itemView's top-left. * @param y Touched Y position. Relative from the itemView's top-left. * @return Whether can start dragging. */ - boolean onCheckCanStartDrag(T holder, int x, int y); + boolean onCheckCanStartDrag(T holder, int position, int x, int y); /** - * Called after the {@link #onCheckCanStartDrag(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method returned true. + * Called after the {@link #onCheckCanStartDrag(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method returned true. * * @param holder The ViewHolder which is associated to item user is attempt to start dragging. + * @param position The position of the item within the adapter's data set. * * @return null: no constraints (= new ItemDraggableRange(0, getItemCount() - 1)), * otherwise: the range specified item can be drag-sortable. */ - ItemDraggableRange onGetItemDraggableRange(T holder); + ItemDraggableRange onGetItemDraggableRange(T holder, int position); /** * Called when item is moved. Should apply the move operation result to data set. diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemViewHolder.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java old mode 100644 new mode 100755 index f74d9515520..745e6cad1b3 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggableItemWrapperAdapter.java @@ -20,16 +20,19 @@ import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.ViewGroup; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.RecyclerViewSwipeManager; +import com.h6ah4i.android.widget.advrecyclerview.swipeable.SwipeableItemAdapter; import com.h6ah4i.android.widget.advrecyclerview.utils.BaseWrapperAdapter; import com.h6ah4i.android.widget.advrecyclerview.utils.WrapperAdapterUtils; -class DraggableItemWrapperAdapter extends BaseWrapperAdapter { +class DraggableItemWrapperAdapter extends BaseWrapperAdapter implements SwipeableItemAdapter { private static final String TAG = "ARVDraggableWrapper"; private static final int STATE_FLAG_INITIAL_VALUE = -1; private static final boolean LOCAL_LOGV = false; private static final boolean LOCAL_LOGD = false; + private static final boolean LOCAL_LOGI = true; private static final boolean DEBUG_BYPASS_MOVE_OPERATION_MODE = false; private RecyclerViewDragDropManager mDragDropManager; @@ -83,7 +86,15 @@ class DraggableItemWrapperAdapter extends Ba position, mDraggingItemInitialPosition, mDraggingItemCurrentPosition); if (itemId == mDraggingItemId && holder != mDraggingItem) { - Log.w(TAG, "an another view holder object for the currently dragging item is assigned"); + if (mDraggingItem == null) { + if (LOCAL_LOGI) { + Log.i(TAG, "a new view holder object for the currently dragging item is assigned"); + } + mDraggingItem = holder; + mDragDropManager.onNewDraggingItemViewBinded(holder); + } else { + Log.e(TAG, "an another view holder object for the currently dragging item is assigned"); + } } int flags = RecyclerViewDragDropManager.STATE_FLAG_DRAGGING; @@ -223,7 +234,7 @@ class DraggableItemWrapperAdapter extends Ba throw new IllegalStateException("dragging target must provides valid ID"); } - mDraggingItemInitialPosition = holder.getPosition(); + mDraggingItemInitialPosition = holder.getAdapterPosition(); mDraggingItemCurrentPosition = mDraggingItemInitialPosition; mDraggingItem = holder; mDraggingItemId = holder.getItemId(); @@ -238,7 +249,7 @@ class DraggableItemWrapperAdapter extends Ba Log.d(TAG, "onDragItemFinished(holder = " + holder + ", result = " + result + ")"); } - if (holder != mDraggingItem) { + if (holder != null && holder != mDraggingItem) { throw new IllegalStateException("onDragItemFinished() - may be a bug (mDraggingItem != holder)"); } @@ -248,8 +259,9 @@ class DraggableItemWrapperAdapter extends Ba if (result && (mDraggingItemCurrentPosition != mDraggingItemInitialPosition)) { // apply to wrapped adapter - ((DraggableItemAdapter) getWrappedAdapter()).onMoveItem( - mDraggingItemInitialPosition, mDraggingItemCurrentPosition); + DraggableItemAdapter adapter = WrapperAdapterUtils.findWrappedAdapter( + getWrappedAdapter(), DraggableItemAdapter.class); + adapter.onMoveItem(mDraggingItemInitialPosition, mDraggingItemCurrentPosition); } mDraggingItemInitialPosition = RecyclerView.NO_POSITION; @@ -265,13 +277,11 @@ class DraggableItemWrapperAdapter extends Ba public void onViewRecycled(VH holder) { if (isDragging()) { if (holder == mDraggingItem) { - try { - throw new IllegalStateException("onViewRecycled() - may be a bug. dragging item should be recycled."); - } catch (IllegalStateException e) { - // TODO is there any way to prevent the dragging item from be recycled...? - - mDragDropManager.cancelDrag(); + if (LOCAL_LOGI) { + Log.i(TAG, "a view holder object which is bound to currently dragging item is recycled"); } + mDraggingItem = null; + mDragDropManager.onDraggingItemViewRecycled(); } } @@ -281,21 +291,21 @@ class DraggableItemWrapperAdapter extends Ba // NOTE: This method is called from RecyclerViewDragDropManager /*package*/ @SuppressWarnings("unchecked") - boolean canStartDrag(RecyclerView.ViewHolder holder, int x, int y) { + boolean canStartDrag(RecyclerView.ViewHolder holder, int position, int x, int y) { if (LOCAL_LOGV) { - Log.v(TAG, "canStartDrag(holder = " + holder + ", x = " + x + ", y = " + y + ")"); + Log.v(TAG, "canStartDrag(holder = " + holder + ", position = " + position + ", x = " + x + ", y = " + y + ")"); } - return mDraggableItemAdapter.onCheckCanStartDrag(holder, x, y); + return mDraggableItemAdapter.onCheckCanStartDrag(holder, position, x, y); } // NOTE: This method is called from RecyclerViewDragDropManager /*package*/ @SuppressWarnings("unchecked") - ItemDraggableRange getItemDraggableRange(RecyclerView.ViewHolder holder) { + ItemDraggableRange getItemDraggableRange(RecyclerView.ViewHolder holder, int position) { if (LOCAL_LOGV) { - Log.v(TAG, "getItemDraggableRange(holder = " + holder + ")"); + Log.v(TAG, "getItemDraggableRange(holder = " + holder + ", position = " + position + ")"); } - return mDraggableItemAdapter.onGetItemDraggableRange(holder); + return mDraggableItemAdapter.onGetItemDraggableRange(holder, position); } // NOTE: This method is called from RecyclerViewDragDropManager @@ -335,6 +345,14 @@ class DraggableItemWrapperAdapter extends Ba return mDragDropManager.isDragging(); } + /*package*/ int getDraggingItemInitialPosition() { + return mDraggingItemInitialPosition; + } + + /*package*/ int getDraggingItemCurrentPosition() { + return mDraggingItemCurrentPosition; + } + private static void safeUpdateFlags(RecyclerView.ViewHolder holder, int flags) { if (!(holder instanceof DraggableItemViewHolder)) { return; @@ -356,4 +374,75 @@ class DraggableItemWrapperAdapter extends Ba private static DraggableItemAdapter getDraggableItemAdapter(RecyclerView.Adapter adapter) { return WrapperAdapterUtils.findWrappedAdapter(adapter, DraggableItemAdapter.class); } + + private int getOriginalPosition(int position) { + int correctedPosition; + + if (isDragging()) { + correctedPosition = convertToOriginalPosition( + position, mDraggingItemInitialPosition, mDraggingItemCurrentPosition); + } else { + correctedPosition = position; + } + return correctedPosition; + } + + // + // SwipeableItemAdapter implementations + // + @SuppressWarnings("unchecked") + @Override + public int onGetSwipeReactionType(VH holder, int position, int x, int y) { + RecyclerView.Adapter adapter = getWrappedAdapter(); + if (!(adapter instanceof SwipeableItemAdapter)) { + return RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT; + } + + int correctedPosition = getOriginalPosition(position); + + SwipeableItemAdapter swipeableItemAdapter = (SwipeableItemAdapter) adapter; + return swipeableItemAdapter.onGetSwipeReactionType(holder, correctedPosition, x, y); + } + + @SuppressWarnings("unchecked") + @Override + public void onSetSwipeBackground(VH holder, int position, int type) { + RecyclerView.Adapter adapter = getWrappedAdapter(); + if (!(adapter instanceof SwipeableItemAdapter)) { + return; + } + + int correctedPosition = getOriginalPosition(position); + + SwipeableItemAdapter swipeableItemAdapter = (SwipeableItemAdapter) adapter; + swipeableItemAdapter.onSetSwipeBackground(holder, correctedPosition, type); + } + + @SuppressWarnings("unchecked") + @Override + public int onSwipeItem(VH holder, int position, int result) { + RecyclerView.Adapter adapter = getWrappedAdapter(); + if (!(adapter instanceof SwipeableItemAdapter)) { + return RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT; + } + + int correctedPosition = getOriginalPosition(position); + + SwipeableItemAdapter swipeableItemAdapter = (SwipeableItemAdapter) adapter; + return swipeableItemAdapter.onSwipeItem(holder, correctedPosition, result); + } + + @SuppressWarnings("unchecked") + @Override + public void onPerformAfterSwipeReaction(VH holder, int position, int result, int reaction) { + RecyclerView.Adapter adapter = getWrappedAdapter(); + if (!(adapter instanceof SwipeableItemAdapter)) { + return; + } + + int correctedPosition = getOriginalPosition(position); + + SwipeableItemAdapter swipeableItemAdapter = (SwipeableItemAdapter) adapter; + swipeableItemAdapter.onPerformAfterSwipeReaction(holder, correctedPosition, result, reaction); + } } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java old mode 100644 new mode 100755 index 00e6bb4d1cd..463fd97b1ad --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/DraggingItemDecorator.java @@ -104,10 +104,14 @@ class DraggingItemDecorator extends BaseDraggableItemDecorator { // return to default position updateDraggingItemPosition(mTranslationY); - moveToDefaultPosition(mDraggingItem.itemView, animate); + if (mDraggingItem != null) { + moveToDefaultPosition(mDraggingItem.itemView, animate); + } // show - mDraggingItem.itemView.setVisibility(View.VISIBLE); + if (mDraggingItem != null) { + mDraggingItem.itemView.setVisibility(View.VISIBLE); + } mDraggingItem = null; if (mDraggingItemImage != null) { @@ -178,6 +182,13 @@ class DraggingItemDecorator extends BaseDraggableItemDecorator { mTranslationY = Math.min(Math.max(mTranslationY, mTranslationTopLimit), mTranslationBottomLimit); } + public boolean isReachedToTopLimit() { + return (mTranslationY == mTranslationTopLimit); + } + + public boolean isReachedToBottomLimit() { + return (mTranslationY == mTranslationBottomLimit); + } private Bitmap createDraggingItemImage(View v, NinePatchDrawable shadow) { int width = v.getWidth() + mShadowPadding.left + mShadowPadding.right; @@ -204,7 +215,9 @@ class DraggingItemDecorator extends BaseDraggableItemDecorator { private void updateDraggingItemPosition(int translationY) { // NOTE: Need to update the view position to make other decorations work properly while dragging - setItemTranslationY(mRecyclerView, mDraggingItem, translationY - mDraggingItem.itemView.getTop()); + if (mDraggingItem != null) { + setItemTranslationY(mRecyclerView, mDraggingItem, translationY - mDraggingItem.itemView.getTop()); + } } public void setIsScrolling(boolean isScrolling) { @@ -238,7 +251,7 @@ class DraggingItemDecorator extends BaseDraggableItemDecorator { final RecyclerView.ViewHolder vh = rv.getChildViewHolder(v2); if (vh != null) { - final int position = vh.getPosition(); + final int position = vh.getLayoutPosition(); if ((position >= firstVisiblePosition) && (position <= lastVisiblePosition) && @@ -266,7 +279,7 @@ class DraggingItemDecorator extends BaseDraggableItemDecorator { final RecyclerView.ViewHolder vh = rv.getChildViewHolder(v2); if (vh != null) { - final int position = vh.getPosition(); + final int position = vh.getLayoutPosition(); if ((position >= firstVisiblePosition) && (position <= lastVisiblePosition) && @@ -279,4 +292,22 @@ class DraggingItemDecorator extends BaseDraggableItemDecorator { return v; } + + public void invalidateDraggingItem() { + if (mDraggingItem != null) { + mDraggingItem.itemView.setVisibility(View.VISIBLE); + } + + mDraggingItem = null; + } + + public void setDraggingItemViewHolder(RecyclerView.ViewHolder holder) { + if (mDraggingItem != null) { + throw new IllegalStateException("A new view holder is attempt to be assigned before invalidating the older one"); + } + + mDraggingItem = holder; + + holder.itemView.setVisibility(View.INVISIBLE); + } } \ No newline at end of file diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java old mode 100644 new mode 100755 index 372a595aec8..4a9454c7590 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/EdgeEffectDecorator.java @@ -82,16 +82,15 @@ public class EdgeEffectDecorator extends RecyclerView.ItemDecoration { public void pullTopGlow(float deltaDistance) { ensureTopGlow(mRecyclerView); - if (mTopGlow.onPull(deltaDistance)) { + if (mTopGlow.onPull(deltaDistance, 0.5f)) { ViewCompat.postInvalidateOnAnimation(mRecyclerView); } } public void pullBottom(float deltaDistance) { ensureBottomGlow(mRecyclerView); - mBottomGlow.onPull(deltaDistance); - if (mBottomGlow.onPull(deltaDistance)) { + if (mBottomGlow.onPull(deltaDistance, 0.5f)) { ViewCompat.postInvalidateOnAnimation(mRecyclerView); } } @@ -114,31 +113,33 @@ public class EdgeEffectDecorator extends RecyclerView.ItemDecoration { } private void ensureTopGlow(RecyclerView rv) { - if (mTopGlow != null) { - return; - } - mTopGlow = new EdgeEffectCompat(rv.getContext()); - if (getClipToPadding(rv)) { - mTopGlow.setSize( - rv.getMeasuredWidth() - rv.getPaddingLeft() - rv.getPaddingRight(), - rv.getMeasuredHeight() - rv.getPaddingTop() - rv.getPaddingBottom()); - } else { - mTopGlow.setSize(rv.getMeasuredWidth(), rv.getMeasuredHeight()); + if (mTopGlow == null) { + mTopGlow = new EdgeEffectCompat(rv.getContext()); } + + updateGlowSize(rv, mTopGlow); } private void ensureBottomGlow(RecyclerView rv) { - if (mBottomGlow != null) { - return; + if (mBottomGlow == null) { + mBottomGlow = new EdgeEffectCompat(rv.getContext()); } - mBottomGlow = new EdgeEffectCompat(rv.getContext()); + updateGlowSize(rv, mBottomGlow); + } + + private static void updateGlowSize(RecyclerView rv, EdgeEffectCompat topGlow) { + int width = rv.getMeasuredWidth(); + int height = rv.getMeasuredHeight(); + if (getClipToPadding(rv)) { - mBottomGlow.setSize( - rv.getMeasuredWidth() - rv.getPaddingLeft() - rv.getPaddingRight(), - rv.getMeasuredHeight() - rv.getPaddingTop() - rv.getPaddingBottom()); - } else { - mBottomGlow.setSize(rv.getMeasuredWidth(), rv.getMeasuredHeight()); + width -= rv.getPaddingLeft() - rv.getPaddingRight(); + height -= rv.getPaddingTop() - rv.getPaddingBottom(); } + + width = Math.max(0, width); + height = Math.max(0, height); + + topGlow.setSize(width, height); } private static boolean getClipToPadding(RecyclerView rv) { diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java old mode 100644 new mode 100755 index 2625cf5120d..e5bbde8baf6 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/ItemDraggableRange.java @@ -41,9 +41,13 @@ public class ItemDraggableRange { return ((position >= mStart) && (position <= mEnd)); } + protected String getClassName() { + return "ItemDraggableRange"; + } + @Override public String toString() { - return "ItemDraggableRange{" + + return getClassName() + "{" + "mStart=" + mStart + ", mEnd=" + mEnd + '}'; diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java old mode 100644 new mode 100755 index 41667008824..9cdef404af8 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/RecyclerViewDragDropManager.java @@ -19,6 +19,8 @@ package com.h6ah4i.android.widget.advrecyclerview.draggable; import android.graphics.Rect; import android.graphics.drawable.NinePatchDrawable; import android.os.Build; +import android.os.Handler; +import android.os.Message; import android.support.v4.view.MotionEventCompat; import android.support.v4.view.ViewCompat; import android.support.v7.widget.RecyclerView; @@ -81,6 +83,29 @@ public class RecyclerViewDragDropManager { // --- + /** + * Used for listening item drag events + */ + public interface OnItemDragEventListener { + /** + * Callback method to be invoked when dragging is started. + * + * @param position The position of the item. + */ + void onItemDragStarted(int position); + + /** + * Callback method to be invoked when dragging is finished. + * + * @param fromPosition Previous position of the item. + * @param toPosition New position of the item. + * @param result Indicates whether the dragging operation was succeeded. + */ + void onItemDragFinished(int fromPosition, int toPosition, boolean result); + } + + // -- + private static final int SCROLL_DIR_NONE = 0; private static final int SCROLL_DIR_UP = (1 << 0); private static final int SCROLL_DIR_DOWN = (1 << 1); @@ -90,7 +115,6 @@ public class RecyclerViewDragDropManager { private static final float SCROLL_THRESHOLD = 0.3f; // 0.0f < X < 0.5f private static final float SCROLL_AMOUNT_COEFF = 25; - private static final float EDGE_EFFECT_THRESHOLD = 0.5f; // <= 1.0f private static final float SCROLL_TOUCH_SLOP_MULTIPLY = 1.5f; private RecyclerView mRecyclerView; @@ -109,6 +133,8 @@ public class RecyclerViewDragDropManager { private int mScrollTouchSlop; private int mInitialTouchY; private long mInitialTouchItemId = RecyclerView.NO_ID; + private boolean mInitiateOnLongPress; + private boolean mInitiateOnMove = true; private boolean mInScrollByMethod; private int mActualScrollByAmount; @@ -135,6 +161,8 @@ public class RecyclerViewDragDropManager { private int mGrabbedItemHeight; private int mOrigOverScrollMode; private ItemDraggableRange mDraggableRange; + private InternalHandler mHandler; + private OnItemDragEventListener mItemDragEventListener; /** * Constructor. @@ -150,6 +178,11 @@ public class RecyclerViewDragDropManager { public void onTouchEvent(RecyclerView rv, MotionEvent e) { RecyclerViewDragDropManager.this.onTouchEvent(rv, e); } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + RecyclerViewDragDropManager.this.onRequestDisallowInterceptTouchEvent(disallowIntercept); + } }; mInternalUseOnScrollListener = new RecyclerView.OnScrollListener() { @@ -171,7 +204,6 @@ public class RecyclerViewDragDropManager { * Create wrapped adapter. * * @param adapter The target adapter. - * * @return Wrapped adapter which is associated to this {@link RecyclerViewDragDropManager} instance. */ @SuppressWarnings("unchecked") @@ -204,6 +236,7 @@ public class RecyclerViewDragDropManager { * @param rv The {@link android.support.v7.widget.RecyclerView} instance */ public void attachRecyclerView(RecyclerView rv) { + //noinspection deprecation attachRecyclerView(rv, null); } @@ -214,10 +247,11 @@ public class RecyclerViewDragDropManager { * the wrapped adapter instance which is returned by the * {@link #createWrappedAdapter(android.support.v7.widget.RecyclerView.Adapter)} method. * - * @param rv The {@link android.support.v7.widget.RecyclerView} instance + * @param rv The {@link android.support.v7.widget.RecyclerView} instance * @param scrollEventDistributor The distributor for {@link android.support.v7.widget.RecyclerView.OnScrollListener} event */ - public void attachRecyclerView(RecyclerView rv, RecyclerViewOnScrollEventDistributor scrollEventDistributor) { + @Deprecated + public void attachRecyclerView(RecyclerView rv, @SuppressWarnings("deprecation") RecyclerViewOnScrollEventDistributor scrollEventDistributor) { if (rv == null) { throw new IllegalArgumentException("RecyclerView cannot be null"); } @@ -248,7 +282,7 @@ public class RecyclerViewDragDropManager { scrollEventDistributor.add(mInternalUseOnScrollListener); mScrollEventRegisteredToDistributor = true; } else { - mRecyclerView.setOnScrollListener(mInternalUseOnScrollListener); + mRecyclerView.addOnScrollListener(mInternalUseOnScrollListener); mScrollEventRegisteredToDistributor = false; } @@ -257,6 +291,7 @@ public class RecyclerViewDragDropManager { mDisplayDensity = mRecyclerView.getResources().getDisplayMetrics().density; mTouchSlop = ViewConfiguration.get(mRecyclerView.getContext()).getScaledTouchSlop(); mScrollTouchSlop = (int) (mTouchSlop * SCROLL_TOUCH_SLOP_MULTIPLY + 0.5f); + mHandler = new InternalHandler(this); if (supportsEdgeEffect()) { // edge effect is available on ICS or later @@ -273,6 +308,11 @@ public class RecyclerViewDragDropManager { public void release() { cancelDrag(); + if (mHandler != null) { + mHandler.release(); + mHandler = null; + } + if (mEdgeEffectDecorator != null) { mEdgeEffectDecorator.finish(); mEdgeEffectDecorator = null; @@ -284,7 +324,7 @@ public class RecyclerViewDragDropManager { mInternalUseOnItemTouchListener = null; if (mRecyclerView != null && mInternalUseOnScrollListener != null && mScrollEventRegisteredToDistributor) { - mRecyclerView.setOnScrollListener(null); + mRecyclerView.removeOnScrollListener(mInternalUseOnScrollListener); } mInternalUseOnScrollListener = null; @@ -304,7 +344,7 @@ public class RecyclerViewDragDropManager { * @return True if currently performing item dragging */ public boolean isDragging() { - return (mDraggingItem != null) && (mDeferredCancelProcess == null); + return (mDraggingItemId != RecyclerView.NO_ID) && (mDeferredCancelProcess == null); } /** @@ -325,6 +365,42 @@ public class RecyclerViewDragDropManager { mSwapTargetTranslationInterpolator = interpolator; } + /** + * Returns whether dragging starts on a long press or not. + * + * @return True if dragging starts on a long press, false otherwise. + */ + public boolean isInitiateOnLongPressEnabled() { + return mInitiateOnLongPress; + } + + /** + * Sets whether dragging starts on a long press. (default: false) + * + * @param initiateOnLongPress True to initiate dragging on long press. + */ + public void setInitiateOnLongPress(boolean initiateOnLongPress) { + mInitiateOnLongPress = initiateOnLongPress; + } + + /** + * Returns whether dragging starts on move motions. + * + * @return True if dragging starts on move motions, false otherwise. + */ + public boolean isInitiateOnMoveEnabled() { + return mInitiateOnMove; + } + + /** + * Sets whether dragging starts on move motions. (default: true) + * + * @param initiateOnMove True to initiate dragging on move motions. + */ + public void setInitiateOnMove(boolean initiateOnMove) { + mInitiateOnMove = initiateOnMove; + } + /** * Gets the interpolator which ise used for determining the position of the swapping item. * @@ -334,6 +410,23 @@ public class RecyclerViewDragDropManager { return mSwapTargetTranslationInterpolator; } + /** + * Gets OnItemDragEventListener listener + * @return The listener object + */ + public OnItemDragEventListener getOnItemDragEventListener() { + return mItemDragEventListener; + } + + /** + * Sets OnItemDragEventListener listener + * + * @param listener The listener object + */ + public void setOnItemDragEventListener(OnItemDragEventListener listener) { + mItemDragEventListener = listener; + } + /*package*/ boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { final int action = MotionEventCompat.getActionMasked(e); @@ -393,6 +486,13 @@ public class RecyclerViewDragDropManager { } } + /*package */ void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + cancelDrag(true); + } + } + + /*package*/ void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (LOCAL_LOGV) { Log.v(TAG, "onScrolled(dx = " + dx + ", dy = " + dy + ")"); @@ -410,6 +510,7 @@ public class RecyclerViewDragDropManager { } private boolean handleActionDown(RecyclerView rv, MotionEvent e) { + final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithoutTranslation(rv, e.getX(), e.getY()); if (!checkTouchedItemState(rv, holder)) { @@ -419,13 +520,25 @@ public class RecyclerViewDragDropManager { mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f); mInitialTouchItemId = holder.getItemId(); + if (mInitiateOnLongPress) { + mHandler.startLongPressDetection(e); + } + return true; } + private void handleOnLongPress(MotionEvent e) { + if (mInitiateOnLongPress) { + checkConditionAndStartDragging(mRecyclerView, e, false); + } + } + @SuppressWarnings("unchecked") private void startDragging(RecyclerView rv, MotionEvent e, RecyclerView.ViewHolder holder, ItemDraggableRange range) { safeEndAnimation(rv, holder); + mHandler.cancelLongPressDetection(); + mDraggingItem = holder; // XXX if setIsRecyclable() is used, another view holder objects will be created @@ -461,7 +574,7 @@ public class RecyclerViewDragDropManager { mAdapter.onDragItemStarted(mDraggingItem, mDraggableRange); // setup decorators - mAdapter.onBindViewHolder(mDraggingItem, mDraggingItem.getPosition()); + mAdapter.onBindViewHolder(mDraggingItem, mDraggingItem.getLayoutPosition()); mDraggingItemDecorator = new DraggingItemDecorator(mRecyclerView, mDraggingItem, mDraggableRange); mDraggingItemDecorator.setShadowDrawable(mShadowDrawable); @@ -477,6 +590,10 @@ public class RecyclerViewDragDropManager { if (mEdgeEffectDecorator != null) { mEdgeEffectDecorator.reorderToTop(); } + + if (mItemDragEventListener != null) { + mItemDragEventListener.onItemDragStarted(mAdapter.getDraggingItemInitialPosition()); + } } /** @@ -490,7 +607,7 @@ public class RecyclerViewDragDropManager { if (immediately) { finishDragging(false); } else { - if (mDraggingItem != null) { + if (isDragging()) { if (mDeferredCancelProcess == null) { mDeferredCancelProcess = new Runnable() { @Override @@ -510,6 +627,10 @@ public class RecyclerViewDragDropManager { private void finishDragging(boolean result) { final RecyclerView.ViewHolder draggedItem = mDraggingItem; + if (draggedItem == null) { + result = false; + } + // cancel deferred request if (mDeferredCancelProcess != null) { mRecyclerView.removeCallbacks(mDeferredCancelProcess); @@ -543,6 +664,10 @@ public class RecyclerViewDragDropManager { mRecyclerView.getParent().requestDisallowInterceptTouchEvent(false); } + if (mRecyclerView != null) { + mRecyclerView.invalidate(); + } + mDraggableRange = null; mDraggingItemDecorator = null; mSwapTargetItemOperator = null; @@ -556,19 +681,34 @@ public class RecyclerViewDragDropManager { mGrabbedPositionY = 0; mGrabbedItemHeight = 0; + + int draggingItemInitialPosition = RecyclerView.NO_POSITION; + int draggingItemCurrentPosition = RecyclerView.NO_POSITION; + // raise onDragItemFinished() event - if ((mAdapter != null) && (draggedItem != null)) { + if (mAdapter != null) { + draggingItemInitialPosition = mAdapter.getDraggingItemInitialPosition(); + draggingItemCurrentPosition = mAdapter.getDraggingItemCurrentPosition(); mAdapter.onDragItemFinished(draggedItem, result); } // if (draggedItem != null) { // draggedItem.setIsRecyclable(true); // } + + if (mItemDragEventListener != null) { + mItemDragEventListener.onItemDragFinished( + draggingItemInitialPosition, + draggingItemCurrentPosition, + result); + } } private boolean handleActionUpOrCancel(RecyclerView rv, MotionEvent e) { final boolean result = (MotionEventCompat.getActionMasked(e) == MotionEvent.ACTION_UP); + mHandler.cancelLongPressDetection(); + mInitialTouchY = 0; mLastTouchY = 0; mDragStartTouchY = 0; @@ -588,6 +728,18 @@ public class RecyclerViewDragDropManager { } private boolean handleActionMoveWhileNotDragging(RecyclerView rv, MotionEvent e) { + if (mInitiateOnMove) { + return checkConditionAndStartDragging(rv, e, true); + } else { + return false; + } + } + + private boolean checkConditionAndStartDragging(RecyclerView rv, MotionEvent e, boolean checkTouchSlop) { + if (mDraggingItem != null) { + return false; + } + final int touchX = (int) (e.getX() + 0.5f); final int touchY = (int) (e.getY() + 0.5f); @@ -597,19 +749,24 @@ public class RecyclerViewDragDropManager { return false; } - if (!(Math.abs(touchY - mInitialTouchY) > mTouchSlop)) { - return false; + if (checkTouchSlop) { + if (!(Math.abs(touchY - mInitialTouchY) > mTouchSlop)) { + return false; + } } final RecyclerView.ViewHolder holder = CustomRecyclerViewUtils.findChildViewHolderUnderWithoutTranslation(rv, e.getX(), e.getY()); - if (!checkTouchedItemState(rv, holder)) { + if (!checkTouchedItemState(rv, holder) || holder.getItemId() != mInitialTouchItemId) { mInitialTouchItemId = RecyclerView.NO_ID; + mHandler.cancelLongPressDetection(); + return false; } - if (holder.getItemId() != mInitialTouchItemId) { - mInitialTouchItemId = RecyclerView.NO_ID; + int position = CustomRecyclerViewUtils.getSynchronizedPosition(holder); + + if (position == RecyclerView.NO_POSITION) { return false; } @@ -619,11 +776,11 @@ public class RecyclerViewDragDropManager { final int viewX = touchX - (view.getLeft() + translateX); final int viewY = touchY - (view.getTop() + translateY); - if (!mAdapter.canStartDrag(holder, viewX, viewY)) { + if (!mAdapter.canStartDrag(holder, position, viewX, viewY)) { return false; } - ItemDraggableRange range = mAdapter.getItemDraggableRange(holder); + ItemDraggableRange range = mAdapter.getItemDraggableRange(holder, position); if (range == null) { range = new ItemDraggableRange(0, Math.max(0, mAdapter.getItemCount() - 1)); @@ -639,7 +796,6 @@ public class RecyclerViewDragDropManager { startDragging(rv, e, holder, range); return true; - } private void verifyItemDraggableRange(ItemDraggableRange range, RecyclerView.ViewHolder holder) { @@ -658,14 +814,15 @@ public class RecyclerViewDragDropManager { throw new IllegalStateException("Invalid range specified --- end >= count (range = " + range + ")"); } - if (!range.checkInRange(holder.getPosition())) { + if (!range.checkInRange(holder.getAdapterPosition())) { throw new IllegalStateException( "Invalid range specified --- does not contain drag target item" - + " (range = " + range + ", position = " + holder.getPosition() + ")"); + + " (range = " + range + ", position = " + holder.getAdapterPosition() + ")"); } } private void handleActionMoveWhileDragging(RecyclerView rv, MotionEvent e) { + mLastTouchY = (int) (e.getY() + 0.5f); mDragMinTouchY = Math.min(mDragMinTouchY, mLastTouchY); mDragMaxTouchY = Math.max(mDragMaxTouchY, mLastTouchY); @@ -765,6 +922,7 @@ public class RecyclerViewDragDropManager { } } + // scroll if ((!reachedToTopHardLimit && (scrollAmount < 0)) || (!reachedToBottomHardLimit && (scrollAmount > 0))) { safeEndAnimations(rv); @@ -788,34 +946,67 @@ public class RecyclerViewDragDropManager { if (mEdgeEffectDecorator != null) { + final float edgeEffectStrength = 0.005f; + final int draggingItemTop = mDraggingItemDecorator.getTranslatedItemPositionTop(); final int draggingItemBottom = mDraggingItemDecorator.getTranslatedItemPositionBottom(); final int draggingItemCenter = (draggingItemTop + draggingItemBottom) / 2; - final int nearEdgePosition = ((draggingItemCenter < (height / 2)) ? draggingItemTop : draggingItemBottom); + final int nearEdgePosition; + + if (firstVisibleChild == 0 && lastVisibleChild == 0) { + // has only 1 item + nearEdgePosition = (scrollAmount < 0) ? draggingItemTop : draggingItemBottom; + } else { + nearEdgePosition = (draggingItemCenter < (height / 2)) ? draggingItemTop : draggingItemBottom; + } final float nearEdgeOffset = (nearEdgePosition * invHeight) - 0.5f; final float absNearEdgeOffset = Math.abs(nearEdgeOffset); - final float edgeEffectAcceleration = Math.max(0.0f, threshold - (0.5f - absNearEdgeOffset)) * invThreshold; + float edgeEffectPullDistance = 0; - if ((edgeEffectAcceleration >= EDGE_EFFECT_THRESHOLD) && (scrollAmount != 0) && !actualIsScrolling) { - // over scrolled - final float distance = edgeEffectAcceleration * 0.02f; - - if (scrollAmount < 0) { + if ((absNearEdgeOffset > 0.4f) && (scrollAmount != 0) && !actualIsScrolling) { + if (nearEdgeOffset < 0) { // upward - mEdgeEffectDecorator.pullTopGlow(distance); + if (mDraggingItemDecorator.isReachedToTopLimit()) { + edgeEffectPullDistance = -mDisplayDensity * edgeEffectStrength; + } } else { // downward - mEdgeEffectDecorator.pullBottom(distance); + if (mDraggingItemDecorator.isReachedToBottomLimit()) { + edgeEffectPullDistance = mDisplayDensity * edgeEffectStrength; + } } - } else { - mEdgeEffectDecorator.releaseBothGlows(); } + + updateEdgeEffect(edgeEffectPullDistance); } - checkItemSwapping(rv); + ViewCompat.postOnAnimation(mRecyclerView, mCheckItemSwappingRunnable); } + private void updateEdgeEffect(float distance) { + if (distance != 0.0f) { + if (distance < 0) { + // upward + mEdgeEffectDecorator.pullTopGlow(distance); + } else { + // downward + mEdgeEffectDecorator.pullBottom(distance); + } + } else { + mEdgeEffectDecorator.releaseBothGlows(); + } + } + + private Runnable mCheckItemSwappingRunnable = new Runnable() { + @Override + public void run() { + if (mDraggingItem != null) { + checkItemSwapping(mRecyclerView); + } + } + }; + private int scrollByYAndGetScrolledAmount(int ry) { // NOTE: mActualScrollByAmount --- Hackish! To detect over scrolling. @@ -843,8 +1034,8 @@ public class RecyclerViewDragDropManager { private void swapItems(RecyclerView rv, RecyclerView.ViewHolder draggingItem, RecyclerView.ViewHolder swapTargetHolder) { final Rect swapTargetMargins = CustomRecyclerViewUtils.getLayoutMargins(swapTargetHolder.itemView, mTmpRect1); - final int fromPosition = draggingItem.getPosition(); - final int toPosition = swapTargetHolder.getPosition(); + final int fromPosition = draggingItem.getAdapterPosition(); + final int toPosition = swapTargetHolder.getAdapterPosition(); final int diffPosition = Math.abs(fromPosition - toPosition); boolean performSwapping = false; @@ -903,11 +1094,13 @@ public class RecyclerViewDragDropManager { firstVisibleTopItem = rv.getChildViewHolder(child); } } - final int prevTopItemPosition = (firstVisibleTopItem != null) ? firstVisibleTopItem.getPosition() : RecyclerView.NO_POSITION; - + final int prevTopItemPosition = (firstVisibleTopItem != null) ? firstVisibleTopItem.getAdapterPosition() : RecyclerView.NO_POSITION; + // NOTE: This method invokes notifyItemMoved() method internally. Be careful! mAdapter.moveItem(fromPosition, toPosition); + safeEndAnimations(rv); + if (fromPosition == prevTopItemPosition) { //noinspection UnnecessaryLocalVariable final Rect margins = swapTargetMargins; @@ -933,7 +1126,7 @@ public class RecyclerViewDragDropManager { return false; } - final int itemPosition = holder.getPosition(); + final int itemPosition = holder.getAdapterPosition(); final RecyclerView.Adapter adapter = rv.getAdapter(); // verify the touched item is valid state @@ -975,7 +1168,7 @@ public class RecyclerViewDragDropManager { static RecyclerView.ViewHolder findSwapTargetItem( RecyclerView rv, RecyclerView.ViewHolder draggingItem, long draggingItemId, int overlayItemTop, ItemDraggableRange range) { - final int draggingItemPosition = draggingItem.getPosition(); + final int draggingItemPosition = draggingItem.getAdapterPosition(); final int draggingViewTop = draggingItem.itemView.getTop(); RecyclerView.ViewHolder swapTargetHolder = null; @@ -984,18 +1177,18 @@ public class RecyclerViewDragDropManager { draggingItem.getItemId() == draggingItemId) { if (overlayItemTop < draggingViewTop) { if (draggingItemPosition > 0) { - swapTargetHolder = rv.findViewHolderForPosition(draggingItemPosition - 1); + swapTargetHolder = rv.findViewHolderForAdapterPosition(draggingItemPosition - 1); } } else if (overlayItemTop > draggingViewTop) { if (draggingItemPosition < (rv.getAdapter().getItemCount() - 1)) { - swapTargetHolder = rv.findViewHolderForPosition(draggingItemPosition + 1); + swapTargetHolder = rv.findViewHolderForAdapterPosition(draggingItemPosition + 1); } } } // check range if (swapTargetHolder != null && range != null) { - if (!range.checkInRange(swapTargetHolder.getPosition())) { + if (!range.checkInRange(swapTargetHolder.getAdapterPosition())) { swapTargetHolder = null; } } @@ -1039,6 +1232,15 @@ public class RecyclerViewDragDropManager { return mItemSettleBackIntoPlaceAnimationInterpolator; } + /*package*/ void onDraggingItemViewRecycled() { + mDraggingItemDecorator.invalidateDraggingItem(); + } + + /*package*/ void onNewDraggingItemViewBinded(RecyclerView.ViewHolder holder) { + mDraggingItem = holder; + mDraggingItemDecorator.setDraggingItemViewHolder(holder); + } + private static class ScrollOnDraggingProcessRunnable implements Runnable { private final WeakReference mHolderRef; private boolean mStarted; @@ -1107,4 +1309,44 @@ public class RecyclerViewDragDropManager { } } } + + private static class InternalHandler extends Handler { + private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout();// + ViewConfiguration.getTapTimeout(); + private static final int MSG_LONGPRESS = 1; + + private RecyclerViewDragDropManager mHolder; + private MotionEvent mDownMotionEvent; + + public InternalHandler(RecyclerViewDragDropManager holder) { + mHolder = holder; + } + + public void release() { + removeCallbacks(null); + mHolder = null; + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_LONGPRESS: + mHolder.handleOnLongPress(mDownMotionEvent); + break; + } + } + + public void startLongPressDetection(MotionEvent e) { + cancelLongPressDetection(); + mDownMotionEvent = MotionEvent.obtain(e); + sendEmptyMessageAtTime(MSG_LONGPRESS, e.getDownTime() + LONGPRESS_TIMEOUT); + } + + public void cancelLongPressDetection() { + removeMessages(MSG_LONGPRESS); + if (mDownMotionEvent != null) { + mDownMotionEvent.recycle(); + mDownMotionEvent = null; + } + } + } } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java old mode 100644 new mode 100755 index 66e58c69121..d88a24aa9b7 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/draggable/SwapTargetItemOperator.java @@ -94,8 +94,8 @@ class SwapTargetItemOperator extends BaseDraggableItemDecorator { private float calculateTranslationPhase(RecyclerView.ViewHolder draggingItem, RecyclerView.ViewHolder swapTargetItem) { final View swapItemView = swapTargetItem.itemView; - final int pos1 = draggingItem.getPosition(); - final int pos2 = swapTargetItem.getPosition(); + final int pos1 = draggingItem.getLayoutPosition(); + final int pos2 = swapTargetItem.getLayoutPosition(); CustomRecyclerViewUtils.getDecorationOffsets( mRecyclerView.getLayoutManager(), swapItemView, mSwapTargetDecorationOffsets); @@ -132,8 +132,8 @@ class SwapTargetItemOperator extends BaseDraggableItemDecorator { private void updateSwapTargetTranslation(RecyclerView.ViewHolder draggingItem, RecyclerView.ViewHolder swapTargetItem, float translationPhase) { final View swapItemView = swapTargetItem.itemView; - final int pos1 = draggingItem.getPosition(); - final int pos2 = swapTargetItem.getPosition(); + final int pos1 = draggingItem.getLayoutPosition(); + final int pos2 = swapTargetItem.getLayoutPosition(); final Rect m1 = mDraggingItemMargins; final Rect d1 = mDraggingItemDecorationOffsets; diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/BaseRecyclerViewEventDistributor.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/BaseRecyclerViewEventDistributor.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewEventDistributorListener.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewEventDistributorListener.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java old mode 100644 new mode 100755 index fb9ff5d79d9..a98ed263356 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewOnScrollEventDistributor.java @@ -20,6 +20,12 @@ import android.support.v7.widget.RecyclerView; import java.lang.ref.WeakReference; +/** + * Deprecated. + * + * Please use {@link RecyclerView#addOnScrollListener(RecyclerView.OnScrollListener)} and {@link RecyclerView#removeOnScrollListener(RecyclerView.OnScrollListener)} instead. + */ +@Deprecated public class RecyclerViewOnScrollEventDistributor extends BaseRecyclerViewEventDistributor { private InternalOnScrollListener mInternalOnScrollListener; @@ -34,17 +40,20 @@ public class RecyclerViewOnScrollEventDistributor extends BaseRecyclerViewEventD protected void onRecyclerViewAttached(RecyclerView rv) { super.onRecyclerViewAttached(rv); - rv.setOnScrollListener(mInternalOnScrollListener); + rv.addOnScrollListener(mInternalOnScrollListener); } @Override protected void onRelease() { - super.onRelease(); - if (mInternalOnScrollListener != null) { + if (mRecyclerView != null) { + mRecyclerView.removeOnScrollListener(mInternalOnScrollListener); + } mInternalOnScrollListener.release(); mInternalOnScrollListener = null; } + + super.onRelease(); } /*package*/ void handleOnScrollStateChanged(RecyclerView recyclerView, int newState) { @@ -67,6 +76,7 @@ public class RecyclerViewOnScrollEventDistributor extends BaseRecyclerViewEventD } } + @SuppressWarnings("deprecation") private static class InternalOnScrollListener extends RecyclerView.OnScrollListener { private WeakReference mRefDistributor; diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewRecyclerEventDistributor.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/event/RecyclerViewRecyclerEventDistributor.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ChildPositionItemDraggableRange.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ChildPositionItemDraggableRange.java new file mode 100755 index 00000000000..0d4f0da0d13 --- /dev/null +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ChildPositionItemDraggableRange.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015 Haruki Hasegawa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.h6ah4i.android.widget.advrecyclerview.expandable; + +import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange; + +public class ChildPositionItemDraggableRange extends ItemDraggableRange { + public ChildPositionItemDraggableRange(int start, int end) { + super(start, end); + } + + protected String getClassName() { + return "ChildPositionItemDraggableRange"; + } +} diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableAdapterHelper.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableAdapterHelper.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableDraggableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableDraggableItemAdapter.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java old mode 100644 new mode 100755 index cc8cef577c8..8fb1de8e28a --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableItemAdapter.java @@ -125,7 +125,7 @@ public interface ExpandableItemAdapter> 32); + final int position = (int) (idAndPos[j] & LOWER_31BIT_MASK); if (id2 < id1) { index = j; + + if (adapter == null || adapter.onHookGroupCollapse(position, fromUser)) { + if (collapseGroup(position)) { + if (collapseListener != null) { + collapseListener.onGroupCollapse(position, fromUser); + } + } + } } else if (id2 == id1) { // matched - final int position = (int) (idAndPos[j] & LOWER_31BIT_MASK); index = j + 1; - expandGroup(position); - break; + + if (adapter == null || adapter.onHookGroupExpand(position, fromUser)) { + if (expandGroup(position)) { + if (expandListener != null) { + expandListener.onGroupExpand(position, fromUser); + } + } + } } else { // id2 > id1 break; } } } + + if (adapter != null || collapseListener != null) { + for (int i = index; i < idAndPos.length; i++) { + final int id2 = (int) (idAndPos[i] >> 32); + final int position = (int) (idAndPos[i] & LOWER_31BIT_MASK); + + if (adapter == null || adapter.onHookGroupCollapse(position, fromUser)) { + if (collapseGroup(position)) { + if (collapseListener != null) { + collapseListener.onGroupCollapse(position, fromUser); + } + } + } + } + } } public int[] getSavedStateArray() { - int [] expandedGroups = new int[mExpandedGroupCount]; + int[] expandedGroups = new int[mExpandedGroupCount]; int index = 0; for (int i = 0; i < mGroupCount; i++) { @@ -132,6 +165,8 @@ class ExpandablePositionTranslator { throw new IllegalStateException("may be a bug (index = " + index + ", mExpandedGroupCount = " + mExpandedGroupCount + ")"); } + Arrays.sort(expandedGroups); + return expandedGroups; } @@ -165,14 +200,11 @@ class ExpandablePositionTranslator { mCachedGroupPosInfo[groupPosition] &= (~FLAG_EXPANDED); mExpandedGroupCount -= 1; - if (childCount > 0) { - mExpandedChildCount -= childCount; - mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition); - // requires notifyItemRangeRemoved() - return true; - } else { - return false; - } + mExpandedChildCount -= childCount; + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition); + + // requires notifyItemRangeRemoved() + return true; } public boolean expandGroup(int groupPosition) { @@ -185,14 +217,11 @@ class ExpandablePositionTranslator { mCachedGroupPosInfo[groupPosition] |= FLAG_EXPANDED; mExpandedGroupCount += 1; - if (childCount > 0) { - mExpandedChildCount += childCount; - mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition); - // requires notifyItemRangeInserted() - return true; - } else { - return false; - } + mExpandedChildCount += childCount; + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition); + + // requires notifyItemRangeInserted() + return true; } public void moveGroupItem(int fromGroupPosition, int toGroupPosition) { @@ -223,7 +252,7 @@ class ExpandablePositionTranslator { final int minPosition = Math.min(fromGroupPosition, toGroupPosition); if (minPosition > 0) { - mEndOfCalculatedOffsetGroupPosition = minPosition - 1; + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, minPosition - 1); } else { mEndOfCalculatedOffsetGroupPosition = RecyclerView.NO_POSITION; } @@ -258,7 +287,7 @@ class ExpandablePositionTranslator { final int minPosition = Math.min(fromGroupPosition, toGroupPosition); if (minPosition > 0) { - mEndOfCalculatedOffsetGroupPosition = minPosition - 1; + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, minPosition - 1); } else { mEndOfCalculatedOffsetGroupPosition = RecyclerView.NO_POSITION; } @@ -330,7 +359,7 @@ class ExpandablePositionTranslator { } // final int startIndex = 0; - final int startIndex = Math.min(groupPosition, mEndOfCalculatedOffsetGroupPosition); + final int startIndex = Math.max(0, Math.min(groupPosition, mEndOfCalculatedOffsetGroupPosition)); int endOfCalculatedOffsetGroupPosition = mEndOfCalculatedOffsetGroupPosition; int offset = (int) (mCachedGroupPosInfo[startIndex] >>> 32); int flatPosition = RecyclerView.NO_POSITION; @@ -399,30 +428,155 @@ class ExpandablePositionTranslator { return lastS; } - public void removeGroupItem(int groupPosition) { - final long t = mCachedGroupPosInfo[groupPosition]; + public void removeChildItem(int groupPosition, int childPosition) { + removeChildItems(groupPosition, childPosition, 1); + } - if ((t & FLAG_EXPANDED) != 0) { - mExpandedChildCount -= (int) (t & LOWER_31BIT_MASK); - mExpandedGroupCount -= 1; + public void removeChildItems(int groupPosition, int childPositionStart, int count) { + final long t = mCachedGroupPosInfo[groupPosition]; + final int curCount = (int) (t & LOWER_31BIT_MASK); + + if (!((childPositionStart >= 0) && ((childPositionStart + count) <= curCount))) { + throw new IllegalStateException( + "Invalid child position " + + "removeChildItems(groupPosition = " + groupPosition + ", childPosition = " + childPositionStart + ", count = " + count + ")"); } - mGroupCount -= 1; + if ((t & FLAG_EXPANDED) != 0) { + mExpandedChildCount -= count; + } + + mCachedGroupPosInfo[groupPosition] = (t & (UPPER_32BIT_MASK | FLAG_EXPANDED)) | (curCount - count); + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition - 1); + } + + public void insertChildItem(int groupPosition, int childPosition) { + insertChildItems(groupPosition, childPosition, 1); + } + + public void insertChildItems(int groupPosition, int childPositionStart, int count) { + final long t = mCachedGroupPosInfo[groupPosition]; + final int curCount = (int) (t & LOWER_31BIT_MASK); + + if (!((childPositionStart >= 0) && (childPositionStart <= curCount))) { + throw new IllegalStateException( + "Invalid child position " + + "insertChildItems(groupPosition = " + groupPosition + ", childPositionStart = " + childPositionStart + ", count = " + count + ")"); + } + + if ((t & FLAG_EXPANDED) != 0) { + mExpandedChildCount += count; + } + + mCachedGroupPosInfo[groupPosition] = (t & (UPPER_32BIT_MASK | FLAG_EXPANDED)) | (curCount + count); + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, groupPosition); + } + + public int insertGroupItems(int groupPosition, int count) { + if (count <= 0) { + return 0; + } + + final int n = count; + + enlargeArraysIfNeeded(mGroupCount + n, true); + + // shift to backward + final ExpandableItemAdapter adapter = mAdapter; + final long[] info = mCachedGroupPosInfo; + final int[] ids = mCachedGroupId; + + int start = mGroupCount - 1 + n; + int end = groupPosition - 1 + n; + for (int i = start; i > end; i--) { + info[i] = info[i - n]; + ids[i] = ids[i - n]; + } + + int end2 = groupPosition + n; + for (int i = groupPosition; i < end2; i++) { + final long groupId = adapter.getGroupId(i); + final int childCount = adapter.getChildCount(i); + + info[i] = (((long) i << 32) | childCount); + ids[i] = (int) (groupId & LOWER_32BIT_MASK); + } + + mGroupCount += n; + + int calculatedOffset = (mGroupCount == 0) ? RecyclerView.NO_POSITION : (groupPosition - 1); + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, calculatedOffset); + + return n; + } + + public int insertGroupItem(int groupPosition) { + return insertGroupItems(groupPosition, 1); + } + + public int removeGroupItems(int groupPosition, int count) { + if (count <= 0) { + return 0; + } + + final int n = count; + int removedVisibleItemCount = 0; + + for (int i = 0; i < n; i++) { + final long t = mCachedGroupPosInfo[groupPosition + i]; + + if ((t & FLAG_EXPANDED) != 0) { + int visibleChildCount = (int) (t & LOWER_31BIT_MASK); + removedVisibleItemCount += visibleChildCount; + mExpandedChildCount -= visibleChildCount; + mExpandedGroupCount -= 1; + } + } + removedVisibleItemCount += n; + mGroupCount -= n; // shift to forward for (int i = groupPosition; i < mGroupCount; i++) { - mCachedGroupPosInfo[i] = mCachedGroupPosInfo[i + 1]; - mCachedGroupId[i] = mCachedGroupId[i + 1]; + mCachedGroupPosInfo[i] = mCachedGroupPosInfo[i + n]; + mCachedGroupId[i] = mCachedGroupId[i + n]; } + + int calculatedOffset = (mGroupCount == 0) ? RecyclerView.NO_POSITION : (groupPosition - 1); + mEndOfCalculatedOffsetGroupPosition = Math.min(mEndOfCalculatedOffsetGroupPosition, calculatedOffset); + + return removedVisibleItemCount; } - public void removeChildItem(int groupPosition, int childPosition) { - final long t = mCachedGroupPosInfo[groupPosition]; + public int removeGroupItem(int groupPosition) { + return removeGroupItems(groupPosition, 1); + } - if ((t & FLAG_EXPANDED) != 0) { - mExpandedChildCount -= 1; + + private void enlargeArraysIfNeeded(int size, boolean preserveData) { + int allocSize = (size + (2 * ALLOCATE_UNIT - 1)) & ~(ALLOCATE_UNIT - 1); + + long[] curInfo = mCachedGroupPosInfo; + int[] curId = mCachedGroupId; + long[] newInfo = curInfo; + int[] newId = curId; + + if (curInfo == null || curInfo.length < size) { + newInfo = new long[allocSize]; + } + if (curId == null || curId.length < size) { + newId = new int[allocSize]; } - mCachedGroupPosInfo[groupPosition] = (t & (UPPER_32BIT_MASK | FLAG_EXPANDED)) | ((t & LOWER_31BIT_MASK) - 1); + if (preserveData) { + if (curInfo != null && curInfo != newInfo) { + System.arraycopy(curInfo, 0, newInfo, 0, curInfo.length); + } + if (curId != null && curId != newId) { + System.arraycopy(curId, 0, newId, 0, curId.length); + } + } + + mCachedGroupPosInfo = newInfo; + mCachedGroupId = newId; } } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java old mode 100644 new mode 100755 index 0d8a3e9b42f..848acecfd42 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableRecyclerViewWrapperAdapter.java @@ -41,7 +41,13 @@ class ExpandableRecyclerViewWrapperAdapter private ExpandableItemAdapter mExpandableItemAdapter; private RecyclerViewExpandableItemManager mExpandableListManager; private ExpandablePositionTranslator mPositionTranslator; - private int mDraggingItemGroupPosition = RecyclerView.NO_POSITION; + private int mDraggingItemGroupRangeStart = RecyclerView.NO_POSITION; + private int mDraggingItemGroupRangeEnd = RecyclerView.NO_POSITION; + private int mDraggingItemChildRangeStart = RecyclerView.NO_POSITION; + private int mDraggingItemChildRangeEnd = RecyclerView.NO_POSITION; + + private RecyclerViewExpandableItemManager.OnGroupExpandListener mOnGroupExpandListener; + private RecyclerViewExpandableItemManager.OnGroupCollapseListener mOnGroupCollapseListener; public ExpandableRecyclerViewWrapperAdapter(RecyclerViewExpandableItemManager manager, RecyclerView.Adapter adapter, int[] expandedItemsSavedState) { super(adapter); @@ -61,7 +67,8 @@ class ExpandableRecyclerViewWrapperAdapter mPositionTranslator.build(mExpandableItemAdapter); if (expandedItemsSavedState != null) { - mPositionTranslator.restoreExpandedGroupItems(expandedItemsSavedState); + // NOTE: do not call hook routines and listener methods + mPositionTranslator.restoreExpandedGroupItems(expandedItemsSavedState, null, null, null); } } @@ -71,6 +78,8 @@ class ExpandableRecyclerViewWrapperAdapter mExpandableItemAdapter = null; mExpandableListManager = null; + mOnGroupExpandListener = null; + mOnGroupCollapseListener = null; } @Override @@ -174,7 +183,7 @@ class ExpandableRecyclerViewWrapperAdapter safeUpdateExpandStateFlags(holder, flags); - correctItemDragStateFlags(holder, groupPosition); + correctItemDragStateFlags(holder, groupPosition, childPosition); if (childPosition == RecyclerView.NO_POSITION) { mExpandableItemAdapter.onBindGroupViewHolder(holder, groupPosition, viewType); @@ -187,7 +196,9 @@ class ExpandableRecyclerViewWrapperAdapter if (mPositionTranslator != null) { int [] savedState = mPositionTranslator.getSavedStateArray(); mPositionTranslator.build(mExpandableItemAdapter); - mPositionTranslator.restoreExpandedGroupItems(savedState); + + // NOTE: do not call hook routines and listener methods + mPositionTranslator.restoreExpandedGroupItems(savedState, null, null, null); } } @@ -235,14 +246,14 @@ class ExpandableRecyclerViewWrapperAdapter @SuppressWarnings("unchecked") @Override - public boolean onCheckCanStartDrag(RecyclerView.ViewHolder holder, int x, int y) { + public boolean onCheckCanStartDrag(RecyclerView.ViewHolder holder, int position, int x, int y) { if (!(mExpandableItemAdapter instanceof ExpandableDraggableItemAdapter)) { return false; } final ExpandableDraggableItemAdapter adapter = (ExpandableDraggableItemAdapter) mExpandableItemAdapter; - final int flatPosition = holder.getPosition(); + final int flatPosition = position; final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); @@ -254,14 +265,17 @@ class ExpandableRecyclerViewWrapperAdapter canStart = adapter.onCheckChildCanStartDrag(holder, groupPosition, childPosition, x, y); } - mDraggingItemGroupPosition = (canStart) ? groupPosition : RecyclerView.NO_POSITION; + mDraggingItemGroupRangeStart = RecyclerView.NO_POSITION; + mDraggingItemGroupRangeEnd = RecyclerView.NO_POSITION; + mDraggingItemChildRangeStart = RecyclerView.NO_POSITION; + mDraggingItemChildRangeEnd = RecyclerView.NO_POSITION; return canStart; } @SuppressWarnings("unchecked") @Override - public ItemDraggableRange onGetItemDraggableRange(RecyclerView.ViewHolder holder) { + public ItemDraggableRange onGetItemDraggableRange(RecyclerView.ViewHolder holder, int position) { if (!(mExpandableItemAdapter instanceof ExpandableDraggableItemAdapter)) { return null; } @@ -272,66 +286,109 @@ class ExpandableRecyclerViewWrapperAdapter final ExpandableDraggableItemAdapter adapter = (ExpandableDraggableItemAdapter) mExpandableItemAdapter; - final int flatPosition = holder.getPosition(); + final int flatPosition = position; final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); if (childPosition == RecyclerView.NO_POSITION) { // group - final ItemDraggableRange groupRange = adapter.onGetGroupItemDraggableRange(holder, groupPosition); + final ItemDraggableRange range = adapter.onGetGroupItemDraggableRange(holder, groupPosition); - if (groupRange == null) { + if (range == null) { final int lastGroup = Math.max(0, mExpandableItemAdapter.getGroupCount() - 1); final int start = 0; final int end = Math.max(start, mPositionTranslator.getItemCount() - mPositionTranslator.getVisibleChildCount(lastGroup) - 1); return new ItemDraggableRange(start, end); - } else { - final long startPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupRange.getStart()); - final long endPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupRange.getEnd()); + } else if (isGroupPositionRange(range)) { + final long startPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(range.getStart()); + final long endPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(range.getEnd()); final int start = mPositionTranslator.getFlatPosition(startPackedGroupPosition); int end = mPositionTranslator.getFlatPosition(endPackedGroupPosition); - if (groupRange.getEnd() > groupPosition) { - end += mPositionTranslator.getVisibleChildCount(groupRange.getEnd()); + if (range.getEnd() > groupPosition) { + end += mPositionTranslator.getVisibleChildCount(range.getEnd()); } + mDraggingItemGroupRangeStart = range.getStart(); + mDraggingItemGroupRangeEnd = range.getEnd(); + return new ItemDraggableRange(start, end); + } else { + throw new IllegalStateException("Invalid range specified: " + range); } } else { // child - final ItemDraggableRange groupRange = adapter.onGetChildItemDraggableRange(holder, groupPosition, childPosition); + final ItemDraggableRange range = adapter.onGetChildItemDraggableRange(holder, groupPosition, childPosition); // NOTE: // This method returns actual drag-sortable range, but the visual drag-sortable range would be different. // Thus appending the STATE_FLAG_IS_IN_RANGE flag at correctItemDragStateFlags() to avoid visual corruption. - if (groupRange == null) { + if (range == null) { final int start = 1; // 1 --- to avoid swapping with the first group item return new ItemDraggableRange(start, Math.max(start, mPositionTranslator.getItemCount() - 1)); - } else { - final long startPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupRange.getStart()); - final long endPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupRange.getEnd()); + } else if (isGroupPositionRange(range)) { + final long startPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(range.getStart()); + final long endPackedGroupPosition = ExpandableAdapterHelper.getPackedPositionForGroup(range.getEnd()); final int end = mPositionTranslator.getFlatPosition(endPackedGroupPosition) - + mPositionTranslator.getVisibleChildCount(groupRange.getEnd()); + + mPositionTranslator.getVisibleChildCount(range.getEnd()); int start = mPositionTranslator.getFlatPosition(startPackedGroupPosition) + 1; start = Math.min(start, end); + mDraggingItemGroupRangeStart = range.getStart(); + mDraggingItemGroupRangeEnd = range.getEnd(); + return new ItemDraggableRange(start, end); + } else if (isChildPositionRange(range)) { + final int maxChildrenPos = Math.max(mPositionTranslator.getVisibleChildCount(groupPosition) - 1, 0); + final int childStart = Math.min(range.getStart(), maxChildrenPos); + final int childEnd = Math.min(range.getEnd(), maxChildrenPos); + final long startPackedChildPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childStart); + final long endPackedChildPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childEnd); + final int start = mPositionTranslator.getFlatPosition(startPackedChildPosition); + final int end = mPositionTranslator.getFlatPosition(endPackedChildPosition); + + mDraggingItemChildRangeStart = childStart; + mDraggingItemChildRangeEnd = childEnd; + + return new ItemDraggableRange(start, end); + } else { + throw new IllegalStateException("Invalid range specified: " + range); } } } + private static boolean isGroupPositionRange(ItemDraggableRange range) { + if (range.getClass().equals(GroupPositionItemDraggableRange.class)) { + return true; + } else if (range.getClass().equals(ItemDraggableRange.class)) { + // NOTE: ItemDraggableRange is regarded as group position + return true; + } + return false; + } + + private static boolean isChildPositionRange(ItemDraggableRange range) { + if (range.getClass().equals(ChildPositionItemDraggableRange.class)) { + return true; + } + return false; + } + @Override public void onMoveItem(int fromPosition, int toPosition) { if (!(mExpandableItemAdapter instanceof ExpandableDraggableItemAdapter)) { return; } - mDraggingItemGroupPosition = RecyclerView.NO_POSITION; + mDraggingItemGroupRangeStart = RecyclerView.NO_POSITION; + mDraggingItemGroupRangeEnd = RecyclerView.NO_POSITION; + mDraggingItemChildRangeStart = RecyclerView.NO_POSITION; + mDraggingItemChildRangeEnd = RecyclerView.NO_POSITION; if (fromPosition == toPosition) { return; @@ -436,14 +493,15 @@ class ExpandableRecyclerViewWrapperAdapter @SuppressWarnings("unchecked") @Override - public int onGetSwipeReactionType(RecyclerView.ViewHolder holder, int x, int y) { + public int onGetSwipeReactionType(RecyclerView.ViewHolder holder, int position, int x, int y) { if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) { return RecyclerViewSwipeManager.REACTION_CAN_NOT_SWIPE_BOTH; } final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter; - final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition()); + final int flatPosition = position; + final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); @@ -456,14 +514,15 @@ class ExpandableRecyclerViewWrapperAdapter @SuppressWarnings("unchecked") @Override - public void onSetSwipeBackground(RecyclerView.ViewHolder holder, int type) { + public void onSetSwipeBackground(RecyclerView.ViewHolder holder, int position, int type) { if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) { return; } final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter; - final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition()); + final int flatPosition = position; + final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); @@ -476,14 +535,15 @@ class ExpandableRecyclerViewWrapperAdapter @SuppressWarnings("unchecked") @Override - public int onSwipeItem(RecyclerView.ViewHolder holder, int result) { + public int onSwipeItem(RecyclerView.ViewHolder holder, int position, int result) { if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) { return RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT; } final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter; - final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition()); + final int flatPosition = position; + final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); @@ -496,14 +556,15 @@ class ExpandableRecyclerViewWrapperAdapter @SuppressWarnings("unchecked") @Override - public void onPerformAfterSwipeReaction(RecyclerView.ViewHolder holder, int result, int reaction) { + public void onPerformAfterSwipeReaction(RecyclerView.ViewHolder holder, int position, int result, int reaction) { if (!(mExpandableItemAdapter instanceof ExpandableSwipeableItemAdapter)) { return; } final ExpandableSwipeableItemAdapter adapter = (ExpandableSwipeableItemAdapter) mExpandableItemAdapter; - final long expandablePosition = mPositionTranslator.getExpandablePosition(holder.getPosition()); + final int flatPosition = position; + final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); @@ -513,16 +574,17 @@ class ExpandableRecyclerViewWrapperAdapter adapter.onPerformAfterSwipeChildReaction(holder, groupPosition, childPosition, result, reaction); } } + // NOTE: This method is called from RecyclerViewExpandableItemManager /*package*/ @SuppressWarnings("unchecked") - boolean onTapItem(RecyclerView.ViewHolder holder, int x, int y) { + boolean onTapItem(RecyclerView.ViewHolder holder, int position, int x, int y) { if (mExpandableItemAdapter == null) { return false; } - final int position = holder.getPosition(); - final long expandablePosition = mPositionTranslator.getExpandablePosition(position); + final int flatPosition = position; + final long expandablePosition = mPositionTranslator.getExpandablePosition(flatPosition); final int groupPosition = ExpandableAdapterHelper.getPackedPositionGroup(expandablePosition); final int childPosition = ExpandableAdapterHelper.getPackedPositionChild(expandablePosition); @@ -539,19 +601,24 @@ class ExpandableRecyclerViewWrapperAdapter } if (expand) { - expandGroup(groupPosition); + expandGroup(groupPosition, true); } else { - collapseGroup(groupPosition); + collapseGroup(groupPosition, true); } return true; } - /*package*/ boolean collapseGroup(int groupPosition) { + /*package*/ boolean collapseGroup(int groupPosition, boolean fromUser) { if (!mPositionTranslator.isGroupExpanded(groupPosition)) { return false; } + // call hook method + if (!mExpandableItemAdapter.onHookGroupCollapse(groupPosition, fromUser)) { + return false; + } + if (mPositionTranslator.collapseGroup(groupPosition)) { final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); @@ -568,14 +635,23 @@ class ExpandableRecyclerViewWrapperAdapter notifyItemChanged(flatPosition); } + // raise onGroupCollapse() event + if (mOnGroupCollapseListener != null) { + mOnGroupCollapseListener.onGroupCollapse(groupPosition, fromUser); + } + return true; } - /*package*/ boolean expandGroup(int groupPosition) { + /*package*/ boolean expandGroup(int groupPosition, boolean fromUser) { if (mPositionTranslator.isGroupExpanded(groupPosition)) { return false; } + // call hook method + if (!mExpandableItemAdapter.onHookGroupExpand(groupPosition, fromUser)) { + return false; + } if (mPositionTranslator.expandGroup(groupPosition)) { final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); @@ -592,6 +668,11 @@ class ExpandableRecyclerViewWrapperAdapter notifyItemChanged(flatPosition); } + // raise onGroupExpand() event + if (mOnGroupExpandListener != null) { + mOnGroupExpandListener.onGroupExpand(groupPosition, fromUser); + } + return true; } @@ -615,6 +696,161 @@ class ExpandableRecyclerViewWrapperAdapter } } + /*package*/ void setOnGroupExpandListener(RecyclerViewExpandableItemManager.OnGroupExpandListener listener) { + mOnGroupExpandListener = listener; + } + + /*package*/ void setOnGroupCollapseListener(RecyclerViewExpandableItemManager.OnGroupCollapseListener listener) { + mOnGroupCollapseListener = listener; + } + + /*package*/ void restoreState(int[] adapterSavedState, boolean callHook, boolean callListeners) { + mPositionTranslator.restoreExpandedGroupItems( + adapterSavedState, + (callHook ? mExpandableItemAdapter : null), + (callListeners ? mOnGroupExpandListener : null), + (callListeners ? mOnGroupCollapseListener : null)); + } + + /*package*/ void notifyGroupItemChanged(int groupPosition) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemChanged(flatPosition); + } + } + + /*package*/ void notifyGroupAndChildrenItemsChanged(int groupPosition) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + final int visibleChildCount = mPositionTranslator.getVisibleChildCount(groupPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemRangeChanged(flatPosition, 1 + visibleChildCount); + } + } + + /*package*/ void notifyChildrenOfGroupItemChanged(int groupPosition) { + final int visibleChildCount = mPositionTranslator.getVisibleChildCount(groupPosition); + + // notify if the group is expanded + if (visibleChildCount > 0) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, 0); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemRangeChanged(flatPosition, visibleChildCount); + } + } + } + + /*package*/ void notifyChildItemChanged(int groupPosition, int childPosition) { + notifyChildItemRangeChanged(groupPosition, childPosition, 1); + } + + /*package*/ void notifyChildItemRangeChanged(int groupPosition, int childPositionStart, int itemCount) { + final int visibleChildCount = mPositionTranslator.getVisibleChildCount(groupPosition); + + // notify if the group is expanded + if ((visibleChildCount > 0) && (childPositionStart < visibleChildCount)) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, 0); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + final int startPosition = flatPosition + childPositionStart; + final int count = Math.min(itemCount, (visibleChildCount - childPositionStart)); + + notifyItemRangeChanged(startPosition, count); + } + } + } + + /*package*/ void notifyChildItemInserted(int groupPosition, int childPosition) { + mPositionTranslator.insertChildItem(groupPosition, childPosition); + + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childPosition); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemInserted(flatPosition); + } + } + + /*package*/ void notifyChildItemRangeInserted(int groupPosition, int childPositionStart, int itemCount) { + mPositionTranslator.insertChildItems(groupPosition, childPositionStart, itemCount); + + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childPositionStart); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemRangeInserted(flatPosition, itemCount); + } + } + + /*package*/ void notifyChildItemRemoved(int groupPosition, int childPosition) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childPosition); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + mPositionTranslator.removeChildItem(groupPosition, childPosition); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemRemoved(flatPosition); + } + } + + /*package*/ void notifyChildItemRangeRemoved(int groupPosition, int childPositionStart, int itemCount) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForChild(groupPosition, childPositionStart); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + mPositionTranslator.removeChildItems(groupPosition, childPositionStart, itemCount); + + if (flatPosition != RecyclerView.NO_POSITION) { + notifyItemRangeRemoved(flatPosition, itemCount); + } + } + + /*package*/ void notifyGroupItemInserted(int groupPosition) { + int insertedCount = mPositionTranslator.insertGroupItem(groupPosition); + if (insertedCount > 0) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + notifyItemInserted(flatPosition); + } + } + + /*package*/ void notifyGroupItemRangeInserted(int groupPositionStart, int count) { + int insertedCount = mPositionTranslator.insertGroupItems(groupPositionStart, count); + if (insertedCount > 0) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPositionStart); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + notifyItemRangeInserted(flatPosition, insertedCount); + } + } + + /*package*/ void notifyGroupItemRemoved(int groupPosition) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPosition); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + int removedCount = mPositionTranslator.removeGroupItem(groupPosition); + if (removedCount > 0) { + notifyItemRangeRemoved(flatPosition, removedCount); + } + } + + /*package*/ void notifyGroupItemRangeRemoved(int groupPositionStart, int count) { + final long packedPosition = ExpandableAdapterHelper.getPackedPositionForGroup(groupPositionStart); + final int flatPosition = mPositionTranslator.getFlatPosition(packedPosition); + + int removedCount = mPositionTranslator.removeGroupItems(groupPositionStart, count); + if (removedCount > 0) { + notifyItemRangeRemoved(flatPosition, removedCount); + } + } + + private static ExpandableItemAdapter getExpandableItemAdapter(RecyclerView.Adapter adapter) { return WrapperAdapterUtils.findWrappedAdapter(adapter, ExpandableItemAdapter.class); } @@ -637,22 +873,43 @@ class ExpandableRecyclerViewWrapperAdapter holder2.setExpandStateFlags(flags); } - private void correctItemDragStateFlags(RecyclerView.ViewHolder holder, int groupPosition) { + private void correctItemDragStateFlags(RecyclerView.ViewHolder holder, int groupPosition, int childPosition) { if (!(holder instanceof DraggableItemViewHolder)) { return; } final DraggableItemViewHolder holder2 = (DraggableItemViewHolder) holder; + final boolean groupRangeSpecified = + (mDraggingItemGroupRangeStart != RecyclerView.NO_POSITION) && + (mDraggingItemGroupRangeEnd != RecyclerView.NO_POSITION); + final boolean childRangeSpecified = + (mDraggingItemChildRangeStart != RecyclerView.NO_POSITION) && + (mDraggingItemChildRangeEnd != RecyclerView.NO_POSITION); + final boolean isInGroupRange = + (groupPosition >= mDraggingItemGroupRangeStart) && + (groupPosition <= mDraggingItemGroupRangeEnd); + final boolean isInChildRange = + (groupPosition != RecyclerView.NO_POSITION) && + (childPosition >= mDraggingItemChildRangeStart) && + (childPosition <= mDraggingItemChildRangeEnd); + final int flags = holder2.getDragStateFlags(); + boolean needCorrection = false; if (((flags & RecyclerViewDragDropManager.STATE_FLAG_DRAGGING) != 0) && ((flags & RecyclerViewDragDropManager.STATE_FLAG_IS_IN_RANGE) == 0)) { - if (mDraggingItemGroupPosition == groupPosition) { - holder2.setDragStateFlags( - flags | RecyclerViewDragDropManager.STATE_FLAG_IS_IN_RANGE | - RecyclerViewDragDropManager.STATE_FLAG_IS_UPDATED); + if (!groupRangeSpecified || isInGroupRange) { + if (!childRangeSpecified || (childRangeSpecified && isInChildRange)) { + needCorrection = true; + } } } + + if (needCorrection) { + holder2.setDragStateFlags( + flags | RecyclerViewDragDropManager.STATE_FLAG_IS_IN_RANGE | + RecyclerViewDragDropManager.STATE_FLAG_IS_UPDATED); + } } } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableSwipeableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/ExpandableSwipeableItemAdapter.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/GroupPositionItemDraggableRange.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/GroupPositionItemDraggableRange.java new file mode 100755 index 00000000000..301b1561e13 --- /dev/null +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/GroupPositionItemDraggableRange.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015 Haruki Hasegawa + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.h6ah4i.android.widget.advrecyclerview.expandable; + +import com.h6ah4i.android.widget.advrecyclerview.draggable.ItemDraggableRange; + +public class GroupPositionItemDraggableRange extends ItemDraggableRange { + public GroupPositionItemDraggableRange(int start, int end) { + super(start, end); + } + + protected String getClassName() { + return "GroupPositionItemDraggableRange"; + } +} diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java old mode 100644 new mode 100755 index 206bf9d81e6..20ee11b90d6 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/expandable/RecyclerViewExpandableItemManager.java @@ -64,6 +64,35 @@ public class RecyclerViewExpandableItemManager { */ public static final int STATE_FLAG_IS_UPDATED = (1 << 31); + + // --- + + /** + * Used for being notified when a group is expanded + */ + public interface OnGroupExpandListener { + /** + * Callback method to be invoked when a group in this expandable list has been expanded. + * + * @param groupPosition The group position that was expanded + * @param fromUser Whether the expand request is issued by a user operation + */ + void onGroupExpand(int groupPosition, boolean fromUser); + } + + /** + * Used for being notified when a group is collapsed + */ + public interface OnGroupCollapseListener { + /** + * Callback method to be invoked when a group in this expandable list has been collapsed. + * + * @param groupPosition The group position that was collapsed + * @param fromUser Whether the collapse request is issued by a user operation + */ + void onGroupCollapse(int groupPosition, boolean fromUser); + } + // --- private SavedState mSavedState; @@ -71,6 +100,8 @@ public class RecyclerViewExpandableItemManager { private RecyclerView mRecyclerView; private ExpandableRecyclerViewWrapperAdapter mAdapter; private RecyclerView.OnItemTouchListener mInternalUseOnItemTouchListener; + private OnGroupExpandListener mOnGroupExpandListener; + private OnGroupCollapseListener mOnGroupCollapseListener; private long mTouchedItemId = RecyclerView.NO_ID; private int mTouchSlop; @@ -92,6 +123,10 @@ public class RecyclerViewExpandableItemManager { @Override public void onTouchEvent(RecyclerView rv, MotionEvent e) { } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + } }; if (savedState instanceof SavedState) { @@ -145,6 +180,8 @@ public class RecyclerViewExpandableItemManager { mRecyclerView.removeOnItemTouchListener(mInternalUseOnItemTouchListener); } mInternalUseOnItemTouchListener = null; + mOnGroupExpandListener = null; + mOnGroupCollapseListener = null; mRecyclerView = null; mSavedState = null; } @@ -167,6 +204,13 @@ public class RecyclerViewExpandableItemManager { mAdapter = new ExpandableRecyclerViewWrapperAdapter(this, adapter, adapterSavedState); + // move listeners to wrapper adapter + mAdapter.setOnGroupExpandListener(mOnGroupExpandListener); + mOnGroupExpandListener = null; + + mAdapter.setOnGroupCollapseListener(mOnGroupCollapseListener); + mOnGroupCollapseListener = null; + return mAdapter; } @@ -201,7 +245,8 @@ public class RecyclerViewExpandableItemManager { case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: if (handleActionUpOrCancel(rv, e)) { - return true; + // NOTE: it requires to return false to work click effect properly (issue #44) + return false; } break; } @@ -215,7 +260,7 @@ public class RecyclerViewExpandableItemManager { mInitialTouchX = (int) (e.getX() + 0.5f); mInitialTouchY = (int) (e.getY() + 0.5f); - if (holder != null) { + if (holder instanceof ExpandableItemViewHolder) { mTouchedItemId = holder.getItemId(); } else { mTouchedItemId = RecyclerView.NO_ID; @@ -251,13 +296,19 @@ public class RecyclerViewExpandableItemManager { return false; } + final int position = CustomRecyclerViewUtils.getSynchronizedPosition(holder); + + if (position == RecyclerView.NO_POSITION) { + return false; + } + final View view = holder.itemView; final int translateX = (int) (ViewCompat.getTranslationX(view) + 0.5f); final int translateY = (int) (ViewCompat.getTranslationY(view) + 0.5f); final int viewX = touchX - (view.getLeft() + translateX); final int viewY = touchY - (view.getTop() + translateY); - return mAdapter.onTapItem(holder, viewX, viewY); + return mAdapter.onTapItem(holder, position, viewX, viewY); } /** @@ -265,10 +316,10 @@ public class RecyclerViewExpandableItemManager { * * @param groupPosition The group position to be expanded * - * @return True if the group was expanded,false otherwise (If the group was already expanded, this will return false) + * @return True if the group was expanded, false otherwise (If the group was already expanded, this will return false) */ public boolean expandGroup(int groupPosition) { - return (mAdapter != null) && mAdapter.expandGroup(groupPosition); + return (mAdapter != null) && mAdapter.expandGroup(groupPosition, false); } /** @@ -276,10 +327,10 @@ public class RecyclerViewExpandableItemManager { * * @param groupPosition The group position to be collapsed * - * @return True if the group was collapsed,false otherwise (If the group was already collapsed, this will return false) + * @return True if the group was collapsed, false otherwise (If the group was already collapsed, this will return false) */ public boolean collapseGroup(int groupPosition) { - return (mAdapter != null) && mAdapter.collapseGroup(groupPosition); + return (mAdapter != null) && mAdapter.collapseGroup(groupPosition, false); } /** @@ -328,7 +379,7 @@ public class RecyclerViewExpandableItemManager { * Returns the packed position representation of a child position. * * In general, a packed position should be used in situations where the position given to/returned from - * {@link RecyclerViewExpandableItemManager} method can eirther be a child or group. + * {@link RecyclerViewExpandableItemManager} method can either be a child or group. * The two positions are packed into a single long which can be unpacked using {@link #getPackedPositionChild(long)} and * {@link #getPackedPositionGroup(long)}. * @@ -438,6 +489,303 @@ public class RecyclerViewExpandableItemManager { return ExpandableAdapterHelper.getChildViewType(rawViewType); } + /** + * Register a callback to be invoked when an group item has been expanded. + * + * @param listener The callback that will be invoked. + */ + public void setOnGroupExpandListener(OnGroupExpandListener listener) { + if (mAdapter != null) { + mAdapter.setOnGroupExpandListener(listener); + } else { + // pending + mOnGroupExpandListener = listener; + } + } + + /** + * Register a callback to be invoked when an group item has been collapsed. + * + * @param listener The callback that will be invoked. + */ + public void setOnGroupCollapseListener(OnGroupCollapseListener listener) { + if (mAdapter != null) { + mAdapter.setOnGroupCollapseListener(listener); + } else { + // pending + mOnGroupCollapseListener = listener; + } + } + + /** + * Restore saves state. See {@link #restoreState(android.os.Parcelable, boolean, boolean)}. + * (This method does not invoke any hook methods and listener events) + * + * @param savedState The saved state object + */ + public void restoreState(Parcelable savedState) { + restoreState(savedState, false, false); + } + + /** + * Restore saves state. + * + * This method is useful when the adapter can not be prepared (because data loading may takes time and processed asynchronously) + * before creating this manager instance. + * + * @param savedState The saved state object + * @param callHooks Whether to call hook routines + * ({@link ExpandableItemAdapter#onHookGroupExpand(int, boolean)}, + * {@link ExpandableItemAdapter#onHookGroupCollapse(int, boolean)}) + * @param callListeners Whether to invoke {@link OnGroupExpandListener} and/or {@link OnGroupCollapseListener} listener events + */ + public void restoreState(Parcelable savedState, boolean callHooks, boolean callListeners) { + if (savedState == null) { + return; // do nothing + } + + if (!(savedState instanceof SavedState)) { + throw new IllegalArgumentException("Illegal saved state object passed"); + } + + if (!((mAdapter != null) && (mRecyclerView != null))) { + throw new IllegalStateException("RecyclerView has not been attached"); + } + + mAdapter.restoreState(((SavedState) savedState).adapterSavedState, callHooks, callListeners); + } + + /** + * Notify any registered observers that the group item at groupPosition has changed. + * + *

This is an group item change event, not a structural change event. It indicates that any + * reflection of the data at groupPosition is out of date and should be updated. + * The item at groupPosition retains the same identity.

+ * + *

This method does not notify for children that are contained in the specified group. + * If children have also changed, use {@link #notifyGroupAndChildrenItemsChanged(int)} instead.

+ * + * @param groupPosition Position of the group item that has changed + * + * @see #notifyGroupAndChildrenItemsChanged(int) + */ + public void notifyGroupItemChanged(int groupPosition) { + mAdapter.notifyGroupItemChanged(groupPosition); + } + + /** + * Notify any registered observers that the group and children items at groupPosition have changed. + * + *

This is an group item change event, not a structural change event. It indicates that any + * reflection of the data at groupPosition is out of date and should be updated. + * The item at groupPosition retains the same identity.

+ * + * @param groupPosition Position of the group item which contains changed children + * + * @see #notifyGroupItemChanged(int) + * @see #notifyChildrenOfGroupItemChanged(int) + */ + public void notifyGroupAndChildrenItemsChanged(int groupPosition) { + mAdapter.notifyGroupAndChildrenItemsChanged(groupPosition); + } + + /** + * Notify any registered observers that the children items contained in the group item at groupPosition have changed. + * + *

This is an group item change event, not a structural change event. It indicates that any + * reflection of the data at groupPosition is out of date and should be updated. + * The item at groupPosition retains the same identity.

+ * + *

This method does not notify for the group item. + * If the group has also changed, use {@link #notifyGroupAndChildrenItemsChanged(int)} instead.

+ * + * @param groupPosition Position of the group item which contains changed children + * + * @see #notifyGroupAndChildrenItemsChanged(int) + */ + public void notifyChildrenOfGroupItemChanged(int groupPosition) { + mAdapter.notifyChildrenOfGroupItemChanged(groupPosition); + } + + /** + * Notify any registered observers that the child item at {groupPosition, childPosition} has changed. + * + *

This is an item change event, not a structural change event. It indicates that any + * reflection of the data at {groupPosition, childPosition} is out of date and should be updated. + * The item at {groupPosition, childPosition} retains the same identity.

+ * + * @param groupPosition Position of the group item which contains the changed child + * @param childPosition Position of the child item in the group that has changed + * + * @see #notifyChildItemRangeChanged(int, int, int) + */ + public void notifyChildItemChanged(int groupPosition, int childPosition) { + mAdapter.notifyChildItemChanged(groupPosition, childPosition); + } + + /** + * Notify any registered observers that the itemCount child items starting at + * position {groupPosition, childPosition} have changed. + * + *

This is an item change event, not a structural change event. It indicates that + * any reflection of the data in the given position range is out of date and should + * be updated. The items in the given range retain the same identity.

+ * + * @param groupPosition Position of the group item which contains the changed child + * @param childPositionStart Position of the first child item in the group that has changed + * @param itemCount Number of items that have changed + * + * @see #notifyChildItemChanged(int, int) + */ + public void notifyChildItemRangeChanged(int groupPosition, int childPositionStart, int itemCount) { + mAdapter.notifyChildItemRangeChanged(groupPosition, childPositionStart, itemCount); + } + + /** + * Notify any registered observers that the group item reflected at groupPosition + * has been newly inserted. The group item previously at groupPosition is now at + * position groupPosition + 1. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered.

+ * + * @param groupPosition Position of the newly inserted group item in the data set + * + * @see #notifyGroupItemRangeInserted(int, int) + */ + public void notifyGroupItemInserted(int groupPosition) { + mAdapter.notifyGroupItemInserted(groupPosition); + } + + /** + * Notify any registered observers that the currently reflected itemCount + * group items starting at groupPositionStart have been newly inserted. The group items + * previously located at groupPositionStart and beyond can now be found starting + * at position groupPositionStart + itemCount. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param groupPositionStart Position of the first group item that was inserted + * @param itemCount Number of group items inserted + * + * @see #notifyGroupItemInserted(int) + */ + public void notifyGroupItemRangeInserted(int groupPositionStart, int itemCount) { + mAdapter.notifyGroupItemRangeInserted(groupPositionStart, itemCount); + } + + + /** + * Notify any registered observers that the group item reflected at groupPosition + * has been newly inserted. The group item previously at groupPosition is now at + * position groupPosition + 1. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their + * positions may be altered.

+ * + * @param groupPosition Position of the group item which contains the inserted child + * @param childPosition Position of the newly inserted child item in the data set + * + * @see #notifyChildItemRangeInserted(int, int, int) + */ + public void notifyChildItemInserted(int groupPosition, int childPosition) { + mAdapter.notifyChildItemInserted(groupPosition, childPosition); + } + + /** + * Notify any registered observers that the currently reflected itemCount + * child items starting at childPositionStart have been newly inserted. The child items + * previously located at childPositionStart and beyond can now be found starting + * at position childPositionStart + itemCount. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param groupPosition Position of the group item which contains the inserted child + * @param childPositionStart Position of the first child item that was inserted + * @param itemCount Number of child items inserted + * + * @see #notifyChildItemInserted(int, int) + */ + public void notifyChildItemRangeInserted(int groupPosition, int childPositionStart, int itemCount) { + mAdapter.notifyChildItemRangeInserted(groupPosition, childPositionStart, itemCount); + } + + /** + * Notify any registered observers that the group item previously located at groupPosition + * has been removed from the data set. The group items previously located at and after + * groupPosition may now be found at oldGroupPosition - 1. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param groupPosition Position of the group item that has now been removed + * + * @see #notifyGroupItemRangeRemoved(int, int) + */ + public void notifyGroupItemRemoved(int groupPosition) { + mAdapter.notifyGroupItemRemoved(groupPosition); + } + + /** + * Notify any registered observers that the itemCount group items previously + * located at groupPositionStart have been removed from the data set. The group items + * previously located at and after groupPositionStart + itemCount may now be found + * at oldPosition - itemCount. + * + *

This is a structural change event. Representations of other existing items in the data + * set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param groupPositionStart Previous position of the first group item that was removed + * @param itemCount Number of group items removed from the data set + */ + public void notifyGroupItemRangeRemoved(int groupPositionStart, int itemCount) { + mAdapter.notifyGroupItemRangeRemoved(groupPositionStart, itemCount); + } + + /** + * Notify any registered observers that the child item previously located at childPosition + * has been removed from the data set. The child items previously located at and after + * childPosition may now be found at oldGroupPosition - 1. + * + *

This is a structural change event. Representations of other existing items in the + * data set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param groupPosition Position of the group item which was the parent of the child item that was removed + * @param childPosition Position of the child item that has now been removed + * + * @see #notifyGroupItemRangeRemoved(int, int) + */ + public void notifyChildItemRemoved(int groupPosition, int childPosition) { + mAdapter.notifyChildItemRemoved(groupPosition, childPosition); + } + + /** + * Notify any registered observers that the itemCount child items previously + * located at childPositionStart have been removed from the data set. The child items + * previously located at and after childPositionStart + itemCount may now be found + * at oldPosition - itemCount. + * + *

This is a structural change event. Representations of other existing items in the data + * set are still considered up to date and will not be rebound, though their positions + * may be altered.

+ * + * @param groupPosition Position of the group item which was the parent of the child item that was removed + * @param childPositionStart Previous position of the first child item that was removed + * @param itemCount Number of child items removed from the data set + */ + public void notifyChildItemRangeRemoved(int groupPosition, int childPositionStart, int itemCount) { + mAdapter.notifyChildItemRangeRemoved(groupPosition, childPositionStart, itemCount); + } + public static class SavedState implements Parcelable { final int [] adapterSavedState; diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java old mode 100644 new mode 100755 index 925a006146c..7ccd5b54fdd --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/ItemSlidingAnimator.java @@ -32,6 +32,7 @@ import android.view.animation.AccelerateInterpolator; import android.view.animation.Interpolator; import android.widget.FrameLayout; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; @@ -41,40 +42,62 @@ public class ItemSlidingAnimator { private Interpolator mSlideToDefaultPositionAnimationInterpolator = new AccelerateDecelerateInterpolator(); private Interpolator mSlideToOutsideOfWindowAnimationInterpolator = new AccelerateInterpolator(0.8f); private List mActive; + private List> mDeferredProcesses; private int[] mTmpLocation = new int[2]; private Rect mTmpRect = new Rect(); private int mImmediatelySetTranslationThreshold; public ItemSlidingAnimator() { mActive = new ArrayList<>(); + mDeferredProcesses = new ArrayList<>(); } public void slideToDefaultPosition(RecyclerView.ViewHolder holder, boolean shouldAnimate, long duration) { + cancelDeferredProcess(holder); slideToSpecifiedPositionInternal(holder, 0, shouldAnimate, duration); } public void slideToOutsideOfWindow(RecyclerView.ViewHolder holder, boolean toLeft, boolean shouldAnimate, long duration) { + cancelDeferredProcess(holder); slideToOutsideOfWindowInternal(holder, toLeft, shouldAnimate, duration); } public void slideToSpecifiedPosition(RecyclerView.ViewHolder holder, float position) { + cancelDeferredProcess(holder); slideToSpecifiedPositionInternal(holder, position, false, 0); } - private boolean slideToSpecifiedPositionInternal(RecyclerView.ViewHolder holder, float position, boolean shouldAnimate, long duration) { - final int translationX; + private void cancelDeferredProcess(RecyclerView.ViewHolder holder) { + int n = mDeferredProcesses.size(); + for (int i = n - 1; i >= 0; i--) { + ViewHolderDeferredProcess dp = mDeferredProcesses.get(i).get(); + if (dp == null || dp.hasTargetViewHolderOrLostReference(holder)) { + mDeferredProcesses.remove(i); + } + } + } + + private boolean slideToSpecifiedPositionInternal(final RecyclerView.ViewHolder holder, final float position, boolean shouldAnimate, long duration) { duration = (shouldAnimate) ? duration : 0; if (position != 0.0f) { final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); + final int width = containerView.getWidth(); - translationX = (int) (containerView.getWidth() * position + 0.5f); + if (width != 0) { + final int translationX; + translationX = (int) (width * position + 0.5f); + return animateSlideInternalCompat(holder, translationX, duration, mSlideToDefaultPositionAnimationInterpolator); + } else { + final DeferredSlideProcess deferredProcess = new DeferredSlideProcess(holder, position); + mDeferredProcesses.add(new WeakReference(deferredProcess)); + containerView.post(deferredProcess); + return false; + } } else { - translationX = 0; + return animateSlideInternalCompat(holder, 0, duration, mSlideToDefaultPositionAnimationInterpolator); } - - return animateSlideInternalCompat(holder, translationX, duration, mSlideToDefaultPositionAnimationInterpolator); } private boolean slideToOutsideOfWindowInternal(RecyclerView.ViewHolder holder, boolean toLeft, boolean shouldAnimate, long duration) { @@ -124,12 +147,20 @@ public class ItemSlidingAnimator { if (supportsViewPropertyAnimator()) { return animateSlideInternal(holder, translationX, duration, interpolator); } else { - return animateSlideInternalPreHoneycomb(holder, translationX); + return slideInternalPreHoneycomb(holder, translationX); + } + } + + static void slideInternalCompat(RecyclerView.ViewHolder holder, int translationX) { + if (supportsViewPropertyAnimator()) { + slideInternal(holder, translationX); + } else { + slideInternalPreHoneycomb(holder, translationX); } } @SuppressLint("RtlHardcoded") - private boolean animateSlideInternalPreHoneycomb(RecyclerView.ViewHolder holder, int translationX) { + private static boolean slideInternalPreHoneycomb(RecyclerView.ViewHolder holder, int translationX) { if (!(holder instanceof SwipeableItemViewHolder)) { return false; } @@ -153,6 +184,29 @@ public class ItemSlidingAnimator { return false; } + private static int getTranslationXPreHoneycomb(RecyclerView.ViewHolder holder) { + final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); + + final ViewGroup.LayoutParams lp = containerView.getLayoutParams(); + if (lp instanceof ViewGroup.MarginLayoutParams) { + final ViewGroup.MarginLayoutParams mlp = (ViewGroup.MarginLayoutParams) lp; + return mlp.leftMargin; + } else { + Log.w(TAG, "should use MarginLayoutParams supported view for compatibility on Android 2.3"); + return 0; + } + } + + private static void slideInternal(final RecyclerView.ViewHolder holder, int translationX) { + if (!(holder instanceof SwipeableItemViewHolder)) { + return; + } + + final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); + ViewCompat.animate(containerView).cancel(); + ViewCompat.setTranslationX(containerView, translationX); + } + private boolean animateSlideInternal(final RecyclerView.ViewHolder holder, int translationX, long duration, Interpolator interpolator) { if (!(holder instanceof SwipeableItemViewHolder)) { return false; @@ -164,8 +218,13 @@ public class ItemSlidingAnimator { endAnimation(holder); + final int curTranslationX = (int) (ViewCompat.getTranslationX(containerView) + 0.5f); final int toX = translationX; + if (curTranslationX == toX) { + return false; + } + if (duration == 0 || Math.abs(toX - prevTranslationX) <= mImmediatelySetTranslationThreshold) { ViewCompat.setTranslationX(containerView, toX); return false; @@ -209,6 +268,8 @@ public class ItemSlidingAnimator { return; } + cancelDeferredProcess(holder); + final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); ViewCompat.animate(containerView).cancel(); @@ -244,4 +305,56 @@ public class ItemSlidingAnimator { private static boolean supportsViewPropertyAnimator() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; } + + public int getSwipeContainerViewTranslationX(RecyclerView.ViewHolder holder) { + if (supportsViewPropertyAnimator()) { + final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); + return (int) (ViewCompat.getTranslationX(containerView) + 0.5f); + } else { + return getTranslationXPreHoneycomb(holder); + } + } + + private static abstract class ViewHolderDeferredProcess implements Runnable { + final WeakReference mRefHolder; + + public ViewHolderDeferredProcess(RecyclerView.ViewHolder holder) { + mRefHolder = new WeakReference(holder); + } + + @Override + public void run() { + RecyclerView.ViewHolder holder = mRefHolder.get(); + + if (holder != null) { + onProcess(holder); + } + } + + public boolean hasTargetViewHolderOrLostReference(RecyclerView.ViewHolder holder) { + RecyclerView.ViewHolder holder2 = mRefHolder.get(); + return ((holder2 == null) || (holder2 == holder)); + } + + protected abstract void onProcess(RecyclerView.ViewHolder holder); + } + + private static final class DeferredSlideProcess extends ViewHolderDeferredProcess { + final float mPosition; + + public DeferredSlideProcess(RecyclerView.ViewHolder holder, float position) { + super(holder); + mPosition = position; + } + + @Override + protected void onProcess(RecyclerView.ViewHolder holder) { + final View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); + final int width = containerView.getWidth(); + final int translationX; + + translationX = (int) (width * mPosition + 0.5f); + slideInternalCompat(holder, translationX); + } + } } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java old mode 100644 new mode 100755 index 810f98835ee..648bc7b7302 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RecyclerViewSwipeManager.java @@ -40,7 +40,7 @@ public class RecyclerViewSwipeManager { /** * Result code of swipe operation. Used for the second argument of the - * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * None. (internal default value, this value is not used for the argument) */ @@ -48,7 +48,7 @@ public class RecyclerViewSwipeManager { /** * Result code of swipe operation. Used for the second argument of the - * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Canceled. */ @@ -56,7 +56,7 @@ public class RecyclerViewSwipeManager { /** * Result code of swipe operation. Used for the second argument of the - * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Swipe left performed. */ @@ -64,7 +64,7 @@ public class RecyclerViewSwipeManager { /** * Result code of swipe operation. Used for the second argument of the - * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onPerformAfterSwipeReaction(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Swipe right performed. */ @@ -73,7 +73,7 @@ public class RecyclerViewSwipeManager { // --- /** * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Indicates "can not swipe left" (completely no reactions) */ @@ -81,7 +81,7 @@ public class RecyclerViewSwipeManager { /** * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Indicates "can not swipe left" (not swipeable, but rubber-band effect applied) */ @@ -89,7 +89,7 @@ public class RecyclerViewSwipeManager { /** * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Indicates "can swipe left" */ @@ -97,7 +97,7 @@ public class RecyclerViewSwipeManager { /** * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Indicates "can not swipe right" (completely no reactions) */ @@ -105,7 +105,7 @@ public class RecyclerViewSwipeManager { /** * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Indicates "can not swipe right" (not swipeable, but rubber-band effect applied) */ @@ -113,7 +113,7 @@ public class RecyclerViewSwipeManager { /** * Reaction type to swipe operation. Used for the return value of the - * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. + * {@link SwipeableItemAdapter#onGetSwipeReactionType(android.support.v7.widget.RecyclerView.ViewHolder, int, int, int)} method. * * Indicates "can swipe right" */ @@ -141,7 +141,7 @@ public class RecyclerViewSwipeManager { // --- /** * Background drawable type used for the second argument of the - * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int)} method. + * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. * * Background image for the neutral (= not swiping) item. */ @@ -149,7 +149,7 @@ public class RecyclerViewSwipeManager { /** * Background drawable type used for the second argument of the - * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int)} method. + * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. * * Background image for the swiping-left item. */ @@ -157,7 +157,7 @@ public class RecyclerViewSwipeManager { /** * Background drawable type used for the second argument of the - * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int)} method. + * {@link SwipeableItemAdapter#onSetSwipeBackground(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. * * Background image for the swiping-right item. */ @@ -217,6 +217,29 @@ public class RecyclerViewSwipeManager { // --- + /** + * Used for listening item swipe events + */ + public interface OnItemSwipeEventListener { + /** + * Callback method to be invoked when swiping is started. + * + * @param position The position of the item. + */ + void onItemSwipeStarted(int position); + + /** + * Callback method to be invoked when swiping is finished. + * + * @param position The position of the item. + * @param result The result code of the swipe operation. + * @param afterSwipeReaction The reaction type to the swipe operation. + */ + void onItemSwipeFinished(int position, int result, int afterSwipeReaction); + } + + // --- + private static final int MIN_DISTANCE_TOUCH_SLOP_MUL = 10; private static final int SLIDE_ITEM_IMMEDIATELY_SET_TRANSLATION_THRESHOLD_DP = 8; @@ -239,12 +262,14 @@ public class RecyclerViewSwipeManager { private ItemSlidingAnimator mItemSlideAnimator; private SwipeableItemWrapperAdapter mAdapter; private RecyclerView.ViewHolder mSwipingItem; + private int mSwipingItemPosition = RecyclerView.NO_POSITION; private Rect mSwipingItemMargins = new Rect(); private int mTouchedItemOffsetX; private int mLastTouchX; private int mSwipingItemReactionType; private VelocityTracker mVelocityTracker; private SwipingItemOperator mSwipingItemOperator; + private OnItemSwipeEventListener mItemSwipeEventListener; /** * Constructor. @@ -260,6 +285,10 @@ public class RecyclerViewSwipeManager { public void onTouchEvent(RecyclerView rv, MotionEvent e) { RecyclerViewSwipeManager.this.onTouchEvent(rv, e); } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + } }; mItemSlideAnimator = new ItemSlidingAnimator(); mVelocityTracker = VelocityTracker.obtain(); @@ -437,7 +466,7 @@ public class RecyclerViewSwipeManager { return false; } - final int itemPosition = holder.getPosition(); + final int itemPosition = CustomRecyclerViewUtils.getSynchronizedPosition(holder); // verify the touched item is valid state if (!(itemPosition >= 0 && itemPosition < adapter.getItemCount())) { @@ -456,7 +485,7 @@ public class RecyclerViewSwipeManager { final int viewX = touchX - (view.getLeft() + translateX); final int viewY = touchY - (view.getTop() + translateY); - final int reactionType = mAdapter.getSwipeReactionType(holder, viewX, viewY); + final int reactionType = mAdapter.getSwipeReactionType(holder, itemPosition, viewX, viewY); if (reactionType == 0) { return false; @@ -538,11 +567,17 @@ public class RecyclerViewSwipeManager { return false; } + final int itemPosition = CustomRecyclerViewUtils.getSynchronizedPosition(holder); + + if (itemPosition == RecyclerView.NO_POSITION) { + return false; + } + if (LOCAL_LOGD) { Log.d(TAG, "swiping started"); } - startSwiping(rv, e, holder); + startSwiping(rv, e, holder, itemPosition); return true; } @@ -556,13 +591,14 @@ public class RecyclerViewSwipeManager { mSwipingItemOperator.update(swipeDistance); } - private void startSwiping(RecyclerView rv, MotionEvent e, RecyclerView.ViewHolder holder) { + private void startSwiping(RecyclerView rv, MotionEvent e, RecyclerView.ViewHolder holder, int itemPosition) { mSwipingItem = holder; + mSwipingItemPosition = itemPosition; mLastTouchX = (int) (e.getX() + 0.5f); mTouchedItemOffsetX = mLastTouchX; CustomRecyclerViewUtils.getLayoutMargins(holder.itemView, mSwipingItemMargins); - mSwipingItemOperator = new SwipingItemOperator(this, mSwipingItem, mSwipingItemReactionType); + mSwipingItemOperator = new SwipingItemOperator(this, mSwipingItem, mSwipingItemPosition, mSwipingItemReactionType); mSwipingItemOperator.start(); mVelocityTracker.clear(); @@ -570,8 +606,13 @@ public class RecyclerViewSwipeManager { mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true); + // raise onItemSwipeStarted() event + if (mItemSwipeEventListener != null) { + mItemSwipeEventListener.onItemSwipeStarted(itemPosition); + } + // raise onSwipeItemStarted() event - mAdapter.onSwipeItemStarted(this, holder); + mAdapter.onSwipeItemStarted(this, holder, itemPosition); } private void finishSwiping(int result) { @@ -585,9 +626,12 @@ public class RecyclerViewSwipeManager { mRecyclerView.getParent().requestDisallowInterceptTouchEvent(false); } + final int itemPosition = mSwipingItemPosition; + mVelocityTracker.clear(); mSwipingItem = null; + mSwipingItemPosition = RecyclerView.NO_POSITION; mLastTouchX = 0; mInitialTouchX = 0; mTouchedItemOffsetX = 0; @@ -603,7 +647,7 @@ public class RecyclerViewSwipeManager { int afterReaction = AFTER_SWIPE_REACTION_DEFAULT; if (mAdapter != null) { - afterReaction = mAdapter.onSwipeItemFinished(swipingItem, result); + afterReaction = mAdapter.onSwipeItemFinished(swipingItem, itemPosition, result); } afterReaction = correctAfterReaction(result, afterReaction); @@ -640,7 +684,12 @@ public class RecyclerViewSwipeManager { } if (mAdapter != null) { - mAdapter.onSwipeItemFinished2(swipingItem, result, afterReaction); + mAdapter.onSwipeItemFinished2(swipingItem, itemPosition, result, afterReaction); + } + + // raise onItemSwipeFinished() event + if (mItemSwipeEventListener != null) { + mItemSwipeEventListener.onItemSwipeFinished(itemPosition, result, afterReaction); } } @@ -711,7 +760,24 @@ public class RecyclerViewSwipeManager { mMoveToOutsideWindowAnimationDuration = duration; } - /*package*/ void applySlideItem(RecyclerView.ViewHolder holder, float prevAmount, float amount, boolean shouldAnimate) { + /** + * Gets OnItemSwipeEventListener listener + * @return The listener object + */ + public OnItemSwipeEventListener getOnItemSwipeEventListener() { + return mItemSwipeEventListener; + } + + /** + * Sets OnItemSwipeEventListener listener + * + * @param listener The listener object + */ + public void setOnItemSwipeEventListener(OnItemSwipeEventListener listener) { + mItemSwipeEventListener = listener; + } + + /*package*/ void applySlideItem(RecyclerView.ViewHolder holder, int itemPosition, float prevAmount, float amount, boolean shouldAnimate) { final SwipeableItemViewHolder holder2 = (SwipeableItemViewHolder) holder; final View containerView = holder2.getSwipeableContainerView(); @@ -737,13 +803,28 @@ public class RecyclerViewSwipeManager { if (amount == 0.0f) { slideItem(holder, amount, shouldAnimate); - mAdapter.setSwipeBackgroundDrawable(holder, reqBackgroundType); + mAdapter.setSwipeBackgroundDrawable(holder, itemPosition, reqBackgroundType); } else { - mAdapter.setSwipeBackgroundDrawable(holder, reqBackgroundType); - slideItem(holder, amount, shouldAnimate); + float adjustedAmount = amount; + + adjustedAmount = Math.max(adjustedAmount, holder2.getMaxLeftSwipeAmount()); + adjustedAmount = Math.min(adjustedAmount, holder2.getMaxRightSwipeAmount()); + + mAdapter.setSwipeBackgroundDrawable(holder, itemPosition, reqBackgroundType); + slideItem(holder, adjustedAmount, shouldAnimate); } } + /*package*/ void cancelPendingAnimations(RecyclerView.ViewHolder holder) { + if (mItemSlideAnimator != null) { + mItemSlideAnimator.endAnimation(holder); + } + } + + /*package*/ int getSwipeContainerViewTranslationX(RecyclerView.ViewHolder holder) { + return mItemSlideAnimator.getSwipeContainerViewTranslationX(holder); + } + private static boolean supportsViewPropertyAnimator() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RemovingItemDecorator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RemovingItemDecorator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RubberBandInterpolator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/RubberBandInterpolator.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeReactionUtils.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeReactionUtils.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java old mode 100644 new mode 100755 index 8065322bf76..7b949f6ff5d --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemAdapter.java @@ -24,6 +24,7 @@ public interface SwipeableItemAdapter { * Called when user is attempt to swipe the item. * * @param holder The ViewHolder which is associated to item user is attempt to start swiping. + * @param position The position of the item within the adapter's data set. * @param x Touched X position. Relative from the itemView's top-left. * @param y Touched Y position. Relative from the itemView's top-left. @@ -35,18 +36,19 @@ public interface SwipeableItemAdapter { * - {@link RecyclerViewSwipeManager#REACTION_CAN_NOT_SWIPE_RIGHT_WITH_RUBBER_BAND_EFFECT} * - {@link RecyclerViewSwipeManager#REACTION_CAN_SWIPE_RIGHT} */ - int onGetSwipeReactionType(T holder, int x, int y); + int onGetSwipeReactionType(T holder, int position, int x, int y); /** * Called when sets background of the swiping item. * * @param holder The ViewHolder which is associated to the swiping item. + * @param position The position of the item within the adapter's data set. * @param type Background type. One of the * {@link RecyclerViewSwipeManager#DRAWABLE_SWIPE_NEUTRAL_BACKGROUND}, * {@link RecyclerViewSwipeManager#DRAWABLE_SWIPE_LEFT_BACKGROUND} or * {@link RecyclerViewSwipeManager#DRAWABLE_SWIPE_RIGHT_BACKGROUND}. */ - void onSetSwipeBackground(T holder, int type); + void onSetSwipeBackground(T holder, int position, int type); /** * Called when item is swiped. @@ -54,6 +56,7 @@ public interface SwipeableItemAdapter { * *Note that do not change data set and do not call notifyDataXXX() methods inside of this method.* * * @param holder The ViewHolder which is associated to the swiped item. + * @param position The position of the item within the adapter's data set. * @param result The result code of user's swipe operation. * {@link RecyclerViewSwipeManager#RESULT_CANCELED}, * {@link RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or @@ -64,14 +67,15 @@ public interface SwipeableItemAdapter { * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}. */ - int onSwipeItem(T holder, int result); + int onSwipeItem(T holder, int position, int result); /** - * Called after {@link #onSwipeItem(android.support.v7.widget.RecyclerView.ViewHolder, int)} method. + * Called after {@link #onSwipeItem(android.support.v7.widget.RecyclerView.ViewHolder, int, int)} method. * * You can update data set and call notifyDataXXX() methods inside of this method. * * @param holder The ViewHolder which is associated to the swiped item. + * @param position The position of the item within the adapter's data set. * @param result The result code of user's swipe operation. * {@link RecyclerViewSwipeManager#RESULT_CANCELED}, * {@link RecyclerViewSwipeManager#RESULT_SWIPED_LEFT} or @@ -80,5 +84,5 @@ public interface SwipeableItemAdapter { * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_MOVE_TO_SWIPED_DIRECTION} or * {@link RecyclerViewSwipeManager#AFTER_SWIPE_REACTION_REMOVE_ITEM}. */ - void onPerformAfterSwipeReaction(T holder, int result, int reaction); + void onPerformAfterSwipeReaction(T holder, int position, int result, int reaction); } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java old mode 100644 new mode 100755 index 5c4714364dc..086a24e7045 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemViewHolder.java @@ -104,6 +104,34 @@ public interface SwipeableItemViewHolder { */ float getSwipeItemSlideAmount(); + /** + * Sets the maximum item right swipe amount. + * + * @param amount Item swipe amount. Generally the range is [-1.0 .. 1.0] + */ + void setMaxLeftSwipeAmount(float amount); + + /** + * Gets the maximum item left swipe amount. + * + * @return Item swipe amount. Generally the range is [-1.0 .. 1.0] + */ + float getMaxLeftSwipeAmount(); + + /** + * Sets the maximum item right swipe amount. + * + * @param amount Item swipe amount. Generally the range is [-1.0 .. 1.0] + */ + void setMaxRightSwipeAmount(float amount); + + /** + * Gets the maximum item right swipe amount. + * + * @return Item swipe amount. Generally the range is [-1.0 .. 1.0] + */ + float getMaxRightSwipeAmount(); + /** * Gets the container view for the swipeable area. * diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java old mode 100644 new mode 100755 index 6cc6ee9607a..0dba924866e --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipeableItemWrapperAdapter.java @@ -67,6 +67,8 @@ class SwipeableItemWrapperAdapter extends Ba // reset SwipeableItemViewHolder state if (holder instanceof SwipeableItemViewHolder) { + mSwipeManager.cancelPendingAnimations(holder); + ((SwipeableItemViewHolder) holder).setSwipeItemSlideAmount(0.0f); View containerView = ((SwipeableItemViewHolder) holder).getSwipeableContainerView(); @@ -116,7 +118,7 @@ class SwipeableItemWrapperAdapter extends Ba if ((prevSwipeItemSlideAmount != swipeItemSlideAmount) || !(mSwipeManager.isSwiping() || mSwipeManager.isAnimationRunning(holder))) { - mSwipeManager.applySlideItem(holder, prevSwipeItemSlideAmount, swipeItemSlideAmount, true); + mSwipeManager.applySlideItem(holder, position, prevSwipeItemSlideAmount, swipeItemSlideAmount, true); } } } @@ -175,32 +177,32 @@ class SwipeableItemWrapperAdapter extends Ba // NOTE: This method is called from RecyclerViewDragDropManager /*package*/ @SuppressWarnings("unchecked") - int getSwipeReactionType(RecyclerView.ViewHolder holder, int x, int y) { + int getSwipeReactionType(RecyclerView.ViewHolder holder, int position, int x, int y) { if (LOCAL_LOGV) { - Log.v(TAG, "getSwipeReactionType(holder = " + holder + ", x = " + x + ", y = " + y + ")"); + Log.v(TAG, "getSwipeReactionType(holder = " + holder + ", position = " + position + ", x = " + x + ", y = " + y + ")"); } - return mSwipeableItemAdapter.onGetSwipeReactionType(holder, x, y); + return mSwipeableItemAdapter.onGetSwipeReactionType(holder, position, x, y); } // NOTE: This method is called from RecyclerViewDragDropManager /*package*/ @SuppressWarnings("unchecked") - void setSwipeBackgroundDrawable(RecyclerView.ViewHolder holder, int type) { + void setSwipeBackgroundDrawable(RecyclerView.ViewHolder holder, int position, int type) { if (LOCAL_LOGV) { - Log.v(TAG, "setSwipeBackgroundDrawable(holder = " + holder + ", type = " + type + ")"); + Log.v(TAG, "setSwipeBackgroundDrawable(holder = " + holder + ", position = " + position + ", type = " + type + ")"); } - mSwipeableItemAdapter.onSetSwipeBackground(holder, type); + mSwipeableItemAdapter.onSetSwipeBackground(holder, position, type); } // NOTE: This method is called from RecyclerViewDragDropManager - /*package*/ void onSwipeItemStarted(RecyclerViewSwipeManager manager, RecyclerView.ViewHolder holder) { + /*package*/ void onSwipeItemStarted(RecyclerViewSwipeManager manager, RecyclerView.ViewHolder holder, int position) { if (LOCAL_LOGD) { - Log.d(TAG, "onSwipeItemStarted(holder = " + holder + ")"); + Log.d(TAG, "onSwipeItemStarted(holder = " + holder + ", position = " + position + ")"); } - mSwipingItemPosition = holder.getPosition(); + mSwipingItemPosition = position; notifyDataSetChanged(); } @@ -208,25 +210,25 @@ class SwipeableItemWrapperAdapter extends Ba // NOTE: This method is called from RecyclerViewDragDropManager /*package*/ @SuppressWarnings("unchecked") - int onSwipeItemFinished(RecyclerView.ViewHolder holder, int result) { + int onSwipeItemFinished(RecyclerView.ViewHolder holder, int position, int result) { if (LOCAL_LOGD) { - Log.d(TAG, "onSwipeItemFinished(holder = " + holder + ", result = " + result + ")"); + Log.d(TAG, "onSwipeItemFinished(holder = " + holder + ", position = " + position + ", result = " + result + ")"); } mSwipingItemPosition = RecyclerView.NO_POSITION; - return mSwipeableItemAdapter.onSwipeItem(holder, result); + return mSwipeableItemAdapter.onSwipeItem(holder, position, result); } /*package*/ @SuppressWarnings("unchecked") - void onSwipeItemFinished2(RecyclerView.ViewHolder holder, int result, int afterReaction) { + void onSwipeItemFinished2(RecyclerView.ViewHolder holder, int position, int result, int afterReaction) { ((SwipeableItemViewHolder) holder).setSwipeResult(result); ((SwipeableItemViewHolder) holder).setAfterSwipeReaction(afterReaction); ((SwipeableItemViewHolder) holder).setSwipeItemSlideAmount(getSwipeAmountFromAfterReaction(result, afterReaction)); - mSwipeableItemAdapter.onPerformAfterSwipeReaction(holder, result, afterReaction); + mSwipeableItemAdapter.onPerformAfterSwipeReaction(holder, position, result, afterReaction); notifyDataSetChanged(); } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java old mode 100644 new mode 100755 index c735d436266..29f05c1e2e6 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/swipeable/SwipingItemOperator.java @@ -29,11 +29,13 @@ class SwipingItemOperator { private static final int REACTION_CAN_SWIPE = SwipeReactionUtils.REACTION_CAN_SWIPE; private static final float RUBBER_BAND_LIMIT = 0.15f; + private static final int MIN_GRABBING_AREA_WIDTH = 48; private static final Interpolator RUBBER_BAND_INTERPOLATOR = new RubberBandInterpolator(RUBBER_BAND_LIMIT); private RecyclerViewSwipeManager mSwipeManager; private RecyclerView.ViewHolder mSwipingItem; + private int mSwipingItemPosition; private View mSwipingItemContainerView; private int mLeftSwipeReactionType; private int mRightSwipeReactionType; @@ -41,10 +43,12 @@ class SwipingItemOperator { private float mInvSwipingItemWidth; private int mSwipeDistance; private float mPrevTranslateAmount; + private int mInitialTranslateAmount; - public SwipingItemOperator(RecyclerViewSwipeManager manager, RecyclerView.ViewHolder swipingItem, int swipeReactionType) { + public SwipingItemOperator(RecyclerViewSwipeManager manager, RecyclerView.ViewHolder swipingItem, int position, int swipeReactionType) { mSwipeManager = manager; mSwipingItem = swipingItem; + mSwipingItemPosition = position; mLeftSwipeReactionType = SwipeReactionUtils.extractLeftReaction(swipeReactionType); mRightSwipeReactionType = SwipeReactionUtils.extractRightReaction(swipeReactionType); @@ -53,9 +57,13 @@ class SwipingItemOperator { mInvSwipingItemWidth = (mSwipingItemWidth != 0) ? (1.0f / mSwipingItemWidth) : 0.0f; } - @SuppressWarnings("EmptyMethod") public void start() { - // nothing to do here. + float density = mSwipingItem.itemView.getResources().getDisplayMetrics().density; + int maxAmount = Math.max(0, mSwipingItemWidth - (int) (density * MIN_GRABBING_AREA_WIDTH)); + + mInitialTranslateAmount = mSwipeManager.getSwipeContainerViewTranslationX(mSwipingItem); + mInitialTranslateAmount = Math.min(mInitialTranslateAmount, maxAmount); + mInitialTranslateAmount = Math.max(mInitialTranslateAmount, -maxAmount); } public void finish() { @@ -67,6 +75,7 @@ class SwipingItemOperator { mLeftSwipeReactionType = REACTION_CAN_NOT_SWIPE; mRightSwipeReactionType = REACTION_CAN_NOT_SWIPE; mPrevTranslateAmount = 0; + mInitialTranslateAmount = 0; mSwipingItemContainerView = null; } @@ -77,7 +86,9 @@ class SwipingItemOperator { mSwipeDistance = swipeDistance; - final int reactionType = (mSwipeDistance > 0) ? mRightSwipeReactionType : mLeftSwipeReactionType; + int distance = mSwipeDistance + mInitialTranslateAmount; + + final int reactionType = (distance > 0) ? mRightSwipeReactionType : mLeftSwipeReactionType; float translateAmount = 0; @@ -85,15 +96,15 @@ class SwipingItemOperator { case REACTION_CAN_NOT_SWIPE: break; case REACTION_CAN_NOT_SWIPE_WITH_RUBBER_EFFECT: - float proportion = Math.min(Math.abs(mSwipeDistance), mSwipingItemWidth) * mInvSwipingItemWidth; - translateAmount = Math.signum(mSwipeDistance) * RUBBER_BAND_INTERPOLATOR.getInterpolation(proportion); + float proportion = Math.min(Math.abs(distance), mSwipingItemWidth) * mInvSwipingItemWidth; + translateAmount = Math.signum(distance) * RUBBER_BAND_INTERPOLATOR.getInterpolation(proportion); break; case REACTION_CAN_SWIPE: - translateAmount = Math.min(Math.max((mSwipeDistance * mInvSwipingItemWidth), -1.0f), 1.0f); + translateAmount = Math.min(Math.max((distance * mInvSwipingItemWidth), -1.0f), 1.0f); break; } - mSwipeManager.applySlideItem(mSwipingItem, mPrevTranslateAmount, translateAmount, false); + mSwipeManager.applySlideItem(mSwipingItem, mSwipingItemPosition, mPrevTranslateAmount, translateAmount, false); mPrevTranslateAmount = translateAmount; } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java old mode 100644 new mode 100755 index 3f8003af242..5d2e27fe4cf --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/touchguard/RecyclerViewTouchActionGuardManager.java @@ -22,6 +22,9 @@ import android.util.Log; import android.view.MotionEvent; import android.view.ViewConfiguration; +/** + * Hooks touch events to avoid unexpected scrolling. + */ public class RecyclerViewTouchActionGuardManager { private static final String TAG = "ARVTouchActionGuardMgr"; @@ -37,6 +40,9 @@ public class RecyclerViewTouchActionGuardManager { private boolean mEnabled; private boolean mInterceptScrollingWhileAnimationRunning; + /** + * Constructor. + */ public RecyclerViewTouchActionGuardManager() { mInternalUseOnItemTouchListener = new RecyclerView.OnItemTouchListener() { @Override @@ -48,13 +54,27 @@ public class RecyclerViewTouchActionGuardManager { public void onTouchEvent(RecyclerView rv, MotionEvent e) { RecyclerViewTouchActionGuardManager.this.onTouchEvent(rv, e); } + + @Override + public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) { + } }; } + /** + * Indicates this manager instance has released or not. + * + * @return True if this manager instance has released + */ public boolean isReleased() { return (mInternalUseOnItemTouchListener == null); } + /** + * Attaches {@link android.support.v7.widget.RecyclerView} instance. + * + * @param rv The {@link android.support.v7.widget.RecyclerView} instance + */ public void attachRecyclerView(RecyclerView rv) { if (rv == null) { throw new IllegalArgumentException("RecyclerView cannot be null"); @@ -74,6 +94,11 @@ public class RecyclerViewTouchActionGuardManager { mTouchSlop = ViewConfiguration.get(rv.getContext()).getScaledTouchSlop(); } + /** + * Detach the {@link android.support.v7.widget.RecyclerView} instance and release internal field references. + * + * This method should be called in order to avoid memory leaks. + */ public void release() { if (mRecyclerView != null && mInternalUseOnItemTouchListener != null) { mRecyclerView.removeOnItemTouchListener(mInternalUseOnItemTouchListener); @@ -163,6 +188,11 @@ public class RecyclerViewTouchActionGuardManager { mGuarding = false; } + /** + * Sets whether to use touch guard feature. If set false, all touch event interceptions will be disabled. + * + * @param enabled enabled / disabled + */ public void setEnabled(boolean enabled) { if (mEnabled == enabled) { return; @@ -174,14 +204,29 @@ public class RecyclerViewTouchActionGuardManager { } } + /** + * Checks whether the touch guard feature is enabled. + * + * @return True for currently touch guard feature is enabled, otherwise false + */ public boolean isEnabled() { return mEnabled; } - public void setInterceptVerticalScrollingWhileAnimationRunning(boolean value) { - mInterceptScrollingWhileAnimationRunning = value; + /** + * Sets whether to use interception of "vertical scroll while animation running". + * + * @param enabled enabled / disabled + */ + public void setInterceptVerticalScrollingWhileAnimationRunning(boolean enabled) { + mInterceptScrollingWhileAnimationRunning = enabled; } + /** + * Checks whether the interception of "vertical scroll while animation running" is enabled. + * + * @return enabled / disabled + */ public boolean isInterceptScrollingWhileAnimationRunning() { return mInterceptScrollingWhileAnimationRunning; } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableItemViewHolder.java old mode 100644 new mode 100755 diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java old mode 100644 new mode 100755 index 3e8dc254a1a..b9d0c9577a1 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractDraggableSwipeableItemViewHolder.java @@ -29,6 +29,8 @@ public abstract class AbstractDraggableSwipeableItemViewHolder extends RecyclerV private int mSwipeResult = RecyclerViewSwipeManager.RESULT_NONE; private int mAfterSwipeReaction = RecyclerViewSwipeManager.AFTER_SWIPE_REACTION_DEFAULT; private float mSwipeAmount; + private float mMaxLeftSwipeAmount = RecyclerViewSwipeManager.OUTSIDE_OF_THE_WINDOW_LEFT; + private float mMaxRightSwipeAmount = RecyclerViewSwipeManager.OUTSIDE_OF_THE_WINDOW_RIGHT; public AbstractDraggableSwipeableItemViewHolder(View itemView) { super(itemView); @@ -84,4 +86,23 @@ public abstract class AbstractDraggableSwipeableItemViewHolder extends RecyclerV mSwipeAmount = amount; } + @Override + public void setMaxLeftSwipeAmount(float amount) { + mMaxLeftSwipeAmount = amount; + } + + @Override + public float getMaxLeftSwipeAmount() { + return mMaxLeftSwipeAmount; + } + + @Override + public void setMaxRightSwipeAmount(float amount) { + mMaxRightSwipeAmount = amount; + } + + @Override + public float getMaxRightSwipeAmount() { + return mMaxRightSwipeAmount; + } } diff --git a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java old mode 100644 new mode 100755 index 6fb94598a9d..59f0bbc59f2 --- a/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java +++ b/mobile/github-advrecyclerview_lib/src/com/h6ah4i/android/widget/advrecyclerview/utils/AbstractExpandableItemAdapter.java @@ -85,4 +85,22 @@ public abstract class AbstractExpandableItemAdapter + + + + + + + + + diff --git a/mobile/google-android-support-v13/.gitignore b/mobile/google-android-support-v13/.gitignore new file mode 100644 index 00000000000..ae3c1726048 --- /dev/null +++ b/mobile/google-android-support-v13/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/mobile/google-android-support-v13/.project b/mobile/google-android-support-v13/.project new file mode 100644 index 00000000000..bedbd244bfd --- /dev/null +++ b/mobile/google-android-support-v13/.project @@ -0,0 +1,33 @@ + + + google-android-support-v13 + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/mobile/google-android-support-v13/.settings/org.eclipse.jdt.core.prefs b/mobile/google-android-support-v13/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..303053a514f --- /dev/null +++ b/mobile/google-android-support-v13/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,101 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=warning +org.eclipse.jdt.core.compiler.problem.deprecation=ignore +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=warning +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=warning +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=warning +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v18/dimens.xml b/mobile/google-android-support-v13/AndroidManifest.xml similarity index 72% rename from mobile/google-android-support-v7-appcompat_lib/res/values-v18/dimens.xml rename to mobile/google-android-support-v13/AndroidManifest.xml index bb784b76b9d..dc6e6c359e3 100644 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-v18/dimens.xml +++ b/mobile/google-android-support-v13/AndroidManifest.xml @@ -1,5 +1,6 @@ - + - + - - 0px + - + \ No newline at end of file diff --git a/mobile/google-android-support-v13/project.properties b/mobile/google-android-support-v13/project.properties new file mode 100644 index 00000000000..362a0a3082a --- /dev/null +++ b/mobile/google-android-support-v13/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-22 +android.library=true diff --git a/mobile/google-android-support-v13/src/META-INF/MANIFEST.MF b/mobile/google-android-support-v13/src/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..58630c02ef4 --- /dev/null +++ b/mobile/google-android-support-v13/src/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompat.java b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompat.java new file mode 100644 index 00000000000..eec3ea50007 --- /dev/null +++ b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompat.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v13.app; + +import android.app.Fragment; + +/** + * Helper for accessing features in {@link Fragment} introduced after + * API level 13 in a backwards compatible fashion. + */ +public class FragmentCompat { + interface FragmentCompatImpl { + void setMenuVisibility(Fragment f, boolean visible); + void setUserVisibleHint(Fragment f, boolean deferStart); + } + + static class BaseFragmentCompatImpl implements FragmentCompatImpl { + public void setMenuVisibility(Fragment f, boolean visible) { + } + public void setUserVisibleHint(Fragment f, boolean deferStart) { + } + } + + static class ICSFragmentCompatImpl extends BaseFragmentCompatImpl { + @Override + public void setMenuVisibility(Fragment f, boolean visible) { + FragmentCompatICS.setMenuVisibility(f, visible); + } + } + + static class ICSMR1FragmentCompatImpl extends ICSFragmentCompatImpl { + @Override + public void setUserVisibleHint(Fragment f, boolean deferStart) { + FragmentCompatICSMR1.setUserVisibleHint(f, deferStart); + } + } + + static final FragmentCompatImpl IMPL; + static { + if (android.os.Build.VERSION.SDK_INT >= 15) { + IMPL = new ICSMR1FragmentCompatImpl(); + } else if (android.os.Build.VERSION.SDK_INT >= 14) { + IMPL = new ICSFragmentCompatImpl(); + } else { + IMPL = new BaseFragmentCompatImpl(); + } + } + + /** + * Call {@link Fragment#setMenuVisibility(boolean) Fragment.setMenuVisibility(boolean)} + * if running on an appropriate version of the platform. + */ + public static void setMenuVisibility(Fragment f, boolean visible) { + IMPL.setMenuVisibility(f, visible); + } + + /** + * Call {@link Fragment#setUserVisibleHint(boolean) setUserVisibleHint(boolean)} + * if running on an appropriate version of the platform. + */ + public static void setUserVisibleHint(Fragment f, boolean deferStart) { + IMPL.setUserVisibleHint(f, deferStart); + } +} diff --git a/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICS.java b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICS.java new file mode 100644 index 00000000000..1beb704db81 --- /dev/null +++ b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICS.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v13.app; + +import android.app.Fragment; + +class FragmentCompatICS { + public static void setMenuVisibility(Fragment f, boolean visible) { + f.setMenuVisibility(visible); + } +} diff --git a/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICSMR1.java b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICSMR1.java new file mode 100644 index 00000000000..39f09229b88 --- /dev/null +++ b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentCompatICSMR1.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v13.app; + +import android.app.Fragment; + +class FragmentCompatICSMR1 { + public static void setUserVisibleHint(Fragment f, boolean isVisible) { + f.setUserVisibleHint(isVisible); + } +} diff --git a/mobile/google-android-support-v13/src/android/support/v13/app/FragmentPagerAdapter.java b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentPagerAdapter.java new file mode 100644 index 00000000000..60aa5d0e747 --- /dev/null +++ b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentPagerAdapter.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v13.app; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.os.Parcelable; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.view.PagerAdapter; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +/** + * Implementation of {@link android.support.v4.view.PagerAdapter} that + * represents each page as a {@link android.app.Fragment} that is persistently + * kept in the fragment manager as long as the user can return to the page. + * + *

This version of the pager is best for use when there are a handful of + * typically more static fragments to be paged through, such as a set of tabs. + * The fragment of each page the user visits will be kept in memory, though its + * view hierarchy may be destroyed when not visible. This can result in using + * a significant amount of memory since fragment instances can hold on to an + * arbitrary amount of state. For larger sets of pages, consider + * {@link FragmentStatePagerAdapter}. + * + *

When using FragmentPagerAdapter the host ViewPager must have a + * valid ID set.

+ * + *

Subclasses only need to implement {@link #getItem(int)} + * and {@link #getCount()} to have a working adapter. + * + *

Here is an example implementation of a pager containing fragments of + * lists: + * + * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentPagerSupport.java + * complete} + * + *

The R.layout.fragment_pager resource of the top-level fragment is: + * + * {@sample development/samples/Support13Demos/res/layout/fragment_pager.xml + * complete} + * + *

The R.layout.fragment_pager_list resource containing each + * individual fragment's layout is: + * + * {@sample development/samples/Support13Demos/res/layout/fragment_pager_list.xml + * complete} + */ +public abstract class FragmentPagerAdapter extends PagerAdapter { + private static final String TAG = "FragmentPagerAdapter"; + private static final boolean DEBUG = false; + + private final FragmentManager mFragmentManager; + private FragmentTransaction mCurTransaction = null; + private Fragment mCurrentPrimaryItem = null; + + public FragmentPagerAdapter(FragmentManager fm) { + mFragmentManager = fm; + } + + /** + * Return the Fragment associated with a specified position. + */ + public abstract Fragment getItem(int position); + + @Override + public void startUpdate(ViewGroup container) { + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + + final long itemId = getItemId(position); + + // Do we already have this fragment? + String name = makeFragmentName(container.getId(), itemId); + Fragment fragment = mFragmentManager.findFragmentByTag(name); + if (fragment != null) { + if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); + mCurTransaction.attach(fragment); + } else { + fragment = getItem(position); + if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); + mCurTransaction.add(container.getId(), fragment, + makeFragmentName(container.getId(), itemId)); + } + if (fragment != mCurrentPrimaryItem) { + FragmentCompat.setMenuVisibility(fragment, false); + FragmentCompat.setUserVisibleHint(fragment, false); + } + + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object + + " v=" + ((Fragment)object).getView()); + mCurTransaction.detach((Fragment)object); + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + FragmentCompat.setMenuVisibility(mCurrentPrimaryItem, false); + FragmentCompat.setUserVisibleHint(mCurrentPrimaryItem, false); + } + if (fragment != null) { + FragmentCompat.setMenuVisibility(fragment, true); + FragmentCompat.setUserVisibleHint(fragment, true); + } + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitAllowingStateLoss(); + mCurTransaction = null; + mFragmentManager.executePendingTransactions(); + } + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return ((Fragment)object).getView() == view; + } + + @Override + public Parcelable saveState() { + return null; + } + + @Override + public void restoreState(Parcelable state, ClassLoader loader) { + } + + /** + * Return a unique identifier for the item at the given position. + * + *

The default implementation returns the given position. + * Subclasses should override this method if the positions of items can change.

+ * + * @param position Position within this adapter + * @return Unique identifier for the item at position + */ + public long getItemId(int position) { + return position; + } + + private static String makeFragmentName(int viewId, long id) { + return "android:switcher:" + viewId + ":" + id; + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v13/src/android/support/v13/app/FragmentStatePagerAdapter.java b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentStatePagerAdapter.java new file mode 100644 index 00000000000..89be82e8e54 --- /dev/null +++ b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentStatePagerAdapter.java @@ -0,0 +1,229 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v13.app; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.view.PagerAdapter; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import java.util.ArrayList; + +/** + * Implementation of {@link android.support.v4.view.PagerAdapter} that + * uses a {@link Fragment} to manage each page. This class also handles + * saving and restoring of fragment's state. + * + *

This version of the pager is more useful when there are a large number + * of pages, working more like a list view. When pages are not visible to + * the user, their entire fragment may be destroyed, only keeping the saved + * state of that fragment. This allows the pager to hold on to much less + * memory associated with each visited page as compared to + * {@link FragmentPagerAdapter} at the cost of potentially more overhead when + * switching between pages. + * + *

When using FragmentPagerAdapter the host ViewPager must have a + * valid ID set.

+ * + *

Subclasses only need to implement {@link #getItem(int)} + * and {@link #getCount()} to have a working adapter. + * + *

Here is an example implementation of a pager containing fragments of + * lists: + * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentStatePagerSupport.java + * complete} + * + *

The R.layout.fragment_pager resource of the top-level fragment is: + * + * {@sample development/samples/Support4Demos/res/layout/fragment_pager.xml + * complete} + * + *

The R.layout.fragment_pager_list resource containing each + * individual fragment's layout is: + * + * {@sample development/samples/Support4Demos/res/layout/fragment_pager_list.xml + * complete} + */ +public abstract class FragmentStatePagerAdapter extends PagerAdapter { + private static final String TAG = "FragmentStatePagerAdapter"; + private static final boolean DEBUG = false; + + private final FragmentManager mFragmentManager; + private FragmentTransaction mCurTransaction = null; + + private ArrayList mSavedState = new ArrayList(); + private ArrayList mFragments = new ArrayList(); + private Fragment mCurrentPrimaryItem = null; + + public FragmentStatePagerAdapter(FragmentManager fm) { + mFragmentManager = fm; + } + + /** + * Return the Fragment associated with a specified position. + */ + public abstract Fragment getItem(int position); + + @Override + public void startUpdate(ViewGroup container) { + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + // If we already have this item instantiated, there is nothing + // to do. This can happen when we are restoring the entire pager + // from its saved state, where the fragment manager has already + // taken care of restoring the fragments we previously had instantiated. + if (mFragments.size() > position) { + Fragment f = mFragments.get(position); + if (f != null) { + return f; + } + } + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + + Fragment fragment = getItem(position); + if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment); + if (mSavedState.size() > position) { + Fragment.SavedState fss = mSavedState.get(position); + if (fss != null) { + fragment.setInitialSavedState(fss); + } + } + while (mFragments.size() <= position) { + mFragments.add(null); + } + FragmentCompat.setMenuVisibility(fragment, false); + FragmentCompat.setUserVisibleHint(fragment, false); + mFragments.set(position, fragment); + mCurTransaction.add(container.getId(), fragment); + + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object + + " v=" + ((Fragment)object).getView()); + while (mSavedState.size() <= position) { + mSavedState.add(null); + } + mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment)); + mFragments.set(position, null); + + mCurTransaction.remove(fragment); + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + FragmentCompat.setMenuVisibility(mCurrentPrimaryItem, false); + FragmentCompat.setUserVisibleHint(mCurrentPrimaryItem, false); + } + if (fragment != null) { + FragmentCompat.setMenuVisibility(fragment, true); + FragmentCompat.setUserVisibleHint(fragment, true); + } + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitAllowingStateLoss(); + mCurTransaction = null; + mFragmentManager.executePendingTransactions(); + } + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return ((Fragment)object).getView() == view; + } + + @Override + public Parcelable saveState() { + Bundle state = null; + if (mSavedState.size() > 0) { + state = new Bundle(); + Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; + mSavedState.toArray(fss); + state.putParcelableArray("states", fss); + } + for (int i=0; i keys = bundle.keySet(); + for (String key: keys) { + if (key.startsWith("f")) { + int index = Integer.parseInt(key.substring(1)); + Fragment f = mFragmentManager.getFragment(bundle, key); + if (f != null) { + while (mFragments.size() <= index) { + mFragments.add(null); + } + FragmentCompat.setMenuVisibility(f, false); + mFragments.set(index, f); + } else { + Log.w(TAG, "Bad fragment at key " + key); + } + } + } + } + } +} diff --git a/mobile/google-android-support-v13/src/android/support/v13/app/FragmentTabHost.java b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentTabHost.java new file mode 100644 index 00000000000..01378d98153 --- /dev/null +++ b/mobile/google-android-support-v13/src/android/support/v13/app/FragmentTabHost.java @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v13.app; + +import java.util.ArrayList; + +import android.app.Fragment; +import android.app.FragmentManager; +import android.app.FragmentTransaction; +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TabHost; +import android.widget.TabWidget; + +/** + * Version of {@link android.support.v4.app.FragmentTabHost} that can be + * used with the platform {@link android.app.Fragment} APIs. You will not + * normally use this, instead using action bar tabs. + */ +public class FragmentTabHost extends TabHost + implements TabHost.OnTabChangeListener { + private final ArrayList mTabs = new ArrayList(); + private FrameLayout mRealTabContent; + private Context mContext; + private FragmentManager mFragmentManager; + private int mContainerId; + private TabHost.OnTabChangeListener mOnTabChangeListener; + private TabInfo mLastTab; + private boolean mAttached; + + static final class TabInfo { + private final String tag; + private final Class clss; + private final Bundle args; + private Fragment fragment; + + TabInfo(String _tag, Class _class, Bundle _args) { + tag = _tag; + clss = _class; + args = _args; + } + } + + static class DummyTabFactory implements TabHost.TabContentFactory { + private final Context mContext; + + public DummyTabFactory(Context context) { + mContext = context; + } + + @Override + public View createTabContent(String tag) { + View v = new View(mContext); + v.setMinimumWidth(0); + v.setMinimumHeight(0); + return v; + } + } + + static class SavedState extends BaseSavedState { + String curTab; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + curTab = in.readString(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeString(curTab); + } + + @Override + public String toString() { + return "FragmentTabHost.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " curTab=" + curTab + "}"; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + public FragmentTabHost(Context context) { + // Note that we call through to the version that takes an AttributeSet, + // because the simple Context construct can result in a broken object! + super(context, null); + initFragmentTabHost(context, null); + } + + public FragmentTabHost(Context context, AttributeSet attrs) { + super(context, attrs); + initFragmentTabHost(context, attrs); + } + + private void initFragmentTabHost(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, + new int[] { android.R.attr.inflatedId }, 0, 0); + mContainerId = a.getResourceId(0, 0); + a.recycle(); + + super.setOnTabChangedListener(this); + } + + private void ensureHierarchy(Context context) { + // If owner hasn't made its own view hierarchy, then as a convenience + // we will construct a standard one here. + if (findViewById(android.R.id.tabs) == null) { + LinearLayout ll = new LinearLayout(context); + ll.setOrientation(LinearLayout.VERTICAL); + addView(ll, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT)); + + TabWidget tw = new TabWidget(context); + tw.setId(android.R.id.tabs); + tw.setOrientation(TabWidget.HORIZONTAL); + ll.addView(tw, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0)); + + FrameLayout fl = new FrameLayout(context); + fl.setId(android.R.id.tabcontent); + ll.addView(fl, new LinearLayout.LayoutParams(0, 0, 0)); + + mRealTabContent = fl = new FrameLayout(context); + mRealTabContent.setId(mContainerId); + ll.addView(fl, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.FILL_PARENT, 0, 1)); + } + } + + /** + * @deprecated Don't call the original TabHost setup, you must instead + * call {@link #setup(Context, FragmentManager)} or + * {@link #setup(Context, FragmentManager, int)}. + */ + @Override @Deprecated + public void setup() { + throw new IllegalStateException( + "Must call setup() that takes a Context and FragmentManager"); + } + + public void setup(Context context, FragmentManager manager) { + ensureHierarchy(context); // Ensure views required by super.setup() + super.setup(); + mContext = context; + mFragmentManager = manager; + ensureContent(); + } + + public void setup(Context context, FragmentManager manager, int containerId) { + ensureHierarchy(context); // Ensure views required by super.setup() + super.setup(); + mContext = context; + mFragmentManager = manager; + mContainerId = containerId; + ensureContent(); + mRealTabContent.setId(containerId); + + // We must have an ID to be able to save/restore our state. If + // the owner hasn't set one at this point, we will set it ourself. + if (getId() == View.NO_ID) { + setId(android.R.id.tabhost); + } + } + + private void ensureContent() { + if (mRealTabContent == null) { + mRealTabContent = (FrameLayout)findViewById(mContainerId); + if (mRealTabContent == null) { + throw new IllegalStateException( + "No tab content FrameLayout found for id " + mContainerId); + } + } + } + + @Override + public void setOnTabChangedListener(OnTabChangeListener l) { + mOnTabChangeListener = l; + } + + public void addTab(TabHost.TabSpec tabSpec, Class clss, Bundle args) { + tabSpec.setContent(new DummyTabFactory(mContext)); + String tag = tabSpec.getTag(); + + TabInfo info = new TabInfo(tag, clss, args); + + if (mAttached) { + // If we are already attached to the window, then check to make + // sure this tab's fragment is inactive if it exists. This shouldn't + // normally happen. + info.fragment = mFragmentManager.findFragmentByTag(tag); + if (info.fragment != null && !info.fragment.isDetached()) { + FragmentTransaction ft = mFragmentManager.beginTransaction(); + ft.detach(info.fragment); + ft.commit(); + } + } + + mTabs.add(info); + addTab(tabSpec); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + String currentTab = getCurrentTabTag(); + + // Go through all tabs and make sure their fragments match + // the correct state. + FragmentTransaction ft = null; + for (int i=0; i + +Support classes to access some of the android.app package features introduced after API level 13 in a backwards compatible fashion. + + diff --git a/mobile/google-android-support-v4/.classpath b/mobile/google-android-support-v4/.classpath new file mode 100644 index 00000000000..6056dba1f7f --- /dev/null +++ b/mobile/google-android-support-v4/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/mobile/google-android-support-v4/.gitignore b/mobile/google-android-support-v4/.gitignore new file mode 100644 index 00000000000..ae3c1726048 --- /dev/null +++ b/mobile/google-android-support-v4/.gitignore @@ -0,0 +1 @@ +/bin/ diff --git a/mobile/google-android-support-v4/.project b/mobile/google-android-support-v4/.project new file mode 100644 index 00000000000..2ba2a9ebbf7 --- /dev/null +++ b/mobile/google-android-support-v4/.project @@ -0,0 +1,33 @@ + + + google-android-support-v4 + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/mobile/google-android-support-v4/.settings/org.eclipse.jdt.core.prefs b/mobile/google-android-support-v4/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..7f47c165658 --- /dev/null +++ b/mobile/google-android-support-v4/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,101 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.annotation.inheritNullAnnotations=disabled +org.eclipse.jdt.core.compiler.annotation.missingNonNullByDefaultAnnotation=ignore +org.eclipse.jdt.core.compiler.annotation.nonnull=org.eclipse.jdt.annotation.NonNull +org.eclipse.jdt.core.compiler.annotation.nonnullbydefault=org.eclipse.jdt.annotation.NonNullByDefault +org.eclipse.jdt.core.compiler.annotation.nullable=org.eclipse.jdt.annotation.Nullable +org.eclipse.jdt.core.compiler.annotation.nullanalysis=disabled +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.methodParameters=do not generate +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.7 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.comparingIdentical=warning +org.eclipse.jdt.core.compiler.problem.deadCode=ignore +org.eclipse.jdt.core.compiler.problem.deprecation=ignore +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.explicitlyClosedAutoCloseable=ignore +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fatalOptionalError=disabled +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.includeNullInfoFromAsserts=disabled +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDefaultCase=ignore +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingEnumCaseDespiteDefault=disabled +org.eclipse.jdt.core.compiler.problem.missingHashCodeMethod=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotationForInterfaceMethodImplementation=enabled +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore +org.eclipse.jdt.core.compiler.problem.missingSynchronizedOnInheritedMethod=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nonnullParameterAnnotationDropped=warning +org.eclipse.jdt.core.compiler.problem.nullAnnotationInferenceConflict=error +org.eclipse.jdt.core.compiler.problem.nullReference=warning +org.eclipse.jdt.core.compiler.problem.nullSpecViolation=error +org.eclipse.jdt.core.compiler.problem.nullUncheckedConversion=warning +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.potentialNullReference=ignore +org.eclipse.jdt.core.compiler.problem.potentiallyUnclosedCloseable=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore +org.eclipse.jdt.core.compiler.problem.redundantNullAnnotation=warning +org.eclipse.jdt.core.compiler.problem.redundantNullCheck=ignore +org.eclipse.jdt.core.compiler.problem.redundantSpecificationOfTypeArguments=ignore +org.eclipse.jdt.core.compiler.problem.redundantSuperinterface=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBePotentiallyStatic=ignore +org.eclipse.jdt.core.compiler.problem.reportMethodCanBeStatic=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressOptionalErrors=disabled +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntacticNullAnalysisForFields=disabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.unavoidableGenericTypeProblems=enabled +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=ignore +org.eclipse.jdt.core.compiler.problem.unclosedCloseable=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=ignore +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionExemptExceptionAndThrowable=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedExceptionParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedImport=ignore +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=ignore +org.eclipse.jdt.core.compiler.problem.unusedObjectAllocation=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterIncludeDocCommentReference=enabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=ignore +org.eclipse.jdt.core.compiler.problem.unusedTypeParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedWarningToken=ignore +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=ignore +org.eclipse.jdt.core.compiler.source=1.7 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-port/bools.xml b/mobile/google-android-support-v4/AndroidManifest.xml similarity index 72% rename from mobile/google-android-support-v7-appcompat_lib/res/values-port/bools.xml rename to mobile/google-android-support-v4/AndroidManifest.xml index 25053bee512..f27326972e4 100644 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-port/bools.xml +++ b/mobile/google-android-support-v4/AndroidManifest.xml @@ -1,5 +1,6 @@ - + - + - false + - + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-sw600dp/config.xml b/mobile/google-android-support-v4/aidl/android/support/v4/media/MediaMetadataCompat.aidl similarity index 61% rename from mobile/google-android-support-v7-appcompat_lib/res/values-sw600dp/config.xml rename to mobile/google-android-support-v4/aidl/android/support/v4/media/MediaMetadataCompat.aidl index fba21e1c06b..6d36b97eca3 100644 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-sw600dp/config.xml +++ b/mobile/google-android-support-v4/aidl/android/support/v4/media/MediaMetadataCompat.aidl @@ -1,7 +1,4 @@ - - - - - - 580dp - \ No newline at end of file +package android.support.v4.media; + +parcelable MediaMetadataCompat; diff --git a/mobile/google-android-support-v4/aidl/android/support/v4/media/RatingCompat.aidl b/mobile/google-android-support-v4/aidl/android/support/v4/media/RatingCompat.aidl new file mode 100644 index 00000000000..223fd5c96f7 --- /dev/null +++ b/mobile/google-android-support-v4/aidl/android/support/v4/media/RatingCompat.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.support.v4.media; + +parcelable RatingCompat; diff --git a/mobile/google-android-support-v4/aidl/android/support/v4/media/session/MediaSessionCompat.aidl b/mobile/google-android-support-v4/aidl/android/support/v4/media/session/MediaSessionCompat.aidl new file mode 100644 index 00000000000..d0c2f6fce2c --- /dev/null +++ b/mobile/google-android-support-v4/aidl/android/support/v4/media/session/MediaSessionCompat.aidl @@ -0,0 +1,20 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.support.v4.media.session; + +parcelable MediaSessionCompat.Token; +parcelable MediaSessionCompat.QueueItem; +parcelable MediaSessionCompat.ResultReceiverWrapper; diff --git a/mobile/google-android-support-v4/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl b/mobile/google-android-support-v4/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl new file mode 100644 index 00000000000..2e77c4fbde0 --- /dev/null +++ b/mobile/google-android-support-v4/aidl/android/support/v4/media/session/ParcelableVolumeInfo.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.support.v4.media.session; + +parcelable ParcelableVolumeInfo; diff --git a/mobile/google-android-support-v4/aidl/android/support/v4/media/session/PlaybackStateCompat.aidl b/mobile/google-android-support-v4/aidl/android/support/v4/media/session/PlaybackStateCompat.aidl new file mode 100644 index 00000000000..3d4ef5956a5 --- /dev/null +++ b/mobile/google-android-support-v4/aidl/android/support/v4/media/session/PlaybackStateCompat.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.support.v4.media.session; + +parcelable PlaybackStateCompat; diff --git a/mobile/google-android-support-v4/project.properties b/mobile/google-android-support-v4/project.properties new file mode 100644 index 00000000000..362a0a3082a --- /dev/null +++ b/mobile/google-android-support-v4/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-22 +android.library=true diff --git a/mobile/google-android-support-v4/src/META-INF/MANIFEST.MF b/mobile/google-android-support-v4/src/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..58630c02ef4 --- /dev/null +++ b/mobile/google-android-support-v4/src/META-INF/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.java b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.java new file mode 100644 index 00000000000..abd208bb6f9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompat.java @@ -0,0 +1,451 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.accessibilityservice; + +import android.accessibilityservice.AccessibilityService; +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.pm.ResolveInfo; +import android.os.Build; +import android.view.View; + +/** + * Helper for accessing features in {@link android.accessibilityservice.AccessibilityService} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AccessibilityServiceInfoCompat { + + static interface AccessibilityServiceInfoVersionImpl { + public String getId(AccessibilityServiceInfo info); + public ResolveInfo getResolveInfo(AccessibilityServiceInfo info); + public boolean getCanRetrieveWindowContent(AccessibilityServiceInfo info); + public String getDescription(AccessibilityServiceInfo info); + public String getSettingsActivityName(AccessibilityServiceInfo info); + public int getCapabilities(AccessibilityServiceInfo info); + } + + static class AccessibilityServiceInfoStubImpl implements AccessibilityServiceInfoVersionImpl { + + public boolean getCanRetrieveWindowContent(AccessibilityServiceInfo info) { + return false; + } + + public String getDescription(AccessibilityServiceInfo info) { + return null; + } + + public String getId(AccessibilityServiceInfo info) { + return null; + } + + public ResolveInfo getResolveInfo(AccessibilityServiceInfo info) { + return null; + } + + public String getSettingsActivityName(AccessibilityServiceInfo info) { + return null; + } + + public int getCapabilities(AccessibilityServiceInfo info) { + return 0; + } + } + + static class AccessibilityServiceInfoIcsImpl extends AccessibilityServiceInfoStubImpl { + + @Override + public boolean getCanRetrieveWindowContent(AccessibilityServiceInfo info) { + return AccessibilityServiceInfoCompatIcs.getCanRetrieveWindowContent(info); + } + + @Override + public String getDescription(AccessibilityServiceInfo info) { + return AccessibilityServiceInfoCompatIcs.getDescription(info); + } + + @Override + public String getId(AccessibilityServiceInfo info) { + return AccessibilityServiceInfoCompatIcs.getId(info); + } + + @Override + public ResolveInfo getResolveInfo(AccessibilityServiceInfo info) { + return AccessibilityServiceInfoCompatIcs.getResolveInfo(info); + } + + @Override + public String getSettingsActivityName(AccessibilityServiceInfo info) { + return AccessibilityServiceInfoCompatIcs.getSettingsActivityName(info); + } + + @Override + public int getCapabilities(AccessibilityServiceInfo info) { + if (getCanRetrieveWindowContent(info)) { + return CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT; + } + return 0; + } + } + + static class AccessibilityServiceInfoJellyBeanMr2 extends AccessibilityServiceInfoIcsImpl { + @Override + public int getCapabilities(AccessibilityServiceInfo info) { + return AccessibilityServiceInfoCompatJellyBeanMr2.getCapabilities(info); + } + } + + static { + if (Build.VERSION.SDK_INT >= 18) { // JellyBean MR2 + IMPL = new AccessibilityServiceInfoJellyBeanMr2(); + } else if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new AccessibilityServiceInfoIcsImpl(); + } else { + IMPL = new AccessibilityServiceInfoStubImpl(); + } + } + + // Capabilities + + private static final AccessibilityServiceInfoVersionImpl IMPL; + + /** + * Capability: This accessibility service can retrieve the active window content. + */ + public static final int CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT = 0x00000001; + + /** + * Capability: This accessibility service can request touch exploration mode in which + * touched items are spoken aloud and the UI can be explored via gestures. + */ + public static final int CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION = 0x00000002; + + /** + * Capability: This accessibility service can request enhanced web accessibility + * enhancements. For example, installing scripts to make app content more accessible. + */ + public static final int CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 0x00000004; + + /** + * Capability: This accessibility service can filter the key event stream. + */ + public static final int CAPABILITY_CAN_FILTER_KEY_EVENTS = 0x00000008; + + // Feedback types + + /** + * Denotes braille feedback. + */ + public static final int FEEDBACK_BRAILLE = 0x0000020; + + /** + * Mask for all feedback types. + * + * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN + * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC + * @see AccessibilityServiceInfo#FEEDBACK_AUDIBLE + * @see AccessibilityServiceInfo#FEEDBACK_VISUAL + * @see AccessibilityServiceInfo#FEEDBACK_GENERIC + * @see FEEDBACK_BRAILLE + */ + public static final int FEEDBACK_ALL_MASK = 0xFFFFFFFF; + + // Flags + + /** + * If an {@link AccessibilityService} is the default for a given type. + * Default service is invoked only if no package specific one exists. In case of + * more than one package specific service only the earlier registered is notified. + */ + public static final int DEFAULT = 0x0000001; + + /** + * If this flag is set the system will regard views that are not important + * for accessibility in addition to the ones that are important for accessibility. + * That is, views that are marked as not important for accessibility via + * {@link View#IMPORTANT_FOR_ACCESSIBILITY_NO} or + * {@link View#IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS} and views that are + * marked as potentially important for accessibility via + * {@link View#IMPORTANT_FOR_ACCESSIBILITY_AUTO} for which the system has determined + * that are not important for accessibility, are both reported while querying the + * window content and also the accessibility service will receive accessibility events + * from them. + *

+ * Note: For accessibility services targeting API version + * {@link Build.VERSION_CODES#JELLY_BEAN} or higher this flag has to be explicitly + * set for the system to regard views that are not important for accessibility. For + * accessibility services targeting API version lower than + * {@link Build.VERSION_CODES#JELLY_BEAN} this flag is ignored and all views are + * regarded for accessibility purposes. + *

+ *

+ * Usually views not important for accessibility are layout managers that do not + * react to user actions, do not draw any content, and do not have any special + * semantics in the context of the screen content. For example, a three by three + * grid can be implemented as three horizontal linear layouts and one vertical, + * or three vertical linear layouts and one horizontal, or one grid layout, etc. + * In this context the actual layout mangers used to achieve the grid configuration + * are not important, rather it is important that there are nine evenly distributed + * elements. + *

+ */ + public static final int FLAG_INCLUDE_NOT_IMPORTANT_VIEWS = 0x0000002; + + /** + * This flag requests that the system gets into touch exploration mode. + * In this mode a single finger moving on the screen behaves as a mouse + * pointer hovering over the user interface. The system will also detect + * certain gestures performed on the touch screen and notify this service. + * The system will enable touch exploration mode if there is at least one + * accessibility service that has this flag set. Hence, clearing this + * flag does not guarantee that the device will not be in touch exploration + * mode since there may be another enabled service that requested it. + *

+ * For accessibility services targeting API version higher than + * {@link Build.VERSION_CODES#JELLY_BEAN_MR1} that want to set + * this flag have to declare this capability in their meta-data by setting + * the attribute canRequestTouchExplorationMode to true, otherwise this flag + * will be ignored. For how to declare the meta-data of a service refer to + * {@value AccessibilityService#SERVICE_META_DATA}. + *

+ *

+ * Services targeting API version equal to or lower than + * {@link Build.VERSION_CODES#JELLY_BEAN_MR1} will work normally, i.e. + * the first time they are run, if this flag is specified, a dialog is + * shown to the user to confirm enabling explore by touch. + *

+ */ + public static final int FLAG_REQUEST_TOUCH_EXPLORATION_MODE = 0x0000004; + + /** + * This flag requests from the system to enable web accessibility enhancing + * extensions. Such extensions aim to provide improved accessibility support + * for content presented in a {@link android.webkit.WebView}. An example of such + * an extension is injecting JavaScript from a secure source. The system will enable + * enhanced web accessibility if there is at least one accessibility service + * that has this flag set. Hence, clearing this flag does not guarantee that the + * device will not have enhanced web accessibility enabled since there may be + * another enabled service that requested it. + *

+ * Services that want to set this flag have to declare this capability + * in their meta-data by setting the attribute canRequestEnhancedWebAccessibility + * to true, otherwise this flag will be ignored. For how to declare the meta-data + * of a service refer to {@value AccessibilityService#SERVICE_META_DATA}. + *

+ */ + public static final int FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY = 0x00000008; + + /** + * This flag requests that the AccessibilityNodeInfos obtained + * by an {@link AccessibilityService} contain the id of the source view. + * The source view id will be a fully qualified resource name of the + * form "package:id/name", for example "foo.bar:id/my_list", and it is + * useful for UI test automation. This flag is not set by default. + */ + public static final int FLAG_REPORT_VIEW_IDS = 0x00000010; + + /** + * This flag requests from the system to filter key events. If this flag + * is set the accessibility service will receive the key events before + * applications allowing it implement global shortcuts. Setting this flag + * does not guarantee that this service will filter key events since only + * one service can do so at any given time. This avoids user confusion due + * to behavior change in case different key filtering services are enabled. + * If there is already another key filtering service enabled, this one will + * not receive key events. + *

+ * Services that want to set this flag have to declare this capability + * in their meta-data by setting the attribute canRequestFilterKeyEvents + * to true, otherwise this flag will be ignored. For how to declare the meta + * -data of a service refer to {@value AccessibilityService#SERVICE_META_DATA}. + *

+ */ + public static final int FLAG_REQUEST_FILTER_KEY_EVENTS = 0x00000020; + + /* + * Hide constructor + */ + private AccessibilityServiceInfoCompat() { + + } + + /** + * The accessibility service id. + *

+ * Generated by the system. + *

+ * + * @return The id. + */ + public static String getId(AccessibilityServiceInfo info) { + return IMPL.getId(info); + } + + /** + * The service {@link ResolveInfo}. + *

+ * Generated by the system. + *

+ * + * @return The info. + */ + public static ResolveInfo getResolveInfo(AccessibilityServiceInfo info) { + return IMPL.getResolveInfo(info); + } + + /** + * The settings activity name. + *

+ * Statically set from {@link AccessibilityService#SERVICE_META_DATA + * meta-data}. + *

+ * + * @return The settings activity name. + */ + public static String getSettingsActivityName(AccessibilityServiceInfo info) { + return IMPL.getSettingsActivityName(info); + } + + /** + * Whether this service can retrieve the current window's content. + *

+ * Statically set from {@link AccessibilityService#SERVICE_META_DATA + * meta-data}. + *

+ * + * @return True window content can be retrieved. + */ + public static boolean getCanRetrieveWindowContent(AccessibilityServiceInfo info) { + return IMPL.getCanRetrieveWindowContent(info); + } + + /** + * Description of the accessibility service. + *

+ * Statically set from {@link AccessibilityService#SERVICE_META_DATA + * meta-data}. + *

+ * + * @return The description. + */ + public static String getDescription(AccessibilityServiceInfo info) { + return IMPL.getDescription(info); + } + + /** + * Returns the string representation of a feedback type. For example, + * {@link AccessibilityServiceInfo#FEEDBACK_SPOKEN} is represented by the + * string FEEDBACK_SPOKEN. + * + * @param feedbackType The feedback type. + * @return The string representation. + */ + public static String feedbackTypeToString(int feedbackType) { + StringBuilder builder = new StringBuilder(); + builder.append("["); + while (feedbackType > 0) { + final int feedbackTypeFlag = 1 << Integer.numberOfTrailingZeros(feedbackType); + feedbackType &= ~feedbackTypeFlag; + if (builder.length() > 1) { + builder.append(", "); + } + switch (feedbackTypeFlag) { + case AccessibilityServiceInfo.FEEDBACK_AUDIBLE: + builder.append("FEEDBACK_AUDIBLE"); + break; + case AccessibilityServiceInfo.FEEDBACK_HAPTIC: + builder.append("FEEDBACK_HAPTIC"); + break; + case AccessibilityServiceInfo.FEEDBACK_GENERIC: + builder.append("FEEDBACK_GENERIC"); + break; + case AccessibilityServiceInfo.FEEDBACK_SPOKEN: + builder.append("FEEDBACK_SPOKEN"); + break; + case AccessibilityServiceInfo.FEEDBACK_VISUAL: + builder.append("FEEDBACK_VISUAL"); + break; + } + } + builder.append("]"); + return builder.toString(); + } + + /** + * Returns the string representation of a flag. For example, + * {@link AccessibilityServiceInfo#DEFAULT} is represented by the + * string DEFAULT. + * + * @param flag The flag. + * @return The string representation. + */ + public static String flagToString(int flag) { + switch (flag) { + case DEFAULT: + return "DEFAULT"; + case FLAG_INCLUDE_NOT_IMPORTANT_VIEWS: + return "FLAG_INCLUDE_NOT_IMPORTANT_VIEWS"; + case FLAG_REQUEST_TOUCH_EXPLORATION_MODE: + return "FLAG_REQUEST_TOUCH_EXPLORATION_MODE"; + case FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY: + return "FLAG_REQUEST_ENHANCED_WEB_ACCESSIBILITY"; + case FLAG_REPORT_VIEW_IDS: + return "FLAG_REPORT_VIEW_IDS"; + case FLAG_REQUEST_FILTER_KEY_EVENTS: + return "FLAG_REQUEST_FILTER_KEY_EVENTS"; + default: + return null; + } + } + + /** + * Returns the bit mask of capabilities this accessibility service has such as + * being able to retrieve the active window content, etc. + * + * @param info The service info whose capabilities to get. + * @return The capability bit mask. + * + * @see #CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT + * @see #CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION + * @see #CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY + * @see #CAPABILITY_CAN_FILTER_KEY_EVENTS + */ + public static int getCapabilities(AccessibilityServiceInfo info) { + return IMPL.getCapabilities(info); + } + + /** + * Returns the string representation of a capability. For example, + * {@link #CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT} is represented + * by the string CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT. + * + * @param capability The capability. + * @return The string representation. + */ + public static String capabilityToString(int capability) { + switch (capability) { + case CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT: + return "CAPABILITY_CAN_RETRIEVE_WINDOW_CONTENT"; + case CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION: + return "CAPABILITY_CAN_REQUEST_TOUCH_EXPLORATION"; + case CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY: + return "CAPABILITY_CAN_REQUEST_ENHANCED_WEB_ACCESSIBILITY"; + case CAPABILITY_CAN_FILTER_KEY_EVENTS: + return "CAPABILITY_CAN_FILTER_KEY_EVENTS"; + default: + return "UNKNOWN"; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatIcs.java new file mode 100644 index 00000000000..ffbea0ade1c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatIcs.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.accessibilityservice; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.pm.ResolveInfo; + +/** + * ICS implementation of the new APIs in AccessibilityServiceInfo. + */ +class AccessibilityServiceInfoCompatIcs { + + public static boolean getCanRetrieveWindowContent(AccessibilityServiceInfo info) { + return info.getCanRetrieveWindowContent(); + } + + public static String getDescription(AccessibilityServiceInfo info) { + return info.getDescription(); + } + + public static String getId(AccessibilityServiceInfo info) { + return info.getId(); + } + + public static ResolveInfo getResolveInfo(AccessibilityServiceInfo info) { + return info.getResolveInfo(); + } + + public static String getSettingsActivityName(AccessibilityServiceInfo info) { + return info.getSettingsActivityName(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatJellyBeanMr2.java b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatJellyBeanMr2.java new file mode 100644 index 00000000000..3a41b7a2c90 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/AccessibilityServiceInfoCompatJellyBeanMr2.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.accessibilityservice; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.content.pm.ResolveInfo; + +/** + * ICS implementation of the new APIs in AccessibilityServiceInfo. + */ +class AccessibilityServiceInfoCompatJellyBeanMr2 { + + public static int getCapabilities(AccessibilityServiceInfo info) { + return info.getCapabilities(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/package.html b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/package.html new file mode 100644 index 00000000000..3d017b0ce0b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/accessibilityservice/package.html @@ -0,0 +1,6 @@ + + +Support android.accessibilityservice classes to assist with development of applications for +android API level 4 or later. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorCompatHelper.java b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorCompatHelper.java new file mode 100644 index 00000000000..5e3b954cde6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorCompatHelper.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.animation; + +import android.os.Build; + +abstract public class AnimatorCompatHelper { + + static AnimatorProvider IMPL; + + static { + if (Build.VERSION.SDK_INT >= 12) { + IMPL = new HoneycombMr1AnimatorCompatProvider(); + } else { + IMPL = new DonutAnimatorCompatProvider(); + } + } + + public static ValueAnimatorCompat emptyValueAnimator() { + return IMPL.emptyValueAnimator(); + } + + AnimatorCompatHelper() { + + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorListenerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorListenerCompat.java new file mode 100644 index 00000000000..c70d6bc890b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorListenerCompat.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.animation; + +/** + *

An animation listener that receives notifications from an animation. + * Notifications indicate animation related events, such as the end or the + * repetition of the animation.

+ * + * @hide + */ +public interface AnimatorListenerCompat { + + /** + *

Notifies the start of the animation.

+ * + * @param animation The started animation. + */ + void onAnimationStart(ValueAnimatorCompat animation); + + /** + *

Notifies the end of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

+ * + * @param animation The animation which reached its end. + */ + void onAnimationEnd(ValueAnimatorCompat animation); + + /** + *

Notifies the cancellation of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

+ * + * @param animation The animation which was canceled. + */ + void onAnimationCancel(ValueAnimatorCompat animation); + + /** + *

Notifies the repetition of the animation.

+ * + * @param animation The animation which was repeated. + */ + void onAnimationRepeat(ValueAnimatorCompat animation); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorProvider.java b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorProvider.java new file mode 100644 index 00000000000..8f7567772d2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.animation; + +/** + * A simple interface to do things in animation pulse. + *

+ * Before Honeycomb, it uses a simple Handler to mimic animation callback. + *

+ * This is only a minimal implementation which is why this class is hidden. + * + * @hide + */ +interface AnimatorProvider { + + /** + * Provides a simple ValueAnimator w/o any start or end values. It provides the same + * Animator callback interface. + */ + ValueAnimatorCompat emptyValueAnimator(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorUpdateListenerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorUpdateListenerCompat.java new file mode 100644 index 00000000000..acbcccf030a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/AnimatorUpdateListenerCompat.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.animation; + +/** + * Implementors of this interface can add themselves as update listeners + * to a ValueAnimator instance to receive callbacks on every animation + * frame, after the current frame's values have been calculated for that + * ValueAnimator. + * + * @hide + */ +public interface AnimatorUpdateListenerCompat { + + /** + *

Notifies the occurrence of another frame of the animation.

+ * + * @param animation The animation which was repeated. + */ + void onAnimationUpdate(ValueAnimatorCompat animation); + +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/DonutAnimatorCompatProvider.java b/mobile/google-android-support-v4/src/android/support/v4/animation/DonutAnimatorCompatProvider.java new file mode 100644 index 00000000000..9ef590cc11f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/DonutAnimatorCompatProvider.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.animation; + +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +/** + * Provides similar functionality to Animators on platforms prior to Honeycomb. + *

+ * This is not a fully implemented API which is why it is not public. + * + * @hide + */ +class DonutAnimatorCompatProvider implements AnimatorProvider { + + @Override + public ValueAnimatorCompat emptyValueAnimator() { + return new DonutFloatValueAnimator(); + } + + private static class DonutFloatValueAnimator implements ValueAnimatorCompat { + + List mListeners = new ArrayList(); + List mUpdateListeners + = new ArrayList(); + View mTarget; + private long mStartTime; + private long mDuration = 200; + private float mFraction = 0f; + + private boolean mStarted = false; + private boolean mEnded = false; + + public DonutFloatValueAnimator() { + } + + private Runnable mLoopRunnable = new Runnable() { + @Override + public void run() { + long dt = getTime() - mStartTime; + float fraction = dt * 1f / mDuration; + if (fraction > 1f || mTarget.getParent() == null) { + fraction = 1f; + } + mFraction = fraction; + notifyUpdateListeners(); + if (mFraction >= 1f) { + dispatchEnd(); + } else { + mTarget.postDelayed(mLoopRunnable, 16); + } + } + }; + + private void notifyUpdateListeners() { + for (int i = mUpdateListeners.size() - 1; i >= 0; i--) { + mUpdateListeners.get(i).onAnimationUpdate(this); + } + } + + @Override + public void setTarget(View view) { + mTarget = view; + } + + @Override + public void addListener(AnimatorListenerCompat listener) { + mListeners.add(listener); + } + + @Override + public void setDuration(long duration) { + if (!mStarted) { + mDuration = duration; + } + } + + @Override + public void start() { + if (mStarted) { + return; + } + mStarted = true; + dispatchStart(); + mFraction = 0f; + mStartTime = getTime(); + mTarget.postDelayed(mLoopRunnable, 16); + } + + private long getTime() { + return mTarget.getDrawingTime(); + } + + private void dispatchStart() { + for (int i = mListeners.size() - 1; i >= 0; i--) { + mListeners.get(i).onAnimationStart(this); + } + } + + private void dispatchEnd() { + for (int i = mListeners.size() - 1; i >= 0; i--) { + mListeners.get(i).onAnimationEnd(this); + } + } + + private void dispatchCancel() { + for (int i = mListeners.size() - 1; i >= 0; i--) { + mListeners.get(i).onAnimationCancel(this); + } + } + + @Override + public void cancel() { + if (mEnded) { + return; + } + mEnded = true; + if (mStarted) { + dispatchCancel(); + } + dispatchEnd(); + } + + @Override + public void addUpdateListener(AnimatorUpdateListenerCompat animatorUpdateListener) { + mUpdateListeners.add(animatorUpdateListener); + } + + @Override + public float getAnimatedFraction() { + return mFraction; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/HoneycombMr1AnimatorCompatProvider.java b/mobile/google-android-support-v4/src/android/support/v4/animation/HoneycombMr1AnimatorCompatProvider.java new file mode 100644 index 00000000000..cc644814342 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/HoneycombMr1AnimatorCompatProvider.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.animation; + +import android.animation.Animator; +import android.animation.PropertyValuesHolder; +import android.animation.TypeEvaluator; +import android.animation.ValueAnimator; +import android.view.View; + +/** + * Uses framework Animators to provide ValueAnimatorCompat interface. + *

+ * This is not a fully implemented API which is why it is not public. + * + * @hide + */ +class HoneycombMr1AnimatorCompatProvider implements AnimatorProvider { + + @Override + public ValueAnimatorCompat emptyValueAnimator() { + return new HoneycombValueAnimatorCompat(ValueAnimator.ofFloat(0f, 1f)); + } + + static class HoneycombValueAnimatorCompat implements ValueAnimatorCompat { + + final Animator mWrapped; + + public HoneycombValueAnimatorCompat(Animator wrapped) { + mWrapped = wrapped; + } + + @Override + public void setTarget(View view) { + mWrapped.setTarget(view); + } + + @Override + public void addListener(AnimatorListenerCompat listener) { + mWrapped.addListener(new AnimatorListenerCompatWrapper(listener, this)); + } + + @Override + public void setDuration(long duration) { + mWrapped.setDuration(duration); + } + + @Override + public void start() { + mWrapped.start(); + } + + @Override + public void cancel() { + mWrapped.cancel(); + } + + @Override + public void addUpdateListener(final AnimatorUpdateListenerCompat animatorUpdateListener) { + if (mWrapped instanceof ValueAnimator) { + ((ValueAnimator) mWrapped).addUpdateListener( + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + animatorUpdateListener + .onAnimationUpdate(HoneycombValueAnimatorCompat.this); + } + }); + } + } + + @Override + public float getAnimatedFraction() { + return ((ValueAnimator) mWrapped).getAnimatedFraction(); + } + } + + static class AnimatorListenerCompatWrapper implements Animator.AnimatorListener { + + final AnimatorListenerCompat mWrapped; + + final ValueAnimatorCompat mValueAnimatorCompat; + + public AnimatorListenerCompatWrapper( + AnimatorListenerCompat wrapped, ValueAnimatorCompat valueAnimatorCompat) { + mWrapped = wrapped; + mValueAnimatorCompat = valueAnimatorCompat; + } + + @Override + public void onAnimationStart(Animator animation) { + mWrapped.onAnimationStart(mValueAnimatorCompat); + } + + @Override + public void onAnimationEnd(Animator animation) { + mWrapped.onAnimationEnd(mValueAnimatorCompat); + } + + @Override + public void onAnimationCancel(Animator animation) { + mWrapped.onAnimationCancel(mValueAnimatorCompat); + } + + @Override + public void onAnimationRepeat(Animator animation) { + mWrapped.onAnimationRepeat(mValueAnimatorCompat); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/animation/ValueAnimatorCompat.java b/mobile/google-android-support-v4/src/android/support/v4/animation/ValueAnimatorCompat.java new file mode 100644 index 00000000000..6d98c977198 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/animation/ValueAnimatorCompat.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.animation; + +import android.view.View; + +/** + * Compatibility implementation for {@link android.animation.Animator}. + * + * @hide + */ +public interface ValueAnimatorCompat { + + public void setTarget(View view); + + public void addListener(AnimatorListenerCompat listener); + + public void setDuration(long duration); + + public void start(); + + public void cancel(); + + void addUpdateListener(AnimatorUpdateListenerCompat animatorUpdateListener); + + public float getAnimatedFraction(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggle.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggle.java new file mode 100644 index 00000000000..a7e782ddc10 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggle.java @@ -0,0 +1,560 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.app; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.InsetDrawable; +import android.os.Build; +import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.GravityCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.widget.DrawerLayout; +import android.view.MenuItem; +import android.view.View; + +/** + * @deprecated Please use ActionBarDrawerToggle in support-v7-appcompat. + * + *

+ * This class provides a handy way to tie together the functionality of + * {@link DrawerLayout} and the framework ActionBar to implement the recommended + * design for navigation drawers. + * + *

To use ActionBarDrawerToggle, create one in your Activity and call through + * to the following methods corresponding to your Activity callbacks:

+ * + *
    + *
  • {@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged}
  • + *
  • {@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected}
  • + *
+ * + *

Call {@link #syncState()} from your Activity's + * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} to synchronize the indicator + * with the state of the linked DrawerLayout after onRestoreInstanceState + * has occurred.

+ * + *

ActionBarDrawerToggle can be used directly as a + * {@link DrawerLayout.DrawerListener}, or if you are already providing your own listener, + * call through to each of the listener methods from your own.

+ * + */ +@Deprecated +public class ActionBarDrawerToggle implements DrawerLayout.DrawerListener { + + /** + * Allows an implementing Activity to return an {@link ActionBarDrawerToggle.Delegate} to use + * with ActionBarDrawerToggle. + */ + public interface DelegateProvider { + + /** + * @return Delegate to use for ActionBarDrawableToggles, or null if the Activity + * does not wish to override the default behavior. + */ + @Nullable + Delegate getDrawerToggleDelegate(); + } + + public interface Delegate { + /** + * @return Up indicator drawable as defined in the Activity's theme, or null if one is not + * defined. + */ + @Nullable + Drawable getThemeUpIndicator(); + + /** + * Set the Action Bar's up indicator drawable and content description. + * + * @param upDrawable - Drawable to set as up indicator + * @param contentDescRes - Content description to set + */ + void setActionBarUpIndicator(Drawable upDrawable, @StringRes int contentDescRes); + + /** + * Set the Action Bar's up indicator content description. + * + * @param contentDescRes - Content description to set + */ + void setActionBarDescription(@StringRes int contentDescRes); + } + + private interface ActionBarDrawerToggleImpl { + Drawable getThemeUpIndicator(Activity activity); + Object setActionBarUpIndicator(Object info, Activity activity, + Drawable themeImage, int contentDescRes); + Object setActionBarDescription(Object info, Activity activity, int contentDescRes); + } + + private static class ActionBarDrawerToggleImplBase implements ActionBarDrawerToggleImpl { + @Override + public Drawable getThemeUpIndicator(Activity activity) { + return null; + } + + @Override + public Object setActionBarUpIndicator(Object info, Activity activity, + Drawable themeImage, int contentDescRes) { + // No action bar to set. + return info; + } + + @Override + public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) { + // No action bar to set + return info; + } + } + + private static class ActionBarDrawerToggleImplHC implements ActionBarDrawerToggleImpl { + @Override + public Drawable getThemeUpIndicator(Activity activity) { + return ActionBarDrawerToggleHoneycomb.getThemeUpIndicator(activity); + } + + @Override + public Object setActionBarUpIndicator(Object info, Activity activity, + Drawable themeImage, int contentDescRes) { + return ActionBarDrawerToggleHoneycomb.setActionBarUpIndicator(info, activity, + themeImage, contentDescRes); + } + + @Override + public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) { + return ActionBarDrawerToggleHoneycomb.setActionBarDescription(info, activity, + contentDescRes); + } + } + + private static class ActionBarDrawerToggleImplJellybeanMR2 + implements ActionBarDrawerToggleImpl { + @Override + public Drawable getThemeUpIndicator(Activity activity) { + return ActionBarDrawerToggleJellybeanMR2.getThemeUpIndicator(activity); + } + + @Override + public Object setActionBarUpIndicator(Object info, Activity activity, + Drawable themeImage, int contentDescRes) { + return ActionBarDrawerToggleJellybeanMR2.setActionBarUpIndicator(info, activity, + themeImage, contentDescRes); + } + + @Override + public Object setActionBarDescription(Object info, Activity activity, int contentDescRes) { + return ActionBarDrawerToggleJellybeanMR2.setActionBarDescription(info, activity, + contentDescRes); + } + } + + private static final ActionBarDrawerToggleImpl IMPL; + + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 18) { + IMPL = new ActionBarDrawerToggleImplJellybeanMR2(); + } else if (version >= 11) { + IMPL = new ActionBarDrawerToggleImplHC(); + } else { + IMPL = new ActionBarDrawerToggleImplBase(); + } + } + + /** Fraction of its total width by which to offset the toggle drawable. */ + private static final float TOGGLE_DRAWABLE_OFFSET = 1 / 3f; + + // android.R.id.home as defined by public API in v11 + private static final int ID_HOME = 0x0102002c; + + private final Activity mActivity; + private final Delegate mActivityImpl; + private final DrawerLayout mDrawerLayout; + private boolean mDrawerIndicatorEnabled = true; + private boolean mHasCustomUpIndicator; + + private Drawable mHomeAsUpIndicator; + private Drawable mDrawerImage; + private SlideDrawable mSlider; + private final int mDrawerImageResource; + private final int mOpenDrawerContentDescRes; + private final int mCloseDrawerContentDescRes; + + private Object mSetIndicatorInfo; + + /** + * Construct a new ActionBarDrawerToggle. + * + *

The given {@link Activity} will be linked to the specified {@link DrawerLayout}. + * The provided drawer indicator drawable will animate slightly off-screen as the drawer + * is opened, indicating that in the open state the drawer will move off-screen when pressed + * and in the closed state the drawer will move on-screen when pressed.

+ * + *

String resources must be provided to describe the open/close drawer actions for + * accessibility services.

+ * + * @param activity The Activity hosting the drawer + * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar + * @param drawerImageRes A Drawable resource to use as the drawer indicator + * @param openDrawerContentDescRes A String resource to describe the "open drawer" action + * for accessibility + * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action + * for accessibility + */ + public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, + @DrawableRes int drawerImageRes, @StringRes int openDrawerContentDescRes, + @StringRes int closeDrawerContentDescRes) { + this(activity, drawerLayout, !assumeMaterial(activity), drawerImageRes, + openDrawerContentDescRes, closeDrawerContentDescRes); + } + + private static boolean assumeMaterial(Context context) { + return context.getApplicationInfo().targetSdkVersion >= 21 && + (Build.VERSION.SDK_INT >= 21); + } + + /** + * Construct a new ActionBarDrawerToggle. + * + *

The given {@link Activity} will be linked to the specified {@link DrawerLayout}. + * The provided drawer indicator drawable will animate slightly off-screen as the drawer + * is opened, indicating that in the open state the drawer will move off-screen when pressed + * and in the closed state the drawer will move on-screen when pressed.

+ * + *

String resources must be provided to describe the open/close drawer actions for + * accessibility services.

+ * + * @param activity The Activity hosting the drawer + * @param drawerLayout The DrawerLayout to link to the given Activity's ActionBar + * @param animate True to animate the drawer indicator along with the drawer's position. + * Material apps should set this to false. + * @param drawerImageRes A Drawable resource to use as the drawer indicator + * @param openDrawerContentDescRes A String resource to describe the "open drawer" action + * for accessibility + * @param closeDrawerContentDescRes A String resource to describe the "close drawer" action + * for accessibility + */ + public ActionBarDrawerToggle(Activity activity, DrawerLayout drawerLayout, boolean animate, + @DrawableRes int drawerImageRes, @StringRes int openDrawerContentDescRes, + @StringRes int closeDrawerContentDescRes) { + mActivity = activity; + + // Allow the Activity to provide an impl + if (activity instanceof DelegateProvider) { + mActivityImpl = ((DelegateProvider) activity).getDrawerToggleDelegate(); + } else { + mActivityImpl = null; + } + + mDrawerLayout = drawerLayout; + mDrawerImageResource = drawerImageRes; + mOpenDrawerContentDescRes = openDrawerContentDescRes; + mCloseDrawerContentDescRes = closeDrawerContentDescRes; + + mHomeAsUpIndicator = getThemeUpIndicator(); + mDrawerImage = ContextCompat.getDrawable(activity, drawerImageRes); + mSlider = new SlideDrawable(mDrawerImage); + mSlider.setOffset(animate ? TOGGLE_DRAWABLE_OFFSET : 0); + } + + /** + * Synchronize the state of the drawer indicator/affordance with the linked DrawerLayout. + * + *

This should be called from your Activity's + * {@link Activity#onPostCreate(android.os.Bundle) onPostCreate} method to synchronize after + * the DrawerLayout's instance state has been restored, and any other time when the state + * may have diverged in such a way that the ActionBarDrawerToggle was not notified. + * (For example, if you stop forwarding appropriate drawer events for a period of time.)

+ */ + public void syncState() { + if (mDrawerLayout.isDrawerOpen(GravityCompat.START)) { + mSlider.setPosition(1); + } else { + mSlider.setPosition(0); + } + + if (mDrawerIndicatorEnabled) { + setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ? + mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); + } + } + + /** + * Set the up indicator to display when the drawer indicator is not + * enabled. + *

+ * If you pass null to this method, the default drawable from + * the theme will be used. + * + * @param indicator A drawable to use for the up indicator, or null to use + * the theme's default + * @see #setDrawerIndicatorEnabled(boolean) + */ + public void setHomeAsUpIndicator(Drawable indicator) { + if (indicator == null) { + mHomeAsUpIndicator = getThemeUpIndicator(); + mHasCustomUpIndicator = false; + } else { + mHomeAsUpIndicator = indicator; + mHasCustomUpIndicator = true; + } + + if (!mDrawerIndicatorEnabled) { + setActionBarUpIndicator(mHomeAsUpIndicator, 0); + } + } + + /** + * Set the up indicator to display when the drawer indicator is not + * enabled. + *

+ * If you pass 0 to this method, the default drawable from the theme will + * be used. + * + * @param resId Resource ID of a drawable to use for the up indicator, or 0 + * to use the theme's default + * @see #setDrawerIndicatorEnabled(boolean) + */ + public void setHomeAsUpIndicator(int resId) { + Drawable indicator = null; + if (resId != 0) { + indicator = ContextCompat.getDrawable(mActivity, resId); + } + + setHomeAsUpIndicator(indicator); + } + + /** + * Enable or disable the drawer indicator. The indicator defaults to enabled. + * + *

When the indicator is disabled, the ActionBar will revert to displaying + * the home-as-up indicator provided by the Activity's theme in the + * android.R.attr.homeAsUpIndicator attribute instead of the animated + * drawer glyph.

+ * + * @param enable true to enable, false to disable + */ + public void setDrawerIndicatorEnabled(boolean enable) { + if (enable != mDrawerIndicatorEnabled) { + if (enable) { + setActionBarUpIndicator(mSlider, mDrawerLayout.isDrawerOpen(GravityCompat.START) ? + mCloseDrawerContentDescRes : mOpenDrawerContentDescRes); + } else { + setActionBarUpIndicator(mHomeAsUpIndicator, 0); + } + mDrawerIndicatorEnabled = enable; + } + } + + /** + * @return true if the enhanced drawer indicator is enabled, false otherwise + * @see #setDrawerIndicatorEnabled(boolean) + */ + public boolean isDrawerIndicatorEnabled() { + return mDrawerIndicatorEnabled; + } + + /** + * This method should always be called by your Activity's + * {@link Activity#onConfigurationChanged(android.content.res.Configuration) onConfigurationChanged} + * method. + * + * @param newConfig The new configuration + */ + public void onConfigurationChanged(Configuration newConfig) { + // Reload drawables that can change with configuration + if (!mHasCustomUpIndicator) { + mHomeAsUpIndicator = getThemeUpIndicator(); + } + mDrawerImage = ContextCompat.getDrawable(mActivity, mDrawerImageResource); + syncState(); + } + + /** + * This method should be called by your Activity's + * {@link Activity#onOptionsItemSelected(android.view.MenuItem) onOptionsItemSelected} method. + * If it returns true, your onOptionsItemSelected method should return true and + * skip further processing. + * + * @param item the MenuItem instance representing the selected menu item + * @return true if the event was handled and further processing should not occur + */ + public boolean onOptionsItemSelected(MenuItem item) { + if (item != null && item.getItemId() == ID_HOME && mDrawerIndicatorEnabled) { + if (mDrawerLayout.isDrawerVisible(GravityCompat.START)) { + mDrawerLayout.closeDrawer(GravityCompat.START); + } else { + mDrawerLayout.openDrawer(GravityCompat.START); + } + return true; + } + return false; + } + + /** + * {@link DrawerLayout.DrawerListener} callback method. If you do not use your + * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call + * through to this method from your own listener object. + * + * @param drawerView The child view that was moved + * @param slideOffset The new offset of this drawer within its range, from 0-1 + */ + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + float glyphOffset = mSlider.getPosition(); + if (slideOffset > 0.5f) { + glyphOffset = Math.max(glyphOffset, Math.max(0.f, slideOffset - 0.5f) * 2); + } else { + glyphOffset = Math.min(glyphOffset, slideOffset * 2); + } + mSlider.setPosition(glyphOffset); + } + + /** + * {@link DrawerLayout.DrawerListener} callback method. If you do not use your + * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call + * through to this method from your own listener object. + * + * @param drawerView Drawer view that is now open + */ + @Override + public void onDrawerOpened(View drawerView) { + mSlider.setPosition(1); + if (mDrawerIndicatorEnabled) { + setActionBarDescription(mCloseDrawerContentDescRes); + } + } + + /** + * {@link DrawerLayout.DrawerListener} callback method. If you do not use your + * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call + * through to this method from your own listener object. + * + * @param drawerView Drawer view that is now closed + */ + @Override + public void onDrawerClosed(View drawerView) { + mSlider.setPosition(0); + if (mDrawerIndicatorEnabled) { + setActionBarDescription(mOpenDrawerContentDescRes); + } + } + + /** + * {@link DrawerLayout.DrawerListener} callback method. If you do not use your + * ActionBarDrawerToggle instance directly as your DrawerLayout's listener, you should call + * through to this method from your own listener object. + * + * @param newState The new drawer motion state + */ + @Override + public void onDrawerStateChanged(int newState) { + } + + Drawable getThemeUpIndicator() { + if (mActivityImpl != null) { + return mActivityImpl.getThemeUpIndicator(); + } + return IMPL.getThemeUpIndicator(mActivity); + } + + void setActionBarUpIndicator(Drawable upDrawable, int contentDescRes) { + if (mActivityImpl != null) { + mActivityImpl.setActionBarUpIndicator(upDrawable, contentDescRes); + return; + } + mSetIndicatorInfo = IMPL + .setActionBarUpIndicator(mSetIndicatorInfo, mActivity, upDrawable, contentDescRes); + } + + void setActionBarDescription(int contentDescRes) { + if (mActivityImpl != null) { + mActivityImpl.setActionBarDescription(contentDescRes); + return; + } + mSetIndicatorInfo = IMPL + .setActionBarDescription(mSetIndicatorInfo, mActivity, contentDescRes); + } + + private class SlideDrawable extends InsetDrawable implements Drawable.Callback { + private final boolean mHasMirroring = Build.VERSION.SDK_INT > 18; + private final Rect mTmpRect = new Rect(); + + private float mPosition; + private float mOffset; + + private SlideDrawable(Drawable wrapped) { + super(wrapped, 0); + } + + /** + * Sets the current position along the offset. + * + * @param position a value between 0 and 1 + */ + public void setPosition(float position) { + mPosition = position; + invalidateSelf(); + } + + public float getPosition() { + return mPosition; + } + + /** + * Specifies the maximum offset when the position is at 1. + * + * @param offset maximum offset as a fraction of the drawable width, + * positive to shift left or negative to shift right. + * @see #setPosition(float) + */ + public void setOffset(float offset) { + mOffset = offset; + invalidateSelf(); + } + + @Override + public void draw(Canvas canvas) { + copyBounds(mTmpRect); + canvas.save(); + + // Layout direction must be obtained from the activity. + final boolean isLayoutRTL = ViewCompat.getLayoutDirection( + mActivity.getWindow().getDecorView()) == ViewCompat.LAYOUT_DIRECTION_RTL; + final int flipRtl = isLayoutRTL ? -1 : 1; + final int width = mTmpRect.width(); + canvas.translate(-mOffset * width * mPosition * flipRtl, 0); + + // Force auto-mirroring if it's not supported by the platform. + if (isLayoutRTL && !mHasMirroring) { + canvas.translate(width, 0); + canvas.scale(-1, 1); + } + + super.draw(canvas); + canvas.restore(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleHoneycomb.java new file mode 100644 index 00000000000..bd279546c0e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleHoneycomb.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.app; + +import android.R; +import android.app.ActionBar; +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import java.lang.reflect.Method; + +/** + * This class encapsulates some awful hacks. + * + * Before JB-MR2 (API 18) it was not possible to change the home-as-up indicator glyph + * in an action bar without some really gross hacks. Since the MR2 SDK is not published as of + * this writing, the new API is accessed via reflection here if available. + */ +class ActionBarDrawerToggleHoneycomb { + private static final String TAG = "ActionBarDrawerToggleHoneycomb"; + + private static final int[] THEME_ATTRS = new int[] { + R.attr.homeAsUpIndicator + }; + + public static Object setActionBarUpIndicator(Object info, Activity activity, + Drawable drawable, int contentDescRes) { + if (info == null) { + info = new SetIndicatorInfo(activity); + } + final SetIndicatorInfo sii = (SetIndicatorInfo) info; + if (sii.setHomeAsUpIndicator != null) { + try { + final ActionBar actionBar = activity.getActionBar(); + sii.setHomeAsUpIndicator.invoke(actionBar, drawable); + sii.setHomeActionContentDescription.invoke(actionBar, contentDescRes); + } catch (Exception e) { + Log.w(TAG, "Couldn't set home-as-up indicator via JB-MR2 API", e); + } + } else if (sii.upIndicatorView != null) { + sii.upIndicatorView.setImageDrawable(drawable); + } else { + Log.w(TAG, "Couldn't set home-as-up indicator"); + } + return info; + } + + public static Object setActionBarDescription(Object info, Activity activity, + int contentDescRes) { + if (info == null) { + info = new SetIndicatorInfo(activity); + } + final SetIndicatorInfo sii = (SetIndicatorInfo) info; + if (sii.setHomeAsUpIndicator != null) { + try { + final ActionBar actionBar = activity.getActionBar(); + sii.setHomeActionContentDescription.invoke(actionBar, contentDescRes); + if (Build.VERSION.SDK_INT <= 19) { + // For API 19 and earlier, we need to manually force the + // action bar to generate a new content description. + actionBar.setSubtitle(actionBar.getSubtitle()); + } + } catch (Exception e) { + Log.w(TAG, "Couldn't set content description via JB-MR2 API", e); + } + } + return info; + } + + public static Drawable getThemeUpIndicator(Activity activity) { + final TypedArray a = activity.obtainStyledAttributes(THEME_ATTRS); + final Drawable result = a.getDrawable(0); + a.recycle(); + return result; + } + + private static class SetIndicatorInfo { + public Method setHomeAsUpIndicator; + public Method setHomeActionContentDescription; + public ImageView upIndicatorView; + + SetIndicatorInfo(Activity activity) { + try { + setHomeAsUpIndicator = ActionBar.class.getDeclaredMethod("setHomeAsUpIndicator", + Drawable.class); + setHomeActionContentDescription = ActionBar.class.getDeclaredMethod( + "setHomeActionContentDescription", Integer.TYPE); + + // If we got the method we won't need the stuff below. + return; + } catch (NoSuchMethodException e) { + // Oh well. We'll use the other mechanism below instead. + } + + final View home = activity.findViewById(android.R.id.home); + if (home == null) { + // Action bar doesn't have a known configuration, an OEM messed with things. + return; + } + + final ViewGroup parent = (ViewGroup) home.getParent(); + final int childCount = parent.getChildCount(); + if (childCount != 2) { + // No idea which one will be the right one, an OEM messed with things. + return; + } + + final View first = parent.getChildAt(0); + final View second = parent.getChildAt(1); + final View up = first.getId() == android.R.id.home ? second : first; + + if (up instanceof ImageView) { + // Jackpot! (Probably...) + upIndicatorView = (ImageView) up; + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java new file mode 100644 index 00000000000..5d98d8856cc --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActionBarDrawerToggleJellybeanMR2.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.app; + +import android.R; +import android.app.ActionBar; +import android.app.Activity; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.Log; + +class ActionBarDrawerToggleJellybeanMR2 { + private static final String TAG = "ActionBarDrawerToggleImplJellybeanMR2"; + + private static final int[] THEME_ATTRS = new int[] { + R.attr.homeAsUpIndicator + }; + + public static Object setActionBarUpIndicator(Object info, Activity activity, + Drawable drawable, int contentDescRes) { + final ActionBar actionBar = activity.getActionBar(); + if (actionBar != null) { + actionBar.setHomeAsUpIndicator(drawable); + actionBar.setHomeActionContentDescription(contentDescRes); + } + return info; + } + + public static Object setActionBarDescription(Object info, Activity activity, + int contentDescRes) { + final ActionBar actionBar = activity.getActionBar(); + if (actionBar != null) { + actionBar.setHomeActionContentDescription(contentDescRes); + } + return info; + } + + public static Drawable getThemeUpIndicator(Activity activity) { + final ActionBar actionBar = activity.getActionBar(); + final Context context; + if (actionBar != null) { + context = actionBar.getThemedContext(); + } else { + context = activity; + } + + final TypedArray a = context.obtainStyledAttributes(null, THEME_ATTRS, + R.attr.actionBarStyle, 0); + final Drawable result = a.getDrawable(0); + a.recycle(); + return result; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat.java new file mode 100644 index 00000000000..8d8bf04b599 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.view.View; + +import java.util.List; +import java.util.Map; + +/** + * Helper for accessing features in {@link android.app.Activity} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ActivityCompat extends ContextCompat { + /** + * Invalidate the activity's options menu, if able. + * + *

Before API level 11 (Android 3.0/Honeycomb) the lifecycle of the + * options menu was controlled primarily by the user's operation of + * the hardware menu key. When the user presses down on the menu key + * for the first time the menu was created and prepared by calls + * to {@link Activity#onCreateOptionsMenu(android.view.Menu)} and + * {@link Activity#onPrepareOptionsMenu(android.view.Menu)} respectively. + * Subsequent presses of the menu key kept the existing instance of the + * Menu itself and called {@link Activity#onPrepareOptionsMenu(android.view.Menu)} + * to give the activity an opportunity to contextually alter the menu + * before the menu panel was shown.

+ * + *

In Android 3.0+ the Action Bar forces the options menu to be built early + * so that items chosen to show as actions may be displayed when the activity + * first becomes visible. The Activity method invalidateOptionsMenu forces + * the entire menu to be destroyed and recreated from + * {@link Activity#onCreateOptionsMenu(android.view.Menu)}, offering a similar + * though heavier-weight opportunity to change the menu's contents. Normally + * this functionality is used to support a changing configuration of Fragments.

+ * + *

Applications may use this support helper to signal a significant change in + * activity state that should cause the options menu to be rebuilt. If the app + * is running on an older platform version that does not support menu invalidation + * the app will still receive {@link Activity#onPrepareOptionsMenu(android.view.Menu)} + * the next time the user presses the menu key and this method will return false. + * If this method returns true the options menu was successfully invalidated.

+ * + * @param activity Invalidate the options menu of this activity + * @return true if this operation was supported and it completed; false if it was not available. + */ + public static boolean invalidateOptionsMenu(Activity activity) { + if (Build.VERSION.SDK_INT >= 11) { + ActivityCompatHoneycomb.invalidateOptionsMenu(activity); + return true; + } + return false; + } + + /** + * Start an activity with additional launch information, if able. + * + *

In Android 4.1+ additional options were introduced to allow for more + * control on activity launch animations. Applications can use this method + * along with {@link ActivityOptionsCompat} to use these animations when + * available. When run on versions of the platform where this feature does + * not exist the activity will be launched normally.

+ * + * @param activity Context to launch activity from. + * @param intent The description of the activity to start. + * @param options Additional options for how the Activity should be started. + * May be null if there are no options. See + * {@link ActivityOptionsCompat} for how to build the Bundle + * supplied here; there are no supported definitions for + * building it manually. + */ + public static void startActivity(Activity activity, Intent intent, @Nullable Bundle options) { + if (Build.VERSION.SDK_INT >= 16) { + ActivityCompatJB.startActivity(activity, intent, options); + } else { + activity.startActivity(intent); + } + } + + /** + * Start new activity with options, if able, for which you would like a + * result when it finished. + * + *

In Android 4.1+ additional options were introduced to allow for more + * control on activity launch animations. Applications can use this method + * along with {@link ActivityOptionsCompat} to use these animations when + * available. When run on versions of the platform where this feature does + * not exist the activity will be launched normally.

+ * + * @param activity Origin activity to launch from. + * @param intent The description of the activity to start. + * @param requestCode If >= 0, this code will be returned in + * onActivityResult() when the activity exits. + * @param options Additional options for how the Activity should be started. + * May be null if there are no options. See + * {@link ActivityOptionsCompat} for how to build the Bundle + * supplied here; there are no supported definitions for + * building it manually. + */ + public static void startActivityForResult(Activity activity, Intent intent, int requestCode, + @Nullable Bundle options) { + if (Build.VERSION.SDK_INT >= 16) { + ActivityCompatJB.startActivityForResult(activity, intent, requestCode, options); + } else { + activity.startActivityForResult(intent, requestCode); + } + } + + /** + * Finish this activity, and tries to finish all activities immediately below it + * in the current task that have the same affinity. + * + *

On Android 4.1+ calling this method will call through to the native version of this + * method. For other platforms {@link Activity#finish()} will be called instead.

+ */ + public static void finishAffinity(Activity activity) { + if (Build.VERSION.SDK_INT >= 16) { + ActivityCompatJB.finishAffinity(activity); + } else { + activity.finish(); + } + } + + /** + * Reverses the Activity Scene entry Transition and triggers the calling Activity + * to reverse its exit Transition. When the exit Transition completes, + * {@link Activity#finish()} is called. If no entry Transition was used, finish() is called + * immediately and the Activity exit Transition is run. + * + *

On Android 4.4 or lower, this method only finishes the Activity with no + * special exit transition.

+ */ + public static void finishAfterTransition(Activity activity) { + if (Build.VERSION.SDK_INT >= 21) { + ActivityCompat21.finishAfterTransition(activity); + } else { + activity.finish(); + } + } + + /** + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, + * android.view.View, String)} was used to start an Activity, callback + * will be called to handle shared elements on the launched Activity. This requires + * {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param callback Used to manipulate shared element transitions on the launched Activity. + */ + public static void setEnterSharedElementCallback(Activity activity, + SharedElementCallback callback) { + if (Build.VERSION.SDK_INT >= 21) { + ActivityCompat21.setEnterSharedElementCallback(activity, createCallback(callback)); + } + } + + /** + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, + * android.view.View, String)} was used to start an Activity, callback + * will be called to handle shared elements on the launching Activity. Most + * calls will only come when returning from the started Activity. + * This requires {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param callback Used to manipulate shared element transitions on the launching Activity. + */ + public static void setExitSharedElementCallback(Activity activity, + SharedElementCallback callback) { + if (Build.VERSION.SDK_INT >= 21) { + ActivityCompat21.setExitSharedElementCallback(activity, createCallback(callback)); + } + } + + public static void postponeEnterTransition(Activity activity) { + if (Build.VERSION.SDK_INT >= 21) { + ActivityCompat21.postponeEnterTransition(activity); + } + } + + public static void startPostponedEnterTransition(Activity activity) { + if (Build.VERSION.SDK_INT >= 21) { + ActivityCompat21.startPostponedEnterTransition(activity); + } + } + + private static ActivityCompat21.SharedElementCallback21 createCallback( + SharedElementCallback callback) { + ActivityCompat21.SharedElementCallback21 newCallback = null; + if (callback != null) { + newCallback = new ActivityCompat.SharedElementCallback21Impl(callback); + } + return newCallback; + } + + private static class SharedElementCallback21Impl + extends ActivityCompat21.SharedElementCallback21 { + + private SharedElementCallback mCallback; + + public SharedElementCallback21Impl(SharedElementCallback callback) { + mCallback = callback; + } + + @Override + public void onSharedElementStart(List sharedElementNames, + List sharedElements, List sharedElementSnapshots) { + mCallback.onSharedElementStart(sharedElementNames, sharedElements, + sharedElementSnapshots); + } + + @Override + public void onSharedElementEnd(List sharedElementNames, List sharedElements, + List sharedElementSnapshots) { + mCallback.onSharedElementEnd(sharedElementNames, sharedElements, + sharedElementSnapshots); + } + + @Override + public void onRejectSharedElements(List rejectedSharedElements) { + mCallback.onRejectSharedElements(rejectedSharedElements); + } + + @Override + public void onMapSharedElements(List names, Map sharedElements) { + mCallback.onMapSharedElements(names, sharedElements); + } + + @Override + public Parcelable onCaptureSharedElementSnapshot(View sharedElement, + Matrix viewToGlobalMatrix, RectF screenBounds) { + return mCallback.onCaptureSharedElementSnapshot(sharedElement, viewToGlobalMatrix, + screenBounds); + } + + @Override + public View onCreateSnapshotView(Context context, Parcelable snapshot) { + return mCallback.onCreateSnapshotView(context, snapshot); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat21.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat21.java new file mode 100644 index 00000000000..f690fff0ed2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompat21.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.app.SharedElementCallback; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.os.Parcelable; +import android.view.View; + +import java.lang.Override; +import java.lang.String; +import java.util.List; +import java.util.Map; + +class ActivityCompat21 { + + public static void finishAfterTransition(Activity activity) { + activity.finishAfterTransition(); + } + + public static void setEnterSharedElementCallback(Activity activity, + SharedElementCallback21 callback) { + activity.setEnterSharedElementCallback(createCallback(callback)); + } + + public static void setExitSharedElementCallback(Activity activity, + SharedElementCallback21 callback) { + activity.setExitSharedElementCallback(createCallback(callback)); + } + + public static void postponeEnterTransition(Activity activity) { + activity.postponeEnterTransition(); + } + + public static void startPostponedEnterTransition(Activity activity) { + activity.startPostponedEnterTransition(); + } + + public abstract static class SharedElementCallback21 { + public abstract void onSharedElementStart(List sharedElementNames, + List sharedElements, List sharedElementSnapshots); + + public abstract void onSharedElementEnd(List sharedElementNames, + List sharedElements, List sharedElementSnapshots); + + public abstract void onRejectSharedElements(List rejectedSharedElements); + + public abstract void onMapSharedElements(List names, + Map sharedElements); + public abstract Parcelable onCaptureSharedElementSnapshot(View sharedElement, + Matrix viewToGlobalMatrix, RectF screenBounds); + public abstract View onCreateSnapshotView(Context context, Parcelable snapshot); + } + + private static SharedElementCallback createCallback(SharedElementCallback21 callback) { + SharedElementCallback newListener = null; + if (callback != null) { + newListener = new SharedElementCallbackImpl(callback); + } + return newListener; + } + + private static class SharedElementCallbackImpl extends SharedElementCallback { + private SharedElementCallback21 mCallback; + + public SharedElementCallbackImpl(SharedElementCallback21 callback) { + mCallback = callback; + } + + @Override + public void onSharedElementStart(List sharedElementNames, + List sharedElements, List sharedElementSnapshots) { + mCallback.onSharedElementStart(sharedElementNames, sharedElements, + sharedElementSnapshots); + } + + @Override + public void onSharedElementEnd(List sharedElementNames, List sharedElements, + List sharedElementSnapshots) { + mCallback.onSharedElementEnd(sharedElementNames, sharedElements, + sharedElementSnapshots); + } + + @Override + public void onRejectSharedElements(List rejectedSharedElements) { + mCallback.onRejectSharedElements(rejectedSharedElements); + } + + @Override + public void onMapSharedElements(List names, Map sharedElements) { + mCallback.onMapSharedElements(names, sharedElements); + } + + @Override + public Parcelable onCaptureSharedElementSnapshot(View sharedElement, + Matrix viewToGlobalMatrix, + RectF screenBounds) { + return mCallback.onCaptureSharedElementSnapshot(sharedElement, viewToGlobalMatrix, + screenBounds); + } + + @Override + public View onCreateSnapshotView(Context context, Parcelable snapshot) { + return mCallback.onCreateSnapshotView(context, snapshot); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatHoneycomb.java new file mode 100644 index 00000000000..615bc4415f9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatHoneycomb.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Implementation of activity compatibility that can call Honeycomb APIs. + */ +class ActivityCompatHoneycomb { + static void invalidateOptionsMenu(Activity activity) { + activity.invalidateOptionsMenu(); + } + + static void dump(Activity activity, String prefix, FileDescriptor fd, + PrintWriter writer, String[] args) { + activity.dump(prefix, fd, writer, args); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatJB.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatJB.java new file mode 100644 index 00000000000..0c7318fa290 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityCompatJB.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +class ActivityCompatJB { + public static void startActivity(Context context, Intent intent, Bundle options) { + context.startActivity(intent, options); + } + + public static void startActivityForResult(Activity activity, Intent intent, int requestCode, Bundle options) { + activity.startActivityForResult(intent, requestCode, options); + } + + public static void finishAffinity(Activity activity) { + activity.finishAffinity(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompat.java new file mode 100644 index 00000000000..ff5c18033f9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompat.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.ActivityManager; +import android.os.Build; +import android.support.annotation.NonNull; + +/** + * Helper for accessing features in {@link android.app.ActivityManager} + * introduced after API level 4 in a backwards compatible fashion. + */ +public final class ActivityManagerCompat { + private ActivityManagerCompat() { + } + + /** + * Returns true if this is a low-RAM device. Exactly whether a device is low-RAM + * is ultimately up to the device configuration, but currently it generally means + * something in the class of a 512MB device with about a 800x480 or less screen. + * This is mostly intended to be used by apps to determine whether they should turn + * off certain features that require more RAM. + */ + public static boolean isLowRamDevice(@NonNull ActivityManager am) { + if (Build.VERSION.SDK_INT >= 19) { + return ActivityManagerCompatKitKat.isLowRamDevice(am); + } + return false; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompatKitKat.java new file mode 100644 index 00000000000..3f889f637ba --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityManagerCompatKitKat.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.ActivityManager; + +class ActivityManagerCompatKitKat { + public static boolean isLowRamDevice(ActivityManager am) { + return am.isLowRamDevice(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat.java new file mode 100644 index 00000000000..6eb93c7268b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat.java @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.util.Pair; +import android.view.View; + +/** + * Helper for accessing features in {@link android.app.ActivityOptions} + * introduced in API level 16 in a backwards compatible fashion. + */ +public class ActivityOptionsCompat { + /** + * Create an ActivityOptions specifying a custom animation to run when the + * activity is displayed. + * + * @param context Who is defining this. This is the application that the + * animation resources will be loaded from. + * @param enterResId A resource ID of the animation resource to use for the + * incoming activity. Use 0 for no animation. + * @param exitResId A resource ID of the animation resource to use for the + * outgoing activity. Use 0 for no animation. + * @return Returns a new ActivityOptions object that you can use to supply + * these options as the options Bundle when starting an activity. + */ + public static ActivityOptionsCompat makeCustomAnimation(Context context, + int enterResId, int exitResId) { + if (Build.VERSION.SDK_INT >= 16) { + return new ActivityOptionsImplJB( + ActivityOptionsCompatJB.makeCustomAnimation(context, enterResId, exitResId)); + } + return new ActivityOptionsCompat(); + } + + /** + * Create an ActivityOptions specifying an animation where the new activity is + * scaled from a small originating area of the screen to its final full + * representation. + *

+ * If the Intent this is being used with has not set its + * {@link android.content.Intent#setSourceBounds(android.graphics.Rect)}, + * those bounds will be filled in for you based on the initial bounds passed + * in here. + * + * @param source The View that the new activity is animating from. This + * defines the coordinate space for startX and startY. + * @param startX The x starting location of the new activity, relative to + * source. + * @param startY The y starting location of the activity, relative to source. + * @param startWidth The initial width of the new activity. + * @param startHeight The initial height of the new activity. + * @return Returns a new ActivityOptions object that you can use to supply + * these options as the options Bundle when starting an activity. + */ + public static ActivityOptionsCompat makeScaleUpAnimation(View source, + int startX, int startY, int startWidth, int startHeight) { + if (Build.VERSION.SDK_INT >= 16) { + return new ActivityOptionsImplJB( + ActivityOptionsCompatJB.makeScaleUpAnimation(source, startX, startY, + startWidth, startHeight)); + } + return new ActivityOptionsCompat(); + } + + /** + * Create an ActivityOptions specifying an animation where a thumbnail is + * scaled from a given position to the new activity window that is being + * started. + *

+ * If the Intent this is being used with has not set its + * {@link android.content.Intent#setSourceBounds(android.graphics.Rect)}, + * those bounds will be filled in for you based on the initial thumbnail + * location and size provided here. + * + * @param source The View that this thumbnail is animating from. This + * defines the coordinate space for startX and startY. + * @param thumbnail The bitmap that will be shown as the initial thumbnail + * of the animation. + * @param startX The x starting location of the bitmap, relative to source. + * @param startY The y starting location of the bitmap, relative to source. + * @return Returns a new ActivityOptions object that you can use to supply + * these options as the options Bundle when starting an activity. + */ + public static ActivityOptionsCompat makeThumbnailScaleUpAnimation(View source, + Bitmap thumbnail, int startX, int startY) { + if (Build.VERSION.SDK_INT >= 16) { + return new ActivityOptionsImplJB( + ActivityOptionsCompatJB.makeThumbnailScaleUpAnimation(source, thumbnail, + startX, startY)); + } + return new ActivityOptionsCompat(); + } + + /** + * Create an ActivityOptions to transition between Activities using cross-Activity scene + * animations. This method carries the position of one shared element to the started Activity. + * The position of sharedElement will be used as the epicenter for the + * exit Transition. The position of the shared element in the launched Activity will be the + * epicenter of its entering Transition. + * + *

This requires {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS} to be + * enabled on the calling Activity to cause an exit transition. The same must be in + * the called Activity to get an entering transition.

+ * @param activity The Activity whose window contains the shared elements. + * @param sharedElement The View to transition to the started Activity. sharedElement must + * have a non-null sharedElementName. + * @param sharedElementName The shared element name as used in the target Activity. This may + * be null if it has the same name as sharedElement. + * @return Returns a new ActivityOptions object that you can use to + * supply these options as the options Bundle when starting an activity. + */ + public static ActivityOptionsCompat makeSceneTransitionAnimation(Activity activity, + View sharedElement, String sharedElementName) { + if (Build.VERSION.SDK_INT >= 21) { + return new ActivityOptionsCompat.ActivityOptionsImpl21( + ActivityOptionsCompat21.makeSceneTransitionAnimation(activity, + sharedElement, sharedElementName)); + } + return new ActivityOptionsCompat(); + } + + /** + * Create an ActivityOptions to transition between Activities using cross-Activity scene + * animations. This method carries the position of multiple shared elements to the started + * Activity. The position of the first element in sharedElements + * will be used as the epicenter for the exit Transition. The position of the associated + * shared element in the launched Activity will be the epicenter of its entering Transition. + * + *

This requires {@link android.view.Window#FEATURE_CONTENT_TRANSITIONS} to be + * enabled on the calling Activity to cause an exit transition. The same must be in + * the called Activity to get an entering transition.

+ * @param activity The Activity whose window contains the shared elements. + * @param sharedElements The names of the shared elements to transfer to the called + * Activity and their associated Views. The Views must each have + * a unique shared element name. + * @return Returns a new ActivityOptions object that you can use to + * supply these options as the options Bundle when starting an activity. + */ + public static ActivityOptionsCompat makeSceneTransitionAnimation(Activity activity, + Pair... sharedElements) { + if (Build.VERSION.SDK_INT >= 21) { + View[] views = null; + String[] names = null; + if (sharedElements != null) { + views = new View[sharedElements.length]; + names = new String[sharedElements.length]; + for (int i = 0; i < sharedElements.length; i++) { + views[i] = sharedElements[i].first; + names[i] = sharedElements[i].second; + } + } + return new ActivityOptionsCompat.ActivityOptionsImpl21( + ActivityOptionsCompat21.makeSceneTransitionAnimation(activity, views, names)); + } + return new ActivityOptionsCompat(); + } + + private static class ActivityOptionsImplJB extends ActivityOptionsCompat { + private final ActivityOptionsCompatJB mImpl; + + ActivityOptionsImplJB(ActivityOptionsCompatJB impl) { + mImpl = impl; + } + + @Override + public Bundle toBundle() { + return mImpl.toBundle(); + } + + @Override + public void update(ActivityOptionsCompat otherOptions) { + if (otherOptions instanceof ActivityOptionsImplJB) { + ActivityOptionsImplJB otherImpl = (ActivityOptionsImplJB)otherOptions; + mImpl.update(otherImpl.mImpl); + } + } + } + + private static class ActivityOptionsImpl21 extends ActivityOptionsCompat { + private final ActivityOptionsCompat21 mImpl; + + ActivityOptionsImpl21(ActivityOptionsCompat21 impl) { + mImpl = impl; + } + + @Override + public Bundle toBundle() { + return mImpl.toBundle(); + } + + @Override + public void update(ActivityOptionsCompat otherOptions) { + if (otherOptions instanceof ActivityOptionsCompat.ActivityOptionsImpl21) { + ActivityOptionsCompat.ActivityOptionsImpl21 + otherImpl = (ActivityOptionsCompat.ActivityOptionsImpl21)otherOptions; + mImpl.update(otherImpl.mImpl); + } + } + } + + protected ActivityOptionsCompat() { + } + + /** + * Returns the created options as a Bundle, which can be passed to + * {@link ActivityCompat#startActivity(android.app.Activity, android.content.Intent, android.os.Bundle)}. + * Note that the returned Bundle is still owned by the ActivityOptions + * object; you must not modify it, but can supply it to the startActivity + * methods that take an options Bundle. + */ + public Bundle toBundle() { + return null; + } + + /** + * Update the current values in this ActivityOptions from those supplied in + * otherOptions. Any values defined in otherOptions replace those in the + * base options. + */ + public void update(ActivityOptionsCompat otherOptions) { + // Do nothing. + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat21.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat21.java new file mode 100644 index 00000000000..d629e653e14 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompat21.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.ActivityOptions; +import android.app.Activity; +import android.os.Bundle; +import android.util.Pair; +import android.view.View; + +class ActivityOptionsCompat21 { + + private final ActivityOptions mActivityOptions; + + public static ActivityOptionsCompat21 makeSceneTransitionAnimation(Activity activity, + View sharedElement, String sharedElementName) { + return new ActivityOptionsCompat21( + ActivityOptions.makeSceneTransitionAnimation(activity, sharedElement, + sharedElementName)); + } + + public static ActivityOptionsCompat21 makeSceneTransitionAnimation(Activity activity, + View[] sharedElements, String[] sharedElementNames) { + Pair[] pairs = null; + if (sharedElements != null) { + pairs = new Pair[sharedElements.length]; + for (int i = 0; i < pairs.length; i++) { + pairs[i] = Pair.create(sharedElements[i], sharedElementNames[i]); + } + } + return new ActivityOptionsCompat21( + ActivityOptions.makeSceneTransitionAnimation(activity, pairs)); + } + + private ActivityOptionsCompat21(ActivityOptions activityOptions) { + mActivityOptions = activityOptions; + } + + public Bundle toBundle() { + return mActivityOptions.toBundle(); + } + + public void update(ActivityOptionsCompat21 otherOptions) { + mActivityOptions.update(otherOptions.mActivityOptions); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompatJB.java b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompatJB.java new file mode 100644 index 00000000000..df9d9879582 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ActivityOptionsCompatJB.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.ActivityOptions; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.view.View; + +class ActivityOptionsCompatJB { + + public static ActivityOptionsCompatJB makeCustomAnimation(Context context, + int enterResId, int exitResId) { + return new ActivityOptionsCompatJB( + ActivityOptions.makeCustomAnimation(context, enterResId, exitResId)); + } + + public static ActivityOptionsCompatJB makeScaleUpAnimation(View source, + int startX, int startY, int startWidth, int startHeight) { + return new ActivityOptionsCompatJB( + ActivityOptions.makeScaleUpAnimation(source, startX, startY, startWidth, startHeight)); + } + + public static ActivityOptionsCompatJB makeThumbnailScaleUpAnimation(View source, + Bitmap thumbnail, int startX, int startY) { + return new ActivityOptionsCompatJB( + ActivityOptions.makeThumbnailScaleUpAnimation(source, thumbnail, startX, startY)); + } + + private final ActivityOptions mActivityOptions; + + private ActivityOptionsCompatJB(ActivityOptions activityOptions) { + mActivityOptions = activityOptions; + } + + public Bundle toBundle() { + return mActivityOptions.toBundle(); + } + + public void update(ActivityOptionsCompatJB otherOptions) { + mActivityOptions.update(otherOptions.mActivityOptions); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/BackStackRecord.java b/mobile/google-android-support-v4/src/android/support/v4/app/BackStackRecord.java new file mode 100644 index 00000000000..25f3ebfba06 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/BackStackRecord.java @@ -0,0 +1,1457 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.graphics.Rect; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.util.LogWriter; +import android.support.v4.util.Pair; +import android.support.v4.util.ArrayMap; +import android.text.TextUtils; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; + +final class BackStackState implements Parcelable { + final int[] mOps; + final int mTransition; + final int mTransitionStyle; + final String mName; + final int mIndex; + final int mBreadCrumbTitleRes; + final CharSequence mBreadCrumbTitleText; + final int mBreadCrumbShortTitleRes; + final CharSequence mBreadCrumbShortTitleText; + final ArrayList mSharedElementSourceNames; + final ArrayList mSharedElementTargetNames; + + public BackStackState(FragmentManagerImpl fm, BackStackRecord bse) { + int numRemoved = 0; + BackStackRecord.Op op = bse.mHead; + while (op != null) { + if (op.removed != null) numRemoved += op.removed.size(); + op = op.next; + } + mOps = new int[bse.mNumOp*7 + numRemoved]; + + if (!bse.mAddToBackStack) { + throw new IllegalStateException("Not on back stack"); + } + + op = bse.mHead; + int pos = 0; + while (op != null) { + mOps[pos++] = op.cmd; + mOps[pos++] = op.fragment != null ? op.fragment.mIndex : -1; + mOps[pos++] = op.enterAnim; + mOps[pos++] = op.exitAnim; + mOps[pos++] = op.popEnterAnim; + mOps[pos++] = op.popExitAnim; + if (op.removed != null) { + final int N = op.removed.size(); + mOps[pos++] = N; + for (int i=0; i= 0) { + Fragment f = fm.mActive.get(findex); + op.fragment = f; + } else { + op.fragment = null; + } + op.enterAnim = mOps[pos++]; + op.exitAnim = mOps[pos++]; + op.popEnterAnim = mOps[pos++]; + op.popExitAnim = mOps[pos++]; + final int N = mOps[pos++]; + if (N > 0) { + op.removed = new ArrayList(N); + for (int i=0; i CREATOR + = new Parcelable.Creator() { + public BackStackState createFromParcel(Parcel in) { + return new BackStackState(in); + } + + public BackStackState[] newArray(int size) { + return new BackStackState[size]; + } + }; +} + +/** + * @hide Entry of an operation on the fragment back stack. + */ +final class BackStackRecord extends FragmentTransaction implements + FragmentManager.BackStackEntry, Runnable { + static final String TAG = FragmentManagerImpl.TAG; + static final boolean SUPPORTS_TRANSITIONS = Build.VERSION.SDK_INT >= 21; + + final FragmentManagerImpl mManager; + + static final int OP_NULL = 0; + static final int OP_ADD = 1; + static final int OP_REPLACE = 2; + static final int OP_REMOVE = 3; + static final int OP_HIDE = 4; + static final int OP_SHOW = 5; + static final int OP_DETACH = 6; + static final int OP_ATTACH = 7; + + static final class Op { + Op next; + Op prev; + int cmd; + Fragment fragment; + int enterAnim; + int exitAnim; + int popEnterAnim; + int popExitAnim; + ArrayList removed; + } + + Op mHead; + Op mTail; + int mNumOp; + int mEnterAnim; + int mExitAnim; + int mPopEnterAnim; + int mPopExitAnim; + int mTransition; + int mTransitionStyle; + boolean mAddToBackStack; + boolean mAllowAddToBackStack = true; + String mName; + boolean mCommitted; + int mIndex = -1; + + int mBreadCrumbTitleRes; + CharSequence mBreadCrumbTitleText; + int mBreadCrumbShortTitleRes; + CharSequence mBreadCrumbShortTitleText; + + ArrayList mSharedElementSourceNames; + ArrayList mSharedElementTargetNames; + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("BackStackEntry{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + if (mIndex >= 0) { + sb.append(" #"); + sb.append(mIndex); + } + if (mName != null) { + sb.append(" "); + sb.append(mName); + } + sb.append("}"); + return sb.toString(); + } + + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + dump(prefix, writer, true); + } + + public void dump(String prefix, PrintWriter writer, boolean full) { + if (full) { + writer.print(prefix); writer.print("mName="); writer.print(mName); + writer.print(" mIndex="); writer.print(mIndex); + writer.print(" mCommitted="); writer.println(mCommitted); + if (mTransition != FragmentTransaction.TRANSIT_NONE) { + writer.print(prefix); writer.print("mTransition=#"); + writer.print(Integer.toHexString(mTransition)); + writer.print(" mTransitionStyle=#"); + writer.println(Integer.toHexString(mTransitionStyle)); + } + if (mEnterAnim != 0 || mExitAnim !=0) { + writer.print(prefix); writer.print("mEnterAnim=#"); + writer.print(Integer.toHexString(mEnterAnim)); + writer.print(" mExitAnim=#"); + writer.println(Integer.toHexString(mExitAnim)); + } + if (mPopEnterAnim != 0 || mPopExitAnim !=0) { + writer.print(prefix); writer.print("mPopEnterAnim=#"); + writer.print(Integer.toHexString(mPopEnterAnim)); + writer.print(" mPopExitAnim=#"); + writer.println(Integer.toHexString(mPopExitAnim)); + } + if (mBreadCrumbTitleRes != 0 || mBreadCrumbTitleText != null) { + writer.print(prefix); writer.print("mBreadCrumbTitleRes=#"); + writer.print(Integer.toHexString(mBreadCrumbTitleRes)); + writer.print(" mBreadCrumbTitleText="); + writer.println(mBreadCrumbTitleText); + } + if (mBreadCrumbShortTitleRes != 0 || mBreadCrumbShortTitleText != null) { + writer.print(prefix); writer.print("mBreadCrumbShortTitleRes=#"); + writer.print(Integer.toHexString(mBreadCrumbShortTitleRes)); + writer.print(" mBreadCrumbShortTitleText="); + writer.println(mBreadCrumbShortTitleText); + } + } + + if (mHead != null) { + writer.print(prefix); writer.println("Operations:"); + String innerPrefix = prefix + " "; + Op op = mHead; + int num = 0; + while (op != null) { + String cmdStr; + switch (op.cmd) { + case OP_NULL: cmdStr="NULL"; break; + case OP_ADD: cmdStr="ADD"; break; + case OP_REPLACE: cmdStr="REPLACE"; break; + case OP_REMOVE: cmdStr="REMOVE"; break; + case OP_HIDE: cmdStr="HIDE"; break; + case OP_SHOW: cmdStr="SHOW"; break; + case OP_DETACH: cmdStr="DETACH"; break; + case OP_ATTACH: cmdStr="ATTACH"; break; + default: cmdStr="cmd=" + op.cmd; break; + } + writer.print(prefix); writer.print(" Op #"); writer.print(num); + writer.print(": "); writer.print(cmdStr); + writer.print(" "); writer.println(op.fragment); + if (full) { + if (op.enterAnim != 0 || op.exitAnim != 0) { + writer.print(prefix); writer.print("enterAnim=#"); + writer.print(Integer.toHexString(op.enterAnim)); + writer.print(" exitAnim=#"); + writer.println(Integer.toHexString(op.exitAnim)); + } + if (op.popEnterAnim != 0 || op.popExitAnim != 0) { + writer.print(prefix); writer.print("popEnterAnim=#"); + writer.print(Integer.toHexString(op.popEnterAnim)); + writer.print(" popExitAnim=#"); + writer.println(Integer.toHexString(op.popExitAnim)); + } + } + if (op.removed != null && op.removed.size() > 0) { + for (int i=0; i(); + mSharedElementTargetNames = new ArrayList(); + } + + mSharedElementSourceNames.add(transitionName); + mSharedElementTargetNames.add(name); + } + return this; + } + + public FragmentTransaction setTransitionStyle(int styleRes) { + mTransitionStyle = styleRes; + return this; + } + + public FragmentTransaction addToBackStack(String name) { + if (!mAllowAddToBackStack) { + throw new IllegalStateException( + "This FragmentTransaction is not allowed to be added to the back stack."); + } + mAddToBackStack = true; + mName = name; + return this; + } + + public boolean isAddToBackStackAllowed() { + return mAllowAddToBackStack; + } + + public FragmentTransaction disallowAddToBackStack() { + if (mAddToBackStack) { + throw new IllegalStateException( + "This transaction is already being added to the back stack"); + } + mAllowAddToBackStack = false; + return this; + } + + public FragmentTransaction setBreadCrumbTitle(int res) { + mBreadCrumbTitleRes = res; + mBreadCrumbTitleText = null; + return this; + } + + public FragmentTransaction setBreadCrumbTitle(CharSequence text) { + mBreadCrumbTitleRes = 0; + mBreadCrumbTitleText = text; + return this; + } + + public FragmentTransaction setBreadCrumbShortTitle(int res) { + mBreadCrumbShortTitleRes = res; + mBreadCrumbShortTitleText = null; + return this; + } + + public FragmentTransaction setBreadCrumbShortTitle(CharSequence text) { + mBreadCrumbShortTitleRes = 0; + mBreadCrumbShortTitleText = text; + return this; + } + + void bumpBackStackNesting(int amt) { + if (!mAddToBackStack) { + return; + } + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting in " + this + + " by " + amt); + Op op = mHead; + while (op != null) { + if (op.fragment != null) { + op.fragment.mBackStackNesting += amt; + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of " + + op.fragment + " to " + op.fragment.mBackStackNesting); + } + if (op.removed != null) { + for (int i=op.removed.size()-1; i>=0; i--) { + Fragment r = op.removed.get(i); + r.mBackStackNesting += amt; + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Bump nesting of " + + r + " to " + r.mBackStackNesting); + } + } + op = op.next; + } + } + + public int commit() { + return commitInternal(false); + } + + public int commitAllowingStateLoss() { + return commitInternal(true); + } + + int commitInternal(boolean allowStateLoss) { + if (mCommitted) throw new IllegalStateException("commit already called"); + if (FragmentManagerImpl.DEBUG) { + Log.v(TAG, "Commit: " + this); + LogWriter logw = new LogWriter(TAG); + PrintWriter pw = new PrintWriter(logw); + dump(" ", null, pw, null); + } + mCommitted = true; + if (mAddToBackStack) { + mIndex = mManager.allocBackStackIndex(this); + } else { + mIndex = -1; + } + mManager.enqueueAction(this, allowStateLoss); + return mIndex; + } + + public void run() { + if (FragmentManagerImpl.DEBUG) Log.v(TAG, "Run: " + this); + + if (mAddToBackStack) { + if (mIndex < 0) { + throw new IllegalStateException("addToBackStack() called after commit()"); + } + } + + bumpBackStackNesting(1); + + TransitionState state = null; + SparseArray firstOutFragments = null; + SparseArray lastInFragments = null; + if (SUPPORTS_TRANSITIONS) { + firstOutFragments = new SparseArray(); + lastInFragments = new SparseArray(); + + calculateFragments(firstOutFragments, lastInFragments); + + state = beginTransition(firstOutFragments, lastInFragments, false); + } + + int transitionStyle = state != null ? 0 : mTransitionStyle; + int transition = state != null ? 0 : mTransition; + Op op = mHead; + while (op != null) { + int enterAnim = state != null ? 0 : op.enterAnim; + int exitAnim = state != null ? 0 : op.exitAnim; + switch (op.cmd) { + case OP_ADD: { + Fragment f = op.fragment; + f.mNextAnim = enterAnim; + mManager.addFragment(f, false); + } break; + case OP_REPLACE: { + Fragment f = op.fragment; + if (mManager.mAdded != null) { + for (int i=0; i fragments, Fragment fragment) { + if (fragment != null) { + int containerId = fragment.mContainerId; + if (containerId != 0 && !fragment.isHidden() && fragment.isAdded() && + fragment.getView() != null && fragments.get(containerId) == null) { + fragments.put(containerId, fragment); + } + } + } + + private void setLastIn(SparseArray fragments, Fragment fragment) { + if (fragment != null) { + int containerId = fragment.mContainerId; + if (containerId != 0) { + fragments.put(containerId, fragment); + } + } + } + + /** + * Finds the first removed fragment and last added fragments when going forward. + * If none of the fragments have transitions, then both lists will be empty. + * + * @param firstOutFragments The list of first fragments to be removed, keyed on the + * container ID. This list will be modified by the method. + * @param lastInFragments The list of last fragments to be added, keyed on the + * container ID. This list will be modified by the method. + */ + private void calculateFragments(SparseArray firstOutFragments, + SparseArray lastInFragments) { + if (!mManager.mContainer.hasView()) { + return; // nothing to see, so no transitions + } + Op op = mHead; + while (op != null) { + switch (op.cmd) { + case OP_ADD: + setLastIn(lastInFragments, op.fragment); + break; + case OP_REPLACE: { + Fragment f = op.fragment; + if (mManager.mAdded != null) { + for (int i = 0; i < mManager.mAdded.size(); i++) { + Fragment old = mManager.mAdded.get(i); + if (f == null || old.mContainerId == f.mContainerId) { + if (old == f) { + f = null; + } else { + setFirstOut(firstOutFragments, old); + } + } + } + } + setLastIn(lastInFragments, f); + break; + } + case OP_REMOVE: + setFirstOut(firstOutFragments, op.fragment); + break; + case OP_HIDE: + setFirstOut(firstOutFragments, op.fragment); + break; + case OP_SHOW: + setLastIn(lastInFragments, op.fragment); + break; + case OP_DETACH: + setFirstOut(firstOutFragments, op.fragment); + break; + case OP_ATTACH: + setLastIn(lastInFragments, op.fragment); + break; + } + + op = op.next; + } + } + + /** + * Finds the first removed fragment and last added fragments when popping the back stack. + * If none of the fragments have transitions, then both lists will be empty. + * + * @param firstOutFragments The list of first fragments to be removed, keyed on the + * container ID. This list will be modified by the method. + * @param lastInFragments The list of last fragments to be added, keyed on the + * container ID. This list will be modified by the method. + */ + public void calculateBackFragments(SparseArray firstOutFragments, + SparseArray lastInFragments) { + if (!mManager.mContainer.hasView()) { + return; // nothing to see, so no transitions + } + Op op = mHead; + while (op != null) { + switch (op.cmd) { + case OP_ADD: + setFirstOut(firstOutFragments, op.fragment); + break; + case OP_REPLACE: + if (op.removed != null) { + for (int i = op.removed.size() - 1; i >= 0; i--) { + setLastIn(lastInFragments, op.removed.get(i)); + } + } + setFirstOut(firstOutFragments, op.fragment); + break; + case OP_REMOVE: + setLastIn(lastInFragments, op.fragment); + break; + case OP_HIDE: + setLastIn(lastInFragments, op.fragment); + break; + case OP_SHOW: + setFirstOut(firstOutFragments, op.fragment); + break; + case OP_DETACH: + setLastIn(lastInFragments, op.fragment); + break; + case OP_ATTACH: + setFirstOut(firstOutFragments, op.fragment); + break; + } + + op = op.next; + } + } + + public TransitionState popFromBackStack(boolean doStateMove, TransitionState state, + SparseArray firstOutFragments, SparseArray lastInFragments) { + if (FragmentManagerImpl.DEBUG) { + Log.v(TAG, "popFromBackStack: " + this); + LogWriter logw = new LogWriter(TAG); + PrintWriter pw = new PrintWriter(logw); + dump(" ", null, pw, null); + } + + if (SUPPORTS_TRANSITIONS) { + if (state == null) { + if (firstOutFragments.size() != 0 || lastInFragments.size() != 0) { + state = beginTransition(firstOutFragments, lastInFragments, true); + } + } else if (!doStateMove) { + setNameOverrides(state, mSharedElementTargetNames, mSharedElementSourceNames); + } + } + + bumpBackStackNesting(-1); + + int transitionStyle = state != null ? 0 : mTransitionStyle; + int transition = state != null ? 0 : mTransition; + Op op = mTail; + while (op != null) { + int popEnterAnim = state != null ? 0 : op.popEnterAnim; + int popExitAnim= state != null ? 0 : op.popExitAnim; + switch (op.cmd) { + case OP_ADD: { + Fragment f = op.fragment; + f.mNextAnim = popExitAnim; + mManager.removeFragment(f, + FragmentManagerImpl.reverseTransit(transition), transitionStyle); + } break; + case OP_REPLACE: { + Fragment f = op.fragment; + if (f != null) { + f.mNextAnim = popExitAnim; + mManager.removeFragment(f, + FragmentManagerImpl.reverseTransit(transition), transitionStyle); + } + if (op.removed != null) { + for (int i=0; i= 0) { + mManager.freeBackStackIndex(mIndex); + mIndex = -1; + } + return state; + } + + public String getName() { + return mName; + } + + public int getTransition() { + return mTransition; + } + + public int getTransitionStyle() { + return mTransitionStyle; + } + + public boolean isEmpty() { + return mNumOp == 0; + } + + /** + * When custom fragment transitions are used, this sets up the state for each transition + * and begins the transition. A different transition is started for each fragment container + * and consists of up to 3 different transitions: the exit transition, a shared element + * transition and an enter transition. + * + *

The exit transition operates against the leaf nodes of the first fragment + * with a view that was removed. If no such fragment was removed, then no exit + * transition is executed. The exit transition comes from the outgoing fragment.

+ * + *

The enter transition operates against the last fragment that was added. If + * that fragment does not have a view or no fragment was added, then no enter + * transition is executed. The enter transition comes from the incoming fragment.

+ * + *

The shared element transition operates against all views and comes either + * from the outgoing fragment or the incoming fragment, depending on whether this + * is going forward or popping the back stack. When going forward, the incoming + * fragment's enter shared element transition is used, but when going back, the + * outgoing fragment's return shared element transition is used. Shared element + * transitions only operate if there is both an incoming and outgoing fragment.

+ * + * @param firstOutFragments The list of first fragments to be removed, keyed on the + * container ID. + * @param lastInFragments The list of last fragments to be added, keyed on the + * container ID. + * @param isBack true if this is popping the back stack or false if this is a + * forward operation. + * @return The TransitionState used to complete the operation of the transition + * in {@link #setNameOverrides(BackStackRecord.TransitionState, java.util.ArrayList, + * java.util.ArrayList)}. + */ + private TransitionState beginTransition(SparseArray firstOutFragments, + SparseArray lastInFragments, boolean isBack) { + TransitionState state = new TransitionState(); + + // Adding a non-existent target view makes sure that the transitions don't target + // any views by default. They'll only target the views we tell add. If we don't + // add any, then no views will be targeted. + state.nonExistentView = new View(mManager.mActivity); + + boolean anyTransitionStarted = false; + // Go over all leaving fragments. + for (int i = 0; i < firstOutFragments.size(); i++) { + int containerId = firstOutFragments.keyAt(i); + if (configureTransitions(containerId, state, isBack, firstOutFragments, + lastInFragments)) { + anyTransitionStarted = true; + } + } + + // Now go over all entering fragments that didn't have a leaving fragment. + for (int i = 0; i < lastInFragments.size(); i++) { + int containerId = lastInFragments.keyAt(i); + if (firstOutFragments.get(containerId) == null && + configureTransitions(containerId, state, isBack, firstOutFragments, + lastInFragments)) { + anyTransitionStarted = true; + } + } + + if (!anyTransitionStarted) { + state = null; + } + + return state; + } + + private static Object getEnterTransition(Fragment inFragment, boolean isBack) { + if (inFragment == null) { + return null; + } + return FragmentTransitionCompat21.cloneTransition(isBack ? + inFragment.getReenterTransition() : inFragment.getEnterTransition()); + } + + private static Object getExitTransition(Fragment outFragment, boolean isBack) { + if (outFragment == null) { + return null; + } + return FragmentTransitionCompat21.cloneTransition(isBack ? + outFragment.getReturnTransition() : outFragment.getExitTransition()); + } + + private static Object getSharedElementTransition(Fragment inFragment, Fragment outFragment, + boolean isBack) { + if (inFragment == null || outFragment == null) { + return null; + } + return FragmentTransitionCompat21.cloneTransition(isBack ? + outFragment.getSharedElementReturnTransition() : + inFragment.getSharedElementEnterTransition()); + } + + private static Object captureExitingViews(Object exitTransition, Fragment outFragment, + ArrayList exitingViews, ArrayMap namedViews, View nonExistentView) { + if (exitTransition != null) { + exitTransition = FragmentTransitionCompat21.captureExitingViews(exitTransition, + outFragment.getView(), exitingViews, namedViews, nonExistentView); + } + return exitTransition; + } + + private ArrayMap remapSharedElements(TransitionState state, Fragment outFragment, + boolean isBack) { + ArrayMap namedViews = new ArrayMap(); + if (mSharedElementSourceNames != null) { + FragmentTransitionCompat21.findNamedViews(namedViews, outFragment.getView()); + if (isBack) { + namedViews.retainAll(mSharedElementTargetNames); + } else { + namedViews = remapNames(mSharedElementSourceNames, mSharedElementTargetNames, + namedViews); + } + } + + if (isBack) { + if (outFragment.mEnterTransitionCallback != null) { + outFragment.mEnterTransitionCallback.onMapSharedElements( + mSharedElementTargetNames, namedViews); + } + setBackNameOverrides(state, namedViews, false); + } else { + if (outFragment.mExitTransitionCallback != null) { + outFragment.mExitTransitionCallback.onMapSharedElements( + mSharedElementTargetNames, namedViews); + } + setNameOverrides(state, namedViews, false); + } + + return namedViews; + } + + /** + * Configures custom transitions for a specific fragment container. + * + * @param containerId The container ID of the fragments to configure the transition for. + * @param state The Transition State keeping track of the executing transitions. + * @param firstOutFragments The list of first fragments to be removed, keyed on the + * container ID. + * @param lastInFragments The list of last fragments to be added, keyed on the + * container ID. + * @param isBack true if this is popping the back stack or false if this is a + * forward operation. + */ + private boolean configureTransitions(int containerId, TransitionState state, boolean isBack, + SparseArray firstOutFragments, SparseArray lastInFragments) { + ViewGroup sceneRoot = (ViewGroup) mManager.mContainer.findViewById(containerId); + if (sceneRoot == null) { + return false; + } + final Fragment inFragment = lastInFragments.get(containerId); + Fragment outFragment = firstOutFragments.get(containerId); + + Object enterTransition = getEnterTransition(inFragment, isBack); + Object sharedElementTransition = getSharedElementTransition(inFragment, outFragment, + isBack); + Object exitTransition = getExitTransition(outFragment, isBack); + if (enterTransition == null && sharedElementTransition == null && + exitTransition == null) { + return false; // no transitions! + } + ArrayMap namedViews = null; + ArrayList sharedElementTargets = new ArrayList(); + if (sharedElementTransition != null) { + namedViews = remapSharedElements(state, outFragment, isBack); + sharedElementTargets.add(state.nonExistentView); + sharedElementTargets.addAll(namedViews.values()); + + // Notify the start of the transition. + SharedElementCallback callback = isBack ? + outFragment.mEnterTransitionCallback : + inFragment.mEnterTransitionCallback; + if (callback != null) { + ArrayList names = new ArrayList(namedViews.keySet()); + ArrayList views = new ArrayList(namedViews.values()); + callback.onSharedElementStart(names, views, null); + } + } + + ArrayList exitingViews = new ArrayList(); + exitTransition = captureExitingViews(exitTransition, outFragment, exitingViews, + namedViews, state.nonExistentView); + + // Set the epicenter of the exit transition + if (mSharedElementTargetNames != null && namedViews != null) { + View epicenterView = namedViews.get(mSharedElementTargetNames.get(0)); + if (epicenterView != null) { + if (exitTransition != null) { + FragmentTransitionCompat21.setEpicenter(exitTransition, epicenterView); + } + if (sharedElementTransition != null) { + FragmentTransitionCompat21.setEpicenter(sharedElementTransition, + epicenterView); + } + } + } + + FragmentTransitionCompat21.ViewRetriever viewRetriever = + new FragmentTransitionCompat21.ViewRetriever() { + @Override + public View getView() { + return inFragment.getView(); + } + }; + + if (sharedElementTransition != null) { + prepareSharedElementTransition(state, sceneRoot, sharedElementTransition, + inFragment, outFragment, isBack, sharedElementTargets); + } + + ArrayList enteringViews = new ArrayList(); + ArrayMap renamedViews = new ArrayMap(); + + boolean allowOverlap = isBack ? inFragment.getAllowReturnTransitionOverlap() : + inFragment.getAllowEnterTransitionOverlap(); + Object transition = FragmentTransitionCompat21.mergeTransitions(enterTransition, + exitTransition, sharedElementTransition, allowOverlap); + + if (transition != null) { + FragmentTransitionCompat21.addTransitionTargets(enterTransition, + sharedElementTransition, sceneRoot, viewRetriever, state.nonExistentView, + state.enteringEpicenterView, state.nameOverrides, enteringViews, + renamedViews, sharedElementTargets); + excludeHiddenFragmentsAfterEnter(sceneRoot, state, containerId, transition); + + // We want to exclude hidden views later, so we need a non-null list in the + // transition now. + FragmentTransitionCompat21.excludeTarget(transition, state.nonExistentView, true); + // Now exclude all currently hidden fragments. + excludeHiddenFragments(state, containerId, transition); + + FragmentTransitionCompat21.beginDelayedTransition(sceneRoot, transition); + + FragmentTransitionCompat21.cleanupTransitions(sceneRoot, state.nonExistentView, + enterTransition, enteringViews, exitTransition, exitingViews, + sharedElementTransition, sharedElementTargets, + transition, state.hiddenFragmentViews, renamedViews); + } + return transition != null; + } + + private void prepareSharedElementTransition(final TransitionState state, final View sceneRoot, + final Object sharedElementTransition, final Fragment inFragment, + final Fragment outFragment, final boolean isBack, + final ArrayList sharedElementTargets) { + sceneRoot.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); + + if (sharedElementTransition != null) { + FragmentTransitionCompat21.removeTargets(sharedElementTransition, + sharedElementTargets); + sharedElementTargets.clear(); + + ArrayMap namedViews = mapSharedElementsIn( + state, isBack, inFragment); + sharedElementTargets.add(state.nonExistentView); + sharedElementTargets.addAll(namedViews.values()); + FragmentTransitionCompat21.addTargets(sharedElementTransition, + sharedElementTargets); + + setEpicenterIn(namedViews, state); + + callSharedElementEnd(state, inFragment, outFragment, isBack, + namedViews); + } + + return true; + } + }); + } + + private void callSharedElementEnd(TransitionState state, Fragment inFragment, + Fragment outFragment, boolean isBack, ArrayMap namedViews) { + SharedElementCallback sharedElementCallback = isBack ? + outFragment.mEnterTransitionCallback : + inFragment.mEnterTransitionCallback; + if (sharedElementCallback != null) { + ArrayList names = new ArrayList(namedViews.keySet()); + ArrayList views = new ArrayList(namedViews.values()); + sharedElementCallback.onSharedElementEnd(names, views, null); + } + } + + private void setEpicenterIn(ArrayMap namedViews, TransitionState state) { + if (mSharedElementTargetNames != null && !namedViews.isEmpty()) { + // now we know the epicenter of the entering transition. + View epicenter = namedViews + .get(mSharedElementTargetNames.get(0)); + if (epicenter != null) { + state.enteringEpicenterView.epicenter = epicenter; + } + } + } + + private ArrayMap mapSharedElementsIn(TransitionState state, + boolean isBack, Fragment inFragment) { + // Now map the shared elements in the incoming fragment + ArrayMap namedViews = mapEnteringSharedElements(state, inFragment, isBack); + + // remap shared elements and set the name mapping used + // in the shared element transition. + if (isBack) { + if (inFragment.mExitTransitionCallback != null) { + inFragment.mExitTransitionCallback.onMapSharedElements( + mSharedElementTargetNames, namedViews); + } + setBackNameOverrides(state, namedViews, true); + } else { + if (inFragment.mEnterTransitionCallback != null) { + inFragment.mEnterTransitionCallback.onMapSharedElements( + mSharedElementTargetNames, namedViews); + } + setNameOverrides(state, namedViews, true); + } + return namedViews; + } + + /** + * Remaps a name-to-View map, substituting different names for keys. + * + * @param inMap A list of keys found in the map, in the order in toGoInMap + * @param toGoInMap A list of keys to use for the new map, in the order of inMap + * @param namedViews The current mapping + * @return A copy of namedViews with the keys coming from toGoInMap. + */ + private static ArrayMap remapNames(ArrayList inMap, + ArrayList toGoInMap, ArrayMap namedViews) { + if (namedViews.isEmpty()) { + return namedViews; + } + ArrayMap remappedViews = new ArrayMap(); + int numKeys = inMap.size(); + for (int i = 0; i < numKeys; i++) { + View view = namedViews.get(inMap.get(i)); + if (view != null) { + remappedViews.put(toGoInMap.get(i), view); + } + } + return remappedViews; + } + + /** + * Maps shared elements to views in the entering fragment. + * + * @param state The transition State as returned from {@link #beginTransition( + * android.util.SparseArray, android.util.SparseArray, boolean)}. + * @param inFragment The last fragment to be added. + * @param isBack true if this is popping the back stack or false if this is a + * forward operation. + */ + private ArrayMap mapEnteringSharedElements(TransitionState state, + Fragment inFragment, boolean isBack) { + ArrayMap namedViews = new ArrayMap(); + View root = inFragment.getView(); + if (root != null) { + if (mSharedElementSourceNames != null) { + FragmentTransitionCompat21.findNamedViews(namedViews, root); + if (isBack) { + namedViews = remapNames(mSharedElementSourceNames, + mSharedElementTargetNames, namedViews); + } else { + namedViews.retainAll(mSharedElementTargetNames); + } + } + } + return namedViews; + } + + private void excludeHiddenFragmentsAfterEnter(final View sceneRoot, final TransitionState state, + final int containerId, final Object transition) { + sceneRoot.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + public boolean onPreDraw() { + sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); + excludeHiddenFragments(state, containerId, transition); + return true; + } + }); + } + + private void excludeHiddenFragments(TransitionState state, int containerId, Object transition) { + if (mManager.mAdded != null) { + for (int i = 0; i < mManager.mAdded.size(); i++) { + Fragment fragment = mManager.mAdded.get(i); + if (fragment.mView != null && fragment.mContainer != null && + fragment.mContainerId == containerId) { + if (fragment.mHidden) { + if (!state.hiddenFragmentViews.contains(fragment.mView)) { + FragmentTransitionCompat21.excludeTarget(transition, fragment.mView, + true); + state.hiddenFragmentViews.add(fragment.mView); + } + } else { + FragmentTransitionCompat21.excludeTarget(transition, fragment.mView, + false); + state.hiddenFragmentViews.remove(fragment.mView); + } + } + } + } + } + + private static void setNameOverride(ArrayMap overrides, + String source, String target) { + if (source != null && target != null && !source.equals(target)) { + for (int index = 0; index < overrides.size(); index++) { + if (source.equals(overrides.valueAt(index))) { + overrides.setValueAt(index, target); + return; + } + } + overrides.put(source, target); + } + } + + private static void setNameOverrides(TransitionState state, ArrayList sourceNames, + ArrayList targetNames) { + if (sourceNames != null) { + for (int i = 0; i < sourceNames.size(); i++) { + String source = sourceNames.get(i); + String target = targetNames.get(i); + setNameOverride(state.nameOverrides, source, target); + } + } + } + + private void setBackNameOverrides(TransitionState state, ArrayMap namedViews, + boolean isEnd) { + int count = mSharedElementTargetNames == null ? 0 : mSharedElementTargetNames.size(); + for (int i = 0; i < count; i++) { + String source = mSharedElementSourceNames.get(i); + String originalTarget = mSharedElementTargetNames.get(i); + View view = namedViews.get(originalTarget); + if (view != null) { + String target = FragmentTransitionCompat21.getTransitionName(view); + if (isEnd) { + setNameOverride(state.nameOverrides, source, target); + } else { + setNameOverride(state.nameOverrides, target, source); + } + } + } + } + + private void setNameOverrides(TransitionState state, ArrayMap namedViews, + boolean isEnd) { + int count = namedViews.size(); + for (int i = 0; i < count; i++) { + String source = namedViews.keyAt(i); + String target = FragmentTransitionCompat21.getTransitionName(namedViews.valueAt(i)); + if (isEnd) { + setNameOverride(state.nameOverrides, source, target); + } else { + setNameOverride(state.nameOverrides, target, source); + } + } + } + + public class TransitionState { + public ArrayMap nameOverrides = new ArrayMap(); + public ArrayList hiddenFragmentViews = new ArrayList(); + + public FragmentTransitionCompat21.EpicenterView enteringEpicenterView = + new FragmentTransitionCompat21.EpicenterView(); + public View nonExistentView; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/BundleUtil.java b/mobile/google-android-support-v4/src/android/support/v4/app/BundleUtil.java new file mode 100644 index 00000000000..fb6350d96b0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/BundleUtil.java @@ -0,0 +1,27 @@ +package android.support.v4.app; + +import android.os.Bundle; +import android.os.Parcelable; + +import java.util.Arrays; + +/** + * @hide + */ +class BundleUtil { + /** + * Get an array of Bundle objects from a parcelable array field in a bundle. + * Update the bundle to have a typed array so fetches in the future don't need + * to do an array copy. + */ + public static Bundle[] getBundleArrayFromBundle(Bundle bundle, String key) { + Parcelable[] array = bundle.getParcelableArray(key); + if (array instanceof Bundle[] || array == null) { + return (Bundle[]) array; + } + Bundle[] typedArray = Arrays.copyOf(array, array.length, + Bundle[].class); + bundle.putParcelableArray(key, typedArray); + return typedArray; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/DialogFragment.java b/mobile/google-android-support-v4/src/android/support/v4/app/DialogFragment.java new file mode 100644 index 00000000000..343c1454781 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/DialogFragment.java @@ -0,0 +1,452 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.StyleRes; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Static library support version of the framework's {@link android.app.DialogFragment}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class DialogFragment extends Fragment + implements DialogInterface.OnCancelListener, DialogInterface.OnDismissListener { + + /** @hide */ + @IntDef({STYLE_NORMAL, STYLE_NO_TITLE, STYLE_NO_FRAME, STYLE_NO_INPUT}) + @Retention(RetentionPolicy.SOURCE) + private @interface DialogStyle {} + + /** + * Style for {@link #setStyle(int, int)}: a basic, + * normal dialog. + */ + public static final int STYLE_NORMAL = 0; + + /** + * Style for {@link #setStyle(int, int)}: don't include + * a title area. + */ + public static final int STYLE_NO_TITLE = 1; + + /** + * Style for {@link #setStyle(int, int)}: don't draw + * any frame at all; the view hierarchy returned by {@link #onCreateView} + * is entirely responsible for drawing the dialog. + */ + public static final int STYLE_NO_FRAME = 2; + + /** + * Style for {@link #setStyle(int, int)}: like + * {@link #STYLE_NO_FRAME}, but also disables all input to the dialog. + * The user can not touch it, and its window will not receive input focus. + */ + public static final int STYLE_NO_INPUT = 3; + + private static final String SAVED_DIALOG_STATE_TAG = "android:savedDialogState"; + private static final String SAVED_STYLE = "android:style"; + private static final String SAVED_THEME = "android:theme"; + private static final String SAVED_CANCELABLE = "android:cancelable"; + private static final String SAVED_SHOWS_DIALOG = "android:showsDialog"; + private static final String SAVED_BACK_STACK_ID = "android:backStackId"; + + int mStyle = STYLE_NORMAL; + int mTheme = 0; + boolean mCancelable = true; + boolean mShowsDialog = true; + int mBackStackId = -1; + + Dialog mDialog; + boolean mViewDestroyed; + boolean mDismissed; + boolean mShownByMe; + + public DialogFragment() { + } + + /** + * Call to customize the basic appearance and behavior of the + * fragment's dialog. This can be used for some common dialog behaviors, + * taking care of selecting flags, theme, and other options for you. The + * same effect can be achieve by manually setting Dialog and Window + * attributes yourself. Calling this after the fragment's Dialog is + * created will have no effect. + * + * @param style Selects a standard style: may be {@link #STYLE_NORMAL}, + * {@link #STYLE_NO_TITLE}, {@link #STYLE_NO_FRAME}, or + * {@link #STYLE_NO_INPUT}. + * @param theme Optional custom theme. If 0, an appropriate theme (based + * on the style) will be selected for you. + */ + public void setStyle(@DialogStyle int style, @StyleRes int theme) { + mStyle = style; + if (mStyle == STYLE_NO_FRAME || mStyle == STYLE_NO_INPUT) { + mTheme = android.R.style.Theme_Panel; + } + if (theme != 0) { + mTheme = theme; + } + } + + /** + * Display the dialog, adding the fragment to the given FragmentManager. This + * is a convenience for explicitly creating a transaction, adding the + * fragment to it with the given tag, and committing it. This does + * not add the transaction to the back stack. When the fragment + * is dismissed, a new transaction will be executed to remove it from + * the activity. + * @param manager The FragmentManager this fragment will be added to. + * @param tag The tag for this fragment, as per + * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. + */ + public void show(FragmentManager manager, String tag) { + mDismissed = false; + mShownByMe = true; + FragmentTransaction ft = manager.beginTransaction(); + ft.add(this, tag); + ft.commit(); + } + + /** + * Display the dialog, adding the fragment using an existing transaction + * and then committing the transaction. + * @param transaction An existing transaction in which to add the fragment. + * @param tag The tag for this fragment, as per + * {@link FragmentTransaction#add(Fragment, String) FragmentTransaction.add}. + * @return Returns the identifier of the committed transaction, as per + * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. + */ + public int show(FragmentTransaction transaction, String tag) { + mDismissed = false; + mShownByMe = true; + transaction.add(this, tag); + mViewDestroyed = false; + mBackStackId = transaction.commit(); + return mBackStackId; + } + + /** + * Dismiss the fragment and its dialog. If the fragment was added to the + * back stack, all back stack state up to and including this entry will + * be popped. Otherwise, a new transaction will be committed to remove + * the fragment. + */ + public void dismiss() { + dismissInternal(false); + } + + /** + * Version of {@link #dismiss()} that uses + * {@link FragmentTransaction#commitAllowingStateLoss() + * FragmentTransaction.commitAllowingStateLoss()}. See linked + * documentation for further details. + */ + public void dismissAllowingStateLoss() { + dismissInternal(true); + } + + void dismissInternal(boolean allowStateLoss) { + if (mDismissed) { + return; + } + mDismissed = true; + mShownByMe = false; + if (mDialog != null) { + mDialog.dismiss(); + mDialog = null; + } + mViewDestroyed = true; + if (mBackStackId >= 0) { + getFragmentManager().popBackStack(mBackStackId, + FragmentManager.POP_BACK_STACK_INCLUSIVE); + mBackStackId = -1; + } else { + FragmentTransaction ft = getFragmentManager().beginTransaction(); + ft.remove(this); + if (allowStateLoss) { + ft.commitAllowingStateLoss(); + } else { + ft.commit(); + } + } + } + + public Dialog getDialog() { + return mDialog; + } + + @StyleRes + public int getTheme() { + return mTheme; + } + + /** + * Control whether the shown Dialog is cancelable. Use this instead of + * directly calling {@link Dialog#setCancelable(boolean) + * Dialog.setCancelable(boolean)}, because DialogFragment needs to change + * its behavior based on this. + * + * @param cancelable If true, the dialog is cancelable. The default + * is true. + */ + public void setCancelable(boolean cancelable) { + mCancelable = cancelable; + if (mDialog != null) mDialog.setCancelable(cancelable); + } + + /** + * Return the current value of {@link #setCancelable(boolean)}. + */ + public boolean isCancelable() { + return mCancelable; + } + + /** + * Controls whether this fragment should be shown in a dialog. If not + * set, no Dialog will be created in {@link #onActivityCreated(Bundle)}, + * and the fragment's view hierarchy will thus not be added to it. This + * allows you to instead use it as a normal fragment (embedded inside of + * its activity). + * + *

This is normally set for you based on whether the fragment is + * associated with a container view ID passed to + * {@link FragmentTransaction#add(int, Fragment) FragmentTransaction.add(int, Fragment)}. + * If the fragment was added with a container, setShowsDialog will be + * initialized to false; otherwise, it will be true. + * + * @param showsDialog If true, the fragment will be displayed in a Dialog. + * If false, no Dialog will be created and the fragment's view hierarchly + * left undisturbed. + */ + public void setShowsDialog(boolean showsDialog) { + mShowsDialog = showsDialog; + } + + /** + * Return the current value of {@link #setShowsDialog(boolean)}. + */ + public boolean getShowsDialog() { + return mShowsDialog; + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (!mShownByMe) { + // If not explicitly shown through our API, take this as an + // indication that the dialog is no longer dismissed. + mDismissed = false; + } + } + + @Override + public void onDetach() { + super.onDetach(); + if (!mShownByMe && !mDismissed) { + // The fragment was not shown by a direct call here, it is not + // dismissed, and now it is being detached... well, okay, thou + // art now dismissed. Have fun. + mDismissed = true; + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mShowsDialog = mContainerId == 0; + + if (savedInstanceState != null) { + mStyle = savedInstanceState.getInt(SAVED_STYLE, STYLE_NORMAL); + mTheme = savedInstanceState.getInt(SAVED_THEME, 0); + mCancelable = savedInstanceState.getBoolean(SAVED_CANCELABLE, true); + mShowsDialog = savedInstanceState.getBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); + mBackStackId = savedInstanceState.getInt(SAVED_BACK_STACK_ID, -1); + } + + } + + /** @hide */ + @Override + public LayoutInflater getLayoutInflater(Bundle savedInstanceState) { + if (!mShowsDialog) { + return super.getLayoutInflater(savedInstanceState); + } + + mDialog = onCreateDialog(savedInstanceState); + switch (mStyle) { + case STYLE_NO_INPUT: + mDialog.getWindow().addFlags( + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE); + // fall through... + case STYLE_NO_FRAME: + case STYLE_NO_TITLE: + mDialog.requestWindowFeature(Window.FEATURE_NO_TITLE); + } + if (mDialog != null) { + return (LayoutInflater) mDialog.getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + } + return (LayoutInflater) mActivity.getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Override to build your own custom Dialog container. This is typically + * used to show an AlertDialog instead of a generic Dialog; when doing so, + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} does not need + * to be implemented since the AlertDialog takes care of its own content. + * + *

This method will be called after {@link #onCreate(Bundle)} and + * before {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. The + * default implementation simply instantiates and returns a {@link Dialog} + * class. + * + *

Note: DialogFragment own the {@link Dialog#setOnCancelListener + * Dialog.setOnCancelListener} and {@link Dialog#setOnDismissListener + * Dialog.setOnDismissListener} callbacks. You must not set them yourself. + * To find out about these events, override {@link #onCancel(DialogInterface)} + * and {@link #onDismiss(DialogInterface)}.

+ * + * @param savedInstanceState The last saved instance state of the Fragment, + * or null if this is a freshly created Fragment. + * + * @return Return a new Dialog instance to be displayed by the Fragment. + */ + @NonNull + public Dialog onCreateDialog(Bundle savedInstanceState) { + return new Dialog(getActivity(), getTheme()); + } + + public void onCancel(DialogInterface dialog) { + } + + public void onDismiss(DialogInterface dialog) { + if (!mViewDestroyed) { + // Note: we need to use allowStateLoss, because the dialog + // dispatches this asynchronously so we can receive the call + // after the activity is paused. Worst case, when the user comes + // back to the activity they see the dialog again. + dismissInternal(true); + } + } + + @Override + public void onActivityCreated(Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + if (!mShowsDialog) { + return; + } + + View view = getView(); + if (view != null) { + if (view.getParent() != null) { + throw new IllegalStateException("DialogFragment can not be attached to a container view"); + } + mDialog.setContentView(view); + } + mDialog.setOwnerActivity(getActivity()); + mDialog.setCancelable(mCancelable); + mDialog.setOnCancelListener(this); + mDialog.setOnDismissListener(this); + if (savedInstanceState != null) { + Bundle dialogState = savedInstanceState.getBundle(SAVED_DIALOG_STATE_TAG); + if (dialogState != null) { + mDialog.onRestoreInstanceState(dialogState); + } + } + } + + @Override + public void onStart() { + super.onStart(); + if (mDialog != null) { + mViewDestroyed = false; + mDialog.show(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mDialog != null) { + Bundle dialogState = mDialog.onSaveInstanceState(); + if (dialogState != null) { + outState.putBundle(SAVED_DIALOG_STATE_TAG, dialogState); + } + } + if (mStyle != STYLE_NORMAL) { + outState.putInt(SAVED_STYLE, mStyle); + } + if (mTheme != 0) { + outState.putInt(SAVED_THEME, mTheme); + } + if (!mCancelable) { + outState.putBoolean(SAVED_CANCELABLE, mCancelable); + } + if (!mShowsDialog) { + outState.putBoolean(SAVED_SHOWS_DIALOG, mShowsDialog); + } + if (mBackStackId != -1) { + outState.putInt(SAVED_BACK_STACK_ID, mBackStackId); + } + } + + @Override + public void onStop() { + super.onStop(); + if (mDialog != null) { + mDialog.hide(); + } + } + + /** + * Remove dialog. + */ + @Override + public void onDestroyView() { + super.onDestroyView(); + if (mDialog != null) { + // Set removed here because this dismissal is just to hide + // the dialog -- we don't want this to cause the fragment to + // actually be removed. + mViewDestroyed = true; + mDialog.dismiss(); + mDialog = null; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/Fragment.java b/mobile/google-android-support-v4/src/android/support/v4/app/Fragment.java new file mode 100644 index 00000000000..eb6ab0d3fad --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/Fragment.java @@ -0,0 +1,2015 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.ComponentCallbacks; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v4.util.SimpleArrayMap; +import android.support.v4.util.DebugUtils; +import android.support.v4.view.LayoutInflaterCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ContextMenu.ContextMenuInfo; +import android.view.View.OnCreateContextMenuListener; +import android.view.animation.Animation; +import android.widget.AdapterView; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +final class FragmentState implements Parcelable { + final String mClassName; + final int mIndex; + final boolean mFromLayout; + final int mFragmentId; + final int mContainerId; + final String mTag; + final boolean mRetainInstance; + final boolean mDetached; + final Bundle mArguments; + + Bundle mSavedFragmentState; + + Fragment mInstance; + + public FragmentState(Fragment frag) { + mClassName = frag.getClass().getName(); + mIndex = frag.mIndex; + mFromLayout = frag.mFromLayout; + mFragmentId = frag.mFragmentId; + mContainerId = frag.mContainerId; + mTag = frag.mTag; + mRetainInstance = frag.mRetainInstance; + mDetached = frag.mDetached; + mArguments = frag.mArguments; + } + + public FragmentState(Parcel in) { + mClassName = in.readString(); + mIndex = in.readInt(); + mFromLayout = in.readInt() != 0; + mFragmentId = in.readInt(); + mContainerId = in.readInt(); + mTag = in.readString(); + mRetainInstance = in.readInt() != 0; + mDetached = in.readInt() != 0; + mArguments = in.readBundle(); + mSavedFragmentState = in.readBundle(); + } + + public Fragment instantiate(FragmentActivity activity, Fragment parent) { + if (mInstance != null) { + return mInstance; + } + + if (mArguments != null) { + mArguments.setClassLoader(activity.getClassLoader()); + } + + mInstance = Fragment.instantiate(activity, mClassName, mArguments); + + if (mSavedFragmentState != null) { + mSavedFragmentState.setClassLoader(activity.getClassLoader()); + mInstance.mSavedFragmentState = mSavedFragmentState; + } + mInstance.setIndex(mIndex, parent); + mInstance.mFromLayout = mFromLayout; + mInstance.mRestored = true; + mInstance.mFragmentId = mFragmentId; + mInstance.mContainerId = mContainerId; + mInstance.mTag = mTag; + mInstance.mRetainInstance = mRetainInstance; + mInstance.mDetached = mDetached; + mInstance.mFragmentManager = activity.mFragments; + + if (FragmentManagerImpl.DEBUG) Log.v(FragmentManagerImpl.TAG, + "Instantiated fragment " + mInstance); + + return mInstance; + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mClassName); + dest.writeInt(mIndex); + dest.writeInt(mFromLayout ? 1 : 0); + dest.writeInt(mFragmentId); + dest.writeInt(mContainerId); + dest.writeString(mTag); + dest.writeInt(mRetainInstance ? 1 : 0); + dest.writeInt(mDetached ? 1 : 0); + dest.writeBundle(mArguments); + dest.writeBundle(mSavedFragmentState); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public FragmentState createFromParcel(Parcel in) { + return new FragmentState(in); + } + + public FragmentState[] newArray(int size) { + return new FragmentState[size]; + } + }; +} + +/** + * Static library support version of the framework's {@link android.app.Fragment}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework {@link android.app.Fragment} + * documentation for a class overview. + * + *

The main differences when using this support version instead of the framework version are: + *

    + *
  • Your activity must extend {@link FragmentActivity} + *
  • You must call {@link FragmentActivity#getSupportFragmentManager} to get the + * {@link FragmentManager} + *
+ * + */ +public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener { + private static final SimpleArrayMap> sClassMap = + new SimpleArrayMap>(); + + static final Object USE_DEFAULT_TRANSITION = new Object(); + + static final int INITIALIZING = 0; // Not yet created. + static final int CREATED = 1; // Created. + static final int ACTIVITY_CREATED = 2; // The activity has finished its creation. + static final int STOPPED = 3; // Fully created, not started. + static final int STARTED = 4; // Created and started, not resumed. + static final int RESUMED = 5; // Created started and resumed. + + int mState = INITIALIZING; + + // Non-null if the fragment's view hierarchy is currently animating away, + // meaning we need to wait a bit on completely destroying it. This is the + // view that is animating. + View mAnimatingAway; + + // If mAnimatingAway != null, this is the state we should move to once the + // animation is done. + int mStateAfterAnimating; + + // When instantiated from saved state, this is the saved state. + Bundle mSavedFragmentState; + SparseArray mSavedViewState; + + // Index into active fragment array. + int mIndex = -1; + + // Internal unique name for this fragment; + String mWho; + + // Construction arguments; + Bundle mArguments; + + // Target fragment. + Fragment mTarget; + + // For use when retaining a fragment: this is the index of the last mTarget. + int mTargetIndex = -1; + + // Target request code. + int mTargetRequestCode; + + // True if the fragment is in the list of added fragments. + boolean mAdded; + + // If set this fragment is being removed from its activity. + boolean mRemoving; + + // True if the fragment is in the resumed state. + boolean mResumed; + + // Set to true if this fragment was instantiated from a layout file. + boolean mFromLayout; + + // Set to true when the view has actually been inflated in its layout. + boolean mInLayout; + + // True if this fragment has been restored from previously saved state. + boolean mRestored; + + // Number of active back stack entries this fragment is in. + int mBackStackNesting; + + // The fragment manager we are associated with. Set as soon as the + // fragment is used in a transaction; cleared after it has been removed + // from all transactions. + FragmentManagerImpl mFragmentManager; + + // Activity this fragment is attached to. + FragmentActivity mActivity; + + // Private fragment manager for child fragments inside of this one. + FragmentManagerImpl mChildFragmentManager; + + // If this Fragment is contained in another Fragment, this is that container. + Fragment mParentFragment; + + // The optional identifier for this fragment -- either the container ID if it + // was dynamically added to the view hierarchy, or the ID supplied in + // layout. + int mFragmentId; + + // When a fragment is being dynamically added to the view hierarchy, this + // is the identifier of the parent container it is being added to. + int mContainerId; + + // The optional named tag for this fragment -- usually used to find + // fragments that are not part of the layout. + String mTag; + + // Set to true when the app has requested that this fragment be hidden + // from the user. + boolean mHidden; + + // Set to true when the app has requested that this fragment be deactivated. + boolean mDetached; + + // If set this fragment would like its instance retained across + // configuration changes. + boolean mRetainInstance; + + // If set this fragment is being retained across the current config change. + boolean mRetaining; + + // If set this fragment has menu items to contribute. + boolean mHasMenu; + + // Set to true to allow the fragment's menu to be shown. + boolean mMenuVisible = true; + + // Used to verify that subclasses call through to super class. + boolean mCalled; + + // If app has requested a specific animation, this is the one to use. + int mNextAnim; + + // The parent container of the fragment after dynamically added to UI. + ViewGroup mContainer; + + // The View generated for this fragment. + View mView; + + // The real inner view that will save/restore state. + View mInnerView; + + // Whether this fragment should defer starting until after other fragments + // have been started and their loaders are finished. + boolean mDeferStart; + + // Hint provided by the app that this fragment is currently visible to the user. + boolean mUserVisibleHint = true; + + LoaderManagerImpl mLoaderManager; + boolean mLoadersStarted; + boolean mCheckedForLoaderManager; + + Object mEnterTransition = null; + Object mReturnTransition = USE_DEFAULT_TRANSITION; + Object mExitTransition = null; + Object mReenterTransition = USE_DEFAULT_TRANSITION; + Object mSharedElementEnterTransition = null; + Object mSharedElementReturnTransition = USE_DEFAULT_TRANSITION; + Boolean mAllowReturnTransitionOverlap; + Boolean mAllowEnterTransitionOverlap; + + SharedElementCallback mEnterTransitionCallback = null; + SharedElementCallback mExitTransitionCallback = null; + + /** + * State information that has been retrieved from a fragment instance + * through {@link FragmentManager#saveFragmentInstanceState(Fragment) + * FragmentManager.saveFragmentInstanceState}. + */ + public static class SavedState implements Parcelable { + final Bundle mState; + + SavedState(Bundle state) { + mState = state; + } + + SavedState(Parcel in, ClassLoader loader) { + mState = in.readBundle(); + if (loader != null && mState != null) { + mState.setClassLoader(loader); + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBundle(mState); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in, null); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + /** + * Thrown by {@link Fragment#instantiate(Context, String, Bundle)} when + * there is an instantiation failure. + */ + static public class InstantiationException extends RuntimeException { + public InstantiationException(String msg, Exception cause) { + super(msg, cause); + } + } + + /** + * Default constructor. Every fragment must have an + * empty constructor, so it can be instantiated when restoring its + * activity's state. It is strongly recommended that subclasses do not + * have other constructors with parameters, since these constructors + * will not be called when the fragment is re-instantiated; instead, + * arguments can be supplied by the caller with {@link #setArguments} + * and later retrieved by the Fragment with {@link #getArguments}. + * + *

Applications should generally not implement a constructor. The + * first place application code an run where the fragment is ready to + * be used is in {@link #onAttach(Activity)}, the point where the fragment + * is actually associated with its activity. Some applications may also + * want to implement {@link #onInflate} to retrieve attributes from a + * layout resource, though should take care here because this happens for + * the fragment is attached to its activity. + */ + public Fragment() { + } + + /** + * Like {@link #instantiate(Context, String, Bundle)} but with a null + * argument Bundle. + */ + public static Fragment instantiate(Context context, String fname) { + return instantiate(context, fname, null); + } + + /** + * Create a new instance of a Fragment with the given class name. This is + * the same as calling its empty constructor. + * + * @param context The calling context being used to instantiate the fragment. + * This is currently just used to get its ClassLoader. + * @param fname The class name of the fragment to instantiate. + * @param args Bundle of arguments to supply to the fragment, which it + * can retrieve with {@link #getArguments()}. May be null. + * @return Returns a new fragment instance. + * @throws InstantiationException If there is a failure in instantiating + * the given fragment class. This is a runtime exception; it is not + * normally expected to happen. + */ + public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) { + try { + Class clazz = sClassMap.get(fname); + if (clazz == null) { + // Class not found in the cache, see if it's real, and try to add it + clazz = context.getClassLoader().loadClass(fname); + sClassMap.put(fname, clazz); + } + Fragment f = (Fragment)clazz.newInstance(); + if (args != null) { + args.setClassLoader(f.getClass().getClassLoader()); + f.mArguments = args; + } + return f; + } catch (ClassNotFoundException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (java.lang.InstantiationException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } catch (IllegalAccessException e) { + throw new InstantiationException("Unable to instantiate fragment " + fname + + ": make sure class name exists, is public, and has an" + + " empty constructor that is public", e); + } + } + + /** + * Determine if the given fragment name is a support library fragment class. + * + * @param context Context used to determine the correct ClassLoader to use + * @param fname Class name of the fragment to test + * @return true if fname is android.support.v4.app.Fragment + * or a subclass, false otherwise. + */ + static boolean isSupportFragmentClass(Context context, String fname) { + try { + Class clazz = sClassMap.get(fname); + if (clazz == null) { + // Class not found in the cache, see if it's real, and try to add it + clazz = context.getClassLoader().loadClass(fname); + sClassMap.put(fname, clazz); + } + return Fragment.class.isAssignableFrom(clazz); + } catch (ClassNotFoundException e) { + return false; + } + } + + final void restoreViewState(Bundle savedInstanceState) { + if (mSavedViewState != null) { + mInnerView.restoreHierarchyState(mSavedViewState); + mSavedViewState = null; + } + mCalled = false; + onViewStateRestored(savedInstanceState); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onViewStateRestored()"); + } + } + + final void setIndex(int index, Fragment parent) { + mIndex = index; + if (parent != null) { + mWho = parent.mWho + ":" + mIndex; + } else { + mWho = "android:fragment:" + mIndex; + } + } + + final boolean isInBackStack() { + return mBackStackNesting > 0; + } + + /** + * Subclasses can not override equals(). + */ + @Override final public boolean equals(Object o) { + return super.equals(o); + } + + /** + * Subclasses can not override hashCode(). + */ + @Override final public int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + DebugUtils.buildShortClassTag(this, sb); + if (mIndex >= 0) { + sb.append(" #"); + sb.append(mIndex); + } + if (mFragmentId != 0) { + sb.append(" id=0x"); + sb.append(Integer.toHexString(mFragmentId)); + } + if (mTag != null) { + sb.append(" "); + sb.append(mTag); + } + sb.append('}'); + return sb.toString(); + } + + /** + * Return the identifier this fragment is known by. This is either + * the android:id value supplied in a layout or the container view ID + * supplied when adding the fragment. + */ + final public int getId() { + return mFragmentId; + } + + /** + * Get the tag name of the fragment, if specified. + */ + final public String getTag() { + return mTag; + } + + /** + * Supply the construction arguments for this fragment. This can only + * be called before the fragment has been attached to its activity; that + * is, you should call it immediately after constructing the fragment. The + * arguments supplied here will be retained across fragment destroy and + * creation. + */ + public void setArguments(Bundle args) { + if (mIndex >= 0) { + throw new IllegalStateException("Fragment already active"); + } + mArguments = args; + } + + /** + * Return the arguments supplied when the fragment was instantiated, + * if any. + */ + final public Bundle getArguments() { + return mArguments; + } + + /** + * Set the initial saved state that this Fragment should restore itself + * from when first being constructed, as returned by + * {@link FragmentManager#saveFragmentInstanceState(Fragment) + * FragmentManager.saveFragmentInstanceState}. + * + * @param state The state the fragment should be restored from. + */ + public void setInitialSavedState(SavedState state) { + if (mIndex >= 0) { + throw new IllegalStateException("Fragment already active"); + } + mSavedFragmentState = state != null && state.mState != null + ? state.mState : null; + } + + /** + * Optional target for this fragment. This may be used, for example, + * if this fragment is being started by another, and when done wants to + * give a result back to the first. The target set here is retained + * across instances via {@link FragmentManager#putFragment + * FragmentManager.putFragment()}. + * + * @param fragment The fragment that is the target of this one. + * @param requestCode Optional request code, for convenience if you + * are going to call back with {@link #onActivityResult(int, int, Intent)}. + */ + public void setTargetFragment(Fragment fragment, int requestCode) { + mTarget = fragment; + mTargetRequestCode = requestCode; + } + + /** + * Return the target fragment set by {@link #setTargetFragment}. + */ + final public Fragment getTargetFragment() { + return mTarget; + } + + /** + * Return the target request code set by {@link #setTargetFragment}. + */ + final public int getTargetRequestCode() { + return mTargetRequestCode; + } + + /** + * Return the Activity this fragment is currently associated with. + */ + final public FragmentActivity getActivity() { + return mActivity; + } + + /** + * Return getActivity().getResources(). + */ + final public Resources getResources() { + if (mActivity == null) { + throw new IllegalStateException("Fragment " + this + " not attached to Activity"); + } + return mActivity.getResources(); + } + + /** + * Return a localized, styled CharSequence from the application's package's + * default string table. + * + * @param resId Resource id for the CharSequence text + */ + public final CharSequence getText(@StringRes int resId) { + return getResources().getText(resId); + } + + /** + * Return a localized string from the application's package's + * default string table. + * + * @param resId Resource id for the string + */ + public final String getString(@StringRes int resId) { + return getResources().getString(resId); + } + + /** + * Return a localized formatted string from the application's package's + * default string table, substituting the format arguments as defined in + * {@link java.util.Formatter} and {@link java.lang.String#format}. + * + * @param resId Resource id for the format string + * @param formatArgs The format arguments that will be used for substitution. + */ + + public final String getString(@StringRes int resId, Object... formatArgs) { + return getResources().getString(resId, formatArgs); + } + + /** + * Return the FragmentManager for interacting with fragments associated + * with this fragment's activity. Note that this will be non-null slightly + * before {@link #getActivity()}, during the time from when the fragment is + * placed in a {@link FragmentTransaction} until it is committed and + * attached to its activity. + * + *

If this Fragment is a child of another Fragment, the FragmentManager + * returned here will be the parent's {@link #getChildFragmentManager()}. + */ + final public FragmentManager getFragmentManager() { + return mFragmentManager; + } + + /** + * Return a private FragmentManager for placing and managing Fragments + * inside of this Fragment. + */ + final public FragmentManager getChildFragmentManager() { + if (mChildFragmentManager == null) { + instantiateChildFragmentManager(); + if (mState >= RESUMED) { + mChildFragmentManager.dispatchResume(); + } else if (mState >= STARTED) { + mChildFragmentManager.dispatchStart(); + } else if (mState >= ACTIVITY_CREATED) { + mChildFragmentManager.dispatchActivityCreated(); + } else if (mState >= CREATED) { + mChildFragmentManager.dispatchCreate(); + } + } + return mChildFragmentManager; + } + + /** + * Returns the parent Fragment containing this Fragment. If this Fragment + * is attached directly to an Activity, returns null. + */ + final public Fragment getParentFragment() { + return mParentFragment; + } + + /** + * Return true if the fragment is currently added to its activity. + */ + final public boolean isAdded() { + return mActivity != null && mAdded; + } + + /** + * Return true if the fragment has been explicitly detached from the UI. + * That is, {@link FragmentTransaction#detach(Fragment) + * FragmentTransaction.detach(Fragment)} has been used on it. + */ + final public boolean isDetached() { + return mDetached; + } + + /** + * Return true if this fragment is currently being removed from its + * activity. This is not whether its activity is finishing, but + * rather whether it is in the process of being removed from its activity. + */ + final public boolean isRemoving() { + return mRemoving; + } + + /** + * Return true if the layout is included as part of an activity view + * hierarchy via the <fragment> tag. This will always be true when + * fragments are created through the <fragment> tag, except + * in the case where an old fragment is restored from a previous state and + * it does not appear in the layout of the current state. + */ + final public boolean isInLayout() { + return mInLayout; + } + + /** + * Return true if the fragment is in the resumed state. This is true + * for the duration of {@link #onResume()} and {@link #onPause()} as well. + */ + final public boolean isResumed() { + return mResumed; + } + + /** + * Return true if the fragment is currently visible to the user. This means + * it: (1) has been added, (2) has its view attached to the window, and + * (3) is not hidden. + */ + final public boolean isVisible() { + return isAdded() && !isHidden() && mView != null + && mView.getWindowToken() != null && mView.getVisibility() == View.VISIBLE; + } + + /** + * Return true if the fragment has been hidden. By default fragments + * are shown. You can find out about changes to this state with + * {@link #onHiddenChanged}. Note that the hidden state is orthogonal + * to other states -- that is, to be visible to the user, a fragment + * must be both started and not hidden. + */ + final public boolean isHidden() { + return mHidden; + } + + /** @hide */ + final public boolean hasOptionsMenu() { + return mHasMenu; + } + + /** @hide */ + final public boolean isMenuVisible() { + return mMenuVisible; + } + + /** + * Called when the hidden state (as returned by {@link #isHidden()} of + * the fragment has changed. Fragments start out not hidden; this will + * be called whenever the fragment changes state from that. + * @param hidden True if the fragment is now hidden, false if it is not + * visible. + */ + public void onHiddenChanged(boolean hidden) { + } + + /** + * Control whether a fragment instance is retained across Activity + * re-creation (such as from a configuration change). This can only + * be used with fragments not in the back stack. If set, the fragment + * lifecycle will be slightly different when an activity is recreated: + *

    + *
  • {@link #onDestroy()} will not be called (but {@link #onDetach()} still + * will be, because the fragment is being detached from its current activity). + *
  • {@link #onCreate(Bundle)} will not be called since the fragment + * is not being re-created. + *
  • {@link #onAttach(Activity)} and {@link #onActivityCreated(Bundle)} will + * still be called. + *
+ */ + public void setRetainInstance(boolean retain) { + if (retain && mParentFragment != null) { + throw new IllegalStateException( + "Can't retain fragements that are nested in other fragments"); + } + mRetainInstance = retain; + } + + final public boolean getRetainInstance() { + return mRetainInstance; + } + + /** + * Report that this fragment would like to participate in populating + * the options menu by receiving a call to {@link #onCreateOptionsMenu} + * and related methods. + * + * @param hasMenu If true, the fragment has menu items to contribute. + */ + public void setHasOptionsMenu(boolean hasMenu) { + if (mHasMenu != hasMenu) { + mHasMenu = hasMenu; + if (isAdded() && !isHidden()) { + mActivity.supportInvalidateOptionsMenu(); + } + } + } + + /** + * Set a hint for whether this fragment's menu should be visible. This + * is useful if you know that a fragment has been placed in your view + * hierarchy so that the user can not currently seen it, so any menu items + * it has should also not be shown. + * + * @param menuVisible The default is true, meaning the fragment's menu will + * be shown as usual. If false, the user will not see the menu. + */ + public void setMenuVisibility(boolean menuVisible) { + if (mMenuVisible != menuVisible) { + mMenuVisible = menuVisible; + if (mHasMenu && isAdded() && !isHidden()) { + mActivity.supportInvalidateOptionsMenu(); + } + } + } + + /** + * Set a hint to the system about whether this fragment's UI is currently visible + * to the user. This hint defaults to true and is persistent across fragment instance + * state save and restore. + * + *

An app may set this to false to indicate that the fragment's UI is + * scrolled out of visibility or is otherwise not directly visible to the user. + * This may be used by the system to prioritize operations such as fragment lifecycle updates + * or loader ordering behavior.

+ * + * @param isVisibleToUser true if this fragment's UI is currently visible to the user (default), + * false if it is not. + */ + public void setUserVisibleHint(boolean isVisibleToUser) { + if (!mUserVisibleHint && isVisibleToUser && mState < STARTED) { + mFragmentManager.performPendingDeferredStart(this); + } + mUserVisibleHint = isVisibleToUser; + mDeferStart = !isVisibleToUser; + } + + /** + * @return The current value of the user-visible hint on this fragment. + * @see #setUserVisibleHint(boolean) + */ + public boolean getUserVisibleHint() { + return mUserVisibleHint; + } + + /** + * Return the LoaderManager for this fragment, creating it if needed. + */ + public LoaderManager getLoaderManager() { + if (mLoaderManager != null) { + return mLoaderManager; + } + if (mActivity == null) { + throw new IllegalStateException("Fragment " + this + " not attached to Activity"); + } + mCheckedForLoaderManager = true; + mLoaderManager = mActivity.getLoaderManager(mWho, mLoadersStarted, true); + return mLoaderManager; + } + + /** + * Call {@link Activity#startActivity(Intent)} on the fragment's + * containing Activity. + */ + public void startActivity(Intent intent) { + if (mActivity == null) { + throw new IllegalStateException("Fragment " + this + " not attached to Activity"); + } + mActivity.startActivityFromFragment(this, intent, -1); + } + + /** + * Call {@link Activity#startActivityForResult(Intent, int)} on the fragment's + * containing Activity. + */ + public void startActivityForResult(Intent intent, int requestCode) { + if (mActivity == null) { + throw new IllegalStateException("Fragment " + this + " not attached to Activity"); + } + mActivity.startActivityFromFragment(this, intent, requestCode); + } + + /** + * Receive the result from a previous call to + * {@link #startActivityForResult(Intent, int)}. This follows the + * related Activity API as described there in + * {@link Activity#onActivityResult(int, int, Intent)}. + * + * @param requestCode The integer request code originally supplied to + * startActivityForResult(), allowing you to identify who this + * result came from. + * @param resultCode The integer result code returned by the child activity + * through its setResult(). + * @param data An Intent, which can return result data to the caller + * (various data can be attached to Intent "extras"). + */ + public void onActivityResult(int requestCode, int resultCode, Intent data) { + } + + /** + * @hide Hack so that DialogFragment can make its Dialog before creating + * its views, and the view construction can use the dialog's context for + * inflation. Maybe this should become a public API. Note sure. + */ + public LayoutInflater getLayoutInflater(Bundle savedInstanceState) { + LayoutInflater result = mActivity.getLayoutInflater().cloneInContext(mActivity); + getChildFragmentManager(); // Init if needed; use raw implementation below. + LayoutInflaterCompat.setFactory(result, mChildFragmentManager.getLayoutInflaterFactory()); + return result; + } + + /** + * Called when a fragment is being created as part of a view layout + * inflation, typically from setting the content view of an activity. This + * may be called immediately after the fragment is created from a + * tag in a layout file. Note this is before the fragment's + * {@link #onAttach(Activity)} has been called; all you should do here is + * parse the attributes and save them away. + * + *

This is called every time the fragment is inflated, even if it is + * being inflated into a new instance with saved state. It typically makes + * sense to re-parse the parameters each time, to allow them to change with + * different configurations.

+ * + *

Here is a typical implementation of a fragment that can take parameters + * both through attributes supplied here as well from {@link #getArguments()}:

+ * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentArguments.java + * fragment} + * + *

Note that parsing the XML attributes uses a "styleable" resource. The + * declaration for the styleable used here is:

+ * + * {@sample development/samples/ApiDemos/res/values/attrs.xml fragment_arguments} + * + *

The fragment can then be declared within its activity's content layout + * through a tag like this:

+ * + * {@sample development/samples/ApiDemos/res/layout/fragment_arguments.xml from_attributes} + * + *

This fragment can also be created dynamically from arguments given + * at runtime in the arguments Bundle; here is an example of doing so at + * creation of the containing activity:

+ * + * {@sample development/samples/ApiDemos/src/com/example/android/apis/app/FragmentArguments.java + * create} + * + * @param activity The Activity that is inflating this fragment. + * @param attrs The attributes at the tag where the fragment is + * being created. + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onInflate(Activity activity, AttributeSet attrs, Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when a fragment is first attached to its activity. + * {@link #onCreate(Bundle)} will be called after this. + */ + public void onAttach(Activity activity) { + mCalled = true; + } + + /** + * Called when a fragment loads an animation. + */ + public Animation onCreateAnimation(int transit, boolean enter, int nextAnim) { + return null; + } + + /** + * Called to do initial creation of a fragment. This is called after + * {@link #onAttach(Activity)} and before + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. + * + *

Note that this can be called while the fragment's activity is + * still in the process of being created. As such, you can not rely + * on things like the activity's content view hierarchy being initialized + * at this point. If you want to do work once the activity itself is + * created, see {@link #onActivityCreated(Bundle)}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onCreate(@Nullable Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called to have the fragment instantiate its user interface view. + * This is optional, and non-graphical fragments can return null (which + * is the default implementation). This will be called between + * {@link #onCreate(Bundle)} and {@link #onActivityCreated(Bundle)}. + * + *

If you return a View from here, you will later be called in + * {@link #onDestroyView} when the view is being released. + * + * @param inflater The LayoutInflater object that can be used to inflate + * any views in the fragment, + * @param container If non-null, this is the parent view that the fragment's + * UI should be attached to. The fragment should not add the view itself, + * but this can be used to generate the LayoutParams of the view. + * @param savedInstanceState If non-null, this fragment is being re-constructed + * from a previous saved state as given here. + * + * @return Return the View for the fragment's UI, or null. + */ + @Nullable + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + return null; + } + + /** + * Called immediately after {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)} + * has returned, but before any saved state has been restored in to the view. + * This gives subclasses a chance to initialize themselves once + * they know their view hierarchy has been completely created. The fragment's + * view hierarchy is not however attached to its parent at this point. + * @param view The View returned by {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}. + * @param savedInstanceState If non-null, this fragment is being re-constructed + * from a previous saved state as given here. + */ + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + } + + /** + * Get the root view for the fragment's layout (the one returned by {@link #onCreateView}), + * if provided. + * + * @return The fragment's root view, or null if it has no layout. + */ + @Nullable + public View getView() { + return mView; + } + + /** + * Called when the fragment's activity has been created and this + * fragment's view hierarchy instantiated. It can be used to do final + * initialization once these pieces are in place, such as retrieving + * views or restoring state. It is also useful for fragments that use + * {@link #setRetainInstance(boolean)} to retain their instance, + * as this callback tells the fragment when it is fully associated with + * the new activity instance. This is called after {@link #onCreateView} + * and before {@link #onViewStateRestored(Bundle)}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when all saved state has been restored into the view hierarchy + * of the fragment. This can be used to do initialization based on saved + * state that you are letting the view hierarchy track itself, such as + * whether check box widgets are currently checked. This is called + * after {@link #onActivityCreated(Bundle)} and before + * {@link #onStart()}. + * + * @param savedInstanceState If the fragment is being re-created from + * a previous saved state, this is the state. + */ + public void onViewStateRestored(@Nullable Bundle savedInstanceState) { + mCalled = true; + } + + /** + * Called when the Fragment is visible to the user. This is generally + * tied to {@link Activity#onStart() Activity.onStart} of the containing + * Activity's lifecycle. + */ + public void onStart() { + mCalled = true; + + if (!mLoadersStarted) { + mLoadersStarted = true; + if (!mCheckedForLoaderManager) { + mCheckedForLoaderManager = true; + mLoaderManager = mActivity.getLoaderManager(mWho, mLoadersStarted, false); + } + if (mLoaderManager != null) { + mLoaderManager.doStart(); + } + } + } + + /** + * Called when the fragment is visible to the user and actively running. + * This is generally + * tied to {@link Activity#onResume() Activity.onResume} of the containing + * Activity's lifecycle. + */ + public void onResume() { + mCalled = true; + } + + /** + * Called to ask the fragment to save its current dynamic state, so it + * can later be reconstructed in a new instance of its process is + * restarted. If a new instance of the fragment later needs to be + * created, the data you place in the Bundle here will be available + * in the Bundle given to {@link #onCreate(Bundle)}, + * {@link #onCreateView(LayoutInflater, ViewGroup, Bundle)}, and + * {@link #onActivityCreated(Bundle)}. + * + *

This corresponds to {@link Activity#onSaveInstanceState(Bundle) + * Activity.onSaveInstanceState(Bundle)} and most of the discussion there + * applies here as well. Note however: this method may be called + * at any time before {@link #onDestroy()}. There are many situations + * where a fragment may be mostly torn down (such as when placed on the + * back stack with no UI showing), but its state will not be saved until + * its owning activity actually needs to save its state. + * + * @param outState Bundle in which to place your saved state. + */ + public void onSaveInstanceState(Bundle outState) { + } + + public void onConfigurationChanged(Configuration newConfig) { + mCalled = true; + } + + /** + * Called when the Fragment is no longer resumed. This is generally + * tied to {@link Activity#onPause() Activity.onPause} of the containing + * Activity's lifecycle. + */ + public void onPause() { + mCalled = true; + } + + /** + * Called when the Fragment is no longer started. This is generally + * tied to {@link Activity#onStop() Activity.onStop} of the containing + * Activity's lifecycle. + */ + public void onStop() { + mCalled = true; + } + + public void onLowMemory() { + mCalled = true; + } + + /** + * Called when the view previously created by {@link #onCreateView} has + * been detached from the fragment. The next time the fragment needs + * to be displayed, a new view will be created. This is called + * after {@link #onStop()} and before {@link #onDestroy()}. It is called + * regardless of whether {@link #onCreateView} returned a + * non-null view. Internally it is called after the view's state has + * been saved but before it has been removed from its parent. + */ + public void onDestroyView() { + mCalled = true; + } + + /** + * Called when the fragment is no longer in use. This is called + * after {@link #onStop()} and before {@link #onDetach()}. + */ + public void onDestroy() { + mCalled = true; + //Log.v("foo", "onDestroy: mCheckedForLoaderManager=" + mCheckedForLoaderManager + // + " mLoaderManager=" + mLoaderManager); + if (!mCheckedForLoaderManager) { + mCheckedForLoaderManager = true; + mLoaderManager = mActivity.getLoaderManager(mWho, mLoadersStarted, false); + } + if (mLoaderManager != null) { + mLoaderManager.doDestroy(); + } + } + + /** + * Called by the fragment manager once this fragment has been removed, + * so that we don't have any left-over state if the application decides + * to re-use the instance. This only clears state that the framework + * internally manages, not things the application sets. + */ + void initState() { + mIndex = -1; + mWho = null; + mAdded = false; + mRemoving = false; + mResumed = false; + mFromLayout = false; + mInLayout = false; + mRestored = false; + mBackStackNesting = 0; + mFragmentManager = null; + mChildFragmentManager = null; + mActivity = null; + mFragmentId = 0; + mContainerId = 0; + mTag = null; + mHidden = false; + mDetached = false; + mRetaining = false; + mLoaderManager = null; + mLoadersStarted = false; + mCheckedForLoaderManager = false; + } + + /** + * Called when the fragment is no longer attached to its activity. This + * is called after {@link #onDestroy()}. + */ + public void onDetach() { + mCalled = true; + } + + /** + * Initialize the contents of the Activity's standard options menu. You + * should place your menu items in to menu. For this method + * to be called, you must have first called {@link #setHasOptionsMenu}. See + * {@link Activity#onCreateOptionsMenu(Menu) Activity.onCreateOptionsMenu} + * for more information. + * + * @param menu The options menu in which you place your items. + * + * @see #setHasOptionsMenu + * @see #onPrepareOptionsMenu + * @see #onOptionsItemSelected + */ + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + } + + /** + * Prepare the Screen's standard options menu to be displayed. This is + * called right before the menu is shown, every time it is shown. You can + * use this method to efficiently enable/disable items or otherwise + * dynamically modify the contents. See + * {@link Activity#onPrepareOptionsMenu(Menu) Activity.onPrepareOptionsMenu} + * for more information. + * + * @param menu The options menu as last shown or first initialized by + * onCreateOptionsMenu(). + * + * @see #setHasOptionsMenu + * @see #onCreateOptionsMenu + */ + public void onPrepareOptionsMenu(Menu menu) { + } + + /** + * Called when this fragment's option menu items are no longer being + * included in the overall options menu. Receiving this call means that + * the menu needed to be rebuilt, but this fragment's items were not + * included in the newly built menu (its {@link #onCreateOptionsMenu(Menu, MenuInflater)} + * was not called). + */ + public void onDestroyOptionsMenu() { + } + + /** + * This hook is called whenever an item in your options menu is selected. + * The default implementation simply returns false to have the normal + * processing happen (calling the item's Runnable or sending a message to + * its Handler as appropriate). You can use this method for any items + * for which you would like to do processing without those other + * facilities. + * + *

Derived classes should call through to the base class for it to + * perform the default menu handling. + * + * @param item The menu item that was selected. + * + * @return boolean Return false to allow normal menu processing to + * proceed, true to consume it here. + * + * @see #onCreateOptionsMenu + */ + public boolean onOptionsItemSelected(MenuItem item) { + return false; + } + + /** + * This hook is called whenever the options menu is being closed (either by the user canceling + * the menu with the back/menu button, or when an item is selected). + * + * @param menu The options menu as last shown or first initialized by + * onCreateOptionsMenu(). + */ + public void onOptionsMenuClosed(Menu menu) { + } + + /** + * Called when a context menu for the {@code view} is about to be shown. + * Unlike {@link #onCreateOptionsMenu}, this will be called every + * time the context menu is about to be shown and should be populated for + * the view (or item inside the view for {@link AdapterView} subclasses, + * this can be found in the {@code menuInfo})). + *

+ * Use {@link #onContextItemSelected(android.view.MenuItem)} to know when an + * item has been selected. + *

+ * The default implementation calls up to + * {@link Activity#onCreateContextMenu Activity.onCreateContextMenu}, though + * you can not call this implementation if you don't want that behavior. + *

+ * It is not safe to hold onto the context menu after this method returns. + * {@inheritDoc} + */ + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { + getActivity().onCreateContextMenu(menu, v, menuInfo); + } + + /** + * Registers a context menu to be shown for the given view (multiple views + * can show the context menu). This method will set the + * {@link OnCreateContextMenuListener} on the view to this fragment, so + * {@link #onCreateContextMenu(ContextMenu, View, ContextMenuInfo)} will be + * called when it is time to show the context menu. + * + * @see #unregisterForContextMenu(View) + * @param view The view that should show a context menu. + */ + public void registerForContextMenu(View view) { + view.setOnCreateContextMenuListener(this); + } + + /** + * Prevents a context menu to be shown for the given view. This method will + * remove the {@link OnCreateContextMenuListener} on the view. + * + * @see #registerForContextMenu(View) + * @param view The view that should stop showing a context menu. + */ + public void unregisterForContextMenu(View view) { + view.setOnCreateContextMenuListener(null); + } + + /** + * This hook is called whenever an item in a context menu is selected. The + * default implementation simply returns false to have the normal processing + * happen (calling the item's Runnable or sending a message to its Handler + * as appropriate). You can use this method for any items for which you + * would like to do processing without those other facilities. + *

+ * Use {@link MenuItem#getMenuInfo()} to get extra information set by the + * View that added this menu item. + *

+ * Derived classes should call through to the base class for it to perform + * the default menu handling. + * + * @param item The context menu item that was selected. + * @return boolean Return false to allow normal context menu processing to + * proceed, true to consume it here. + */ + public boolean onContextItemSelected(MenuItem item) { + return false; + } + + /** + * When custom transitions are used with Fragments, the enter transition callback + * is called when this Fragment is attached or detached when not popping the back stack. + * + * @param callback Used to manipulate the shared element transitions on this Fragment + * when added not as a pop from the back stack. + */ + public void setEnterSharedElementCallback(SharedElementCallback callback) { + mEnterTransitionCallback = callback; + } + + /** + * When custom transitions are used with Fragments, the exit transition callback + * is called when this Fragment is attached or detached when popping the back stack. + * + * @param callback Used to manipulate the shared element transitions on this Fragment + * when added as a pop from the back stack. + */ + public void setExitSharedElementCallback(SharedElementCallback callback) { + mExitTransitionCallback = callback; + } + + /** + * Sets the Transition that will be used to move Views into the initial scene. The entering + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#INVISIBLE} to {@link View#VISIBLE}. If transition is null, + * entering Views will remain unaffected. + * + * @param transition The Transition to use to move Views into the initial Scene. + */ + public void setEnterTransition(Object transition) { + mEnterTransition = transition; + } + + /** + * Returns the Transition that will be used to move Views into the initial scene. The entering + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#INVISIBLE} to {@link View#VISIBLE}. + * + * @return the Transition to use to move Views into the initial Scene. + */ + public Object getEnterTransition() { + return mEnterTransition; + } + + /** + * Sets the Transition that will be used to move Views out of the scene when the Fragment is + * preparing to be removed, hidden, or detached because of popping the back stack. The exiting + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, + * entering Views will remain unaffected. If nothing is set, the default will be to + * use the same value as set in {@link #setEnterTransition(Object)}. + * + * @param transition The Transition to use to move Views out of the Scene when the Fragment + * is preparing to close. transition must be an + * android.transition.Transition. + */ + public void setReturnTransition(Object transition) { + mReturnTransition = transition; + } + + /** + * Returns the Transition that will be used to move Views out of the scene when the Fragment is + * preparing to be removed, hidden, or detached because of popping the back stack. The exiting + * Views will be those that are regular Views or ViewGroups that have + * {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as entering is governed by changing visibility from + * {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, + * entering Views will remain unaffected. + * + * @return the Transition to use to move Views out of the Scene when the Fragment + * is preparing to close. + */ + public Object getReturnTransition() { + return mReturnTransition == USE_DEFAULT_TRANSITION ? getEnterTransition() + : mReturnTransition; + } + + /** + * Sets the Transition that will be used to move Views out of the scene when the + * fragment is removed, hidden, or detached when not popping the back stack. + * The exiting Views will be those that are regular Views or ViewGroups that + * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as exiting is governed by changing visibility + * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will + * remain unaffected. + * + * @param transition The Transition to use to move Views out of the Scene when the Fragment + * is being closed not due to popping the back stack. transition + * must be an android.transition.Transition. + */ + public void setExitTransition(Object transition) { + mExitTransition = transition; + } + + /** + * Returns the Transition that will be used to move Views out of the scene when the + * fragment is removed, hidden, or detached when not popping the back stack. + * The exiting Views will be those that are regular Views or ViewGroups that + * have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions will extend + * {@link android.transition.Visibility} as exiting is governed by changing visibility + * from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, the views will + * remain unaffected. + * + * @return the Transition to use to move Views out of the Scene when the Fragment + * is being closed not due to popping the back stack. + */ + public Object getExitTransition() { + return mExitTransition; + } + + /** + * Sets the Transition that will be used to move Views in to the scene when returning due + * to popping a back stack. The entering Views will be those that are regular Views + * or ViewGroups that have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions + * will extend {@link android.transition.Visibility} as exiting is governed by changing + * visibility from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, + * the views will remain unaffected. If nothing is set, the default will be to use the same + * transition as {@link #setExitTransition(Object)}. + * + * @param transition The Transition to use to move Views into the scene when reentering from a + * previously-started Activity. transition + * must be an android.transition.Transition. + */ + public void setReenterTransition(Object transition) { + mReenterTransition = transition; + } + + /** + * Returns the Transition that will be used to move Views in to the scene when returning due + * to popping a back stack. The entering Views will be those that are regular Views + * or ViewGroups that have {@link ViewGroup#isTransitionGroup} return true. Typical Transitions + * will extend {@link android.transition.Visibility} as exiting is governed by changing + * visibility from {@link View#VISIBLE} to {@link View#INVISIBLE}. If transition is null, + * the views will remain unaffected. If nothing is set, the default will be to use the same + * transition as {@link #setExitTransition(Object)}. + * + * @return the Transition to use to move Views into the scene when reentering from a + * previously-started Activity. + */ + public Object getReenterTransition() { + return mReenterTransition == USE_DEFAULT_TRANSITION ? getExitTransition() + : mReenterTransition; + } + + /** + * Sets the Transition that will be used for shared elements transferred into the content + * Scene. Typical Transitions will affect size and location, such as + * {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * + * @param transition The Transition to use for shared elements transferred into the content + * Scene. transition must be an android.transition.Transition. + */ + public void setSharedElementEnterTransition(Object transition) { + mSharedElementEnterTransition = transition; + } + + /** + * Returns the Transition that will be used for shared elements transferred into the content + * Scene. Typical Transitions will affect size and location, such as + * {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * + * @return The Transition to use for shared elements transferred into the content + * Scene. + */ + public Object getSharedElementEnterTransition() { + return mSharedElementEnterTransition; + } + + /** + * Sets the Transition that will be used for shared elements transferred back during a + * pop of the back stack. This Transition acts in the leaving Fragment. + * Typical Transitions will affect size and location, such as + * {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * If no value is set, the default will be to use the same value as + * {@link #setSharedElementEnterTransition(Object)}. + * + * @param transition The Transition to use for shared elements transferred out of the content + * Scene. transition must be an android.transition.Transition. + */ + public void setSharedElementReturnTransition(Object transition) { + mSharedElementReturnTransition = transition; + } + + /** + * Return the Transition that will be used for shared elements transferred back during a + * pop of the back stack. This Transition acts in the leaving Fragment. + * Typical Transitions will affect size and location, such as + * {@link android.transition.ChangeBounds}. A null + * value will cause transferred shared elements to blink to the final position. + * If no value is set, the default will be to use the same value as + * {@link #setSharedElementEnterTransition(Object)}. + * + * @return The Transition to use for shared elements transferred out of the content + * Scene. + */ + public Object getSharedElementReturnTransition() { + return mSharedElementReturnTransition == USE_DEFAULT_TRANSITION ? + getSharedElementEnterTransition() : mSharedElementReturnTransition; + } + + /** + * Sets whether the the exit transition and enter transition overlap or not. + * When true, the enter transition will start as soon as possible. When false, the + * enter transition will wait until the exit transition completes before starting. + * + * @param allow true to start the enter transition when possible or false to + * wait until the exiting transition completes. + */ + public void setAllowEnterTransitionOverlap(boolean allow) { + mAllowEnterTransitionOverlap = allow; + } + + /** + * Returns whether the the exit transition and enter transition overlap or not. + * When true, the enter transition will start as soon as possible. When false, the + * enter transition will wait until the exit transition completes before starting. + * + * @return true when the enter transition should start as soon as possible or false to + * when it should wait until the exiting transition completes. + */ + public boolean getAllowEnterTransitionOverlap() { + return (mAllowEnterTransitionOverlap == null) ? true : mAllowEnterTransitionOverlap; + } + + /** + * Sets whether the the return transition and reenter transition overlap or not. + * When true, the reenter transition will start as soon as possible. When false, the + * reenter transition will wait until the return transition completes before starting. + * + * @param allow true to start the reenter transition when possible or false to wait until the + * return transition completes. + */ + public void setAllowReturnTransitionOverlap(boolean allow) { + mAllowReturnTransitionOverlap = allow; + } + + /** + * Returns whether the the return transition and reenter transition overlap or not. + * When true, the reenter transition will start as soon as possible. When false, the + * reenter transition will wait until the return transition completes before starting. + * + * @return true to start the reenter transition when possible or false to wait until the + * return transition completes. + */ + public boolean getAllowReturnTransitionOverlap() { + return (mAllowReturnTransitionOverlap == null) ? true : mAllowReturnTransitionOverlap; + } + + /** + * Print the Fragments's state into the given stream. + * + * @param prefix Text to print at the front of each line. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer The PrintWriter to which you should dump your state. This will be + * closed for you after you return. + * @param args additional arguments to the dump request. + */ + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + writer.print(prefix); writer.print("mFragmentId=#"); + writer.print(Integer.toHexString(mFragmentId)); + writer.print(" mContainerId=#"); + writer.print(Integer.toHexString(mContainerId)); + writer.print(" mTag="); writer.println(mTag); + writer.print(prefix); writer.print("mState="); writer.print(mState); + writer.print(" mIndex="); writer.print(mIndex); + writer.print(" mWho="); writer.print(mWho); + writer.print(" mBackStackNesting="); writer.println(mBackStackNesting); + writer.print(prefix); writer.print("mAdded="); writer.print(mAdded); + writer.print(" mRemoving="); writer.print(mRemoving); + writer.print(" mResumed="); writer.print(mResumed); + writer.print(" mFromLayout="); writer.print(mFromLayout); + writer.print(" mInLayout="); writer.println(mInLayout); + writer.print(prefix); writer.print("mHidden="); writer.print(mHidden); + writer.print(" mDetached="); writer.print(mDetached); + writer.print(" mMenuVisible="); writer.print(mMenuVisible); + writer.print(" mHasMenu="); writer.println(mHasMenu); + writer.print(prefix); writer.print("mRetainInstance="); writer.print(mRetainInstance); + writer.print(" mRetaining="); writer.print(mRetaining); + writer.print(" mUserVisibleHint="); writer.println(mUserVisibleHint); + if (mFragmentManager != null) { + writer.print(prefix); writer.print("mFragmentManager="); + writer.println(mFragmentManager); + } + if (mActivity != null) { + writer.print(prefix); writer.print("mActivity="); + writer.println(mActivity); + } + if (mParentFragment != null) { + writer.print(prefix); writer.print("mParentFragment="); + writer.println(mParentFragment); + } + if (mArguments != null) { + writer.print(prefix); writer.print("mArguments="); writer.println(mArguments); + } + if (mSavedFragmentState != null) { + writer.print(prefix); writer.print("mSavedFragmentState="); + writer.println(mSavedFragmentState); + } + if (mSavedViewState != null) { + writer.print(prefix); writer.print("mSavedViewState="); + writer.println(mSavedViewState); + } + if (mTarget != null) { + writer.print(prefix); writer.print("mTarget="); writer.print(mTarget); + writer.print(" mTargetRequestCode="); + writer.println(mTargetRequestCode); + } + if (mNextAnim != 0) { + writer.print(prefix); writer.print("mNextAnim="); writer.println(mNextAnim); + } + if (mContainer != null) { + writer.print(prefix); writer.print("mContainer="); writer.println(mContainer); + } + if (mView != null) { + writer.print(prefix); writer.print("mView="); writer.println(mView); + } + if (mInnerView != null) { + writer.print(prefix); writer.print("mInnerView="); writer.println(mView); + } + if (mAnimatingAway != null) { + writer.print(prefix); writer.print("mAnimatingAway="); writer.println(mAnimatingAway); + writer.print(prefix); writer.print("mStateAfterAnimating="); + writer.println(mStateAfterAnimating); + } + if (mLoaderManager != null) { + writer.print(prefix); writer.println("Loader Manager:"); + mLoaderManager.dump(prefix + " ", fd, writer, args); + } + if (mChildFragmentManager != null) { + writer.print(prefix); writer.println("Child " + mChildFragmentManager + ":"); + mChildFragmentManager.dump(prefix + " ", fd, writer, args); + } + } + + Fragment findFragmentByWho(String who) { + if (who.equals(mWho)) { + return this; + } + if (mChildFragmentManager != null) { + return mChildFragmentManager.findFragmentByWho(who); + } + return null; + } + + void instantiateChildFragmentManager() { + mChildFragmentManager = new FragmentManagerImpl(); + mChildFragmentManager.attachActivity(mActivity, new FragmentContainer() { + @Override + @Nullable + public View findViewById(int id) { + if (mView == null) { + throw new IllegalStateException("Fragment does not have a view"); + } + return mView.findViewById(id); + } + + @Override + public boolean hasView() { + return (mView != null); + } + }, this); + } + + void performCreate(Bundle savedInstanceState) { + if (mChildFragmentManager != null) { + mChildFragmentManager.noteStateNotSaved(); + } + mCalled = false; + onCreate(savedInstanceState); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onCreate()"); + } + if (savedInstanceState != null) { + Parcelable p = savedInstanceState.getParcelable( + FragmentActivity.FRAGMENTS_TAG); + if (p != null) { + if (mChildFragmentManager == null) { + instantiateChildFragmentManager(); + } + mChildFragmentManager.restoreAllState(p, null); + mChildFragmentManager.dispatchCreate(); + } + } + } + + View performCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + if (mChildFragmentManager != null) { + mChildFragmentManager.noteStateNotSaved(); + } + return onCreateView(inflater, container, savedInstanceState); + } + + void performActivityCreated(Bundle savedInstanceState) { + if (mChildFragmentManager != null) { + mChildFragmentManager.noteStateNotSaved(); + } + mCalled = false; + onActivityCreated(savedInstanceState); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onActivityCreated()"); + } + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchActivityCreated(); + } + } + + void performStart() { + if (mChildFragmentManager != null) { + mChildFragmentManager.noteStateNotSaved(); + mChildFragmentManager.execPendingActions(); + } + mCalled = false; + onStart(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onStart()"); + } + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchStart(); + } + if (mLoaderManager != null) { + mLoaderManager.doReportStart(); + } + } + + void performResume() { + if (mChildFragmentManager != null) { + mChildFragmentManager.noteStateNotSaved(); + mChildFragmentManager.execPendingActions(); + } + mCalled = false; + onResume(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onResume()"); + } + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchResume(); + mChildFragmentManager.execPendingActions(); + } + } + + void performConfigurationChanged(Configuration newConfig) { + onConfigurationChanged(newConfig); + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchConfigurationChanged(newConfig); + } + } + + void performLowMemory() { + onLowMemory(); + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchLowMemory(); + } + } + + /* + void performTrimMemory(int level) { + onTrimMemory(level); + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchTrimMemory(level); + } + } + */ + + boolean performCreateOptionsMenu(Menu menu, MenuInflater inflater) { + boolean show = false; + if (!mHidden) { + if (mHasMenu && mMenuVisible) { + show = true; + onCreateOptionsMenu(menu, inflater); + } + if (mChildFragmentManager != null) { + show |= mChildFragmentManager.dispatchCreateOptionsMenu(menu, inflater); + } + } + return show; + } + + boolean performPrepareOptionsMenu(Menu menu) { + boolean show = false; + if (!mHidden) { + if (mHasMenu && mMenuVisible) { + show = true; + onPrepareOptionsMenu(menu); + } + if (mChildFragmentManager != null) { + show |= mChildFragmentManager.dispatchPrepareOptionsMenu(menu); + } + } + return show; + } + + boolean performOptionsItemSelected(MenuItem item) { + if (!mHidden) { + if (mHasMenu && mMenuVisible) { + if (onOptionsItemSelected(item)) { + return true; + } + } + if (mChildFragmentManager != null) { + if (mChildFragmentManager.dispatchOptionsItemSelected(item)) { + return true; + } + } + } + return false; + } + + boolean performContextItemSelected(MenuItem item) { + if (!mHidden) { + if (onContextItemSelected(item)) { + return true; + } + if (mChildFragmentManager != null) { + if (mChildFragmentManager.dispatchContextItemSelected(item)) { + return true; + } + } + } + return false; + } + + void performOptionsMenuClosed(Menu menu) { + if (!mHidden) { + if (mHasMenu && mMenuVisible) { + onOptionsMenuClosed(menu); + } + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchOptionsMenuClosed(menu); + } + } + } + + void performSaveInstanceState(Bundle outState) { + onSaveInstanceState(outState); + if (mChildFragmentManager != null) { + Parcelable p = mChildFragmentManager.saveAllState(); + if (p != null) { + outState.putParcelable(FragmentActivity.FRAGMENTS_TAG, p); + } + } + } + + void performPause() { + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchPause(); + } + mCalled = false; + onPause(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onPause()"); + } + } + + void performStop() { + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchStop(); + } + mCalled = false; + onStop(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onStop()"); + } + } + + void performReallyStop() { + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchReallyStop(); + } + if (mLoadersStarted) { + mLoadersStarted = false; + if (!mCheckedForLoaderManager) { + mCheckedForLoaderManager = true; + mLoaderManager = mActivity.getLoaderManager(mWho, mLoadersStarted, false); + } + if (mLoaderManager != null) { + if (!mActivity.mRetaining) { + mLoaderManager.doStop(); + } else { + mLoaderManager.doRetain(); + } + } + } + } + + void performDestroyView() { + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchDestroyView(); + } + mCalled = false; + onDestroyView(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onDestroyView()"); + } + if (mLoaderManager != null) { + mLoaderManager.doReportNextStart(); + } + } + + void performDestroy() { + if (mChildFragmentManager != null) { + mChildFragmentManager.dispatchDestroy(); + } + mCalled = false; + onDestroy(); + if (!mCalled) { + throw new SuperNotCalledException("Fragment " + this + + " did not call through to super.onDestroy()"); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/FragmentActivity.java b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentActivity.java new file mode 100644 index 00000000000..566ffed6734 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentActivity.java @@ -0,0 +1,880 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.os.Parcelable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.util.SimpleArrayMap; +import android.util.AttributeSet; +import android.util.Log; +import android.view.KeyEvent; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; + +/** + * Base class for activities that want to use the support-based + * {@link android.support.v4.app.Fragment} and + * {@link android.support.v4.content.Loader} APIs. + * + *

When using this class as opposed to new platform's built-in fragment + * and loader support, you must use the {@link #getSupportFragmentManager()} + * and {@link #getSupportLoaderManager()} methods respectively to access + * those features. + * + *

Note: If you want to implement an activity that includes + * an action bar, you should instead use + * the {@link android.support.v7.app.ActionBarActivity} class, which is a subclass of this one, + * so allows you to use {@link android.support.v4.app.Fragment} APIs on API level 7 and higher.

+ * + *

Known limitations:

+ *
    + *
  • When using the <fragment> tag, this implementation can not + * use the parent view's ID as the new fragment's ID. You must explicitly + * specify an ID (or tag) in the <fragment>.

    + *
  • Prior to Honeycomb (3.0), an activity's state was saved before pausing. + * Fragments are a significant amount of new state, and dynamic enough that one + * often wants them to change between pausing and stopping. These classes + * throw an exception if you try to change the fragment state after it has been + * saved, to avoid accidental loss of UI state. However this is too restrictive + * prior to Honeycomb, where the state is saved before pausing. To address this, + * when running on platforms prior to Honeycomb an exception will not be thrown + * if you change fragments between the state save and the activity being stopped. + * This means that in some cases if the activity is restored from its last saved + * state, this may be a snapshot slightly before what the user last saw.

    + *
+ */ +public class FragmentActivity extends Activity { + private static final String TAG = "FragmentActivity"; + + static final String FRAGMENTS_TAG = "android:support:fragments"; + + // This is the SDK API version of Honeycomb (3.0). + private static final int HONEYCOMB = 11; + + static final int MSG_REALLY_STOPPED = 1; + static final int MSG_RESUME_PENDING = 2; + + final Handler mHandler = new Handler() { + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_REALLY_STOPPED: + if (mStopped) { + doReallyStop(false); + } + break; + case MSG_RESUME_PENDING: + onResumeFragments(); + mFragments.execPendingActions(); + break; + default: + super.handleMessage(msg); + } + } + + }; + final FragmentManagerImpl mFragments = new FragmentManagerImpl(); + final FragmentContainer mContainer = new FragmentContainer() { + @Override + @Nullable + public View findViewById(int id) { + return FragmentActivity.this.findViewById(id); + } + + @Override + public boolean hasView() { + Window window = FragmentActivity.this.getWindow(); + return (window != null && window.peekDecorView() != null); + } + }; + + boolean mCreated; + boolean mResumed; + boolean mStopped; + boolean mReallyStopped; + boolean mRetaining; + + boolean mOptionsMenuInvalidated; + + boolean mCheckedForLoaderManager; + boolean mLoadersStarted; + SimpleArrayMap mAllLoaderManagers; + LoaderManagerImpl mLoaderManager; + + static final class NonConfigurationInstances { + Object activity; + Object custom; + SimpleArrayMap children; + ArrayList fragments; + SimpleArrayMap loaders; + } + + // ------------------------------------------------------------------------ + // HOOKS INTO ACTIVITY + // ------------------------------------------------------------------------ + + /** + * Dispatch incoming result to the correct fragment. + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + mFragments.noteStateNotSaved(); + int index = requestCode>>16; + if (index != 0) { + index--; + if (mFragments.mActive == null || index < 0 || index >= mFragments.mActive.size()) { + Log.w(TAG, "Activity result fragment index out of range: 0x" + + Integer.toHexString(requestCode)); + return; + } + Fragment frag = mFragments.mActive.get(index); + if (frag == null) { + Log.w(TAG, "Activity result no fragment exists for index: 0x" + + Integer.toHexString(requestCode)); + } else { + frag.onActivityResult(requestCode&0xffff, resultCode, data); + } + return; + } + + super.onActivityResult(requestCode, resultCode, data); + } + + /** + * Take care of popping the fragment back stack or finishing the activity + * as appropriate. + */ + public void onBackPressed() { + if (!mFragments.popBackStackImmediate()) { + supportFinishAfterTransition(); + } + } + + /** + * Reverses the Activity Scene entry Transition and triggers the calling Activity + * to reverse its exit Transition. When the exit Transition completes, + * {@link #finish()} is called. If no entry Transition was used, finish() is called + * immediately and the Activity exit Transition is run. + * + *

On Android 4.4 or lower, this method only finishes the Activity with no + * special exit transition.

+ */ + public void supportFinishAfterTransition() { + ActivityCompat.finishAfterTransition(this); + } + + /** + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, + * android.view.View, String)} was used to start an Activity, callback + * will be called to handle shared elements on the launched Activity. This requires + * {@link Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param callback Used to manipulate shared element transitions on the launched Activity. + */ + public void setEnterSharedElementCallback(SharedElementCallback callback) { + ActivityCompat.setEnterSharedElementCallback(this, callback); + } + + /** + * When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity, + * android.view.View, String)} was used to start an Activity, listener + * will be called to handle shared elements on the launching Activity. Most + * calls will only come when returning from the started Activity. + * This requires {@link Window#FEATURE_CONTENT_TRANSITIONS}. + * + * @param listener Used to manipulate shared element transitions on the launching Activity. + */ + public void setExitSharedElementCallback(SharedElementCallback listener) { + ActivityCompat.setExitSharedElementCallback(this, listener); + } + + /** + * Support library version of {@link android.app.Activity#postponeEnterTransition()} that works + * only on API 21 and later. + */ + public void supportPostponeEnterTransition() { + ActivityCompat.postponeEnterTransition(this); + } + + /** + * Support library version of {@link android.app.Activity#startPostponedEnterTransition()} + * that only works with API 21 and later. + */ + public void supportStartPostponedEnterTransition() { + ActivityCompat.startPostponedEnterTransition(this); + } + + /** + * Dispatch configuration change to all fragments. + */ + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + mFragments.dispatchConfigurationChanged(newConfig); + } + + /** + * Perform initialization of all fragments and loaders. + */ + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + mFragments.attachActivity(this, mContainer, null); + // Old versions of the platform didn't do this! + if (getLayoutInflater().getFactory() == null) { + getLayoutInflater().setFactory(this); + } + + super.onCreate(savedInstanceState); + + NonConfigurationInstances nc = (NonConfigurationInstances) + getLastNonConfigurationInstance(); + if (nc != null) { + mAllLoaderManagers = nc.loaders; + } + if (savedInstanceState != null) { + Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG); + mFragments.restoreAllState(p, nc != null ? nc.fragments : null); + } + mFragments.dispatchCreate(); + } + + /** + * Dispatch to Fragment.onCreateOptionsMenu(). + */ + @Override + public boolean onCreatePanelMenu(int featureId, Menu menu) { + if (featureId == Window.FEATURE_OPTIONS_PANEL) { + boolean show = super.onCreatePanelMenu(featureId, menu); + show |= mFragments.dispatchCreateOptionsMenu(menu, getMenuInflater()); + if (android.os.Build.VERSION.SDK_INT >= HONEYCOMB) { + return show; + } + // Prior to Honeycomb, the framework can't invalidate the options + // menu, so we must always say we have one in case the app later + // invalidates it and needs to have it shown. + return true; + } + return super.onCreatePanelMenu(featureId, menu); + } + + /** + * Add support for inflating the <fragment> tag. + */ + @Override + @Nullable + public View onCreateView(String name, @NonNull Context context, @NonNull AttributeSet attrs) { + if (!"fragment".equals(name)) { + return super.onCreateView(name, context, attrs); + } + + final View v = mFragments.onCreateView(null, name, context, attrs); + if (v == null) { + return super.onCreateView(name, context, attrs); + } + return v; + } + + /** + * Destroy all fragments and loaders. + */ + @Override + protected void onDestroy() { + super.onDestroy(); + + doReallyStop(false); + + mFragments.dispatchDestroy(); + if (mLoaderManager != null) { + mLoaderManager.doDestroy(); + } + } + + /** + * Take care of calling onBackPressed() for pre-Eclair platforms. + */ + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (android.os.Build.VERSION.SDK_INT < 5 /* ECLAIR */ + && keyCode == KeyEvent.KEYCODE_BACK + && event.getRepeatCount() == 0) { + // Take care of calling this method on earlier versions of + // the platform where it doesn't exist. + onBackPressed(); + return true; + } + + return super.onKeyDown(keyCode, event); + } + + /** + * Dispatch onLowMemory() to all fragments. + */ + @Override + public void onLowMemory() { + super.onLowMemory(); + mFragments.dispatchLowMemory(); + } + + /** + * Dispatch context and options menu to fragments. + */ + @Override + public boolean onMenuItemSelected(int featureId, MenuItem item) { + if (super.onMenuItemSelected(featureId, item)) { + return true; + } + + switch (featureId) { + case Window.FEATURE_OPTIONS_PANEL: + return mFragments.dispatchOptionsItemSelected(item); + + case Window.FEATURE_CONTEXT_MENU: + return mFragments.dispatchContextItemSelected(item); + + default: + return false; + } + } + + /** + * Call onOptionsMenuClosed() on fragments. + */ + @Override + public void onPanelClosed(int featureId, Menu menu) { + switch (featureId) { + case Window.FEATURE_OPTIONS_PANEL: + mFragments.dispatchOptionsMenuClosed(menu); + break; + } + super.onPanelClosed(featureId, menu); + } + + /** + * Dispatch onPause() to fragments. + */ + @Override + protected void onPause() { + super.onPause(); + mResumed = false; + if (mHandler.hasMessages(MSG_RESUME_PENDING)) { + mHandler.removeMessages(MSG_RESUME_PENDING); + onResumeFragments(); + } + mFragments.dispatchPause(); + } + + /** + * Handle onNewIntent() to inform the fragment manager that the + * state is not saved. If you are handling new intents and may be + * making changes to the fragment state, you want to be sure to call + * through to the super-class here first. Otherwise, if your state + * is saved but the activity is not stopped, you could get an + * onNewIntent() call which happens before onResume() and trying to + * perform fragment operations at that point will throw IllegalStateException + * because the fragment manager thinks the state is still saved. + */ + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + mFragments.noteStateNotSaved(); + } + + /** + * Dispatch onResume() to fragments. Note that for better inter-operation + * with older versions of the platform, at the point of this call the + * fragments attached to the activity are not resumed. This means + * that in some cases the previous state may still be saved, not allowing + * fragment transactions that modify the state. To correctly interact + * with fragments in their proper state, you should instead override + * {@link #onResumeFragments()}. + */ + @Override + protected void onResume() { + super.onResume(); + mHandler.sendEmptyMessage(MSG_RESUME_PENDING); + mResumed = true; + mFragments.execPendingActions(); + } + + /** + * Dispatch onResume() to fragments. + */ + @Override + protected void onPostResume() { + super.onPostResume(); + mHandler.removeMessages(MSG_RESUME_PENDING); + onResumeFragments(); + mFragments.execPendingActions(); + } + + /** + * This is the fragment-orientated version of {@link #onResume()} that you + * can override to perform operations in the Activity at the same point + * where its fragments are resumed. Be sure to always call through to + * the super-class. + */ + protected void onResumeFragments() { + mFragments.dispatchResume(); + } + + /** + * Dispatch onPrepareOptionsMenu() to fragments. + */ + @Override + public boolean onPreparePanel(int featureId, View view, Menu menu) { + if (featureId == Window.FEATURE_OPTIONS_PANEL && menu != null) { + if (mOptionsMenuInvalidated) { + mOptionsMenuInvalidated = false; + menu.clear(); + onCreatePanelMenu(featureId, menu); + } + boolean goforit = onPrepareOptionsPanel(view, menu); + goforit |= mFragments.dispatchPrepareOptionsMenu(menu); + return goforit; + } + return super.onPreparePanel(featureId, view, menu); + } + + /** + * @hide + */ + protected boolean onPrepareOptionsPanel(View view, Menu menu) { + return super.onPreparePanel(Window.FEATURE_OPTIONS_PANEL, view, menu); + } + + /** + * Retain all appropriate fragment and loader state. You can NOT + * override this yourself! Use {@link #onRetainCustomNonConfigurationInstance()} + * if you want to retain your own state. + */ + @Override + public final Object onRetainNonConfigurationInstance() { + if (mStopped) { + doReallyStop(true); + } + + Object custom = onRetainCustomNonConfigurationInstance(); + + ArrayList fragments = mFragments.retainNonConfig(); + boolean retainLoaders = false; + if (mAllLoaderManagers != null) { + // prune out any loader managers that were already stopped and so + // have nothing useful to retain. + final int N = mAllLoaderManagers.size(); + LoaderManagerImpl loaders[] = new LoaderManagerImpl[N]; + for (int i=N-1; i>=0; i--) { + loaders[i] = mAllLoaderManagers.valueAt(i); + } + for (int i=0; i=0; i--) { + loaders[i] = mAllLoaderManagers.valueAt(i); + } + for (int i=0; iInvalidate the activity's options menu. This will cause relevant presentations + * of the menu to fully update via calls to onCreateOptionsMenu and + * onPrepareOptionsMenu the next time the menu is requested. + */ + public void supportInvalidateOptionsMenu() { + if (android.os.Build.VERSION.SDK_INT >= HONEYCOMB) { + // If we are running on HC or greater, we can use the framework + // API to invalidate the options menu. + ActivityCompatHoneycomb.invalidateOptionsMenu(this); + return; + } + + // Whoops, older platform... we'll use a hack, to manually rebuild + // the options menu the next time it is prepared. + mOptionsMenuInvalidated = true; + } + + /** + * Print the Activity's state into the given stream. This gets invoked if + * you run "adb shell dumpsys activity ". + * + * @param prefix Desired prefix to prepend at each line of output. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer The PrintWriter to which you should dump your state. This will be + * closed for you after you return. + * @param args additional arguments to the dump request. + */ + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + if (android.os.Build.VERSION.SDK_INT >= HONEYCOMB) { + // XXX This can only work if we can call the super-class impl. :/ + //ActivityCompatHoneycomb.dump(this, prefix, fd, writer, args); + } + writer.print(prefix); writer.print("Local FragmentActivity "); + writer.print(Integer.toHexString(System.identityHashCode(this))); + writer.println(" State:"); + String innerPrefix = prefix + " "; + writer.print(innerPrefix); writer.print("mCreated="); + writer.print(mCreated); writer.print("mResumed="); + writer.print(mResumed); writer.print(" mStopped="); + writer.print(mStopped); writer.print(" mReallyStopped="); + writer.println(mReallyStopped); + writer.print(innerPrefix); writer.print("mLoadersStarted="); + writer.println(mLoadersStarted); + if (mLoaderManager != null) { + writer.print(prefix); writer.print("Loader Manager "); + writer.print(Integer.toHexString(System.identityHashCode(mLoaderManager))); + writer.println(":"); + mLoaderManager.dump(prefix + " ", fd, writer, args); + } + mFragments.dump(prefix, fd, writer, args); + writer.print(prefix); writer.println("View Hierarchy:"); + dumpViewHierarchy(prefix + " ", writer, getWindow().getDecorView()); + } + + private static String viewToString(View view) { + StringBuilder out = new StringBuilder(128); + out.append(view.getClass().getName()); + out.append('{'); + out.append(Integer.toHexString(System.identityHashCode(view))); + out.append(' '); + switch (view.getVisibility()) { + case View.VISIBLE: out.append('V'); break; + case View.INVISIBLE: out.append('I'); break; + case View.GONE: out.append('G'); break; + default: out.append('.'); break; + } + out.append(view.isFocusable() ? 'F' : '.'); + out.append(view.isEnabled() ? 'E' : '.'); + out.append(view.willNotDraw() ? '.' : 'D'); + out.append(view.isHorizontalScrollBarEnabled()? 'H' : '.'); + out.append(view.isVerticalScrollBarEnabled() ? 'V' : '.'); + out.append(view.isClickable() ? 'C' : '.'); + out.append(view.isLongClickable() ? 'L' : '.'); + out.append(' '); + out.append(view.isFocused() ? 'F' : '.'); + out.append(view.isSelected() ? 'S' : '.'); + out.append(view.isPressed() ? 'P' : '.'); + out.append(' '); + out.append(view.getLeft()); + out.append(','); + out.append(view.getTop()); + out.append('-'); + out.append(view.getRight()); + out.append(','); + out.append(view.getBottom()); + final int id = view.getId(); + if (id != View.NO_ID) { + out.append(" #"); + out.append(Integer.toHexString(id)); + final Resources r = view.getResources(); + if (id != 0 && r != null) { + try { + String pkgname; + switch (id&0xff000000) { + case 0x7f000000: + pkgname="app"; + break; + case 0x01000000: + pkgname="android"; + break; + default: + pkgname = r.getResourcePackageName(id); + break; + } + String typename = r.getResourceTypeName(id); + String entryname = r.getResourceEntryName(id); + out.append(" "); + out.append(pkgname); + out.append(":"); + out.append(typename); + out.append("/"); + out.append(entryname); + } catch (Resources.NotFoundException e) { + } + } + } + out.append("}"); + return out.toString(); + } + + private void dumpViewHierarchy(String prefix, PrintWriter writer, View view) { + writer.print(prefix); + if (view == null) { + writer.println("null"); + return; + } + writer.println(viewToString(view)); + if (!(view instanceof ViewGroup)) { + return; + } + ViewGroup grp = (ViewGroup)view; + final int N = grp.getChildCount(); + if (N <= 0) { + return; + } + prefix = prefix + " "; + for (int i=0; i(); + } + LoaderManagerImpl lm = mAllLoaderManagers.get(who); + if (lm == null) { + if (create) { + lm = new LoaderManagerImpl(who, this, started); + mAllLoaderManagers.put(who, lm); + } + } else { + lm.updateActivity(this); + } + return lm; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/FragmentManager.java b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentManager.java new file mode 100644 index 00000000000..6d41d455692 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentManager.java @@ -0,0 +1,2224 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.TypedArray; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.IdRes; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.v4.util.DebugUtils; +import android.support.v4.util.LogWriter; +import android.support.v4.view.LayoutInflaterFactory; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.animation.AnimationSet; +import android.view.animation.AnimationUtils; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.ScaleAnimation; +import android.view.animation.Animation.AnimationListener; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Static library support version of the framework's {@link android.app.FragmentManager}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework {@link FragmentManager} + * documentation for a class overview. + * + *

Your activity must derive from {@link FragmentActivity} to use this. From such an activity, + * you can acquire the {@link FragmentManager} by calling + * {@link FragmentActivity#getSupportFragmentManager}. + */ +public abstract class FragmentManager { + /** + * Representation of an entry on the fragment back stack, as created + * with {@link FragmentTransaction#addToBackStack(String) + * FragmentTransaction.addToBackStack()}. Entries can later be + * retrieved with {@link FragmentManager#getBackStackEntryAt(int) + * FragmentManager.getBackStackEntry()}. + * + *

Note that you should never hold on to a BackStackEntry object; + * the identifier as returned by {@link #getId} is the only thing that + * will be persisted across activity instances. + */ + public interface BackStackEntry { + /** + * Return the unique identifier for the entry. This is the only + * representation of the entry that will persist across activity + * instances. + */ + public int getId(); + + /** + * Get the name that was supplied to + * {@link FragmentTransaction#addToBackStack(String) + * FragmentTransaction.addToBackStack(String)} when creating this entry. + */ + public String getName(); + + /** + * Return the full bread crumb title resource identifier for the entry, + * or 0 if it does not have one. + */ + @StringRes + public int getBreadCrumbTitleRes(); + + /** + * Return the short bread crumb title resource identifier for the entry, + * or 0 if it does not have one. + */ + @StringRes + public int getBreadCrumbShortTitleRes(); + + /** + * Return the full bread crumb title for the entry, or null if it + * does not have one. + */ + public CharSequence getBreadCrumbTitle(); + + /** + * Return the short bread crumb title for the entry, or null if it + * does not have one. + */ + public CharSequence getBreadCrumbShortTitle(); + } + + /** + * Interface to watch for changes to the back stack. + */ + public interface OnBackStackChangedListener { + /** + * Called whenever the contents of the back stack change. + */ + public void onBackStackChanged(); + } + + /** + * Start a series of edit operations on the Fragments associated with + * this FragmentManager. + * + *

Note: A fragment transaction can only be created/committed prior + * to an activity saving its state. If you try to commit a transaction + * after {@link FragmentActivity#onSaveInstanceState FragmentActivity.onSaveInstanceState()} + * (and prior to a following {@link FragmentActivity#onStart FragmentActivity.onStart} + * or {@link FragmentActivity#onResume FragmentActivity.onResume()}, you will get an error. + * This is because the framework takes care of saving your current fragments + * in the state, and if changes are made after the state is saved then they + * will be lost.

+ */ + public abstract FragmentTransaction beginTransaction(); + + /** @hide -- remove once prebuilts are in. */ + @Deprecated + public FragmentTransaction openTransaction() { + return beginTransaction(); + } + + /** + * After a {@link FragmentTransaction} is committed with + * {@link FragmentTransaction#commit FragmentTransaction.commit()}, it + * is scheduled to be executed asynchronously on the process's main thread. + * If you want to immediately executing any such pending operations, you + * can call this function (only from the main thread) to do so. Note that + * all callbacks and other related behavior will be done from within this + * call, so be careful about where this is called from. + * + * @return Returns true if there were any pending transactions to be + * executed. + */ + public abstract boolean executePendingTransactions(); + + /** + * Finds a fragment that was identified by the given id either when inflated + * from XML or as the container ID when added in a transaction. This first + * searches through fragments that are currently added to the manager's + * activity; if no such fragment is found, then all fragments currently + * on the back stack associated with this ID are searched. + * @return The fragment if found or null otherwise. + */ + public abstract Fragment findFragmentById(@IdRes int id); + + /** + * Finds a fragment that was identified by the given tag either when inflated + * from XML or as supplied when added in a transaction. This first + * searches through fragments that are currently added to the manager's + * activity; if no such fragment is found, then all fragments currently + * on the back stack are searched. + * @return The fragment if found or null otherwise. + */ + public abstract Fragment findFragmentByTag(String tag); + + /** + * Flag for {@link #popBackStack(String, int)} + * and {@link #popBackStack(int, int)}: If set, and the name or ID of + * a back stack entry has been supplied, then all matching entries will + * be consumed until one that doesn't match is found or the bottom of + * the stack is reached. Otherwise, all entries up to but not including that entry + * will be removed. + */ + public static final int POP_BACK_STACK_INCLUSIVE = 1<<0; + + /** + * Pop the top state off the back stack. Returns true if there was one + * to pop, else false. This function is asynchronous -- it enqueues the + * request to pop, but the action will not be performed until the application + * returns to its event loop. + */ + public abstract void popBackStack(); + + /** + * Like {@link #popBackStack()}, but performs the operation immediately + * inside of the call. This is like calling {@link #executePendingTransactions()} + * afterwards. + * @return Returns true if there was something popped, else false. + */ + public abstract boolean popBackStackImmediate(); + + /** + * Pop the last fragment transition from the manager's fragment + * back stack. If there is nothing to pop, false is returned. + * This function is asynchronous -- it enqueues the + * request to pop, but the action will not be performed until the application + * returns to its event loop. + * + * @param name If non-null, this is the name of a previous back state + * to look for; if found, all states up to that state will be popped. The + * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether + * the named state itself is popped. If null, only the top state is popped. + * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}. + */ + public abstract void popBackStack(String name, int flags); + + /** + * Like {@link #popBackStack(String, int)}, but performs the operation immediately + * inside of the call. This is like calling {@link #executePendingTransactions()} + * afterwards. + * @return Returns true if there was something popped, else false. + */ + public abstract boolean popBackStackImmediate(String name, int flags); + + /** + * Pop all back stack states up to the one with the given identifier. + * This function is asynchronous -- it enqueues the + * request to pop, but the action will not be performed until the application + * returns to its event loop. + * + * @param id Identifier of the stated to be popped. If no identifier exists, + * false is returned. + * The identifier is the number returned by + * {@link FragmentTransaction#commit() FragmentTransaction.commit()}. The + * {@link #POP_BACK_STACK_INCLUSIVE} flag can be used to control whether + * the named state itself is popped. + * @param flags Either 0 or {@link #POP_BACK_STACK_INCLUSIVE}. + */ + public abstract void popBackStack(int id, int flags); + + /** + * Like {@link #popBackStack(int, int)}, but performs the operation immediately + * inside of the call. This is like calling {@link #executePendingTransactions()} + * afterwards. + * @return Returns true if there was something popped, else false. + */ + public abstract boolean popBackStackImmediate(int id, int flags); + + /** + * Return the number of entries currently in the back stack. + */ + public abstract int getBackStackEntryCount(); + + /** + * Return the BackStackEntry at index index in the back stack; + * entries start index 0 being the bottom of the stack. + */ + public abstract BackStackEntry getBackStackEntryAt(int index); + + /** + * Add a new listener for changes to the fragment back stack. + */ + public abstract void addOnBackStackChangedListener(OnBackStackChangedListener listener); + + /** + * Remove a listener that was previously added with + * {@link #addOnBackStackChangedListener(OnBackStackChangedListener)}. + */ + public abstract void removeOnBackStackChangedListener(OnBackStackChangedListener listener); + + /** + * Put a reference to a fragment in a Bundle. This Bundle can be + * persisted as saved state, and when later restoring + * {@link #getFragment(Bundle, String)} will return the current + * instance of the same fragment. + * + * @param bundle The bundle in which to put the fragment reference. + * @param key The name of the entry in the bundle. + * @param fragment The Fragment whose reference is to be stored. + */ + public abstract void putFragment(Bundle bundle, String key, Fragment fragment); + + /** + * Retrieve the current Fragment instance for a reference previously + * placed with {@link #putFragment(Bundle, String, Fragment)}. + * + * @param bundle The bundle from which to retrieve the fragment reference. + * @param key The name of the entry in the bundle. + * @return Returns the current Fragment instance that is associated with + * the given reference. + */ + public abstract Fragment getFragment(Bundle bundle, String key); + + /** + * Get a list of all fragments that have been added to the fragment manager. + * + * @return The list of all fragments or null if none. + * @hide + */ + public abstract List getFragments(); + + /** + * Save the current instance state of the given Fragment. This can be + * used later when creating a new instance of the Fragment and adding + * it to the fragment manager, to have it create itself to match the + * current state returned here. Note that there are limits on how + * this can be used: + * + *
    + *
  • The Fragment must currently be attached to the FragmentManager. + *
  • A new Fragment created using this saved state must be the same class + * type as the Fragment it was created from. + *
  • The saved state can not contain dependencies on other fragments -- + * that is it can't use {@link #putFragment(Bundle, String, Fragment)} to + * store a fragment reference because that reference may not be valid when + * this saved state is later used. Likewise the Fragment's target and + * result code are not included in this state. + *
+ * + * @param f The Fragment whose state is to be saved. + * @return The generated state. This will be null if there was no + * interesting state created by the fragment. + */ + public abstract Fragment.SavedState saveFragmentInstanceState(Fragment f); + + /** + * Returns true if the final {@link android.app.Activity#onDestroy() Activity.onDestroy()} + * call has been made on the FragmentManager's Activity, so this instance is now dead. + */ + public abstract boolean isDestroyed(); + + /** + * Print the FragmentManager's state into the given stream. + * + * @param prefix Text to print at the front of each line. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer A PrintWriter to which the dump is to be set. + * @param args Additional arguments to the dump request. + */ + public abstract void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args); + + /** + * Control whether the framework's internal fragment manager debugging + * logs are turned on. If enabled, you will see output in logcat as + * the framework performs fragment operations. + */ + public static void enableDebugLogging(boolean enabled) { + FragmentManagerImpl.DEBUG = enabled; + } +} + +final class FragmentManagerState implements Parcelable { + FragmentState[] mActive; + int[] mAdded; + BackStackState[] mBackStack; + + public FragmentManagerState() { + } + + public FragmentManagerState(Parcel in) { + mActive = in.createTypedArray(FragmentState.CREATOR); + mAdded = in.createIntArray(); + mBackStack = in.createTypedArray(BackStackState.CREATOR); + } + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel dest, int flags) { + dest.writeTypedArray(mActive, flags); + dest.writeIntArray(mAdded); + dest.writeTypedArray(mBackStack, flags); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public FragmentManagerState createFromParcel(Parcel in) { + return new FragmentManagerState(in); + } + + public FragmentManagerState[] newArray(int size) { + return new FragmentManagerState[size]; + } + }; +} + +/** + * Callbacks from FragmentManagerImpl to its container. + */ +interface FragmentContainer { + @Nullable + public View findViewById(@IdRes int id); + public boolean hasView(); +} + +/** + * Container for fragments associated with an activity. + */ +final class FragmentManagerImpl extends FragmentManager implements LayoutInflaterFactory { + static boolean DEBUG = false; + static final String TAG = "FragmentManager"; + + static final boolean HONEYCOMB = android.os.Build.VERSION.SDK_INT >= 11; + + static final String TARGET_REQUEST_CODE_STATE_TAG = "android:target_req_state"; + static final String TARGET_STATE_TAG = "android:target_state"; + static final String VIEW_STATE_TAG = "android:view_state"; + static final String USER_VISIBLE_HINT_TAG = "android:user_visible_hint"; + + ArrayList mPendingActions; + Runnable[] mTmpActions; + boolean mExecutingActions; + + ArrayList mActive; + ArrayList mAdded; + ArrayList mAvailIndices; + ArrayList mBackStack; + ArrayList mCreatedMenus; + + // Must be accessed while locked. + ArrayList mBackStackIndices; + ArrayList mAvailBackStackIndices; + + ArrayList mBackStackChangeListeners; + + int mCurState = Fragment.INITIALIZING; + FragmentActivity mActivity; + FragmentContainer mContainer; + Fragment mParent; + + boolean mNeedMenuInvalidate; + boolean mStateSaved; + boolean mDestroyed; + String mNoTransactionsBecause; + boolean mHavePendingDeferredStart; + + // Temporary vars for state save and restore. + Bundle mStateBundle = null; + SparseArray mStateArray = null; + + Runnable mExecCommit = new Runnable() { + @Override + public void run() { + execPendingActions(); + } + }; + + private void throwException(RuntimeException ex) { + Log.e(TAG, ex.getMessage()); + Log.e(TAG, "Activity state:"); + LogWriter logw = new LogWriter(TAG); + PrintWriter pw = new PrintWriter(logw); + if (mActivity != null) { + try { + mActivity.dump(" ", null, pw, new String[] { }); + } catch (Exception e) { + Log.e(TAG, "Failed dumping state", e); + } + } else { + try { + dump(" ", null, pw, new String[] { }); + } catch (Exception e) { + Log.e(TAG, "Failed dumping state", e); + } + } + throw ex; + } + + @Override + public FragmentTransaction beginTransaction() { + return new BackStackRecord(this); + } + + @Override + public boolean executePendingTransactions() { + return execPendingActions(); + } + + @Override + public void popBackStack() { + enqueueAction(new Runnable() { + @Override public void run() { + popBackStackState(mActivity.mHandler, null, -1, 0); + } + }, false); + } + + @Override + public boolean popBackStackImmediate() { + checkStateLoss(); + executePendingTransactions(); + return popBackStackState(mActivity.mHandler, null, -1, 0); + } + + @Override + public void popBackStack(final String name, final int flags) { + enqueueAction(new Runnable() { + @Override public void run() { + popBackStackState(mActivity.mHandler, name, -1, flags); + } + }, false); + } + + @Override + public boolean popBackStackImmediate(String name, int flags) { + checkStateLoss(); + executePendingTransactions(); + return popBackStackState(mActivity.mHandler, name, -1, flags); + } + + @Override + public void popBackStack(final int id, final int flags) { + if (id < 0) { + throw new IllegalArgumentException("Bad id: " + id); + } + enqueueAction(new Runnable() { + @Override public void run() { + popBackStackState(mActivity.mHandler, null, id, flags); + } + }, false); + } + + @Override + public boolean popBackStackImmediate(int id, int flags) { + checkStateLoss(); + executePendingTransactions(); + if (id < 0) { + throw new IllegalArgumentException("Bad id: " + id); + } + return popBackStackState(mActivity.mHandler, null, id, flags); + } + + @Override + public int getBackStackEntryCount() { + return mBackStack != null ? mBackStack.size() : 0; + } + + @Override + public BackStackEntry getBackStackEntryAt(int index) { + return mBackStack.get(index); + } + + @Override + public void addOnBackStackChangedListener(OnBackStackChangedListener listener) { + if (mBackStackChangeListeners == null) { + mBackStackChangeListeners = new ArrayList(); + } + mBackStackChangeListeners.add(listener); + } + + @Override + public void removeOnBackStackChangedListener(OnBackStackChangedListener listener) { + if (mBackStackChangeListeners != null) { + mBackStackChangeListeners.remove(listener); + } + } + + @Override + public void putFragment(Bundle bundle, String key, Fragment fragment) { + if (fragment.mIndex < 0) { + throwException(new IllegalStateException("Fragment " + fragment + + " is not currently in the FragmentManager")); + } + bundle.putInt(key, fragment.mIndex); + } + + @Override + public Fragment getFragment(Bundle bundle, String key) { + int index = bundle.getInt(key, -1); + if (index == -1) { + return null; + } + if (index >= mActive.size()) { + throwException(new IllegalStateException("Fragment no longer exists for key " + + key + ": index " + index)); + } + Fragment f = mActive.get(index); + if (f == null) { + throwException(new IllegalStateException("Fragment no longer exists for key " + + key + ": index " + index)); + } + return f; + } + + @Override + public List getFragments() { + return mActive; + } + + @Override + public Fragment.SavedState saveFragmentInstanceState(Fragment fragment) { + if (fragment.mIndex < 0) { + throwException( new IllegalStateException("Fragment " + fragment + + " is not currently in the FragmentManager")); + } + if (fragment.mState > Fragment.INITIALIZING) { + Bundle result = saveFragmentBasicState(fragment); + return result != null ? new Fragment.SavedState(result) : null; + } + return null; + } + + @Override + public boolean isDestroyed() { + return mDestroyed; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("FragmentManager{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" in "); + if (mParent != null) { + DebugUtils.buildShortClassTag(mParent, sb); + } else { + DebugUtils.buildShortClassTag(mActivity, sb); + } + sb.append("}}"); + return sb.toString(); + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + String innerPrefix = prefix + " "; + + int N; + if (mActive != null) { + N = mActive.size(); + if (N > 0) { + writer.print(prefix); writer.print("Active Fragments in "); + writer.print(Integer.toHexString(System.identityHashCode(this))); + writer.println(":"); + for (int i=0; i 0) { + writer.print(prefix); writer.println("Added Fragments:"); + for (int i=0; i 0) { + writer.print(prefix); writer.println("Fragments Created Menus:"); + for (int i=0; i 0) { + writer.print(prefix); writer.println("Back Stack:"); + for (int i=0; i 0) { + writer.print(prefix); writer.println("Back Stack Indices:"); + for (int i=0; i 0) { + writer.print(prefix); writer.print("mAvailBackStackIndices: "); + writer.println(Arrays.toString(mAvailBackStackIndices.toArray())); + } + } + + if (mPendingActions != null) { + N = mPendingActions.size(); + if (N > 0) { + writer.print(prefix); writer.println("Pending Actions:"); + for (int i=0; i Fragment.CREATED) { + newState = Fragment.CREATED; + } + if (f.mRemoving && newState > f.mState) { + // While removing a fragment, we can't change it to a higher state. + newState = f.mState; + } + // Defer start if requested; don't allow it to move to STARTED or higher + // if it's not already started. + if (f.mDeferStart && f.mState < Fragment.STARTED && newState > Fragment.STOPPED) { + newState = Fragment.STOPPED; + } + if (f.mState < newState) { + // For fragments that are created from a layout, when restoring from + // state we don't want to allow them to be created until they are + // being reloaded from the layout. + if (f.mFromLayout && !f.mInLayout) { + return; + } + if (f.mAnimatingAway != null) { + // The fragment is currently being animated... but! Now we + // want to move our state back up. Give up on waiting for the + // animation, move to whatever the final state should be once + // the animation is done, and then we can proceed from there. + f.mAnimatingAway = null; + moveToState(f, f.mStateAfterAnimating, 0, 0, true); + } + switch (f.mState) { + case Fragment.INITIALIZING: + if (DEBUG) Log.v(TAG, "moveto CREATED: " + f); + if (f.mSavedFragmentState != null) { + f.mSavedFragmentState.setClassLoader(mActivity.getClassLoader()); + f.mSavedViewState = f.mSavedFragmentState.getSparseParcelableArray( + FragmentManagerImpl.VIEW_STATE_TAG); + f.mTarget = getFragment(f.mSavedFragmentState, + FragmentManagerImpl.TARGET_STATE_TAG); + if (f.mTarget != null) { + f.mTargetRequestCode = f.mSavedFragmentState.getInt( + FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, 0); + } + f.mUserVisibleHint = f.mSavedFragmentState.getBoolean( + FragmentManagerImpl.USER_VISIBLE_HINT_TAG, true); + if (!f.mUserVisibleHint) { + f.mDeferStart = true; + if (newState > Fragment.STOPPED) { + newState = Fragment.STOPPED; + } + } + } + f.mActivity = mActivity; + f.mParentFragment = mParent; + f.mFragmentManager = mParent != null + ? mParent.mChildFragmentManager : mActivity.mFragments; + f.mCalled = false; + f.onAttach(mActivity); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onAttach()"); + } + if (f.mParentFragment == null) { + mActivity.onAttachFragment(f); + } + + if (!f.mRetaining) { + f.performCreate(f.mSavedFragmentState); + } + f.mRetaining = false; + if (f.mFromLayout) { + // For fragments that are part of the content view + // layout, we need to instantiate the view immediately + // and the inflater will take care of adding it. + f.mView = f.performCreateView(f.getLayoutInflater( + f.mSavedFragmentState), null, f.mSavedFragmentState); + if (f.mView != null) { + f.mInnerView = f.mView; + if (Build.VERSION.SDK_INT >= 11) { + ViewCompat.setSaveFromParentEnabled(f.mView, false); + } else { + f.mView = NoSaveStateFrameLayout.wrap(f.mView); + } + if (f.mHidden) f.mView.setVisibility(View.GONE); + f.onViewCreated(f.mView, f.mSavedFragmentState); + } else { + f.mInnerView = null; + } + } + case Fragment.CREATED: + if (newState > Fragment.CREATED) { + if (DEBUG) Log.v(TAG, "moveto ACTIVITY_CREATED: " + f); + if (!f.mFromLayout) { + ViewGroup container = null; + if (f.mContainerId != 0) { + container = (ViewGroup)mContainer.findViewById(f.mContainerId); + if (container == null && !f.mRestored) { + throwException(new IllegalArgumentException( + "No view found for id 0x" + + Integer.toHexString(f.mContainerId) + " (" + + f.getResources().getResourceName(f.mContainerId) + + ") for fragment " + f)); + } + } + f.mContainer = container; + f.mView = f.performCreateView(f.getLayoutInflater( + f.mSavedFragmentState), container, f.mSavedFragmentState); + if (f.mView != null) { + f.mInnerView = f.mView; + if (Build.VERSION.SDK_INT >= 11) { + ViewCompat.setSaveFromParentEnabled(f.mView, false); + } else { + f.mView = NoSaveStateFrameLayout.wrap(f.mView); + } + if (container != null) { + Animation anim = loadAnimation(f, transit, true, + transitionStyle); + if (anim != null) { + f.mView.startAnimation(anim); + } + container.addView(f.mView); + } + if (f.mHidden) f.mView.setVisibility(View.GONE); + f.onViewCreated(f.mView, f.mSavedFragmentState); + } else { + f.mInnerView = null; + } + } + + f.performActivityCreated(f.mSavedFragmentState); + if (f.mView != null) { + f.restoreViewState(f.mSavedFragmentState); + } + f.mSavedFragmentState = null; + } + case Fragment.ACTIVITY_CREATED: + case Fragment.STOPPED: + if (newState > Fragment.STOPPED) { + if (DEBUG) Log.v(TAG, "moveto STARTED: " + f); + f.performStart(); + } + case Fragment.STARTED: + if (newState > Fragment.STARTED) { + if (DEBUG) Log.v(TAG, "moveto RESUMED: " + f); + f.mResumed = true; + f.performResume(); + f.mSavedFragmentState = null; + f.mSavedViewState = null; + } + } + } else if (f.mState > newState) { + switch (f.mState) { + case Fragment.RESUMED: + if (newState < Fragment.RESUMED) { + if (DEBUG) Log.v(TAG, "movefrom RESUMED: " + f); + f.performPause(); + f.mResumed = false; + } + case Fragment.STARTED: + if (newState < Fragment.STARTED) { + if (DEBUG) Log.v(TAG, "movefrom STARTED: " + f); + f.performStop(); + } + case Fragment.STOPPED: + if (newState < Fragment.STOPPED) { + if (DEBUG) Log.v(TAG, "movefrom STOPPED: " + f); + f.performReallyStop(); + } + case Fragment.ACTIVITY_CREATED: + if (newState < Fragment.ACTIVITY_CREATED) { + if (DEBUG) Log.v(TAG, "movefrom ACTIVITY_CREATED: " + f); + if (f.mView != null) { + // Need to save the current view state if not + // done already. + if (!mActivity.isFinishing() && f.mSavedViewState == null) { + saveFragmentViewState(f); + } + } + f.performDestroyView(); + if (f.mView != null && f.mContainer != null) { + Animation anim = null; + if (mCurState > Fragment.INITIALIZING && !mDestroyed) { + anim = loadAnimation(f, transit, false, + transitionStyle); + } + if (anim != null) { + final Fragment fragment = f; + f.mAnimatingAway = f.mView; + f.mStateAfterAnimating = newState; + anim.setAnimationListener(new AnimationListener() { + @Override + public void onAnimationEnd(Animation animation) { + if (fragment.mAnimatingAway != null) { + fragment.mAnimatingAway = null; + moveToState(fragment, fragment.mStateAfterAnimating, + 0, 0, false); + } + } + @Override + public void onAnimationRepeat(Animation animation) { + } + @Override + public void onAnimationStart(Animation animation) { + } + }); + f.mView.startAnimation(anim); + } + f.mContainer.removeView(f.mView); + } + f.mContainer = null; + f.mView = null; + f.mInnerView = null; + } + case Fragment.CREATED: + if (newState < Fragment.CREATED) { + if (mDestroyed) { + if (f.mAnimatingAway != null) { + // The fragment's containing activity is + // being destroyed, but this fragment is + // currently animating away. Stop the + // animation right now -- it is not needed, + // and we can't wait any more on destroying + // the fragment. + View v = f.mAnimatingAway; + f.mAnimatingAway = null; + v.clearAnimation(); + } + } + if (f.mAnimatingAway != null) { + // We are waiting for the fragment's view to finish + // animating away. Just make a note of the state + // the fragment now should move to once the animation + // is done. + f.mStateAfterAnimating = newState; + newState = Fragment.CREATED; + } else { + if (DEBUG) Log.v(TAG, "movefrom CREATED: " + f); + if (!f.mRetaining) { + f.performDestroy(); + } + + f.mCalled = false; + f.onDetach(); + if (!f.mCalled) { + throw new SuperNotCalledException("Fragment " + f + + " did not call through to super.onDetach()"); + } + if (!keepActive) { + if (!f.mRetaining) { + makeInactive(f); + } else { + f.mActivity = null; + f.mParentFragment = null; + f.mFragmentManager = null; + f.mChildFragmentManager = null; + } + } + } + } + } + } + + f.mState = newState; + } + + void moveToState(Fragment f) { + moveToState(f, mCurState, 0, 0, false); + } + + void moveToState(int newState, boolean always) { + moveToState(newState, 0, 0, always); + } + + void moveToState(int newState, int transit, int transitStyle, boolean always) { + if (mActivity == null && newState != Fragment.INITIALIZING) { + throw new IllegalStateException("No activity"); + } + + if (!always && mCurState == newState) { + return; + } + + mCurState = newState; + if (mActive != null) { + boolean loadersRunning = false; + for (int i=0; i= 0) { + return; + } + + if (mAvailIndices == null || mAvailIndices.size() <= 0) { + if (mActive == null) { + mActive = new ArrayList(); + } + f.setIndex(mActive.size(), mParent); + mActive.add(f); + + } else { + f.setIndex(mAvailIndices.remove(mAvailIndices.size()-1), mParent); + mActive.set(f.mIndex, f); + } + if (DEBUG) Log.v(TAG, "Allocated fragment index " + f); + } + + void makeInactive(Fragment f) { + if (f.mIndex < 0) { + return; + } + + if (DEBUG) Log.v(TAG, "Freeing fragment index " + f); + mActive.set(f.mIndex, null); + if (mAvailIndices == null) { + mAvailIndices = new ArrayList(); + } + mAvailIndices.add(f.mIndex); + mActivity.invalidateSupportFragment(f.mWho); + f.initState(); + } + + public void addFragment(Fragment fragment, boolean moveToStateNow) { + if (mAdded == null) { + mAdded = new ArrayList(); + } + if (DEBUG) Log.v(TAG, "add: " + fragment); + makeActive(fragment); + if (!fragment.mDetached) { + if (mAdded.contains(fragment)) { + throw new IllegalStateException("Fragment already added: " + fragment); + } + mAdded.add(fragment); + fragment.mAdded = true; + fragment.mRemoving = false; + if (fragment.mHasMenu && fragment.mMenuVisible) { + mNeedMenuInvalidate = true; + } + if (moveToStateNow) { + moveToState(fragment); + } + } + } + + public void removeFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "remove: " + fragment + " nesting=" + fragment.mBackStackNesting); + final boolean inactive = !fragment.isInBackStack(); + if (!fragment.mDetached || inactive) { + if (mAdded != null) { + mAdded.remove(fragment); + } + if (fragment.mHasMenu && fragment.mMenuVisible) { + mNeedMenuInvalidate = true; + } + fragment.mAdded = false; + fragment.mRemoving = true; + moveToState(fragment, inactive ? Fragment.INITIALIZING : Fragment.CREATED, + transition, transitionStyle, false); + } + } + + public void hideFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "hide: " + fragment); + if (!fragment.mHidden) { + fragment.mHidden = true; + if (fragment.mView != null) { + Animation anim = loadAnimation(fragment, transition, false, + transitionStyle); + if (anim != null) { + fragment.mView.startAnimation(anim); + } + fragment.mView.setVisibility(View.GONE); + } + if (fragment.mAdded && fragment.mHasMenu && fragment.mMenuVisible) { + mNeedMenuInvalidate = true; + } + fragment.onHiddenChanged(true); + } + } + + public void showFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "show: " + fragment); + if (fragment.mHidden) { + fragment.mHidden = false; + if (fragment.mView != null) { + Animation anim = loadAnimation(fragment, transition, true, + transitionStyle); + if (anim != null) { + fragment.mView.startAnimation(anim); + } + fragment.mView.setVisibility(View.VISIBLE); + } + if (fragment.mAdded && fragment.mHasMenu && fragment.mMenuVisible) { + mNeedMenuInvalidate = true; + } + fragment.onHiddenChanged(false); + } + } + + public void detachFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "detach: " + fragment); + if (!fragment.mDetached) { + fragment.mDetached = true; + if (fragment.mAdded) { + // We are not already in back stack, so need to remove the fragment. + if (mAdded != null) { + if (DEBUG) Log.v(TAG, "remove from detach: " + fragment); + mAdded.remove(fragment); + } + if (fragment.mHasMenu && fragment.mMenuVisible) { + mNeedMenuInvalidate = true; + } + fragment.mAdded = false; + moveToState(fragment, Fragment.CREATED, transition, transitionStyle, false); + } + } + } + + public void attachFragment(Fragment fragment, int transition, int transitionStyle) { + if (DEBUG) Log.v(TAG, "attach: " + fragment); + if (fragment.mDetached) { + fragment.mDetached = false; + if (!fragment.mAdded) { + if (mAdded == null) { + mAdded = new ArrayList(); + } + if (mAdded.contains(fragment)) { + throw new IllegalStateException("Fragment already added: " + fragment); + } + if (DEBUG) Log.v(TAG, "add from attach: " + fragment); + mAdded.add(fragment); + fragment.mAdded = true; + if (fragment.mHasMenu && fragment.mMenuVisible) { + mNeedMenuInvalidate = true; + } + moveToState(fragment, mCurState, transition, transitionStyle, false); + } + } + } + + public Fragment findFragmentById(int id) { + if (mAdded != null) { + // First look through added fragments. + for (int i=mAdded.size()-1; i>=0; i--) { + Fragment f = mAdded.get(i); + if (f != null && f.mFragmentId == id) { + return f; + } + } + } + if (mActive != null) { + // Now for any known fragment. + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && f.mFragmentId == id) { + return f; + } + } + } + return null; + } + + public Fragment findFragmentByTag(String tag) { + if (mAdded != null && tag != null) { + // First look through added fragments. + for (int i=mAdded.size()-1; i>=0; i--) { + Fragment f = mAdded.get(i); + if (f != null && tag.equals(f.mTag)) { + return f; + } + } + } + if (mActive != null && tag != null) { + // Now for any known fragment. + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && tag.equals(f.mTag)) { + return f; + } + } + } + return null; + } + + public Fragment findFragmentByWho(String who) { + if (mActive != null && who != null) { + for (int i=mActive.size()-1; i>=0; i--) { + Fragment f = mActive.get(i); + if (f != null && (f=f.findFragmentByWho(who)) != null) { + return f; + } + } + } + return null; + } + + private void checkStateLoss() { + if (mStateSaved) { + throw new IllegalStateException( + "Can not perform this action after onSaveInstanceState"); + } + if (mNoTransactionsBecause != null) { + throw new IllegalStateException( + "Can not perform this action inside of " + mNoTransactionsBecause); + } + } + + /** + * Adds an action to the queue of pending actions. + * + * @param action the action to add + * @param allowStateLoss whether to allow loss of state information + * @throws IllegalStateException if the activity has been destroyed + */ + public void enqueueAction(Runnable action, boolean allowStateLoss) { + if (!allowStateLoss) { + checkStateLoss(); + } + synchronized (this) { + if (mDestroyed || mActivity == null) { + throw new IllegalStateException("Activity has been destroyed"); + } + if (mPendingActions == null) { + mPendingActions = new ArrayList(); + } + mPendingActions.add(action); + if (mPendingActions.size() == 1) { + mActivity.mHandler.removeCallbacks(mExecCommit); + mActivity.mHandler.post(mExecCommit); + } + } + } + + public int allocBackStackIndex(BackStackRecord bse) { + synchronized (this) { + if (mAvailBackStackIndices == null || mAvailBackStackIndices.size() <= 0) { + if (mBackStackIndices == null) { + mBackStackIndices = new ArrayList(); + } + int index = mBackStackIndices.size(); + if (DEBUG) Log.v(TAG, "Setting back stack index " + index + " to " + bse); + mBackStackIndices.add(bse); + return index; + + } else { + int index = mAvailBackStackIndices.remove(mAvailBackStackIndices.size()-1); + if (DEBUG) Log.v(TAG, "Adding back stack index " + index + " with " + bse); + mBackStackIndices.set(index, bse); + return index; + } + } + } + + public void setBackStackIndex(int index, BackStackRecord bse) { + synchronized (this) { + if (mBackStackIndices == null) { + mBackStackIndices = new ArrayList(); + } + int N = mBackStackIndices.size(); + if (index < N) { + if (DEBUG) Log.v(TAG, "Setting back stack index " + index + " to " + bse); + mBackStackIndices.set(index, bse); + } else { + while (N < index) { + mBackStackIndices.add(null); + if (mAvailBackStackIndices == null) { + mAvailBackStackIndices = new ArrayList(); + } + if (DEBUG) Log.v(TAG, "Adding available back stack index " + N); + mAvailBackStackIndices.add(N); + N++; + } + if (DEBUG) Log.v(TAG, "Adding back stack index " + index + " with " + bse); + mBackStackIndices.add(bse); + } + } + } + + public void freeBackStackIndex(int index) { + synchronized (this) { + mBackStackIndices.set(index, null); + if (mAvailBackStackIndices == null) { + mAvailBackStackIndices = new ArrayList(); + } + if (DEBUG) Log.v(TAG, "Freeing back stack index " + index); + mAvailBackStackIndices.add(index); + } + } + + /** + * Only call from main thread! + */ + public boolean execPendingActions() { + if (mExecutingActions) { + throw new IllegalStateException("Recursive entry to executePendingTransactions"); + } + + if (Looper.myLooper() != mActivity.mHandler.getLooper()) { + throw new IllegalStateException("Must be called from main thread of process"); + } + + boolean didSomething = false; + + while (true) { + int numActions; + + synchronized (this) { + if (mPendingActions == null || mPendingActions.size() == 0) { + break; + } + + numActions = mPendingActions.size(); + if (mTmpActions == null || mTmpActions.length < numActions) { + mTmpActions = new Runnable[numActions]; + } + mPendingActions.toArray(mTmpActions); + mPendingActions.clear(); + mActivity.mHandler.removeCallbacks(mExecCommit); + } + + mExecutingActions = true; + for (int i=0; i(); + } + mBackStack.add(state); + reportBackStackChanged(); + } + + boolean popBackStackState(Handler handler, String name, int id, int flags) { + if (mBackStack == null) { + return false; + } + if (name == null && id < 0 && (flags&POP_BACK_STACK_INCLUSIVE) == 0) { + int last = mBackStack.size()-1; + if (last < 0) { + return false; + } + final BackStackRecord bss = mBackStack.remove(last); + SparseArray firstOutFragments = new SparseArray(); + SparseArray lastInFragments = new SparseArray(); + bss.calculateBackFragments(firstOutFragments, lastInFragments); + bss.popFromBackStack(true, null, firstOutFragments, lastInFragments); + reportBackStackChanged(); + } else { + int index = -1; + if (name != null || id >= 0) { + // If a name or ID is specified, look for that place in + // the stack. + index = mBackStack.size()-1; + while (index >= 0) { + BackStackRecord bss = mBackStack.get(index); + if (name != null && name.equals(bss.getName())) { + break; + } + if (id >= 0 && id == bss.mIndex) { + break; + } + index--; + } + if (index < 0) { + return false; + } + if ((flags&POP_BACK_STACK_INCLUSIVE) != 0) { + index--; + // Consume all following entries that match. + while (index >= 0) { + BackStackRecord bss = mBackStack.get(index); + if ((name != null && name.equals(bss.getName())) + || (id >= 0 && id == bss.mIndex)) { + index--; + continue; + } + break; + } + } + } + if (index == mBackStack.size()-1) { + return false; + } + final ArrayList states + = new ArrayList(); + for (int i=mBackStack.size()-1; i>index; i--) { + states.add(mBackStack.remove(i)); + } + final int LAST = states.size()-1; + SparseArray firstOutFragments = new SparseArray(); + SparseArray lastInFragments = new SparseArray(); + for (int i=0; i<=LAST; i++) { + states.get(i).calculateBackFragments(firstOutFragments, lastInFragments); + } + BackStackRecord.TransitionState state = null; + for (int i=0; i<=LAST; i++) { + if (DEBUG) Log.v(TAG, "Popping back stack state: " + states.get(i)); + state = states.get(i).popFromBackStack(i == LAST, state, + firstOutFragments, lastInFragments); + } + reportBackStackChanged(); + } + return true; + } + + ArrayList retainNonConfig() { + ArrayList fragments = null; + if (mActive != null) { + for (int i=0; i(); + } + fragments.add(f); + f.mRetaining = true; + f.mTargetIndex = f.mTarget != null ? f.mTarget.mIndex : -1; + if (DEBUG) Log.v(TAG, "retainNonConfig: keeping retained " + f); + } + } + } + return fragments; + } + + void saveFragmentViewState(Fragment f) { + if (f.mInnerView == null) { + return; + } + if (mStateArray == null) { + mStateArray = new SparseArray(); + } else { + mStateArray.clear(); + } + f.mInnerView.saveHierarchyState(mStateArray); + if (mStateArray.size() > 0) { + f.mSavedViewState = mStateArray; + mStateArray = null; + } + } + + Bundle saveFragmentBasicState(Fragment f) { + Bundle result = null; + + if (mStateBundle == null) { + mStateBundle = new Bundle(); + } + f.performSaveInstanceState(mStateBundle); + if (!mStateBundle.isEmpty()) { + result = mStateBundle; + mStateBundle = null; + } + + if (f.mView != null) { + saveFragmentViewState(f); + } + if (f.mSavedViewState != null) { + if (result == null) { + result = new Bundle(); + } + result.putSparseParcelableArray( + FragmentManagerImpl.VIEW_STATE_TAG, f.mSavedViewState); + } + if (!f.mUserVisibleHint) { + if (result == null) { + result = new Bundle(); + } + // Only add this if it's not the default value + result.putBoolean(FragmentManagerImpl.USER_VISIBLE_HINT_TAG, f.mUserVisibleHint); + } + + return result; + } + + Parcelable saveAllState() { + // Make sure all pending operations have now been executed to get + // our state update-to-date. + execPendingActions(); + + if (HONEYCOMB) { + // As of Honeycomb, we save state after pausing. Prior to that + // it is before pausing. With fragments this is an issue, since + // there are many things you may do after pausing but before + // stopping that change the fragment state. For those older + // devices, we will not at this point say that we have saved + // the state, so we will allow them to continue doing fragment + // transactions. This retains the same semantics as Honeycomb, + // though you do have the risk of losing the very most recent state + // if the process is killed... we'll live with that. + mStateSaved = true; + } + + if (mActive == null || mActive.size() <= 0) { + return null; + } + + // First collect all active fragments. + int N = mActive.size(); + FragmentState[] active = new FragmentState[N]; + boolean haveFragments = false; + for (int i=0; i Fragment.INITIALIZING && fs.mSavedFragmentState == null) { + fs.mSavedFragmentState = saveFragmentBasicState(f); + + if (f.mTarget != null) { + if (f.mTarget.mIndex < 0) { + throwException(new IllegalStateException( + "Failure saving state: " + f + + " has target not in fragment manager: " + f.mTarget)); + } + if (fs.mSavedFragmentState == null) { + fs.mSavedFragmentState = new Bundle(); + } + putFragment(fs.mSavedFragmentState, + FragmentManagerImpl.TARGET_STATE_TAG, f.mTarget); + if (f.mTargetRequestCode != 0) { + fs.mSavedFragmentState.putInt( + FragmentManagerImpl.TARGET_REQUEST_CODE_STATE_TAG, + f.mTargetRequestCode); + } + } + + } else { + fs.mSavedFragmentState = f.mSavedFragmentState; + } + + if (DEBUG) Log.v(TAG, "Saved state of " + f + ": " + + fs.mSavedFragmentState); + } + } + + if (!haveFragments) { + if (DEBUG) Log.v(TAG, "saveAllState: no fragments!"); + return null; + } + + int[] added = null; + BackStackState[] backStack = null; + + // Build list of currently added fragments. + if (mAdded != null) { + N = mAdded.size(); + if (N > 0) { + added = new int[N]; + for (int i=0; i 0) { + backStack = new BackStackState[N]; + for (int i=0; i nonConfig) { + // If there is no saved state at all, then there can not be + // any nonConfig fragments either, so that is that. + if (state == null) return; + FragmentManagerState fms = (FragmentManagerState)state; + if (fms.mActive == null) return; + + // First re-attach any non-config instances we are retaining back + // to their saved state, so we don't try to instantiate them again. + if (nonConfig != null) { + for (int i=0; i(fms.mActive.length); + if (mAvailIndices != null) { + mAvailIndices.clear(); + } + for (int i=0; i(); + } + if (DEBUG) Log.v(TAG, "restoreAllState: avail #" + i); + mAvailIndices.add(i); + } + } + + // Update the target of all retained fragments. + if (nonConfig != null) { + for (int i=0; i= 0) { + if (f.mTargetIndex < mActive.size()) { + f.mTarget = mActive.get(f.mTargetIndex); + } else { + Log.w(TAG, "Re-attaching retained fragment " + f + + " target no longer exists: " + f.mTargetIndex); + f.mTarget = null; + } + } + } + } + + // Build the list of currently added fragments. + if (fms.mAdded != null) { + mAdded = new ArrayList(fms.mAdded.length); + for (int i=0; i(fms.mBackStack.length); + for (int i=0; i= 0) { + setBackStackIndex(bse.mIndex, bse); + } + } + } else { + mBackStack = null; + } + } + + public void attachActivity(FragmentActivity activity, + FragmentContainer container, Fragment parent) { + if (mActivity != null) throw new IllegalStateException("Already attached"); + mActivity = activity; + mContainer = container; + mParent = parent; + } + + public void noteStateNotSaved() { + mStateSaved = false; + } + + public void dispatchCreate() { + mStateSaved = false; + moveToState(Fragment.CREATED, false); + } + + public void dispatchActivityCreated() { + mStateSaved = false; + moveToState(Fragment.ACTIVITY_CREATED, false); + } + + public void dispatchStart() { + mStateSaved = false; + moveToState(Fragment.STARTED, false); + } + + public void dispatchResume() { + mStateSaved = false; + moveToState(Fragment.RESUMED, false); + } + + public void dispatchPause() { + moveToState(Fragment.STARTED, false); + } + + public void dispatchStop() { + // See saveAllState() for the explanation of this. We do this for + // all platform versions, to keep our behavior more consistent between + // them. + mStateSaved = true; + + moveToState(Fragment.STOPPED, false); + } + + public void dispatchReallyStop() { + moveToState(Fragment.ACTIVITY_CREATED, false); + } + + public void dispatchDestroyView() { + moveToState(Fragment.CREATED, false); + } + + public void dispatchDestroy() { + mDestroyed = true; + execPendingActions(); + moveToState(Fragment.INITIALIZING, false); + mActivity = null; + mContainer = null; + mParent = null; + } + + public void dispatchConfigurationChanged(Configuration newConfig) { + if (mAdded != null) { + for (int i=0; i newMenus = null; + if (mAdded != null) { + for (int i=0; i(); + } + newMenus.add(f); + } + } + } + } + + if (mCreatedMenus != null) { + for (int i=0; iThis version of the pager is best for use when there are a handful of + * typically more static fragments to be paged through, such as a set of tabs. + * The fragment of each page the user visits will be kept in memory, though its + * view hierarchy may be destroyed when not visible. This can result in using + * a significant amount of memory since fragment instances can hold on to an + * arbitrary amount of state. For larger sets of pages, consider + * {@link FragmentStatePagerAdapter}. + * + *

When using FragmentPagerAdapter the host ViewPager must have a + * valid ID set.

+ * + *

Subclasses only need to implement {@link #getItem(int)} + * and {@link #getCount()} to have a working adapter. + * + *

Here is an example implementation of a pager containing fragments of + * lists: + * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentPagerSupport.java + * complete} + * + *

The R.layout.fragment_pager resource of the top-level fragment is: + * + * {@sample development/samples/Support4Demos/res/layout/fragment_pager.xml + * complete} + * + *

The R.layout.fragment_pager_list resource containing each + * individual fragment's layout is: + * + * {@sample development/samples/Support4Demos/res/layout/fragment_pager_list.xml + * complete} + */ +public abstract class FragmentPagerAdapter extends PagerAdapter { + private static final String TAG = "FragmentPagerAdapter"; + private static final boolean DEBUG = false; + + private final FragmentManager mFragmentManager; + private FragmentTransaction mCurTransaction = null; + private Fragment mCurrentPrimaryItem = null; + + public FragmentPagerAdapter(FragmentManager fm) { + mFragmentManager = fm; + } + + /** + * Return the Fragment associated with a specified position. + */ + public abstract Fragment getItem(int position); + + @Override + public void startUpdate(ViewGroup container) { + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + + final long itemId = getItemId(position); + + // Do we already have this fragment? + String name = makeFragmentName(container.getId(), itemId); + Fragment fragment = mFragmentManager.findFragmentByTag(name); + if (fragment != null) { + if (DEBUG) Log.v(TAG, "Attaching item #" + itemId + ": f=" + fragment); + mCurTransaction.attach(fragment); + } else { + fragment = getItem(position); + if (DEBUG) Log.v(TAG, "Adding item #" + itemId + ": f=" + fragment); + mCurTransaction.add(container.getId(), fragment, + makeFragmentName(container.getId(), itemId)); + } + if (fragment != mCurrentPrimaryItem) { + fragment.setMenuVisibility(false); + fragment.setUserVisibleHint(false); + } + + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) Log.v(TAG, "Detaching item #" + getItemId(position) + ": f=" + object + + " v=" + ((Fragment)object).getView()); + mCurTransaction.detach((Fragment)object); + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + mCurrentPrimaryItem.setMenuVisibility(false); + mCurrentPrimaryItem.setUserVisibleHint(false); + } + if (fragment != null) { + fragment.setMenuVisibility(true); + fragment.setUserVisibleHint(true); + } + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitAllowingStateLoss(); + mCurTransaction = null; + mFragmentManager.executePendingTransactions(); + } + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return ((Fragment)object).getView() == view; + } + + @Override + public Parcelable saveState() { + return null; + } + + @Override + public void restoreState(Parcelable state, ClassLoader loader) { + } + + /** + * Return a unique identifier for the item at the given position. + * + *

The default implementation returns the given position. + * Subclasses should override this method if the positions of items can change.

+ * + * @param position Position within this adapter + * @return Unique identifier for the item at position + */ + public long getItemId(int position) { + return position; + } + + private static String makeFragmentName(int viewId, long id) { + return "android:switcher:" + viewId + ":" + id; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/FragmentStatePagerAdapter.java b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentStatePagerAdapter.java new file mode 100644 index 00000000000..0c3c6c5adfa --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentStatePagerAdapter.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import java.util.ArrayList; + +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v4.view.PagerAdapter; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +/** + * Implementation of {@link android.support.v4.view.PagerAdapter} that + * uses a {@link Fragment} to manage each page. This class also handles + * saving and restoring of fragment's state. + * + *

This version of the pager is more useful when there are a large number + * of pages, working more like a list view. When pages are not visible to + * the user, their entire fragment may be destroyed, only keeping the saved + * state of that fragment. This allows the pager to hold on to much less + * memory associated with each visited page as compared to + * {@link FragmentPagerAdapter} at the cost of potentially more overhead when + * switching between pages. + * + *

When using FragmentPagerAdapter the host ViewPager must have a + * valid ID set.

+ * + *

Subclasses only need to implement {@link #getItem(int)} + * and {@link #getCount()} to have a working adapter. + * + *

Here is an example implementation of a pager containing fragments of + * lists: + * + * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/FragmentStatePagerSupport.java + * complete} + * + *

The R.layout.fragment_pager resource of the top-level fragment is: + * + * {@sample development/samples/Support13Demos/res/layout/fragment_pager.xml + * complete} + * + *

The R.layout.fragment_pager_list resource containing each + * individual fragment's layout is: + * + * {@sample development/samples/Support13Demos/res/layout/fragment_pager_list.xml + * complete} + */ +public abstract class FragmentStatePagerAdapter extends PagerAdapter { + private static final String TAG = "FragmentStatePagerAdapter"; + private static final boolean DEBUG = false; + + private final FragmentManager mFragmentManager; + private FragmentTransaction mCurTransaction = null; + + private ArrayList mSavedState = new ArrayList(); + private ArrayList mFragments = new ArrayList(); + private Fragment mCurrentPrimaryItem = null; + + public FragmentStatePagerAdapter(FragmentManager fm) { + mFragmentManager = fm; + } + + /** + * Return the Fragment associated with a specified position. + */ + public abstract Fragment getItem(int position); + + @Override + public void startUpdate(ViewGroup container) { + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + // If we already have this item instantiated, there is nothing + // to do. This can happen when we are restoring the entire pager + // from its saved state, where the fragment manager has already + // taken care of restoring the fragments we previously had instantiated. + if (mFragments.size() > position) { + Fragment f = mFragments.get(position); + if (f != null) { + return f; + } + } + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + + Fragment fragment = getItem(position); + if (DEBUG) Log.v(TAG, "Adding item #" + position + ": f=" + fragment); + if (mSavedState.size() > position) { + Fragment.SavedState fss = mSavedState.get(position); + if (fss != null) { + fragment.setInitialSavedState(fss); + } + } + while (mFragments.size() <= position) { + mFragments.add(null); + } + fragment.setMenuVisibility(false); + fragment.setUserVisibleHint(false); + mFragments.set(position, fragment); + mCurTransaction.add(container.getId(), fragment); + + return fragment; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + + if (mCurTransaction == null) { + mCurTransaction = mFragmentManager.beginTransaction(); + } + if (DEBUG) Log.v(TAG, "Removing item #" + position + ": f=" + object + + " v=" + ((Fragment)object).getView()); + while (mSavedState.size() <= position) { + mSavedState.add(null); + } + mSavedState.set(position, mFragmentManager.saveFragmentInstanceState(fragment)); + mFragments.set(position, null); + + mCurTransaction.remove(fragment); + } + + @Override + public void setPrimaryItem(ViewGroup container, int position, Object object) { + Fragment fragment = (Fragment)object; + if (fragment != mCurrentPrimaryItem) { + if (mCurrentPrimaryItem != null) { + mCurrentPrimaryItem.setMenuVisibility(false); + mCurrentPrimaryItem.setUserVisibleHint(false); + } + if (fragment != null) { + fragment.setMenuVisibility(true); + fragment.setUserVisibleHint(true); + } + mCurrentPrimaryItem = fragment; + } + } + + @Override + public void finishUpdate(ViewGroup container) { + if (mCurTransaction != null) { + mCurTransaction.commitAllowingStateLoss(); + mCurTransaction = null; + mFragmentManager.executePendingTransactions(); + } + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return ((Fragment)object).getView() == view; + } + + @Override + public Parcelable saveState() { + Bundle state = null; + if (mSavedState.size() > 0) { + state = new Bundle(); + Fragment.SavedState[] fss = new Fragment.SavedState[mSavedState.size()]; + mSavedState.toArray(fss); + state.putParcelableArray("states", fss); + } + for (int i=0; i keys = bundle.keySet(); + for (String key: keys) { + if (key.startsWith("f")) { + int index = Integer.parseInt(key.substring(1)); + Fragment f = mFragmentManager.getFragment(bundle, key); + if (f != null) { + while (mFragments.size() <= index) { + mFragments.add(null); + } + f.setMenuVisibility(false); + mFragments.set(index, f); + } else { + Log.w(TAG, "Bad fragment at key " + key); + } + } + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/FragmentTabHost.java b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentTabHost.java new file mode 100644 index 00000000000..7ffabdda4fa --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentTabHost.java @@ -0,0 +1,355 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import java.util.ArrayList; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TabHost; +import android.widget.TabWidget; + +/** + * Special TabHost that allows the use of {@link Fragment} objects for + * its tab content. When placing this in a view hierarchy, after inflating + * the hierarchy you must call {@link #setup(Context, FragmentManager, int)} + * to complete the initialization of the tab host. + * + *

Here is a simple example of using a FragmentTabHost in an Activity: + * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentTabs.java + * complete} + * + *

This can also be used inside of a fragment through fragment nesting: + * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/app/FragmentTabsFragmentSupport.java + * complete} + */ +public class FragmentTabHost extends TabHost + implements TabHost.OnTabChangeListener { + private final ArrayList mTabs = new ArrayList(); + private FrameLayout mRealTabContent; + private Context mContext; + private FragmentManager mFragmentManager; + private int mContainerId; + private TabHost.OnTabChangeListener mOnTabChangeListener; + private TabInfo mLastTab; + private boolean mAttached; + + static final class TabInfo { + private final String tag; + private final Class clss; + private final Bundle args; + private Fragment fragment; + + TabInfo(String _tag, Class _class, Bundle _args) { + tag = _tag; + clss = _class; + args = _args; + } + } + + static class DummyTabFactory implements TabHost.TabContentFactory { + private final Context mContext; + + public DummyTabFactory(Context context) { + mContext = context; + } + + @Override + public View createTabContent(String tag) { + View v = new View(mContext); + v.setMinimumWidth(0); + v.setMinimumHeight(0); + return v; + } + } + + static class SavedState extends BaseSavedState { + String curTab; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + curTab = in.readString(); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeString(curTab); + } + + @Override + public String toString() { + return "FragmentTabHost.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " curTab=" + curTab + "}"; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + public FragmentTabHost(Context context) { + // Note that we call through to the version that takes an AttributeSet, + // because the simple Context construct can result in a broken object! + super(context, null); + initFragmentTabHost(context, null); + } + + public FragmentTabHost(Context context, AttributeSet attrs) { + super(context, attrs); + initFragmentTabHost(context, attrs); + } + + private void initFragmentTabHost(Context context, AttributeSet attrs) { + TypedArray a = context.obtainStyledAttributes(attrs, + new int[] { android.R.attr.inflatedId }, 0, 0); + mContainerId = a.getResourceId(0, 0); + a.recycle(); + + super.setOnTabChangedListener(this); + } + + private void ensureHierarchy(Context context) { + // If owner hasn't made its own view hierarchy, then as a convenience + // we will construct a standard one here. + if (findViewById(android.R.id.tabs) == null) { + LinearLayout ll = new LinearLayout(context); + ll.setOrientation(LinearLayout.VERTICAL); + addView(ll, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.FILL_PARENT)); + + TabWidget tw = new TabWidget(context); + tw.setId(android.R.id.tabs); + tw.setOrientation(TabWidget.HORIZONTAL); + ll.addView(tw, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, 0)); + + FrameLayout fl = new FrameLayout(context); + fl.setId(android.R.id.tabcontent); + ll.addView(fl, new LinearLayout.LayoutParams(0, 0, 0)); + + mRealTabContent = fl = new FrameLayout(context); + mRealTabContent.setId(mContainerId); + ll.addView(fl, new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.FILL_PARENT, 0, 1)); + } + } + + /** + * @deprecated Don't call the original TabHost setup, you must instead + * call {@link #setup(Context, FragmentManager)} or + * {@link #setup(Context, FragmentManager, int)}. + */ + @Override @Deprecated + public void setup() { + throw new IllegalStateException( + "Must call setup() that takes a Context and FragmentManager"); + } + + public void setup(Context context, FragmentManager manager) { + ensureHierarchy(context); // Ensure views required by super.setup() + super.setup(); + mContext = context; + mFragmentManager = manager; + ensureContent(); + } + + public void setup(Context context, FragmentManager manager, int containerId) { + ensureHierarchy(context); // Ensure views required by super.setup() + super.setup(); + mContext = context; + mFragmentManager = manager; + mContainerId = containerId; + ensureContent(); + mRealTabContent.setId(containerId); + + // We must have an ID to be able to save/restore our state. If + // the owner hasn't set one at this point, we will set it ourself. + if (getId() == View.NO_ID) { + setId(android.R.id.tabhost); + } + } + + private void ensureContent() { + if (mRealTabContent == null) { + mRealTabContent = (FrameLayout)findViewById(mContainerId); + if (mRealTabContent == null) { + throw new IllegalStateException( + "No tab content FrameLayout found for id " + mContainerId); + } + } + } + + @Override + public void setOnTabChangedListener(OnTabChangeListener l) { + mOnTabChangeListener = l; + } + + public void addTab(TabHost.TabSpec tabSpec, Class clss, Bundle args) { + tabSpec.setContent(new DummyTabFactory(mContext)); + String tag = tabSpec.getTag(); + + TabInfo info = new TabInfo(tag, clss, args); + + if (mAttached) { + // If we are already attached to the window, then check to make + // sure this tab's fragment is inactive if it exists. This shouldn't + // normally happen. + info.fragment = mFragmentManager.findFragmentByTag(tag); + if (info.fragment != null && !info.fragment.isDetached()) { + FragmentTransaction ft = mFragmentManager.beginTransaction(); + ft.detach(info.fragment); + ft.commit(); + } + } + + mTabs.add(info); + addTab(tabSpec); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + String currentTab = getCurrentTabTag(); + + // Go through all tabs and make sure their fragments match + // the correct state. + FragmentTransaction ft = null; + for (int i=0; i

A transaction can only be committed with this method + * prior to its containing activity saving its state. If the commit is + * attempted after that point, an exception will be thrown. This is + * because the state after the commit can be lost if the activity needs to + * be restored from its state. See {@link #commitAllowingStateLoss()} for + * situations where it may be okay to lose the commit.

+ * + * @return Returns the identifier of this transaction's back stack entry, + * if {@link #addToBackStack(String)} had been called. Otherwise, returns + * a negative number. + */ + public abstract int commit(); + + /** + * Like {@link #commit} but allows the commit to be executed after an + * activity's state is saved. This is dangerous because the commit can + * be lost if the activity needs to later be restored from its state, so + * this should only be used for cases where it is okay for the UI state + * to change unexpectedly on the user. + */ + public abstract int commitAllowingStateLoss(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/FragmentTransitionCompat21.java b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentTransitionCompat21.java new file mode 100644 index 00000000000..ef41045c1ae --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/FragmentTransitionCompat21.java @@ -0,0 +1,383 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Rect; +import android.transition.Transition; +import android.transition.TransitionInflater; +import android.transition.TransitionManager; +import android.transition.TransitionSet; +import android.util.ArrayMap; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewTreeObserver; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +class FragmentTransitionCompat21 { + public static String getTransitionName(View view) { + return view.getTransitionName(); + } + + public static Object cloneTransition(Object transition) { + if (transition != null) { + transition = ((Transition)transition).clone(); + } + return transition; + } + + public static Object captureExitingViews(Object exitTransition, View root, + ArrayList viewList, Map namedViews, View nonExistentView) { + if (exitTransition != null) { + captureTransitioningViews(viewList, root); + if (namedViews != null) { + viewList.removeAll(namedViews.values()); + } + if (viewList.isEmpty()) { + exitTransition = null; + } else { + viewList.add(nonExistentView); + addTargets((Transition) exitTransition, viewList); + } + } + return exitTransition; + } + + public static void excludeTarget(Object transitionObject, View view, boolean exclude) { + Transition transition = (Transition) transitionObject; + transition.excludeTarget(view, exclude); + } + + public static void beginDelayedTransition(ViewGroup sceneRoot, Object transitionObject) { + Transition transition = (Transition) transitionObject; + TransitionManager.beginDelayedTransition(sceneRoot, transition); + } + + public static void setEpicenter(Object transitionObject, View view) { + Transition transition = (Transition) transitionObject; + final Rect epicenter = getBoundsOnScreen(view); + + transition.setEpicenterCallback(new Transition.EpicenterCallback() { + @Override + public Rect onGetEpicenter(Transition transition) { + return epicenter; + } + }); + } + + /** + * Prepares the enter transition by adding a non-existent view to the transition's target list + * and setting it epicenter callback. By adding a non-existent view to the target list, + * we can prevent any view from being targeted at the beginning of the transition. + * We will add to the views before the end state of the transition is captured so that the + * views will appear. At the start of the transition, we clear the list of targets so that + * we can restore the state of the transition and use it again. + * + *

The shared element transition maps its shared elements immediately prior to + * capturing the final state of the Transition.

+ */ + public static void addTransitionTargets(Object enterTransitionObject, + Object sharedElementTransitionObject, final View container, + final ViewRetriever inFragment, final View nonExistentView, + EpicenterView epicenterView, final Map nameOverrides, + final ArrayList enteringViews, final Map renamedViews, + final ArrayList sharedElementTargets) { + if (enterTransitionObject != null || sharedElementTransitionObject != null) { + final Transition enterTransition = (Transition) enterTransitionObject; + if (enterTransition != null) { + enterTransition.addTarget(nonExistentView); + } + if (sharedElementTransitionObject != null) { + Transition sharedElementTransition = (Transition) sharedElementTransitionObject; + addTargets(sharedElementTransition, sharedElementTargets); + } + + if (inFragment != null) { + container.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + public boolean onPreDraw() { + container.getViewTreeObserver().removeOnPreDrawListener(this); + View fragmentView = inFragment.getView(); + if (fragmentView != null) { + if (!nameOverrides.isEmpty()) { + findNamedViews(renamedViews, fragmentView); + renamedViews.keySet().retainAll(nameOverrides.values()); + for (Map.Entry entry : nameOverrides.entrySet()) { + String to = entry.getValue(); + View view = renamedViews.get(to); + if (view != null) { + String from = entry.getKey(); + view.setTransitionName(from); + } + } + } + if (enterTransition != null) { + captureTransitioningViews(enteringViews, fragmentView); + enteringViews.removeAll(renamedViews.values()); + enteringViews.add(nonExistentView); + enterTransition.removeTarget(nonExistentView); + addTargets(enterTransition, enteringViews); + } + } + return true; + } + }); + } + setSharedElementEpicenter(enterTransition, epicenterView); + } + } + + public static Object mergeTransitions(Object enterTransitionObject, + Object exitTransitionObject, Object sharedElementTransitionObject, + boolean allowOverlap) { + boolean overlap = true; + Transition enterTransition = (Transition) enterTransitionObject; + Transition exitTransition = (Transition) exitTransitionObject; + Transition sharedElementTransition = (Transition) sharedElementTransitionObject; + + if (enterTransition != null && exitTransition != null) { + overlap = allowOverlap; + } + + // Wrap the transitions. Explicit targets like in enter and exit will cause the + // views to be targeted regardless of excluded views. If that happens, then the + // excluded fragments views (hidden fragments) will still be in the transition. + + Transition transition; + if (overlap) { + // Regular transition -- do it all together + TransitionSet transitionSet = new TransitionSet(); + if (enterTransition != null) { + transitionSet.addTransition(enterTransition); + } + if (exitTransition != null) { + transitionSet.addTransition(exitTransition); + } + if (sharedElementTransition != null) { + transitionSet.addTransition(sharedElementTransition); + } + transition = transitionSet; + } else { + // First do exit, then enter, but allow shared element transition to happen + // during both. + Transition staggered = null; + if (exitTransition != null && enterTransition != null) { + staggered = new TransitionSet() + .addTransition(exitTransition) + .addTransition(enterTransition) + .setOrdering(TransitionSet.ORDERING_SEQUENTIAL); + } else if (exitTransition != null) { + staggered = exitTransition; + } else if (enterTransition != null) { + staggered = enterTransition; + } + if (sharedElementTransition != null) { + TransitionSet together = new TransitionSet(); + if (staggered != null) { + together.addTransition(staggered); + } + together.addTransition(sharedElementTransition); + transition = together; + } else { + transition = staggered; + } + } + return transition; + } + + + + private static void setSharedElementEpicenter(Transition transition, + final EpicenterView epicenterView) { + if (transition != null) { + transition.setEpicenterCallback(new Transition.EpicenterCallback() { + private Rect mEpicenter; + + @Override + public Rect onGetEpicenter(Transition transition) { + if (mEpicenter == null && epicenterView.epicenter != null) { + mEpicenter = getBoundsOnScreen(epicenterView.epicenter); + } + return mEpicenter; + } + }); + } + } + + private static Rect getBoundsOnScreen(View view) { + Rect epicenter = new Rect(); + int[] loc = new int[2]; + view.getLocationOnScreen(loc); + // not as good as View.getBoundsOnScreen, but that's not public + epicenter.set(loc[0], loc[1], loc[0] + view.getWidth(), loc[1] + view.getHeight()); + return epicenter; + } + + private static void captureTransitioningViews(ArrayList transitioningViews, View view) { + if (view.getVisibility() == View.VISIBLE) { + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + if (viewGroup.isTransitionGroup()) { + transitioningViews.add(viewGroup); + } else { + int count = viewGroup.getChildCount(); + for (int i = 0; i < count; i++) { + View child = viewGroup.getChildAt(i); + captureTransitioningViews(transitioningViews, child); + } + } + } else { + transitioningViews.add(view); + } + } + } + + public static void findNamedViews(Map namedViews, View view) { + if (view.getVisibility() == View.VISIBLE) { + String transitionName = view.getTransitionName(); + if (transitionName != null) { + namedViews.put(transitionName, view); + } + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + int count = viewGroup.getChildCount(); + for (int i = 0; i < count; i++) { + View child = viewGroup.getChildAt(i); + findNamedViews(namedViews, child); + } + } + } + } + + public static void cleanupTransitions(final View sceneRoot, final View nonExistentView, + Object enterTransitionObject, final ArrayList enteringViews, + Object exitTransitionObject, final ArrayList exitingViews, + Object sharedElementTransitionObject, final ArrayList sharedElementTargets, + Object overallTransitionObject, final ArrayList hiddenViews, + final Map renamedViews) { + final Transition enterTransition = (Transition) enterTransitionObject; + final Transition exitTransition = (Transition) exitTransitionObject; + final Transition sharedElementTransition = (Transition) sharedElementTransitionObject; + final Transition overallTransition = (Transition) overallTransitionObject; + if (overallTransition != null) { + sceneRoot.getViewTreeObserver().addOnPreDrawListener( + new ViewTreeObserver.OnPreDrawListener() { + public boolean onPreDraw() { + sceneRoot.getViewTreeObserver().removeOnPreDrawListener(this); + if (enterTransition != null) { + enterTransition.removeTarget(nonExistentView); + removeTargets(enterTransition, enteringViews); + } + if (exitTransition != null) { + removeTargets(exitTransition, exitingViews); + } + if (sharedElementTransition != null) { + removeTargets(sharedElementTransition, sharedElementTargets); + } + for (Map.Entry entry : renamedViews.entrySet()) { + View view = entry.getValue(); + String name = entry.getKey(); + view.setTransitionName(name); + } + int numViews = hiddenViews.size(); + for (int i = 0; i < numViews; i++) { + overallTransition.excludeTarget(hiddenViews.get(i), false); + } + overallTransition.excludeTarget(nonExistentView, false); + return true; + } + }); + } + } + + /** + * This method removes the views from transitions that target ONLY those views. + * The views list should match those added in addTargets and should contain + * one view that is not in the view hierarchy (state.nonExistentView). + */ + public static void removeTargets(Object transitionObject, ArrayList views) { + Transition transition = (Transition) transitionObject; + if (transition instanceof TransitionSet) { + TransitionSet set = (TransitionSet) transition; + int numTransitions = set.getTransitionCount(); + for (int i = 0; i < numTransitions; i++) { + Transition child = set.getTransitionAt(i); + removeTargets(child, views); + } + } else if (!hasSimpleTarget(transition)) { + List targets = transition.getTargets(); + if (targets != null && targets.size() == views.size() && + targets.containsAll(views)) { + // We have an exact match. We must have added these earlier in addTargets + for (int i = views.size() - 1; i >= 0; i--) { + transition.removeTarget(views.get(i)); + } + } + } + } + + /** + * This method adds views as targets to the transition, but only if the transition + * doesn't already have a target. It is best for views to contain one View object + * that does not exist in the view hierarchy (state.nonExistentView) so that + * when they are removed later, a list match will suffice to remove the targets. + * Otherwise, if you happened to have targeted the exact views for the transition, + * the removeTargets call will remove them unexpectedly. + */ + public static void addTargets(Object transitionObject, ArrayList views) { + Transition transition = (Transition) transitionObject; + if (transition instanceof TransitionSet) { + TransitionSet set = (TransitionSet) transition; + int numTransitions = set.getTransitionCount(); + for (int i = 0; i < numTransitions; i++) { + Transition child = set.getTransitionAt(i); + addTargets(child, views); + } + } else if (!hasSimpleTarget(transition)) { + List targets = transition.getTargets(); + if (isNullOrEmpty(targets)) { + // We can just add the target views + int numViews = views.size(); + for (int i = 0; i < numViews; i++) { + transition.addTarget(views.get(i)); + } + } + } + } + + private static boolean hasSimpleTarget(Transition transition) { + return !isNullOrEmpty(transition.getTargetIds()) || + !isNullOrEmpty(transition.getTargetNames()) || + !isNullOrEmpty(transition.getTargetTypes()); + } + + private static boolean isNullOrEmpty(List list) { + return list == null || list.isEmpty(); + } + + public interface ViewRetriever { + View getView(); + } + + public static class EpicenterView { + public View epicenter; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/INotificationSideChannel.aidl b/mobile/google-android-support-v4/src/android/support/v4/app/INotificationSideChannel.aidl new file mode 100644 index 00000000000..9df1577eae9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/INotificationSideChannel.aidl @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; + +/** + * Interface used for delivering notifications via a side channel that bypasses + * the NotificationManagerService. + * + * @hide + */ +oneway interface INotificationSideChannel { + /** + * Send an ambient notification to the service. + */ + void notify(String packageName, int id, String tag, in Notification notification); + + /** + * Cancel an already-notified notification. + */ + void cancel(String packageName, int id, String tag); + + /** + * Cancel all notifications for the given package. + */ + void cancelAll(String packageName); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ListFragment.java b/mobile/google-android-support-v4/src/android/support/v4/app/ListFragment.java new file mode 100644 index 00000000000..1ddea1c7906 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ListFragment.java @@ -0,0 +1,375 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.content.Context; +import android.os.Bundle; +import android.os.Handler; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.widget.AdapterView; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TextView; + +/** + * Static library support version of the framework's {@link android.app.ListFragment}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class ListFragment extends Fragment { + static final int INTERNAL_EMPTY_ID = 0x00ff0001; + static final int INTERNAL_PROGRESS_CONTAINER_ID = 0x00ff0002; + static final int INTERNAL_LIST_CONTAINER_ID = 0x00ff0003; + + final private Handler mHandler = new Handler(); + + final private Runnable mRequestFocus = new Runnable() { + public void run() { + mList.focusableViewAvailable(mList); + } + }; + + final private AdapterView.OnItemClickListener mOnClickListener + = new AdapterView.OnItemClickListener() { + public void onItemClick(AdapterView parent, View v, int position, long id) { + onListItemClick((ListView)parent, v, position, id); + } + }; + + ListAdapter mAdapter; + ListView mList; + View mEmptyView; + TextView mStandardEmptyView; + View mProgressContainer; + View mListContainer; + CharSequence mEmptyText; + boolean mListShown; + + public ListFragment() { + } + + /** + * Provide default implementation to return a simple list view. Subclasses + * can override to replace with their own layout. If doing so, the + * returned view hierarchy must have a ListView whose id + * is {@link android.R.id#list android.R.id.list} and can optionally + * have a sibling view id {@link android.R.id#empty android.R.id.empty} + * that is to be shown when the list is empty. + * + *

If you are overriding this method with your own custom content, + * consider including the standard layout {@link android.R.layout#list_content} + * in your layout file, so that you continue to retain all of the standard + * behavior of ListFragment. In particular, this is currently the only + * way to have the built-in indeterminant progress state be shown. + */ + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + final Context context = getActivity(); + + FrameLayout root = new FrameLayout(context); + + // ------------------------------------------------------------------ + + LinearLayout pframe = new LinearLayout(context); + pframe.setId(INTERNAL_PROGRESS_CONTAINER_ID); + pframe.setOrientation(LinearLayout.VERTICAL); + pframe.setVisibility(View.GONE); + pframe.setGravity(Gravity.CENTER); + + ProgressBar progress = new ProgressBar(context, null, + android.R.attr.progressBarStyleLarge); + pframe.addView(progress, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + + root.addView(pframe, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + // ------------------------------------------------------------------ + + FrameLayout lframe = new FrameLayout(context); + lframe.setId(INTERNAL_LIST_CONTAINER_ID); + + TextView tv = new TextView(getActivity()); + tv.setId(INTERNAL_EMPTY_ID); + tv.setGravity(Gravity.CENTER); + lframe.addView(tv, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + ListView lv = new ListView(getActivity()); + lv.setId(android.R.id.list); + lv.setDrawSelectorOnTop(false); + lframe.addView(lv, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + root.addView(lframe, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + // ------------------------------------------------------------------ + + root.setLayoutParams(new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT)); + + return root; + } + + /** + * Attach to list view once the view hierarchy has been created. + */ + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + ensureList(); + } + + /** + * Detach from list view. + */ + @Override + public void onDestroyView() { + mHandler.removeCallbacks(mRequestFocus); + mList = null; + mListShown = false; + mEmptyView = mProgressContainer = mListContainer = null; + mStandardEmptyView = null; + super.onDestroyView(); + } + + /** + * This method will be called when an item in the list is selected. + * Subclasses should override. Subclasses can call + * getListView().getItemAtPosition(position) if they need to access the + * data associated with the selected item. + * + * @param l The ListView where the click happened + * @param v The view that was clicked within the ListView + * @param position The position of the view in the list + * @param id The row id of the item that was clicked + */ + public void onListItemClick(ListView l, View v, int position, long id) { + } + + /** + * Provide the cursor for the list view. + */ + public void setListAdapter(ListAdapter adapter) { + boolean hadAdapter = mAdapter != null; + mAdapter = adapter; + if (mList != null) { + mList.setAdapter(adapter); + if (!mListShown && !hadAdapter) { + // The list was hidden, and previously didn't have an + // adapter. It is now time to show it. + setListShown(true, getView().getWindowToken() != null); + } + } + } + + /** + * Set the currently selected list item to the specified + * position with the adapter's data + * + * @param position + */ + public void setSelection(int position) { + ensureList(); + mList.setSelection(position); + } + + /** + * Get the position of the currently selected list item. + */ + public int getSelectedItemPosition() { + ensureList(); + return mList.getSelectedItemPosition(); + } + + /** + * Get the cursor row ID of the currently selected list item. + */ + public long getSelectedItemId() { + ensureList(); + return mList.getSelectedItemId(); + } + + /** + * Get the activity's list view widget. + */ + public ListView getListView() { + ensureList(); + return mList; + } + + /** + * The default content for a ListFragment has a TextView that can + * be shown when the list is empty. If you would like to have it + * shown, call this method to supply the text it should use. + */ + public void setEmptyText(CharSequence text) { + ensureList(); + if (mStandardEmptyView == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + mStandardEmptyView.setText(text); + if (mEmptyText == null) { + mList.setEmptyView(mStandardEmptyView); + } + mEmptyText = text; + } + + /** + * Control whether the list is being displayed. You can make it not + * displayed if you are waiting for the initial data to show in it. During + * this time an indeterminant progress indicator will be shown instead. + * + *

Applications do not normally need to use this themselves. The default + * behavior of ListFragment is to start with the list not being shown, only + * showing it once an adapter is given with {@link #setListAdapter(ListAdapter)}. + * If the list at that point had not been shown, when it does get shown + * it will be do without the user ever seeing the hidden state. + * + * @param shown If true, the list view is shown; if false, the progress + * indicator. The initial value is true. + */ + public void setListShown(boolean shown) { + setListShown(shown, true); + } + + /** + * Like {@link #setListShown(boolean)}, but no animation is used when + * transitioning from the previous state. + */ + public void setListShownNoAnimation(boolean shown) { + setListShown(shown, false); + } + + /** + * Control whether the list is being displayed. You can make it not + * displayed if you are waiting for the initial data to show in it. During + * this time an indeterminant progress indicator will be shown instead. + * + * @param shown If true, the list view is shown; if false, the progress + * indicator. The initial value is true. + * @param animate If true, an animation will be used to transition to the + * new state. + */ + private void setListShown(boolean shown, boolean animate) { + ensureList(); + if (mProgressContainer == null) { + throw new IllegalStateException("Can't be used with a custom content view"); + } + if (mListShown == shown) { + return; + } + mListShown = shown; + if (shown) { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + } else { + mProgressContainer.clearAnimation(); + mListContainer.clearAnimation(); + } + mProgressContainer.setVisibility(View.GONE); + mListContainer.setVisibility(View.VISIBLE); + } else { + if (animate) { + mProgressContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_in)); + mListContainer.startAnimation(AnimationUtils.loadAnimation( + getActivity(), android.R.anim.fade_out)); + } else { + mProgressContainer.clearAnimation(); + mListContainer.clearAnimation(); + } + mProgressContainer.setVisibility(View.VISIBLE); + mListContainer.setVisibility(View.GONE); + } + } + + /** + * Get the ListAdapter associated with this activity's ListView. + */ + public ListAdapter getListAdapter() { + return mAdapter; + } + + private void ensureList() { + if (mList != null) { + return; + } + View root = getView(); + if (root == null) { + throw new IllegalStateException("Content view not yet created"); + } + if (root instanceof ListView) { + mList = (ListView)root; + } else { + mStandardEmptyView = (TextView)root.findViewById(INTERNAL_EMPTY_ID); + if (mStandardEmptyView == null) { + mEmptyView = root.findViewById(android.R.id.empty); + } else { + mStandardEmptyView.setVisibility(View.GONE); + } + mProgressContainer = root.findViewById(INTERNAL_PROGRESS_CONTAINER_ID); + mListContainer = root.findViewById(INTERNAL_LIST_CONTAINER_ID); + View rawListView = root.findViewById(android.R.id.list); + if (!(rawListView instanceof ListView)) { + if (rawListView == null) { + throw new RuntimeException( + "Your content must have a ListView whose id attribute is " + + "'android.R.id.list'"); + } + throw new RuntimeException( + "Content has view with id attribute 'android.R.id.list' " + + "that is not a ListView class"); + } + mList = (ListView)rawListView; + if (mEmptyView != null) { + mList.setEmptyView(mEmptyView); + } else if (mEmptyText != null) { + mStandardEmptyView.setText(mEmptyText); + mList.setEmptyView(mStandardEmptyView); + } + } + mListShown = true; + mList.setOnItemClickListener(mOnClickListener); + if (mAdapter != null) { + ListAdapter adapter = mAdapter; + mAdapter = null; + setListAdapter(adapter); + } else { + // We are starting without an adapter, so assume we won't + // have our data right away and start with the progress indicator. + if (mProgressContainer != null) { + setListShown(false, false); + } + } + mHandler.post(mRequestFocus); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/LoaderManager.java b/mobile/google-android-support-v4/src/android/support/v4/app/LoaderManager.java new file mode 100644 index 00000000000..57c0a30158c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/LoaderManager.java @@ -0,0 +1,833 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.os.Bundle; +import android.support.v4.content.Loader; +import android.support.v4.util.DebugUtils; +import android.support.v4.util.SparseArrayCompat; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.lang.reflect.Modifier; + +/** + * Static library support version of the framework's {@link android.app.LoaderManager}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + * + *

Your activity must derive from {@link FragmentActivity} to use this. + */ +public abstract class LoaderManager { + /** + * Callback interface for a client to interact with the manager. + */ + public interface LoaderCallbacks { + /** + * Instantiate and return a new Loader for the given ID. + * + * @param id The ID whose loader is to be created. + * @param args Any arguments supplied by the caller. + * @return Return a new Loader instance that is ready to start loading. + */ + public Loader onCreateLoader(int id, Bundle args); + + /** + * Called when a previously created loader has finished its load. Note + * that normally an application is not allowed to commit fragment + * transactions while in this call, since it can happen after an + * activity's state is saved. See {@link FragmentManager#beginTransaction() + * FragmentManager.openTransaction()} for further discussion on this. + * + *

This function is guaranteed to be called prior to the release of + * the last data that was supplied for this Loader. At this point + * you should remove all use of the old data (since it will be released + * soon), but should not do your own release of the data since its Loader + * owns it and will take care of that. The Loader will take care of + * management of its data so you don't have to. In particular: + * + *

    + *
  • The Loader will monitor for changes to the data, and report + * them to you through new calls here. You should not monitor the + * data yourself. For example, if the data is a {@link android.database.Cursor} + * and you place it in a {@link android.widget.CursorAdapter}, use + * the {@link android.widget.CursorAdapter#CursorAdapter(android.content.Context, + * android.database.Cursor, int)} constructor without passing + * in either {@link android.widget.CursorAdapter#FLAG_AUTO_REQUERY} + * or {@link android.widget.CursorAdapter#FLAG_REGISTER_CONTENT_OBSERVER} + * (that is, use 0 for the flags argument). This prevents the CursorAdapter + * from doing its own observing of the Cursor, which is not needed since + * when a change happens you will get a new Cursor throw another call + * here. + *

  • The Loader will release the data once it knows the application + * is no longer using it. For example, if the data is + * a {@link android.database.Cursor} from a {@link android.content.CursorLoader}, + * you should not call close() on it yourself. If the Cursor is being placed in a + * {@link android.widget.CursorAdapter}, you should use the + * {@link android.widget.CursorAdapter#swapCursor(android.database.Cursor)} + * method so that the old Cursor is not closed. + *
+ * + * @param loader The Loader that has finished. + * @param data The data generated by the Loader. + */ + public void onLoadFinished(Loader loader, D data); + + /** + * Called when a previously created loader is being reset, and thus + * making its data unavailable. The application should at this point + * remove any references it has to the Loader's data. + * + * @param loader The Loader that is being reset. + */ + public void onLoaderReset(Loader loader); + } + + /** + * Ensures a loader is initialized and active. If the loader doesn't + * already exist, one is created and (if the activity/fragment is currently + * started) starts the loader. Otherwise the last created + * loader is re-used. + * + *

In either case, the given callback is associated with the loader, and + * will be called as the loader state changes. If at the point of call + * the caller is in its started state, and the requested loader + * already exists and has generated its data, then + * callback {@link LoaderCallbacks#onLoadFinished} will + * be called immediately (inside of this function), so you must be prepared + * for this to happen. + * + * @param id A unique identifier for this loader. Can be whatever you want. + * Identifiers are scoped to a particular LoaderManager instance. + * @param args Optional arguments to supply to the loader at construction. + * If a loader already exists (a new one does not need to be created), this + * parameter will be ignored and the last arguments continue to be used. + * @param callback Interface the LoaderManager will call to report about + * changes in the state of the loader. Required. + */ + public abstract Loader initLoader(int id, Bundle args, + LoaderManager.LoaderCallbacks callback); + + /** + * Starts a new or restarts an existing {@link android.content.Loader} in + * this manager, registers the callbacks to it, + * and (if the activity/fragment is currently started) starts loading it. + * If a loader with the same id has previously been + * started it will automatically be destroyed when the new loader completes + * its work. The callback will be delivered before the old loader + * is destroyed. + * + * @param id A unique identifier for this loader. Can be whatever you want. + * Identifiers are scoped to a particular LoaderManager instance. + * @param args Optional arguments to supply to the loader at construction. + * @param callback Interface the LoaderManager will call to report about + * changes in the state of the loader. Required. + */ + public abstract Loader restartLoader(int id, Bundle args, + LoaderManager.LoaderCallbacks callback); + + /** + * Stops and removes the loader with the given ID. If this loader + * had previously reported data to the client through + * {@link LoaderCallbacks#onLoadFinished(Loader, Object)}, a call + * will be made to {@link LoaderCallbacks#onLoaderReset(Loader)}. + */ + public abstract void destroyLoader(int id); + + /** + * Return the Loader with the given id or null if no matching Loader + * is found. + */ + public abstract Loader getLoader(int id); + + /** + * Print the LoaderManager's state into the given stream. + * + * @param prefix Text to print at the front of each line. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer A PrintWriter to which the dump is to be set. + * @param args Additional arguments to the dump request. + */ + public abstract void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args); + + /** + * Control whether the framework's internal loader manager debugging + * logs are turned on. If enabled, you will see output in logcat as + * the framework performs loader operations. + */ + public static void enableDebugLogging(boolean enabled) { + LoaderManagerImpl.DEBUG = enabled; + } + + /** + * Returns true if any loaders managed are currently running and have not + * returned data to the application yet. + */ + public boolean hasRunningLoaders() { return false; } +} + +class LoaderManagerImpl extends LoaderManager { + static final String TAG = "LoaderManager"; + static boolean DEBUG = false; + + // These are the currently active loaders. A loader is here + // from the time its load is started until it has been explicitly + // stopped or restarted by the application. + final SparseArrayCompat mLoaders = new SparseArrayCompat(); + + // These are previously run loaders. This list is maintained internally + // to avoid destroying a loader while an application is still using it. + // It allows an application to restart a loader, but continue using its + // previously run loader until the new loader's data is available. + final SparseArrayCompat mInactiveLoaders = new SparseArrayCompat(); + + final String mWho; + + FragmentActivity mActivity; + boolean mStarted; + boolean mRetaining; + boolean mRetainingStarted; + + boolean mCreatingLoader; + + final class LoaderInfo implements Loader.OnLoadCompleteListener { + final int mId; + final Bundle mArgs; + LoaderManager.LoaderCallbacks mCallbacks; + Loader mLoader; + boolean mHaveData; + boolean mDeliveredData; + Object mData; + boolean mStarted; + boolean mRetaining; + boolean mRetainingStarted; + boolean mReportNextStart; + boolean mDestroyed; + boolean mListenerRegistered; + + LoaderInfo mPendingLoader; + + public LoaderInfo(int id, Bundle args, LoaderManager.LoaderCallbacks callbacks) { + mId = id; + mArgs = args; + mCallbacks = callbacks; + } + + void start() { + if (mRetaining && mRetainingStarted) { + // Our owner is started, but we were being retained from a + // previous instance in the started state... so there is really + // nothing to do here, since the loaders are still started. + mStarted = true; + return; + } + + if (mStarted) { + // If loader already started, don't restart. + return; + } + + mStarted = true; + + if (DEBUG) Log.v(TAG, " Starting: " + this); + if (mLoader == null && mCallbacks != null) { + mLoader = mCallbacks.onCreateLoader(mId, mArgs); + } + if (mLoader != null) { + if (mLoader.getClass().isMemberClass() + && !Modifier.isStatic(mLoader.getClass().getModifiers())) { + throw new IllegalArgumentException( + "Object returned from onCreateLoader must not be a non-static inner member class: " + + mLoader); + } + if (!mListenerRegistered) { + mLoader.registerListener(mId, this); + mListenerRegistered = true; + } + mLoader.startLoading(); + } + } + + void retain() { + if (DEBUG) Log.v(TAG, " Retaining: " + this); + mRetaining = true; + mRetainingStarted = mStarted; + mStarted = false; + mCallbacks = null; + } + + void finishRetain() { + if (mRetaining) { + if (DEBUG) Log.v(TAG, " Finished Retaining: " + this); + mRetaining = false; + if (mStarted != mRetainingStarted) { + if (!mStarted) { + // This loader was retained in a started state, but + // at the end of retaining everything our owner is + // no longer started... so make it stop. + stop(); + } + } + } + + if (mStarted && mHaveData && !mReportNextStart) { + // This loader has retained its data, either completely across + // a configuration change or just whatever the last data set + // was after being restarted from a stop, and now at the point of + // finishing the retain we find we remain started, have + // our data, and the owner has a new callback... so + // let's deliver the data now. + callOnLoadFinished(mLoader, mData); + } + } + + void reportStart() { + if (mStarted) { + if (mReportNextStart) { + mReportNextStart = false; + if (mHaveData) { + callOnLoadFinished(mLoader, mData); + } + } + } + } + + void stop() { + if (DEBUG) Log.v(TAG, " Stopping: " + this); + mStarted = false; + if (!mRetaining) { + if (mLoader != null && mListenerRegistered) { + // Let the loader know we're done with it + mListenerRegistered = false; + mLoader.unregisterListener(this); + mLoader.stopLoading(); + } + } + } + + void destroy() { + if (DEBUG) Log.v(TAG, " Destroying: " + this); + mDestroyed = true; + boolean needReset = mDeliveredData; + mDeliveredData = false; + if (mCallbacks != null && mLoader != null && mHaveData && needReset) { + if (DEBUG) Log.v(TAG, " Reseting: " + this); + String lastBecause = null; + if (mActivity != null) { + lastBecause = mActivity.mFragments.mNoTransactionsBecause; + mActivity.mFragments.mNoTransactionsBecause = "onLoaderReset"; + } + try { + mCallbacks.onLoaderReset(mLoader); + } finally { + if (mActivity != null) { + mActivity.mFragments.mNoTransactionsBecause = lastBecause; + } + } + } + mCallbacks = null; + mData = null; + mHaveData = false; + if (mLoader != null) { + if (mListenerRegistered) { + mListenerRegistered = false; + mLoader.unregisterListener(this); + } + mLoader.reset(); + } + if (mPendingLoader != null) { + mPendingLoader.destroy(); + } + } + + @Override public void onLoadComplete(Loader loader, Object data) { + if (DEBUG) Log.v(TAG, "onLoadComplete: " + this); + + if (mDestroyed) { + if (DEBUG) Log.v(TAG, " Ignoring load complete -- destroyed"); + return; + } + + if (mLoaders.get(mId) != this) { + // This data is not coming from the current active loader. + // We don't care about it. + if (DEBUG) Log.v(TAG, " Ignoring load complete -- not active"); + return; + } + + LoaderInfo pending = mPendingLoader; + if (pending != null) { + // There is a new request pending and we were just + // waiting for the old one to complete before starting + // it. So now it is time, switch over to the new loader. + if (DEBUG) Log.v(TAG, " Switching to pending loader: " + pending); + mPendingLoader = null; + mLoaders.put(mId, null); + destroy(); + installLoader(pending); + return; + } + + // Notify of the new data so the app can switch out the old data before + // we try to destroy it. + if (mData != data || !mHaveData) { + mData = data; + mHaveData = true; + if (mStarted) { + callOnLoadFinished(loader, data); + } + } + + //if (DEBUG) Log.v(TAG, " onLoadFinished returned: " + this); + + // We have now given the application the new loader with its + // loaded data, so it should have stopped using the previous + // loader. If there is a previous loader on the inactive list, + // clean it up. + LoaderInfo info = mInactiveLoaders.get(mId); + if (info != null && info != this) { + info.mDeliveredData = false; + info.destroy(); + mInactiveLoaders.remove(mId); + } + + if (mActivity != null && !hasRunningLoaders()) { + mActivity.mFragments.startPendingDeferredFragments(); + } + } + + void callOnLoadFinished(Loader loader, Object data) { + if (mCallbacks != null) { + String lastBecause = null; + if (mActivity != null) { + lastBecause = mActivity.mFragments.mNoTransactionsBecause; + mActivity.mFragments.mNoTransactionsBecause = "onLoadFinished"; + } + try { + if (DEBUG) Log.v(TAG, " onLoadFinished in " + loader + ": " + + loader.dataToString(data)); + mCallbacks.onLoadFinished(loader, data); + } finally { + if (mActivity != null) { + mActivity.mFragments.mNoTransactionsBecause = lastBecause; + } + } + mDeliveredData = true; + } + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(64); + sb.append("LoaderInfo{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" #"); + sb.append(mId); + sb.append(" : "); + DebugUtils.buildShortClassTag(mLoader, sb); + sb.append("}}"); + return sb.toString(); + } + + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + writer.print(prefix); writer.print("mId="); writer.print(mId); + writer.print(" mArgs="); writer.println(mArgs); + writer.print(prefix); writer.print("mCallbacks="); writer.println(mCallbacks); + writer.print(prefix); writer.print("mLoader="); writer.println(mLoader); + if (mLoader != null) { + mLoader.dump(prefix + " ", fd, writer, args); + } + if (mHaveData || mDeliveredData) { + writer.print(prefix); writer.print("mHaveData="); writer.print(mHaveData); + writer.print(" mDeliveredData="); writer.println(mDeliveredData); + writer.print(prefix); writer.print("mData="); writer.println(mData); + } + writer.print(prefix); writer.print("mStarted="); writer.print(mStarted); + writer.print(" mReportNextStart="); writer.print(mReportNextStart); + writer.print(" mDestroyed="); writer.println(mDestroyed); + writer.print(prefix); writer.print("mRetaining="); writer.print(mRetaining); + writer.print(" mRetainingStarted="); writer.print(mRetainingStarted); + writer.print(" mListenerRegistered="); writer.println(mListenerRegistered); + if (mPendingLoader != null) { + writer.print(prefix); writer.println("Pending Loader "); + writer.print(mPendingLoader); writer.println(":"); + mPendingLoader.dump(prefix + " ", fd, writer, args); + } + } + } + + LoaderManagerImpl(String who, FragmentActivity activity, boolean started) { + mWho = who; + mActivity = activity; + mStarted = started; + } + + void updateActivity(FragmentActivity activity) { + mActivity = activity; + } + + private LoaderInfo createLoader(int id, Bundle args, + LoaderManager.LoaderCallbacks callback) { + LoaderInfo info = new LoaderInfo(id, args, (LoaderManager.LoaderCallbacks)callback); + Loader loader = callback.onCreateLoader(id, args); + info.mLoader = (Loader)loader; + return info; + } + + private LoaderInfo createAndInstallLoader(int id, Bundle args, + LoaderManager.LoaderCallbacks callback) { + try { + mCreatingLoader = true; + LoaderInfo info = createLoader(id, args, callback); + installLoader(info); + return info; + } finally { + mCreatingLoader = false; + } + } + + void installLoader(LoaderInfo info) { + mLoaders.put(info.mId, info); + if (mStarted) { + // The activity will start all existing loaders in it's onStart(), + // so only start them here if we're past that point of the activitiy's + // life cycle + info.start(); + } + } + + /** + * Call to initialize a particular ID with a Loader. If this ID already + * has a Loader associated with it, it is left unchanged and any previous + * callbacks replaced with the newly provided ones. If there is not currently + * a Loader for the ID, a new one is created and started. + * + *

This function should generally be used when a component is initializing, + * to ensure that a Loader it relies on is created. This allows it to re-use + * an existing Loader's data if there already is one, so that for example + * when an {@link Activity} is re-created after a configuration change it + * does not need to re-create its loaders. + * + *

Note that in the case where an existing Loader is re-used, the + * args given here will be ignored because you will + * continue using the previous Loader. + * + * @param id A unique (to this LoaderManager instance) identifier under + * which to manage the new Loader. + * @param args Optional arguments that will be propagated to + * {@link LoaderCallbacks#onCreateLoader(int, Bundle) LoaderCallbacks.onCreateLoader()}. + * @param callback Interface implementing management of this Loader. Required. + * Its onCreateLoader() method will be called while inside of the function to + * instantiate the Loader object. + */ + @SuppressWarnings("unchecked") + public Loader initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks callback) { + if (mCreatingLoader) { + throw new IllegalStateException("Called while creating a loader"); + } + + LoaderInfo info = mLoaders.get(id); + + if (DEBUG) Log.v(TAG, "initLoader in " + this + ": args=" + args); + + if (info == null) { + // Loader doesn't already exist; create. + info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks)callback); + if (DEBUG) Log.v(TAG, " Created new loader " + info); + } else { + if (DEBUG) Log.v(TAG, " Re-using existing loader " + info); + info.mCallbacks = (LoaderManager.LoaderCallbacks)callback; + } + + if (info.mHaveData && mStarted) { + // If the loader has already generated its data, report it now. + info.callOnLoadFinished(info.mLoader, info.mData); + } + + return (Loader)info.mLoader; + } + + /** + * Call to re-create the Loader associated with a particular ID. If there + * is currently a Loader associated with this ID, it will be + * canceled/stopped/destroyed as appropriate. A new Loader with the given + * arguments will be created and its data delivered to you once available. + * + *

This function does some throttling of Loaders. If too many Loaders + * have been created for the given ID but not yet generated their data, + * new calls to this function will create and return a new Loader but not + * actually start it until some previous loaders have completed. + * + *

After calling this function, any previous Loaders associated with + * this ID will be considered invalid, and you will receive no further + * data updates from them. + * + * @param id A unique (to this LoaderManager instance) identifier under + * which to manage the new Loader. + * @param args Optional arguments that will be propagated to + * {@link LoaderCallbacks#onCreateLoader(int, Bundle) LoaderCallbacks.onCreateLoader()}. + * @param callback Interface implementing management of this Loader. Required. + * Its onCreateLoader() method will be called while inside of the function to + * instantiate the Loader object. + */ + @SuppressWarnings("unchecked") + public Loader restartLoader(int id, Bundle args, LoaderManager.LoaderCallbacks callback) { + if (mCreatingLoader) { + throw new IllegalStateException("Called while creating a loader"); + } + + LoaderInfo info = mLoaders.get(id); + if (DEBUG) Log.v(TAG, "restartLoader in " + this + ": args=" + args); + if (info != null) { + LoaderInfo inactive = mInactiveLoaders.get(id); + if (inactive != null) { + if (info.mHaveData) { + // This loader now has data... we are probably being + // called from within onLoadComplete, where we haven't + // yet destroyed the last inactive loader. So just do + // that now. + if (DEBUG) Log.v(TAG, " Removing last inactive loader: " + info); + inactive.mDeliveredData = false; + inactive.destroy(); + info.mLoader.abandon(); + mInactiveLoaders.put(id, info); + } else { + // We already have an inactive loader for this ID that we are + // waiting for! What to do, what to do... + if (!info.mStarted) { + // The current Loader has not been started... we thus + // have no reason to keep it around, so bam, slam, + // thank-you-ma'am. + if (DEBUG) Log.v(TAG, " Current loader is stopped; replacing"); + mLoaders.put(id, null); + info.destroy(); + } else { + // Now we have three active loaders... we'll queue + // up this request to be processed once one of the other loaders + // finishes. + if (info.mPendingLoader != null) { + if (DEBUG) Log.v(TAG, " Removing pending loader: " + info.mPendingLoader); + info.mPendingLoader.destroy(); + info.mPendingLoader = null; + } + if (DEBUG) Log.v(TAG, " Enqueuing as new pending loader"); + info.mPendingLoader = createLoader(id, args, + (LoaderManager.LoaderCallbacks)callback); + return (Loader)info.mPendingLoader.mLoader; + } + } + } else { + // Keep track of the previous instance of this loader so we can destroy + // it when the new one completes. + if (DEBUG) Log.v(TAG, " Making last loader inactive: " + info); + info.mLoader.abandon(); + mInactiveLoaders.put(id, info); + } + } + + info = createAndInstallLoader(id, args, (LoaderManager.LoaderCallbacks)callback); + return (Loader)info.mLoader; + } + + /** + * Rip down, tear apart, shred to pieces a current Loader ID. After returning + * from this function, any Loader objects associated with this ID are + * destroyed. Any data associated with them is destroyed. You better not + * be using it when you do this. + * @param id Identifier of the Loader to be destroyed. + */ + public void destroyLoader(int id) { + if (mCreatingLoader) { + throw new IllegalStateException("Called while creating a loader"); + } + + if (DEBUG) Log.v(TAG, "destroyLoader in " + this + " of " + id); + int idx = mLoaders.indexOfKey(id); + if (idx >= 0) { + LoaderInfo info = mLoaders.valueAt(idx); + mLoaders.removeAt(idx); + info.destroy(); + } + idx = mInactiveLoaders.indexOfKey(id); + if (idx >= 0) { + LoaderInfo info = mInactiveLoaders.valueAt(idx); + mInactiveLoaders.removeAt(idx); + info.destroy(); + } + if (mActivity != null && !hasRunningLoaders()) { + mActivity.mFragments.startPendingDeferredFragments(); + } + } + + /** + * Return the most recent Loader object associated with the + * given ID. + */ + @SuppressWarnings("unchecked") + public Loader getLoader(int id) { + if (mCreatingLoader) { + throw new IllegalStateException("Called while creating a loader"); + } + + LoaderInfo loaderInfo = mLoaders.get(id); + if (loaderInfo != null) { + if (loaderInfo.mPendingLoader != null) { + return (Loader)loaderInfo.mPendingLoader.mLoader; + } + return (Loader)loaderInfo.mLoader; + } + return null; + } + + void doStart() { + if (DEBUG) Log.v(TAG, "Starting in " + this); + if (mStarted) { + RuntimeException e = new RuntimeException("here"); + e.fillInStackTrace(); + Log.w(TAG, "Called doStart when already started: " + this, e); + return; + } + + mStarted = true; + + // Call out to sub classes so they can start their loaders + // Let the existing loaders know that we want to be notified when a load is complete + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).start(); + } + } + + void doStop() { + if (DEBUG) Log.v(TAG, "Stopping in " + this); + if (!mStarted) { + RuntimeException e = new RuntimeException("here"); + e.fillInStackTrace(); + Log.w(TAG, "Called doStop when not started: " + this, e); + return; + } + + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).stop(); + } + mStarted = false; + } + + void doRetain() { + if (DEBUG) Log.v(TAG, "Retaining in " + this); + if (!mStarted) { + RuntimeException e = new RuntimeException("here"); + e.fillInStackTrace(); + Log.w(TAG, "Called doRetain when not started: " + this, e); + return; + } + + mRetaining = true; + mStarted = false; + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).retain(); + } + } + + void finishRetain() { + if (mRetaining) { + if (DEBUG) Log.v(TAG, "Finished Retaining in " + this); + + mRetaining = false; + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).finishRetain(); + } + } + } + + void doReportNextStart() { + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).mReportNextStart = true; + } + } + + void doReportStart() { + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).reportStart(); + } + } + + void doDestroy() { + if (!mRetaining) { + if (DEBUG) Log.v(TAG, "Destroying Active in " + this); + for (int i = mLoaders.size()-1; i >= 0; i--) { + mLoaders.valueAt(i).destroy(); + } + mLoaders.clear(); + } + + if (DEBUG) Log.v(TAG, "Destroying Inactive in " + this); + for (int i = mInactiveLoaders.size()-1; i >= 0; i--) { + mInactiveLoaders.valueAt(i).destroy(); + } + mInactiveLoaders.clear(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(128); + sb.append("LoaderManager{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" in "); + DebugUtils.buildShortClassTag(mActivity, sb); + sb.append("}}"); + return sb.toString(); + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + if (mLoaders.size() > 0) { + writer.print(prefix); writer.println("Active Loaders:"); + String innerPrefix = prefix + " "; + for (int i=0; i < mLoaders.size(); i++) { + LoaderInfo li = mLoaders.valueAt(i); + writer.print(prefix); writer.print(" #"); writer.print(mLoaders.keyAt(i)); + writer.print(": "); writer.println(li.toString()); + li.dump(innerPrefix, fd, writer, args); + } + } + if (mInactiveLoaders.size() > 0) { + writer.print(prefix); writer.println("Inactive Loaders:"); + String innerPrefix = prefix + " "; + for (int i=0; i < mInactiveLoaders.size(); i++) { + LoaderInfo li = mInactiveLoaders.valueAt(i); + writer.print(prefix); writer.print(" #"); writer.print(mInactiveLoaders.keyAt(i)); + writer.print(": "); writer.println(li.toString()); + li.dump(innerPrefix, fd, writer, args); + } + } + } + + @Override + public boolean hasRunningLoaders() { + boolean loadersRunning = false; + final int count = mLoaders.size(); + for (int i = 0; i < count; i++) { + final LoaderInfo li = mLoaders.valueAt(i); + loadersRunning |= li.mStarted && !li.mDeliveredData; + } + return loadersRunning; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NavUtils.java b/mobile/google-android-support-v4/src/android/support/v4/app/NavUtils.java new file mode 100644 index 00000000000..682aaaf5e5c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NavUtils.java @@ -0,0 +1,309 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.support.annotation.Nullable; +import android.support.v4.content.IntentCompat; +import android.util.Log; + +/** + * NavUtils provides helper functionality for applications implementing + * recommended Android UI navigation patterns. For information about recommended + * navigation patterns see + * Tasks and Back Stack + * from the developer guide and Navigation + * from the design guide. + */ +public class NavUtils { + private static final String TAG = "NavUtils"; + public static final String PARENT_ACTIVITY = "android.support.PARENT_ACTIVITY"; + + interface NavUtilsImpl { + Intent getParentActivityIntent(Activity activity); + boolean shouldUpRecreateTask(Activity activity, Intent targetIntent); + void navigateUpTo(Activity activity, Intent upIntent); + String getParentActivityName(Context context, ActivityInfo info); + } + + static class NavUtilsImplBase implements NavUtilsImpl { + + @Override + public Intent getParentActivityIntent(Activity activity) { + String parentName = NavUtils.getParentActivityName(activity); + if (parentName == null) return null; + + // If the parent itself has no parent, generate a main activity intent. + final ComponentName target = new ComponentName(activity, parentName); + try { + final String grandparent = NavUtils.getParentActivityName(activity, target); + final Intent parentIntent = grandparent == null + ? IntentCompat.makeMainActivity(target) + : new Intent().setComponent(target); + return parentIntent; + } catch (NameNotFoundException e) { + Log.e(TAG, "getParentActivityIntent: bad parentActivityName '" + parentName + + "' in manifest"); + return null; + } + } + + @Override + public boolean shouldUpRecreateTask(Activity activity, Intent targetIntent) { + String action = activity.getIntent().getAction(); + return action != null && !action.equals(Intent.ACTION_MAIN); + } + + @Override + public void navigateUpTo(Activity activity, Intent upIntent) { + upIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + activity.startActivity(upIntent); + activity.finish(); + } + + @Override + public String getParentActivityName(Context context, ActivityInfo info) { + if (info.metaData == null) return null; + String parentActivity = info.metaData.getString(PARENT_ACTIVITY); + if (parentActivity == null) return null; + if (parentActivity.charAt(0) == '.') { + parentActivity = context.getPackageName() + parentActivity; + } + return parentActivity; + } + } + + static class NavUtilsImplJB extends NavUtilsImplBase { + + @Override + public Intent getParentActivityIntent(Activity activity) { + // Prefer the "real" JB definition if available, + // else fall back to the meta-data element. + Intent result = NavUtilsJB.getParentActivityIntent(activity); + if (result == null) { + result = superGetParentActivityIntent(activity); + } + return result; + } + + Intent superGetParentActivityIntent(Activity activity) { + return super.getParentActivityIntent(activity); + } + + @Override + public boolean shouldUpRecreateTask(Activity activity, Intent targetIntent) { + return NavUtilsJB.shouldUpRecreateTask(activity, targetIntent); + } + + @Override + public void navigateUpTo(Activity activity, Intent upIntent) { + NavUtilsJB.navigateUpTo(activity, upIntent); + } + + @Override + public String getParentActivityName(Context context, ActivityInfo info) { + String result = NavUtilsJB.getParentActivityName(info); + if (result == null) { + result = super.getParentActivityName(context, info); + } + return result; + } + } + + private static final NavUtilsImpl IMPL; + + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 16) { + IMPL = new NavUtilsImplJB(); + } else { + IMPL = new NavUtilsImplBase(); + } + } + + /** + * Returns true if sourceActivity should recreate the task when navigating 'up' + * by using targetIntent. + * + *

If this method returns false the app can trivially call + * {@link #navigateUpTo(Activity, Intent)} using the same parameters to correctly perform + * up navigation. If this method returns true, the app should synthesize a new task stack + * by using {@link TaskStackBuilder} or another similar mechanism to perform up navigation.

+ * + * @param sourceActivity The current activity from which the user is attempting to navigate up + * @param targetIntent An intent representing the target destination for up navigation + * @return true if navigating up should recreate a new task stack, false if the same task + * should be used for the destination + */ + public static boolean shouldUpRecreateTask(Activity sourceActivity, Intent targetIntent) { + return IMPL.shouldUpRecreateTask(sourceActivity, targetIntent); + } + + /** + * Convenience method that is equivalent to calling + * {@link #navigateUpTo(Activity, Intent) navigateUpTo}(sourceActivity, + * {@link #getParentActivityIntent(Activity) getParentActivityIntent} (sourceActivity)). + * sourceActivity will be finished by this call. + * + *

Note: This method should only be used when sourceActivity and the corresponding + * parent are within the same task. If up navigation should cross tasks in some cases, see + * {@link #shouldUpRecreateTask(Activity, Intent)}.

+ * + * @param sourceActivity The current activity from which the user is attempting to navigate up + */ + public static void navigateUpFromSameTask(Activity sourceActivity) { + Intent upIntent = getParentActivityIntent(sourceActivity); + + if (upIntent == null) { + throw new IllegalArgumentException("Activity " + + sourceActivity.getClass().getSimpleName() + + " does not have a parent activity name specified." + + " (Did you forget to add the android.support.PARENT_ACTIVITY " + + " element in your manifest?)"); + } + + navigateUpTo(sourceActivity, upIntent); + } + + /** + * Navigate from sourceActivity to the activity specified by upIntent, finishing sourceActivity + * in the process. upIntent will have the flag {@link Intent#FLAG_ACTIVITY_CLEAR_TOP} set + * by this method, along with any others required for proper up navigation as outlined + * in the Android Design Guide. + * + *

This method should be used when performing up navigation from within the same task + * as the destination. If up navigation should cross tasks in some cases, see + * {@link #shouldUpRecreateTask(Activity, Intent)}.

+ * + * @param sourceActivity The current activity from which the user is attempting to navigate up + * @param upIntent An intent representing the target destination for up navigation + */ + public static void navigateUpTo(Activity sourceActivity, Intent upIntent) { + IMPL.navigateUpTo(sourceActivity, upIntent); + } + + /** + * Obtain an {@link Intent} that will launch an explicit target activity + * specified by sourceActivity's {@link #PARENT_ACTIVITY} <meta-data> + * element in the application's manifest. If the device is running + * Jellybean or newer, the android:parentActivityName attribute will be preferred + * if it is present. + * + * @param sourceActivity Activity to fetch a parent intent for + * @return a new Intent targeting the defined parent activity of sourceActivity + */ + public static Intent getParentActivityIntent(Activity sourceActivity) { + return IMPL.getParentActivityIntent(sourceActivity); + } + + /** + * Obtain an {@link Intent} that will launch an explicit target activity + * specified by sourceActivityClass's {@link #PARENT_ACTIVITY} <meta-data> + * element in the application's manifest. + * + * @param context Context for looking up the activity component for sourceActivityClass + * @param sourceActivityClass {@link java.lang.Class} object for an Activity class + * @return a new Intent targeting the defined parent activity of sourceActivity + * @throws NameNotFoundException if the ComponentName for sourceActivityClass is invalid + */ + public static Intent getParentActivityIntent(Context context, Class sourceActivityClass) + throws NameNotFoundException { + String parentActivity = getParentActivityName(context, + new ComponentName(context, sourceActivityClass)); + if (parentActivity == null) return null; + + // If the parent itself has no parent, generate a main activity intent. + final ComponentName target = new ComponentName(context, parentActivity); + final String grandparent = getParentActivityName(context, target); + final Intent parentIntent = grandparent == null + ? IntentCompat.makeMainActivity(target) + : new Intent().setComponent(target); + return parentIntent; + } + + /** + * Obtain an {@link Intent} that will launch an explicit target activity + * specified by sourceActivityClass's {@link #PARENT_ACTIVITY} <meta-data> + * element in the application's manifest. + * + * @param context Context for looking up the activity component for the source activity + * @param componentName ComponentName for the source Activity + * @return a new Intent targeting the defined parent activity of sourceActivity + * @throws NameNotFoundException if the ComponentName for sourceActivityClass is invalid + */ + public static Intent getParentActivityIntent(Context context, ComponentName componentName) + throws NameNotFoundException { + String parentActivity = getParentActivityName(context, componentName); + if (parentActivity == null) return null; + + // If the parent itself has no parent, generate a main activity intent. + final ComponentName target = new ComponentName( + componentName.getPackageName(), parentActivity); + final String grandparent = getParentActivityName(context, target); + final Intent parentIntent = grandparent == null + ? IntentCompat.makeMainActivity(target) + : new Intent().setComponent(target); + return parentIntent; + } + + /** + * Return the fully qualified class name of sourceActivity's parent activity as specified by + * a {@link #PARENT_ACTIVITY} <meta-data> element within the activity element in + * the application's manifest. + * + * @param sourceActivity Activity to fetch a parent class name for + * @return The fully qualified class name of sourceActivity's parent activity or null if + * it was not specified + */ + @Nullable + public static String getParentActivityName(Activity sourceActivity) { + try { + return getParentActivityName(sourceActivity, sourceActivity.getComponentName()); + } catch (NameNotFoundException e) { + // Component name of supplied activity does not exist...? + throw new IllegalArgumentException(e); + } + } + /** + * Return the fully qualified class name of a source activity's parent activity as specified by + * a {@link #PARENT_ACTIVITY} <meta-data> element within the activity element in + * the application's manifest. The source activity is provided by componentName. + * + * @param context Context for looking up the activity component for the source activity + * @param componentName ComponentName for the source Activity + * @return The fully qualified class name of sourceActivity's parent activity or null if + * it was not specified + */ + @Nullable + public static String getParentActivityName(Context context, ComponentName componentName) + throws NameNotFoundException { + PackageManager pm = context.getPackageManager(); + ActivityInfo info = pm.getActivityInfo(componentName, PackageManager.GET_META_DATA); + String parentActivity = IMPL.getParentActivityName(context, info); + return parentActivity; + } + + /** No instances! */ + private NavUtils() { + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NavUtilsJB.java b/mobile/google-android-support-v4/src/android/support/v4/app/NavUtilsJB.java new file mode 100644 index 00000000000..a9a76c2263c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NavUtilsJB.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.Intent; +import android.content.pm.ActivityInfo; + +class NavUtilsJB { + public static Intent getParentActivityIntent(Activity activity) { + return activity.getParentActivityIntent(); + } + + public static boolean shouldUpRecreateTask(Activity activity, Intent targetIntent) { + return activity.shouldUpRecreateTask(targetIntent); + } + + public static void navigateUpTo(Activity activity, Intent upIntent) { + activity.navigateUpTo(upIntent); + } + + public static String getParentActivityName(ActivityInfo info) { + return info.parentActivityName; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NoSaveStateFrameLayout.java b/mobile/google-android-support-v4/src/android/support/v4/app/NoSaveStateFrameLayout.java new file mode 100644 index 00000000000..d0ebb96e8e0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NoSaveStateFrameLayout.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.content.Context; +import android.os.Parcelable; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +/** + * Pre-Honeycomb versions of the platform don't have {@link View#setSaveFromParentEnabled(boolean)}, + * so instead we insert this between the view and its parent. + */ +class NoSaveStateFrameLayout extends FrameLayout { + static ViewGroup wrap(View child) { + NoSaveStateFrameLayout wrapper = new NoSaveStateFrameLayout(child.getContext()); + ViewGroup.LayoutParams childParams = child.getLayoutParams(); + if (childParams != null) { + wrapper.setLayoutParams(childParams); + } + NoSaveStateFrameLayout.LayoutParams lp = new NoSaveStateFrameLayout.LayoutParams( + ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.FILL_PARENT); + child.setLayoutParams(lp); + wrapper.addView(child); + return wrapper; + } + + public NoSaveStateFrameLayout(Context context) { + super(context); + } + + /** + * Override to prevent freezing of any child views. + */ + @Override + protected void dispatchSaveInstanceState(SparseArray container) { + dispatchFreezeSelfOnly(container); + } + + /** + * Override to prevent thawing of any child views. + */ + @Override + protected void dispatchRestoreInstanceState(SparseArray container) { + dispatchThawSelfOnly(container); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithActions.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithActions.java new file mode 100644 index 00000000000..8e8d8ce4b74 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithActions.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +/** + * Interface implemented by notification compat builders that support adding actions. + */ +interface NotificationBuilderWithActions { + public void addAction(NotificationCompatBase.Action action); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithBuilderAccessor.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithBuilderAccessor.java new file mode 100644 index 00000000000..f1b2d82ba67 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationBuilderWithBuilderAccessor.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; + +/** + * Interface implemented by notification compat builders that support + * an accessor for {@link Notification.Builder}. {@link Notification.Builder} + * was introduced in HoneyComb. + * + * @hide + */ +public interface NotificationBuilderWithBuilderAccessor { + public Notification.Builder getBuilder(); + public Notification build(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompat.java new file mode 100644 index 00000000000..99c269ea5bb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompat.java @@ -0,0 +1,3360 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; +import android.support.v4.view.GravityCompat; +import android.view.Gravity; +import android.widget.RemoteViews; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Helper for accessing features in {@link android.app.Notification} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class NotificationCompat { + + /** + * Use all default values (where applicable). + */ + public static final int DEFAULT_ALL = ~0; + + /** + * Use the default notification sound. This will ignore any sound set using + * {@link Builder#setSound} + * + *

+ * A notification that is noisy is more likely to be presented as a heads-up notification, + * on some platforms. + *

+ * + * @see Builder#setDefaults + */ + public static final int DEFAULT_SOUND = 1; + + /** + * Use the default notification vibrate. This will ignore any vibrate set using + * {@link Builder#setVibrate}. Using phone vibration requires the + * {@link android.Manifest.permission#VIBRATE VIBRATE} permission. + * + *

+ * A notification that vibrates is more likely to be presented as a heads-up notification, + * on some platforms. + *

+ * + * @see Builder#setDefaults + */ + public static final int DEFAULT_VIBRATE = 2; + + /** + * Use the default notification lights. This will ignore the + * {@link #FLAG_SHOW_LIGHTS} bit, and values set with {@link Builder#setLights}. + * + * @see Builder#setDefaults + */ + public static final int DEFAULT_LIGHTS = 4; + + /** + * Use this constant as the value for audioStreamType to request that + * the default stream type for notifications be used. Currently the + * default stream type is {@link AudioManager#STREAM_NOTIFICATION}. + */ + public static final int STREAM_DEFAULT = -1; + + /** + * Bit set in the Notification flags field when LEDs should be turned on + * for this notification. + */ + public static final int FLAG_SHOW_LIGHTS = 0x00000001; + + /** + * Bit set in the Notification flags field if this notification is in + * reference to something that is ongoing, like a phone call. It should + * not be set if this notification is in reference to something that + * happened at a particular point in time, like a missed phone call. + */ + public static final int FLAG_ONGOING_EVENT = 0x00000002; + + /** + * Bit set in the Notification flags field if + * the audio will be repeated until the notification is + * cancelled or the notification window is opened. + */ + public static final int FLAG_INSISTENT = 0x00000004; + + /** + * Bit set in the Notification flags field if the notification's sound, + * vibrate and ticker should only be played if the notification is not already showing. + */ + public static final int FLAG_ONLY_ALERT_ONCE = 0x00000008; + + /** + * Bit set in the Notification flags field if the notification should be canceled when + * it is clicked by the user. + */ + public static final int FLAG_AUTO_CANCEL = 0x00000010; + + /** + * Bit set in the Notification flags field if the notification should not be canceled + * when the user clicks the Clear all button. + */ + public static final int FLAG_NO_CLEAR = 0x00000020; + + /** + * Bit set in the Notification flags field if this notification represents a currently + * running service. This will normally be set for you by + * {@link android.app.Service#startForeground}. + */ + public static final int FLAG_FOREGROUND_SERVICE = 0x00000040; + + /** + * Obsolete flag indicating high-priority notifications; use the priority field instead. + * + * @deprecated Use {@link NotificationCompat.Builder#setPriority(int)} with a positive value. + */ + public static final int FLAG_HIGH_PRIORITY = 0x00000080; + + /** + * Bit set in the Notification flags field if this notification is relevant to the current + * device only and it is not recommended that it bridge to other devices. + */ + public static final int FLAG_LOCAL_ONLY = 0x00000100; + + /** + * Bit set in the Notification flags field if this notification is the group summary for a + * group of notifications. Grouped notifications may display in a cluster or stack on devices + * which support such rendering. Requires a group key also be set using + * {@link Builder#setGroup}. + */ + public static final int FLAG_GROUP_SUMMARY = 0x00000200; + + /** + * Default notification priority for {@link NotificationCompat.Builder#setPriority(int)}. + * If your application does not prioritize its own notifications, + * use this value for all notifications. + */ + public static final int PRIORITY_DEFAULT = 0; + + /** + * Lower notification priority for {@link NotificationCompat.Builder#setPriority(int)}, + * for items that are less important. The UI may choose to show + * these items smaller, or at a different position in the list, + * compared with your app's {@link #PRIORITY_DEFAULT} items. + */ + public static final int PRIORITY_LOW = -1; + + /** + * Lowest notification priority for {@link NotificationCompat.Builder#setPriority(int)}; + * these items might not be shown to the user except under + * special circumstances, such as detailed notification logs. + */ + public static final int PRIORITY_MIN = -2; + + /** + * Higher notification priority for {@link NotificationCompat.Builder#setPriority(int)}, + * for more important notifications or alerts. The UI may choose + * to show these items larger, or at a different position in + * notification lists, compared with your app's {@link #PRIORITY_DEFAULT} items. + */ + public static final int PRIORITY_HIGH = 1; + + /** + * Highest notification priority for {@link NotificationCompat.Builder#setPriority(int)}, + * for your application's most important items that require the user's + * prompt attention or input. + */ + public static final int PRIORITY_MAX = 2; + + /** + * Notification extras key: this is the title of the notification, + * as supplied to {@link Builder#setContentTitle(CharSequence)}. + */ + public static final String EXTRA_TITLE = "android.title"; + + /** + * Notification extras key: this is the title of the notification when shown in expanded form, + * e.g. as supplied to {@link BigTextStyle#setBigContentTitle(CharSequence)}. + */ + public static final String EXTRA_TITLE_BIG = EXTRA_TITLE + ".big"; + + /** + * Notification extras key: this is the main text payload, as supplied to + * {@link Builder#setContentText(CharSequence)}. + */ + public static final String EXTRA_TEXT = "android.text"; + + /** + * Notification extras key: this is a third line of text, as supplied to + * {@link Builder#setSubText(CharSequence)}. + */ + public static final String EXTRA_SUB_TEXT = "android.subText"; + + /** + * Notification extras key: this is a small piece of additional text as supplied to + * {@link Builder#setContentInfo(CharSequence)}. + */ + public static final String EXTRA_INFO_TEXT = "android.infoText"; + + /** + * Notification extras key: this is a line of summary information intended to be shown + * alongside expanded notifications, as supplied to (e.g.) + * {@link BigTextStyle#setSummaryText(CharSequence)}. + */ + public static final String EXTRA_SUMMARY_TEXT = "android.summaryText"; + + /** + * Notification extras key: this is the longer text shown in the big form of a + * {@link BigTextStyle} notification, as supplied to + * {@link BigTextStyle#bigText(CharSequence)}. + */ + public static final String EXTRA_BIG_TEXT = "android.bigText"; + + /** + * Notification extras key: this is the resource ID of the notification's main small icon, as + * supplied to {@link Builder#setSmallIcon(int)}. + */ + public static final String EXTRA_SMALL_ICON = "android.icon"; + + /** + * Notification extras key: this is a bitmap to be used instead of the small icon when showing the + * notification payload, as + * supplied to {@link Builder#setLargeIcon(android.graphics.Bitmap)}. + */ + public static final String EXTRA_LARGE_ICON = "android.largeIcon"; + + /** + * Notification extras key: this is a bitmap to be used instead of the one from + * {@link Builder#setLargeIcon(android.graphics.Bitmap)} when the notification is + * shown in its expanded form, as supplied to + * {@link BigPictureStyle#bigLargeIcon(android.graphics.Bitmap)}. + */ + public static final String EXTRA_LARGE_ICON_BIG = EXTRA_LARGE_ICON + ".big"; + + /** + * Notification extras key: this is the progress value supplied to + * {@link Builder#setProgress(int, int, boolean)}. + */ + public static final String EXTRA_PROGRESS = "android.progress"; + + /** + * Notification extras key: this is the maximum value supplied to + * {@link Builder#setProgress(int, int, boolean)}. + */ + public static final String EXTRA_PROGRESS_MAX = "android.progressMax"; + + /** + * Notification extras key: whether the progress bar is indeterminate, supplied to + * {@link Builder#setProgress(int, int, boolean)}. + */ + public static final String EXTRA_PROGRESS_INDETERMINATE = "android.progressIndeterminate"; + + /** + * Notification extras key: whether the when field set using {@link Builder#setWhen} should + * be shown as a count-up timer (specifically a {@link android.widget.Chronometer}) instead + * of a timestamp, as supplied to {@link Builder#setUsesChronometer(boolean)}. + */ + public static final String EXTRA_SHOW_CHRONOMETER = "android.showChronometer"; + + /** + * Notification extras key: whether the when field set using {@link Builder#setWhen} should + * be shown, as supplied to {@link Builder#setShowWhen(boolean)}. + */ + public static final String EXTRA_SHOW_WHEN = "android.showWhen"; + + /** + * Notification extras key: this is a bitmap to be shown in {@link BigPictureStyle} expanded + * notifications, supplied to {@link BigPictureStyle#bigPicture(android.graphics.Bitmap)}. + */ + public static final String EXTRA_PICTURE = "android.picture"; + + /** + * Notification extras key: An array of CharSequences to show in {@link InboxStyle} expanded + * notifications, each of which was supplied to {@link InboxStyle#addLine(CharSequence)}. + */ + public static final String EXTRA_TEXT_LINES = "android.textLines"; + + /** + * Notification extras key: A string representing the name of the specific + * {@link android.app.Notification.Style} used to create this notification. + */ + public static final String EXTRA_TEMPLATE = "android.template"; + + /** + * Notification extras key: A String array containing the people that this + * notification relates to, each of which was supplied to + * {@link Builder#addPerson(String)}. + */ + public static final String EXTRA_PEOPLE = "android.people"; + + /** + * Notification extras key: A + * {@link android.content.ContentUris content URI} pointing to an image that can be displayed + * in the background when the notification is selected. The URI must point to an image stream + * suitable for passing into + * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream) + * BitmapFactory.decodeStream}; all other content types will be ignored. The content provider + * URI used for this purpose must require no permissions to read the image data. + */ + public static final String EXTRA_BACKGROUND_IMAGE_URI = "android.backgroundImageUri"; + + /** + * Notification key: A + * {@link android.media.session.MediaSession.Token} associated with a + * {@link android.app.Notification.MediaStyle} notification. + */ + public static final String EXTRA_MEDIA_SESSION = "android.mediaSession"; + + /** + * Notification extras key: the indices of actions to be shown in the compact view, + * as supplied to (e.g.) {@link Notification.MediaStyle#setShowActionsInCompactView(int...)}. + */ + public static final String EXTRA_COMPACT_ACTIONS = "android.compactActions"; + + /** + * Value of {@link Notification#color} equal to 0 (also known as + * {@link android.graphics.Color#TRANSPARENT Color.TRANSPARENT}), + * telling the system not to decorate this notification with any special color but instead use + * default colors when presenting this notification. + */ + public static final int COLOR_DEFAULT = Color.TRANSPARENT; + + /** + * Notification visibility: Show this notification in its entirety on all lockscreens. + * + * {@see android.app.Notification#visibility} + */ + public static final int VISIBILITY_PUBLIC = 1; + + /** + * Notification visibility: Show this notification on all lockscreens, but conceal sensitive or + * private information on secure lockscreens. + * + * {@see android.app.Notification#visibility} + */ + public static final int VISIBILITY_PRIVATE = 0; + + /** + * Notification visibility: Do not reveal any part of this notification on a secure lockscreen. + * + * {@see android.app.Notification#visibility} + */ + public static final int VISIBILITY_SECRET = -1; + + /** + * Notification category: incoming call (voice or video) or similar synchronous communication request. + */ + public static final String CATEGORY_CALL = NotificationCompatApi21.CATEGORY_CALL; + + /** + * Notification category: incoming direct message (SMS, instant message, etc.). + */ + public static final String CATEGORY_MESSAGE = NotificationCompatApi21.CATEGORY_MESSAGE; + + /** + * Notification category: asynchronous bulk message (email). + */ + public static final String CATEGORY_EMAIL = NotificationCompatApi21.CATEGORY_EMAIL; + + /** + * Notification category: calendar event. + */ + public static final String CATEGORY_EVENT = NotificationCompatApi21.CATEGORY_EVENT; + + /** + * Notification category: promotion or advertisement. + */ + public static final String CATEGORY_PROMO = NotificationCompatApi21.CATEGORY_PROMO; + + /** + * Notification category: alarm or timer. + */ + public static final String CATEGORY_ALARM = NotificationCompatApi21.CATEGORY_ALARM; + + /** + * Notification category: progress of a long-running background operation. + */ + public static final String CATEGORY_PROGRESS = NotificationCompatApi21.CATEGORY_PROGRESS; + + /** + * Notification category: social network or sharing update. + */ + public static final String CATEGORY_SOCIAL = NotificationCompatApi21.CATEGORY_SOCIAL; + + /** + * Notification category: error in background operation or authentication status. + */ + public static final String CATEGORY_ERROR = NotificationCompatApi21.CATEGORY_ERROR; + + /** + * Notification category: media transport control for playback. + */ + public static final String CATEGORY_TRANSPORT = NotificationCompatApi21.CATEGORY_TRANSPORT; + + /** + * Notification category: system or device status update. Reserved for system use. + */ + public static final String CATEGORY_SYSTEM = NotificationCompatApi21.CATEGORY_SYSTEM; + + /** + * Notification category: indication of running background service. + */ + public static final String CATEGORY_SERVICE = NotificationCompatApi21.CATEGORY_SERVICE; + + /** + * Notification category: a specific, timely recommendation for a single thing. + * For example, a news app might want to recommend a news story it believes the user will + * want to read next. + */ + public static final String CATEGORY_RECOMMENDATION = + NotificationCompatApi21.CATEGORY_RECOMMENDATION; + + /** + * Notification category: ongoing information about device or contextual status. + */ + public static final String CATEGORY_STATUS = NotificationCompatApi21.CATEGORY_STATUS; + + private static final NotificationCompatImpl IMPL; + + interface NotificationCompatImpl { + public Notification build(Builder b, BuilderExtender extender); + public Bundle getExtras(Notification n); + public int getActionCount(Notification n); + public Action getAction(Notification n, int actionIndex); + public Action[] getActionsFromParcelableArrayList(ArrayList parcelables); + public ArrayList getParcelableArrayListForActions(Action[] actions); + public String getCategory(Notification n); + public boolean getLocalOnly(Notification n); + public String getGroup(Notification n); + public boolean isGroupSummary(Notification n); + public String getSortKey(Notification n); + Bundle getBundleForUnreadConversation(NotificationCompatBase.UnreadConversation uc); + NotificationCompatBase.UnreadConversation getUnreadConversationFromBundle( + Bundle b, NotificationCompatBase.UnreadConversation.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory); + } + + /** + * Interface for appcompat to extend v4 builder with media style. + * + * @hide + */ + protected static class BuilderExtender { + public Notification build(Builder b, NotificationBuilderWithBuilderAccessor builder) { + return builder.build(); + } + } + + static class NotificationCompatImplBase implements NotificationCompatImpl { + @Override + public Notification build(Builder b, BuilderExtender extender) { + Notification result = b.mNotification; + result.setLatestEventInfo(b.mContext, b.mContentTitle, + b.mContentText, b.mContentIntent); + // translate high priority requests into legacy flag + if (b.mPriority > PRIORITY_DEFAULT) { + result.flags |= FLAG_HIGH_PRIORITY; + } + return result; + } + + @Override + public Bundle getExtras(Notification n) { + return null; + } + + @Override + public int getActionCount(Notification n) { + return 0; + } + + @Override + public Action getAction(Notification n, int actionIndex) { + return null; + } + + @Override + public Action[] getActionsFromParcelableArrayList( + ArrayList parcelables) { + return null; + } + + @Override + public ArrayList getParcelableArrayListForActions(Action[] actions) { + return null; + } + + @Override + public String getCategory(Notification n) { + return null; + } + + @Override + public boolean getLocalOnly(Notification n) { + return false; + } + + @Override + public String getGroup(Notification n) { + return null; + } + + @Override + public boolean isGroupSummary(Notification n) { + return false; + } + + @Override + public String getSortKey(Notification n) { + return null; + } + + @Override + public Bundle getBundleForUnreadConversation(NotificationCompatBase.UnreadConversation uc) { + return null; + } + + @Override + public NotificationCompatBase.UnreadConversation getUnreadConversationFromBundle( + Bundle b, NotificationCompatBase.UnreadConversation.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + return null; + } + } + + static class NotificationCompatImplGingerbread extends NotificationCompatImplBase { + @Override + public Notification build(Builder b, BuilderExtender extender) { + Notification result = b.mNotification; + result.setLatestEventInfo(b.mContext, b.mContentTitle, + b.mContentText, b.mContentIntent); + result = NotificationCompatGingerbread.add(result, b.mContext, + b.mContentTitle, b.mContentText, b.mContentIntent, b.mFullScreenIntent); + // translate high priority requests into legacy flag + if (b.mPriority > PRIORITY_DEFAULT) { + result.flags |= FLAG_HIGH_PRIORITY; + } + return result; + } + } + + static class NotificationCompatImplHoneycomb extends NotificationCompatImplBase { + @Override + public Notification build(Builder b, BuilderExtender extender) { + return NotificationCompatHoneycomb.add(b.mContext, b.mNotification, + b.mContentTitle, b.mContentText, b.mContentInfo, b.mTickerView, + b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon); + } + } + + static class NotificationCompatImplIceCreamSandwich extends NotificationCompatImplBase { + @Override + public Notification build(Builder b, BuilderExtender extender) { + NotificationCompatIceCreamSandwich.Builder builder = + new NotificationCompatIceCreamSandwich.Builder( + b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo, + b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon, + b.mProgressMax, b.mProgress, b.mProgressIndeterminate); + return extender.build(b, builder); + } + } + + static class NotificationCompatImplJellybean extends NotificationCompatImplBase { + @Override + public Notification build(Builder b, BuilderExtender extender) { + NotificationCompatJellybean.Builder builder = new NotificationCompatJellybean.Builder( + b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo, + b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon, + b.mProgressMax, b.mProgress, b.mProgressIndeterminate, + b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mExtras, + b.mGroupKey, b.mGroupSummary, b.mSortKey); + addActionsToBuilder(builder, b.mActions); + addStyleToBuilderJellybean(builder, b.mStyle); + return extender.build(b, builder); + } + + @Override + public Bundle getExtras(Notification n) { + return NotificationCompatJellybean.getExtras(n); + } + + @Override + public int getActionCount(Notification n) { + return NotificationCompatJellybean.getActionCount(n); + } + + @Override + public Action getAction(Notification n, int actionIndex) { + return (Action) NotificationCompatJellybean.getAction(n, actionIndex, Action.FACTORY, + RemoteInput.FACTORY); + } + + @Override + public Action[] getActionsFromParcelableArrayList( + ArrayList parcelables) { + return (Action[]) NotificationCompatJellybean.getActionsFromParcelableArrayList( + parcelables, Action.FACTORY, RemoteInput.FACTORY); + } + + @Override + public ArrayList getParcelableArrayListForActions( + Action[] actions) { + return NotificationCompatJellybean.getParcelableArrayListForActions(actions); + } + + @Override + public boolean getLocalOnly(Notification n) { + return NotificationCompatJellybean.getLocalOnly(n); + } + + @Override + public String getGroup(Notification n) { + return NotificationCompatJellybean.getGroup(n); + } + + @Override + public boolean isGroupSummary(Notification n) { + return NotificationCompatJellybean.isGroupSummary(n); + } + + @Override + public String getSortKey(Notification n) { + return NotificationCompatJellybean.getSortKey(n); + } + } + + static class NotificationCompatImplKitKat extends NotificationCompatImplJellybean { + @Override + public Notification build(Builder b, BuilderExtender extender) { + NotificationCompatKitKat.Builder builder = new NotificationCompatKitKat.Builder( + b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo, + b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon, + b.mProgressMax, b.mProgress, b.mProgressIndeterminate, b.mShowWhen, + b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, + b.mPeople, b.mExtras, b.mGroupKey, b.mGroupSummary, b.mSortKey); + addActionsToBuilder(builder, b.mActions); + addStyleToBuilderJellybean(builder, b.mStyle); + return extender.build(b, builder); + } + + @Override + public Bundle getExtras(Notification n) { + return NotificationCompatKitKat.getExtras(n); + } + + @Override + public int getActionCount(Notification n) { + return NotificationCompatKitKat.getActionCount(n); + } + + @Override + public Action getAction(Notification n, int actionIndex) { + return (Action) NotificationCompatKitKat.getAction(n, actionIndex, Action.FACTORY, + RemoteInput.FACTORY); + } + + @Override + public boolean getLocalOnly(Notification n) { + return NotificationCompatKitKat.getLocalOnly(n); + } + + @Override + public String getGroup(Notification n) { + return NotificationCompatKitKat.getGroup(n); + } + + @Override + public boolean isGroupSummary(Notification n) { + return NotificationCompatKitKat.isGroupSummary(n); + } + + @Override + public String getSortKey(Notification n) { + return NotificationCompatKitKat.getSortKey(n); + } + } + + static class NotificationCompatImplApi20 extends NotificationCompatImplKitKat { + @Override + public Notification build(Builder b, BuilderExtender extender) { + NotificationCompatApi20.Builder builder = new NotificationCompatApi20.Builder( + b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo, + b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon, + b.mProgressMax, b.mProgress, b.mProgressIndeterminate, b.mShowWhen, + b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mPeople, b.mExtras, + b.mGroupKey, b.mGroupSummary, b.mSortKey); + addActionsToBuilder(builder, b.mActions); + addStyleToBuilderJellybean(builder, b.mStyle); + return extender.build(b, builder); + } + + @Override + public Action getAction(Notification n, int actionIndex) { + return (Action) NotificationCompatApi20.getAction(n, actionIndex, Action.FACTORY, + RemoteInput.FACTORY); + } + + @Override + public Action[] getActionsFromParcelableArrayList( + ArrayList parcelables) { + return (Action[]) NotificationCompatApi20.getActionsFromParcelableArrayList( + parcelables, Action.FACTORY, RemoteInput.FACTORY); + } + + @Override + public ArrayList getParcelableArrayListForActions( + Action[] actions) { + return NotificationCompatApi20.getParcelableArrayListForActions(actions); + } + + @Override + public boolean getLocalOnly(Notification n) { + return NotificationCompatApi20.getLocalOnly(n); + } + + @Override + public String getGroup(Notification n) { + return NotificationCompatApi20.getGroup(n); + } + + @Override + public boolean isGroupSummary(Notification n) { + return NotificationCompatApi20.isGroupSummary(n); + } + + @Override + public String getSortKey(Notification n) { + return NotificationCompatApi20.getSortKey(n); + } + } + + static class NotificationCompatImplApi21 extends NotificationCompatImplApi20 { + @Override + public Notification build(Builder b, BuilderExtender extender) { + NotificationCompatApi21.Builder builder = new NotificationCompatApi21.Builder( + b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo, + b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon, + b.mProgressMax, b.mProgress, b.mProgressIndeterminate, b.mShowWhen, + b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mCategory, + b.mPeople, b.mExtras, b.mColor, b.mVisibility, b.mPublicVersion, + b.mGroupKey, b.mGroupSummary, b.mSortKey); + addActionsToBuilder(builder, b.mActions); + addStyleToBuilderJellybean(builder, b.mStyle); + return extender.build(b, builder); + } + + @Override + public String getCategory(Notification notif) { + return NotificationCompatApi21.getCategory(notif); + } + + @Override + public Bundle getBundleForUnreadConversation(NotificationCompatBase.UnreadConversation uc) { + return NotificationCompatApi21.getBundleForUnreadConversation(uc); + } + + @Override + public NotificationCompatBase.UnreadConversation getUnreadConversationFromBundle( + Bundle b, NotificationCompatBase.UnreadConversation.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + return NotificationCompatApi21.getUnreadConversationFromBundle( + b, factory, remoteInputFactory); + } + } + + private static void addActionsToBuilder(NotificationBuilderWithActions builder, + ArrayList actions) { + for (Action action : actions) { + builder.addAction(action); + } + } + + private static void addStyleToBuilderJellybean(NotificationBuilderWithBuilderAccessor builder, + Style style) { + if (style != null) { + if (style instanceof BigTextStyle) { + BigTextStyle bigTextStyle = (BigTextStyle) style; + NotificationCompatJellybean.addBigTextStyle(builder, + bigTextStyle.mBigContentTitle, + bigTextStyle.mSummaryTextSet, + bigTextStyle.mSummaryText, + bigTextStyle.mBigText); + } else if (style instanceof InboxStyle) { + InboxStyle inboxStyle = (InboxStyle) style; + NotificationCompatJellybean.addInboxStyle(builder, + inboxStyle.mBigContentTitle, + inboxStyle.mSummaryTextSet, + inboxStyle.mSummaryText, + inboxStyle.mTexts); + } else if (style instanceof BigPictureStyle) { + BigPictureStyle bigPictureStyle = (BigPictureStyle) style; + NotificationCompatJellybean.addBigPictureStyle(builder, + bigPictureStyle.mBigContentTitle, + bigPictureStyle.mSummaryTextSet, + bigPictureStyle.mSummaryText, + bigPictureStyle.mPicture, + bigPictureStyle.mBigLargeIcon, + bigPictureStyle.mBigLargeIconSet); + } + } + } + + static { + if (Build.VERSION.SDK_INT >= 21) { + IMPL = new NotificationCompatImplApi21(); + } else if (Build.VERSION.SDK_INT >= 20) { + IMPL = new NotificationCompatImplApi20(); + } else if (Build.VERSION.SDK_INT >= 19) { + IMPL = new NotificationCompatImplKitKat(); + } else if (Build.VERSION.SDK_INT >= 16) { + IMPL = new NotificationCompatImplJellybean(); + } else if (Build.VERSION.SDK_INT >= 14) { + IMPL = new NotificationCompatImplIceCreamSandwich(); + } else if (Build.VERSION.SDK_INT >= 11) { + IMPL = new NotificationCompatImplHoneycomb(); + } else if (Build.VERSION.SDK_INT >= 9) { + IMPL = new NotificationCompatImplGingerbread(); + } else { + IMPL = new NotificationCompatImplBase(); + } + } + + /** + * Builder class for {@link NotificationCompat} objects. Allows easier control over + * all the flags, as well as help constructing the typical notification layouts. + *

+ * On platform versions that don't offer expanded notifications, methods that depend on + * expanded notifications have no effect. + *

+ *

+ * For example, action buttons won't appear on platforms prior to Android 4.1. Action + * buttons depend on expanded notifications, which are only available in Android 4.1 + * and later. + *

+ * For this reason, you should always ensure that UI controls in a notification are also + * available in an {@link android.app.Activity} in your app, and you should always start that + * {@link android.app.Activity} when users click the notification. To do this, use the + * {@link NotificationCompat.Builder#setContentIntent setContentIntent()} + * method. + *

+ * + */ + public static class Builder { + /** + * Maximum length of CharSequences accepted by Builder and friends. + * + *

+ * Avoids spamming the system with overly large strings such as full e-mails. + */ + private static final int MAX_CHARSEQUENCE_LENGTH = 5 * 1024; + + // All these variables are declared public/hidden so they can be accessed by a builder + // extender. + + /** @hide */ + public Context mContext; + + /** @hide */ + public CharSequence mContentTitle; + /** @hide */ + public CharSequence mContentText; + PendingIntent mContentIntent; + PendingIntent mFullScreenIntent; + RemoteViews mTickerView; + /** @hide */ + public Bitmap mLargeIcon; + /** @hide */ + public CharSequence mContentInfo; + /** @hide */ + public int mNumber; + int mPriority; + boolean mShowWhen = true; + /** @hide */ + public boolean mUseChronometer; + /** @hide */ + public Style mStyle; + /** @hide */ + public CharSequence mSubText; + int mProgressMax; + int mProgress; + boolean mProgressIndeterminate; + String mGroupKey; + boolean mGroupSummary; + String mSortKey; + /** @hide */ + public ArrayList mActions = new ArrayList(); + boolean mLocalOnly = false; + String mCategory; + Bundle mExtras; + int mColor = COLOR_DEFAULT; + int mVisibility = VISIBILITY_PRIVATE; + Notification mPublicVersion; + + /** @hide */ + public Notification mNotification = new Notification(); + public ArrayList mPeople; + + /** + * Constructor. + * + * Automatically sets the when field to {@link System#currentTimeMillis() + * System.currentTimeMillis()} and the audio stream to the + * {@link Notification#STREAM_DEFAULT}. + * + * @param context A {@link Context} that will be used to construct the + * RemoteViews. The Context will not be held past the lifetime of this + * Builder object. + */ + public Builder(Context context) { + mContext = context; + + // Set defaults to match the defaults of a Notification + mNotification.when = System.currentTimeMillis(); + mNotification.audioStreamType = Notification.STREAM_DEFAULT; + mPriority = PRIORITY_DEFAULT; + mPeople = new ArrayList(); + } + + /** + * Set the time that the event occurred. Notifications in the panel are + * sorted by this time. + */ + public Builder setWhen(long when) { + mNotification.when = when; + return this; + } + + /** + * Control whether the timestamp set with {@link #setWhen(long) setWhen} is shown + * in the content view. + */ + public Builder setShowWhen(boolean show) { + mShowWhen = show; + return this; + } + + /** + * Show the {@link Notification#when} field as a stopwatch. + * + * Instead of presenting when as a timestamp, the notification will show an + * automatically updating display of the minutes and seconds since when. + * + * Useful when showing an elapsed time (like an ongoing phone call). + * + * @see android.widget.Chronometer + * @see Notification#when + */ + public Builder setUsesChronometer(boolean b) { + mUseChronometer = b; + return this; + } + + /** + * Set the small icon to use in the notification layouts. Different classes of devices + * may return different sizes. See the UX guidelines for more information on how to + * design these icons. + * + * @param icon A resource ID in the application's package of the drawble to use. + */ + public Builder setSmallIcon(int icon) { + mNotification.icon = icon; + return this; + } + + /** + * A variant of {@link #setSmallIcon(int) setSmallIcon(int)} that takes an additional + * level parameter for when the icon is a {@link android.graphics.drawable.LevelListDrawable + * LevelListDrawable}. + * + * @param icon A resource ID in the application's package of the drawble to use. + * @param level The level to use for the icon. + * + * @see android.graphics.drawable.LevelListDrawable + */ + public Builder setSmallIcon(int icon, int level) { + mNotification.icon = icon; + mNotification.iconLevel = level; + return this; + } + + /** + * Set the title (first row) of the notification, in a standard notification. + */ + public Builder setContentTitle(CharSequence title) { + mContentTitle = limitCharSequenceLength(title); + return this; + } + + /** + * Set the text (second row) of the notification, in a standard notification. + */ + public Builder setContentText(CharSequence text) { + mContentText = limitCharSequenceLength(text); + return this; + } + + /** + * Set the third line of text in the platform notification template. + * Don't use if you're also using {@link #setProgress(int, int, boolean)}; + * they occupy the same location in the standard template. + *
+ * If the platform does not provide large-format notifications, this method has no effect. + * The third line of text only appears in expanded view. + *
+ */ + public Builder setSubText(CharSequence text) { + mSubText = limitCharSequenceLength(text); + return this; + } + + /** + * Set the large number at the right-hand side of the notification. This is + * equivalent to setContentInfo, although it might show the number in a different + * font size for readability. + */ + public Builder setNumber(int number) { + mNumber = number; + return this; + } + + /** + * Set the large text at the right-hand side of the notification. + */ + public Builder setContentInfo(CharSequence info) { + mContentInfo = limitCharSequenceLength(info); + return this; + } + + /** + * Set the progress this notification represents, which may be + * represented as a {@link android.widget.ProgressBar}. + */ + public Builder setProgress(int max, int progress, boolean indeterminate) { + mProgressMax = max; + mProgress = progress; + mProgressIndeterminate = indeterminate; + return this; + } + + /** + * Supply a custom RemoteViews to use instead of the standard one. + */ + public Builder setContent(RemoteViews views) { + mNotification.contentView = views; + return this; + } + + /** + * Supply a {@link PendingIntent} to send when the notification is clicked. + * If you do not supply an intent, you can now add PendingIntents to individual + * views to be launched when clicked by calling {@link RemoteViews#setOnClickPendingIntent + * RemoteViews.setOnClickPendingIntent(int,PendingIntent)}. Be sure to + * read {@link Notification#contentIntent Notification.contentIntent} for + * how to correctly use this. + */ + public Builder setContentIntent(PendingIntent intent) { + mContentIntent = intent; + return this; + } + + /** + * Supply a {@link PendingIntent} to send when the notification is cleared by the user + * directly from the notification panel. For example, this intent is sent when the user + * clicks the "Clear all" button, or the individual "X" buttons on notifications. This + * intent is not sent when the application calls + * {@link android.app.NotificationManager#cancel NotificationManager.cancel(int)}. + */ + public Builder setDeleteIntent(PendingIntent intent) { + mNotification.deleteIntent = intent; + return this; + } + + /** + * An intent to launch instead of posting the notification to the status bar. + * Only for use with extremely high-priority notifications demanding the user's + * immediate attention, such as an incoming phone call or + * alarm clock that the user has explicitly set to a particular time. + * If this facility is used for something else, please give the user an option + * to turn it off and use a normal notification, as this can be extremely + * disruptive. + * + *

+ * On some platforms, the system UI may choose to display a heads-up notification, + * instead of launching this intent, while the user is using the device. + *

+ * + * @param intent The pending intent to launch. + * @param highPriority Passing true will cause this notification to be sent + * even if other notifications are suppressed. + */ + public Builder setFullScreenIntent(PendingIntent intent, boolean highPriority) { + mFullScreenIntent = intent; + setFlag(FLAG_HIGH_PRIORITY, highPriority); + return this; + } + + /** + * Set the text that is displayed in the status bar when the notification first + * arrives. + */ + public Builder setTicker(CharSequence tickerText) { + mNotification.tickerText = limitCharSequenceLength(tickerText); + return this; + } + + /** + * Set the text that is displayed in the status bar when the notification first + * arrives, and also a RemoteViews object that may be displayed instead on some + * devices. + */ + public Builder setTicker(CharSequence tickerText, RemoteViews views) { + mNotification.tickerText = limitCharSequenceLength(tickerText); + mTickerView = views; + return this; + } + + /** + * Set the large icon that is shown in the ticker and notification. + */ + public Builder setLargeIcon(Bitmap icon) { + mLargeIcon = icon; + return this; + } + + /** + * Set the sound to play. It will play on the default stream. + * + *

+ * On some platforms, a notification that is noisy is more likely to be presented + * as a heads-up notification. + *

+ */ + public Builder setSound(Uri sound) { + mNotification.sound = sound; + mNotification.audioStreamType = Notification.STREAM_DEFAULT; + return this; + } + + /** + * Set the sound to play. It will play on the stream you supply. + * + *

+ * On some platforms, a notification that is noisy is more likely to be presented + * as a heads-up notification. + *

+ * + * @see Notification#STREAM_DEFAULT + * @see AudioManager for the STREAM_ constants. + */ + public Builder setSound(Uri sound, int streamType) { + mNotification.sound = sound; + mNotification.audioStreamType = streamType; + return this; + } + + /** + * Set the vibration pattern to use. + * + *

+ * On some platforms, a notification that vibrates is more likely to be presented + * as a heads-up notification. + *

+ * + * @see android.os.Vibrator for a discussion of the pattern + * parameter. + */ + public Builder setVibrate(long[] pattern) { + mNotification.vibrate = pattern; + return this; + } + + /** + * Set the argb value that you would like the LED on the device to blnk, as well as the + * rate. The rate is specified in terms of the number of milliseconds to be on + * and then the number of milliseconds to be off. + */ + public Builder setLights(int argb, int onMs, int offMs) { + mNotification.ledARGB = argb; + mNotification.ledOnMS = onMs; + mNotification.ledOffMS = offMs; + boolean showLights = mNotification.ledOnMS != 0 && mNotification.ledOffMS != 0; + mNotification.flags = (mNotification.flags & ~Notification.FLAG_SHOW_LIGHTS) | + (showLights ? Notification.FLAG_SHOW_LIGHTS : 0); + return this; + } + + /** + * Set whether this is an ongoing notification. + * + *

Ongoing notifications differ from regular notifications in the following ways: + *

    + *
  • Ongoing notifications are sorted above the regular notifications in the + * notification panel.
  • + *
  • Ongoing notifications do not have an 'X' close button, and are not affected + * by the "Clear all" button. + *
+ */ + public Builder setOngoing(boolean ongoing) { + setFlag(Notification.FLAG_ONGOING_EVENT, ongoing); + return this; + } + + /** + * Set this flag if you would only like the sound, vibrate + * and ticker to be played if the notification is not already showing. + */ + public Builder setOnlyAlertOnce(boolean onlyAlertOnce) { + setFlag(Notification.FLAG_ONLY_ALERT_ONCE, onlyAlertOnce); + return this; + } + + /** + * Setting this flag will make it so the notification is automatically + * canceled when the user clicks it in the panel. The PendingIntent + * set with {@link #setDeleteIntent} will be broadcast when the notification + * is canceled. + */ + public Builder setAutoCancel(boolean autoCancel) { + setFlag(Notification.FLAG_AUTO_CANCEL, autoCancel); + return this; + } + + /** + * Set whether or not this notification is only relevant to the current device. + * + *

Some notifications can be bridged to other devices for remote display. + * This hint can be set to recommend this notification not be bridged. + */ + public Builder setLocalOnly(boolean b) { + mLocalOnly = b; + return this; + } + + /** + * Set the notification category. + * + *

Must be one of the predefined notification categories (see the CATEGORY_* + * constants in {@link Notification}) that best describes this notification. + * May be used by the system for ranking and filtering. + */ + public Builder setCategory(String category) { + mCategory = category; + return this; + } + + /** + * Set the default notification options that will be used. + *

+ * The value should be one or more of the following fields combined with + * bitwise-or: + * {@link Notification#DEFAULT_SOUND}, {@link Notification#DEFAULT_VIBRATE}, + * {@link Notification#DEFAULT_LIGHTS}. + *

+ * For all default values, use {@link Notification#DEFAULT_ALL}. + */ + public Builder setDefaults(int defaults) { + mNotification.defaults = defaults; + if ((defaults & Notification.DEFAULT_LIGHTS) != 0) { + mNotification.flags |= Notification.FLAG_SHOW_LIGHTS; + } + return this; + } + + private void setFlag(int mask, boolean value) { + if (value) { + mNotification.flags |= mask; + } else { + mNotification.flags &= ~mask; + } + } + + /** + * Set the relative priority for this notification. + * + * Priority is an indication of how much of the user's + * valuable attention should be consumed by this + * notification. Low-priority notifications may be hidden from + * the user in certain situations, while the user might be + * interrupted for a higher-priority notification. + * The system sets a notification's priority based on various factors including the + * setPriority value. The effect may differ slightly on different platforms. + * + * @param pri Relative priority for this notification. Must be one of + * the priority constants defined by {@link NotificationCompat}. + * Acceptable values range from {@link + * NotificationCompat#PRIORITY_MIN} (-2) to {@link + * NotificationCompat#PRIORITY_MAX} (2). + */ + public Builder setPriority(int pri) { + mPriority = pri; + return this; + } + + /** + * Add a person that is relevant to this notification. + * + *

+ * Depending on user preferences, this annotation may allow the notification to pass + * through interruption filters, and to appear more prominently in the user interface. + *

+ * + *

+ * The person should be specified by the {@code String} representation of a + * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}. + *

+ * + *

The system will also attempt to resolve {@code mailto:} and {@code tel:} schema + * URIs. The path part of these URIs must exist in the contacts database, in the + * appropriate column, or the reference will be discarded as invalid. Telephone schema + * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}. + *

+ * + * @param uri A URI for the person. + * @see Notification#EXTRA_PEOPLE + */ + public Builder addPerson(String uri) { + mPeople.add(uri); + return this; + } + + /** + * Set this notification to be part of a group of notifications sharing the same key. + * Grouped notifications may display in a cluster or stack on devices which + * support such rendering. + * + *

To make this notification the summary for its group, also call + * {@link #setGroupSummary}. A sort order can be specified for group members by using + * {@link #setSortKey}. + * @param groupKey The group key of the group. + * @return this object for method chaining + */ + public Builder setGroup(String groupKey) { + mGroupKey = groupKey; + return this; + } + + /** + * Set this notification to be the group summary for a group of notifications. + * Grouped notifications may display in a cluster or stack on devices which + * support such rendering. Requires a group key also be set using {@link #setGroup}. + * @param isGroupSummary Whether this notification should be a group summary. + * @return this object for method chaining + */ + public Builder setGroupSummary(boolean isGroupSummary) { + mGroupSummary = isGroupSummary; + return this; + } + + /** + * Set a sort key that orders this notification among other notifications from the + * same package. This can be useful if an external sort was already applied and an app + * would like to preserve this. Notifications will be sorted lexicographically using this + * value, although providing different priorities in addition to providing sort key may + * cause this value to be ignored. + * + *

This sort key can also be used to order members of a notification group. See + * {@link Builder#setGroup}. + * + * @see String#compareTo(String) + */ + public Builder setSortKey(String sortKey) { + mSortKey = sortKey; + return this; + } + + /** + * Merge additional metadata into this notification. + * + *

Values within the Bundle will replace existing extras values in this Builder. + * + * @see Notification#extras + */ + public Builder addExtras(Bundle extras) { + if (extras != null) { + if (mExtras == null) { + mExtras = new Bundle(extras); + } else { + mExtras.putAll(extras); + } + } + return this; + } + + /** + * Set metadata for this notification. + * + *

A reference to the Bundle is held for the lifetime of this Builder, and the Bundle's + * current contents are copied into the Notification each time {@link #build()} is + * called. + * + *

Replaces any existing extras values with those from the provided Bundle. + * Use {@link #addExtras} to merge in metadata instead. + * + * @see Notification#extras + */ + public Builder setExtras(Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Get the current metadata Bundle used by this notification Builder. + * + *

The returned Bundle is shared with this Builder. + * + *

The current contents of this Bundle are copied into the Notification each time + * {@link #build()} is called. + * + * @see Notification#extras + */ + public Bundle getExtras() { + if (mExtras == null) { + mExtras = new Bundle(); + } + return mExtras; + } + + /** + * Add an action to this notification. Actions are typically displayed by + * the system as a button adjacent to the notification content. + *
+ * Action buttons won't appear on platforms prior to Android 4.1. Action + * buttons depend on expanded notifications, which are only available in Android 4.1 + * and later. To ensure that an action button's functionality is always available, first + * implement the functionality in the {@link android.app.Activity} that starts when a user + * clicks the notification (see {@link #setContentIntent setContentIntent()}), and then + * enhance the notification by implementing the same functionality with + * {@link #addAction addAction()}. + * + * @param icon Resource ID of a drawable that represents the action. + * @param title Text describing the action. + * @param intent {@link android.app.PendingIntent} to be fired when the action is invoked. + */ + public Builder addAction(int icon, CharSequence title, PendingIntent intent) { + mActions.add(new Action(icon, title, intent)); + return this; + } + + /** + * Add an action to this notification. Actions are typically displayed by + * the system as a button adjacent to the notification content. + *
+ * Action buttons won't appear on platforms prior to Android 4.1. Action + * buttons depend on expanded notifications, which are only available in Android 4.1 + * and later. To ensure that an action button's functionality is always available, first + * implement the functionality in the {@link android.app.Activity} that starts when a user + * clicks the notification (see {@link #setContentIntent setContentIntent()}), and then + * enhance the notification by implementing the same functionality with + * {@link #addAction addAction()}. + * + * @param action The action to add. + */ + public Builder addAction(Action action) { + mActions.add(action); + return this; + } + + /** + * Add a rich notification style to be applied at build time. + *
+ * If the platform does not provide rich notification styles, this method has no effect. The + * user will always see the normal notification style. + * + * @param style Object responsible for modifying the notification style. + */ + public Builder setStyle(Style style) { + if (mStyle != style) { + mStyle = style; + if (mStyle != null) { + mStyle.setBuilder(this); + } + } + return this; + } + + /** + * Sets {@link Notification#color}. + * + * @param argb The accent color to use + * + * @return The same Builder. + */ + public Builder setColor(int argb) { + mColor = argb; + return this; + } + + /** + * Sets {@link Notification#visibility}. + * + * @param visibility One of {@link Notification#VISIBILITY_PRIVATE} (the default), + * {@link Notification#VISIBILITY_PUBLIC}, or + * {@link Notification#VISIBILITY_SECRET}. + */ + public Builder setVisibility(int visibility) { + mVisibility = visibility; + return this; + } + + /** + * Supply a replacement Notification whose contents should be shown in insecure contexts + * (i.e. atop the secure lockscreen). See {@link Notification#visibility} and + * {@link #VISIBILITY_PUBLIC}. + * + * @param n A replacement notification, presumably with some or all info redacted. + * @return The same Builder. + */ + public Builder setPublicVersion(Notification n) { + mPublicVersion = n; + return this; + } + + /** + * Apply an extender to this notification builder. Extenders may be used to add + * metadata or change options on this builder. + */ + public Builder extend(Extender extender) { + extender.extend(this); + return this; + } + + /** + * @deprecated Use {@link #build()} instead. + */ + @Deprecated + public Notification getNotification() { + return build(); + } + + /** + * Combine all of the options that have been set and return a new {@link Notification} + * object. + */ + public Notification build() { + return IMPL.build(this, getExtender()); + } + + /** + * @hide + */ + protected BuilderExtender getExtender() { + return new BuilderExtender(); + } + + protected static CharSequence limitCharSequenceLength(CharSequence cs) { + if (cs == null) return cs; + if (cs.length() > MAX_CHARSEQUENCE_LENGTH) { + cs = cs.subSequence(0, MAX_CHARSEQUENCE_LENGTH); + } + return cs; + } + } + + /** + * An object that can apply a rich notification style to a {@link Notification.Builder} + * object. + *
+ * If the platform does not provide rich notification styles, methods in this class have no + * effect. + */ + public static abstract class Style { + Builder mBuilder; + CharSequence mBigContentTitle; + CharSequence mSummaryText; + boolean mSummaryTextSet = false; + + public void setBuilder(Builder builder) { + if (mBuilder != builder) { + mBuilder = builder; + if (mBuilder != null) { + mBuilder.setStyle(this); + } + } + } + + public Notification build() { + Notification notification = null; + if (mBuilder != null) { + notification = mBuilder.build(); + } + return notification; + } + } + + /** + * Helper class for generating large-format notifications that include a large image attachment. + *
+ * If the platform does not provide large-format notifications, this method has no effect. The + * user will always see the normal notification view. + *
+ * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so: + *

+     * Notification notif = new Notification.Builder(mContext)
+     *     .setContentTitle("New photo from " + sender.toString())
+     *     .setContentText(subject)
+     *     .setSmallIcon(R.drawable.new_post)
+     *     .setLargeIcon(aBitmap)
+     *     .setStyle(new Notification.BigPictureStyle()
+     *         .bigPicture(aBigBitmap))
+     *     .build();
+     * 
+ * + * @see Notification#bigContentView + */ + public static class BigPictureStyle extends Style { + Bitmap mPicture; + Bitmap mBigLargeIcon; + boolean mBigLargeIconSet; + + public BigPictureStyle() { + } + + public BigPictureStyle(Builder builder) { + setBuilder(builder); + } + + /** + * Overrides ContentTitle in the big form of the template. + * This defaults to the value passed to setContentTitle(). + */ + public BigPictureStyle setBigContentTitle(CharSequence title) { + mBigContentTitle = Builder.limitCharSequenceLength(title); + return this; + } + + /** + * Set the first line of text after the detail section in the big form of the template. + */ + public BigPictureStyle setSummaryText(CharSequence cs) { + mSummaryText = Builder.limitCharSequenceLength(cs); + mSummaryTextSet = true; + return this; + } + + /** + * Provide the bitmap to be used as the payload for the BigPicture notification. + */ + public BigPictureStyle bigPicture(Bitmap b) { + mPicture = b; + return this; + } + + /** + * Override the large icon when the big notification is shown. + */ + public BigPictureStyle bigLargeIcon(Bitmap b) { + mBigLargeIcon = b; + mBigLargeIconSet = true; + return this; + } + } + + /** + * Helper class for generating large-format notifications that include a lot of text. + * + *
+ * If the platform does not provide large-format notifications, this method has no effect. The + * user will always see the normal notification view. + *
+ * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so: + *
+     * Notification notif = new Notification.Builder(mContext)
+     *     .setContentTitle("New mail from " + sender.toString())
+     *     .setContentText(subject)
+     *     .setSmallIcon(R.drawable.new_mail)
+     *     .setLargeIcon(aBitmap)
+     *     .setStyle(new Notification.BigTextStyle()
+     *         .bigText(aVeryLongString))
+     *     .build();
+     * 
+ * + * @see Notification#bigContentView + */ + public static class BigTextStyle extends Style { + CharSequence mBigText; + + public BigTextStyle() { + } + + public BigTextStyle(Builder builder) { + setBuilder(builder); + } + + /** + * Overrides ContentTitle in the big form of the template. + * This defaults to the value passed to setContentTitle(). + */ + public BigTextStyle setBigContentTitle(CharSequence title) { + mBigContentTitle = Builder.limitCharSequenceLength(title); + return this; + } + + /** + * Set the first line of text after the detail section in the big form of the template. + */ + public BigTextStyle setSummaryText(CharSequence cs) { + mSummaryText = Builder.limitCharSequenceLength(cs); + mSummaryTextSet = true; + return this; + } + + /** + * Provide the longer text to be displayed in the big form of the + * template in place of the content text. + */ + public BigTextStyle bigText(CharSequence cs) { + mBigText = Builder.limitCharSequenceLength(cs); + return this; + } + } + + /** + * Helper class for generating large-format notifications that include a list of (up to 5) strings. + * + *
+ * If the platform does not provide large-format notifications, this method has no effect. The + * user will always see the normal notification view. + *
+ * This class is a "rebuilder": It attaches to a Builder object and modifies its behavior, like so: + *
+     * Notification noti = new Notification.Builder()
+     *     .setContentTitle("5 New mails from " + sender.toString())
+     *     .setContentText(subject)
+     *     .setSmallIcon(R.drawable.new_mail)
+     *     .setLargeIcon(aBitmap)
+     *     .setStyle(new Notification.InboxStyle()
+     *         .addLine(str1)
+     *         .addLine(str2)
+     *         .setContentTitle("")
+     *         .setSummaryText("+3 more"))
+     *     .build();
+     * 
+ * + * @see Notification#bigContentView + */ + public static class InboxStyle extends Style { + ArrayList mTexts = new ArrayList(); + + public InboxStyle() { + } + + public InboxStyle(Builder builder) { + setBuilder(builder); + } + + /** + * Overrides ContentTitle in the big form of the template. + * This defaults to the value passed to setContentTitle(). + */ + public InboxStyle setBigContentTitle(CharSequence title) { + mBigContentTitle = Builder.limitCharSequenceLength(title); + return this; + } + + /** + * Set the first line of text after the detail section in the big form of the template. + */ + public InboxStyle setSummaryText(CharSequence cs) { + mSummaryText = Builder.limitCharSequenceLength(cs); + mSummaryTextSet = true; + return this; + } + + /** + * Append a line to the digest section of the Inbox notification. + */ + public InboxStyle addLine(CharSequence cs) { + mTexts.add(Builder.limitCharSequenceLength(cs)); + return this; + } + } + + /** + * Structure to encapsulate a named action that can be shown as part of this notification. + * It must include an icon, a label, and a {@link PendingIntent} to be fired when the action is + * selected by the user. Action buttons won't appear on platforms prior to Android 4.1. + *

+ * Apps should use {@link NotificationCompat.Builder#addAction(int, CharSequence, PendingIntent)} + * or {@link NotificationCompat.Builder#addAction(NotificationCompat.Action)} + * to attach actions. + */ + public static class Action extends NotificationCompatBase.Action { + private final Bundle mExtras; + private final RemoteInput[] mRemoteInputs; + + /** + * Small icon representing the action. + */ + public int icon; + /** + * Title of the action. + */ + public CharSequence title; + /** + * Intent to send when the user invokes this action. May be null, in which case the action + * may be rendered in a disabled presentation. + */ + public PendingIntent actionIntent; + + public Action(int icon, CharSequence title, PendingIntent intent) { + this(icon, title, intent, new Bundle(), null); + } + + private Action(int icon, CharSequence title, PendingIntent intent, Bundle extras, + RemoteInput[] remoteInputs) { + this.icon = icon; + this.title = NotificationCompat.Builder.limitCharSequenceLength(title); + this.actionIntent = intent; + this.mExtras = extras != null ? extras : new Bundle(); + this.mRemoteInputs = remoteInputs; + } + + @Override + public int getIcon() { + return icon; + } + + @Override + public CharSequence getTitle() { + return title; + } + + @Override + public PendingIntent getActionIntent() { + return actionIntent; + } + + /** + * Get additional metadata carried around with this Action. + */ + @Override + public Bundle getExtras() { + return mExtras; + } + + /** + * Get the list of inputs to be collected from the user when this action is sent. + * May return null if no remote inputs were added. + */ + @Override + public RemoteInput[] getRemoteInputs() { + return mRemoteInputs; + } + + /** + * Builder class for {@link Action} objects. + */ + public static final class Builder { + private final int mIcon; + private final CharSequence mTitle; + private final PendingIntent mIntent; + private final Bundle mExtras; + private ArrayList mRemoteInputs; + + /** + * Construct a new builder for {@link Action} object. + * @param icon icon to show for this action + * @param title the title of the action + * @param intent the {@link PendingIntent} to fire when users trigger this action + */ + public Builder(int icon, CharSequence title, PendingIntent intent) { + this(icon, title, intent, new Bundle()); + } + + /** + * Construct a new builder for {@link Action} object using the fields from an + * {@link Action}. + * @param action the action to read fields from. + */ + public Builder(Action action) { + this(action.icon, action.title, action.actionIntent, new Bundle(action.mExtras)); + } + + private Builder(int icon, CharSequence title, PendingIntent intent, Bundle extras) { + mIcon = icon; + mTitle = NotificationCompat.Builder.limitCharSequenceLength(title); + mIntent = intent; + mExtras = extras; + } + + /** + * Merge additional metadata into this builder. + * + *

Values within the Bundle will replace existing extras values in this Builder. + * + * @see NotificationCompat.Action#getExtras + */ + public Builder addExtras(Bundle extras) { + if (extras != null) { + mExtras.putAll(extras); + } + return this; + } + + /** + * Get the metadata Bundle used by this Builder. + * + *

The returned Bundle is shared with this Builder. + */ + public Bundle getExtras() { + return mExtras; + } + + /** + * Add an input to be collected from the user when this action is sent. + * Response values can be retrieved from the fired intent by using the + * {@link RemoteInput#getResultsFromIntent} function. + * @param remoteInput a {@link RemoteInput} to add to the action + * @return this object for method chaining + */ + public Builder addRemoteInput(RemoteInput remoteInput) { + if (mRemoteInputs == null) { + mRemoteInputs = new ArrayList(); + } + mRemoteInputs.add(remoteInput); + return this; + } + + /** + * Apply an extender to this action builder. Extenders may be used to add + * metadata or change options on this builder. + */ + public Builder extend(Extender extender) { + extender.extend(this); + return this; + } + + /** + * Combine all of the options that have been set and return a new {@link Action} + * object. + * @return the built action + */ + public Action build() { + RemoteInput[] remoteInputs = mRemoteInputs != null + ? mRemoteInputs.toArray(new RemoteInput[mRemoteInputs.size()]) : null; + return new Action(mIcon, mTitle, mIntent, mExtras, remoteInputs); + } + } + + + /** + * Extender interface for use with {@link Builder#extend}. Extenders may be used to add + * metadata or change options on an action builder. + */ + public interface Extender { + /** + * Apply this extender to a notification action builder. + * @param builder the builder to be modified. + * @return the build object for chaining. + */ + public Builder extend(Builder builder); + } + + /** + * Wearable extender for notification actions. To add extensions to an action, + * create a new {@link NotificationCompat.Action.WearableExtender} object using + * the {@code WearableExtender()} constructor and apply it to a + * {@link NotificationCompat.Action.Builder} using + * {@link NotificationCompat.Action.Builder#extend}. + * + *

+         * NotificationCompat.Action action = new NotificationCompat.Action.Builder(
+         *         R.drawable.archive_all, "Archive all", actionIntent)
+         *         .extend(new NotificationCompat.Action.WearableExtender()
+         *                 .setAvailableOffline(false))
+         *         .build();
+ */ + public static final class WearableExtender implements Extender { + /** Notification action extra which contains wearable extensions */ + private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"; + + // Keys within EXTRA_WEARABLE_EXTENSIONS for wearable options. + private static final String KEY_FLAGS = "flags"; + private static final String KEY_IN_PROGRESS_LABEL = "inProgressLabel"; + private static final String KEY_CONFIRM_LABEL = "confirmLabel"; + private static final String KEY_CANCEL_LABEL = "cancelLabel"; + + // Flags bitwise-ored to mFlags + private static final int FLAG_AVAILABLE_OFFLINE = 0x1; + + // Default value for flags integer + private static final int DEFAULT_FLAGS = FLAG_AVAILABLE_OFFLINE; + + private int mFlags = DEFAULT_FLAGS; + + private CharSequence mInProgressLabel; + private CharSequence mConfirmLabel; + private CharSequence mCancelLabel; + + /** + * Create a {@link NotificationCompat.Action.WearableExtender} with default + * options. + */ + public WearableExtender() { + } + + /** + * Create a {@link NotificationCompat.Action.WearableExtender} by reading + * wearable options present in an existing notification action. + * @param action the notification action to inspect. + */ + public WearableExtender(Action action) { + Bundle wearableBundle = action.getExtras().getBundle(EXTRA_WEARABLE_EXTENSIONS); + if (wearableBundle != null) { + mFlags = wearableBundle.getInt(KEY_FLAGS, DEFAULT_FLAGS); + mInProgressLabel = wearableBundle.getCharSequence(KEY_IN_PROGRESS_LABEL); + mConfirmLabel = wearableBundle.getCharSequence(KEY_CONFIRM_LABEL); + mCancelLabel = wearableBundle.getCharSequence(KEY_CANCEL_LABEL); + } + } + + /** + * Apply wearable extensions to a notification action that is being built. This is + * typically called by the {@link NotificationCompat.Action.Builder#extend} + * method of {@link NotificationCompat.Action.Builder}. + */ + @Override + public Action.Builder extend(Action.Builder builder) { + Bundle wearableBundle = new Bundle(); + + if (mFlags != DEFAULT_FLAGS) { + wearableBundle.putInt(KEY_FLAGS, mFlags); + } + if (mInProgressLabel != null) { + wearableBundle.putCharSequence(KEY_IN_PROGRESS_LABEL, mInProgressLabel); + } + if (mConfirmLabel != null) { + wearableBundle.putCharSequence(KEY_CONFIRM_LABEL, mConfirmLabel); + } + if (mCancelLabel != null) { + wearableBundle.putCharSequence(KEY_CANCEL_LABEL, mCancelLabel); + } + + builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle); + return builder; + } + + @Override + public WearableExtender clone() { + WearableExtender that = new WearableExtender(); + that.mFlags = this.mFlags; + that.mInProgressLabel = this.mInProgressLabel; + that.mConfirmLabel = this.mConfirmLabel; + that.mCancelLabel = this.mCancelLabel; + return that; + } + + /** + * Set whether this action is available when the wearable device is not connected to + * a companion device. The user can still trigger this action when the wearable device + * is offline, but a visual hint will indicate that the action may not be available. + * Defaults to true. + */ + public WearableExtender setAvailableOffline(boolean availableOffline) { + setFlag(FLAG_AVAILABLE_OFFLINE, availableOffline); + return this; + } + + /** + * Get whether this action is available when the wearable device is not connected to + * a companion device. The user can still trigger this action when the wearable device + * is offline, but a visual hint will indicate that the action may not be available. + * Defaults to true. + */ + public boolean isAvailableOffline() { + return (mFlags & FLAG_AVAILABLE_OFFLINE) != 0; + } + + private void setFlag(int mask, boolean value) { + if (value) { + mFlags |= mask; + } else { + mFlags &= ~mask; + } + } + + /** + * Set a label to display while the wearable is preparing to automatically execute the + * action. This is usually a 'ing' verb ending in ellipsis like "Sending..." + * + * @param label the label to display while the action is being prepared to execute + * @return this object for method chaining + */ + public WearableExtender setInProgressLabel(CharSequence label) { + mInProgressLabel = label; + return this; + } + + /** + * Get the label to display while the wearable is preparing to automatically execute + * the action. This is usually a 'ing' verb ending in ellipsis like "Sending..." + * + * @return the label to display while the action is being prepared to execute + */ + public CharSequence getInProgressLabel() { + return mInProgressLabel; + } + + /** + * Set a label to display to confirm that the action should be executed. + * This is usually an imperative verb like "Send". + * + * @param label the label to confirm the action should be executed + * @return this object for method chaining + */ + public WearableExtender setConfirmLabel(CharSequence label) { + mConfirmLabel = label; + return this; + } + + /** + * Get the label to display to confirm that the action should be executed. + * This is usually an imperative verb like "Send". + * + * @return the label to confirm the action should be executed + */ + public CharSequence getConfirmLabel() { + return mConfirmLabel; + } + + /** + * Set a label to display to cancel the action. + * This is usually an imperative verb, like "Cancel". + * + * @param label the label to display to cancel the action + * @return this object for method chaining + */ + public WearableExtender setCancelLabel(CharSequence label) { + mCancelLabel = label; + return this; + } + + /** + * Get the label to display to cancel the action. + * This is usually an imperative verb like "Cancel". + * + * @return the label to display to cancel the action + */ + public CharSequence getCancelLabel() { + return mCancelLabel; + } + } + + /** @hide */ + public static final Factory FACTORY = new Factory() { + @Override + public Action build(int icon, CharSequence title, + PendingIntent actionIntent, Bundle extras, + RemoteInputCompatBase.RemoteInput[] remoteInputs) { + return new Action(icon, title, actionIntent, extras, + (RemoteInput[]) remoteInputs); + } + + @Override + public Action[] newArray(int length) { + return new Action[length]; + } + }; + } + + + /** + * Extender interface for use with {@link Builder#extend}. Extenders may be used to add + * metadata or change options on a notification builder. + */ + public interface Extender { + /** + * Apply this extender to a notification builder. + * @param builder the builder to be modified. + * @return the build object for chaining. + */ + public Builder extend(Builder builder); + } + + /** + * Helper class to add wearable extensions to notifications. + *

See + * Creating Notifications + * for Android Wear for more information on how to use this class. + *

+ * To create a notification with wearable extensions: + *

    + *
  1. Create a {@link NotificationCompat.Builder}, setting any desired + * properties. + *
  2. Create a {@link NotificationCompat.WearableExtender}. + *
  3. Set wearable-specific properties using the + * {@code add} and {@code set} methods of {@link NotificationCompat.WearableExtender}. + *
  4. Call {@link NotificationCompat.Builder#extend} to apply the extensions to a + * notification. + *
  5. Post the notification to the notification + * system with the {@code NotificationManagerCompat.notify(...)} methods + * and not the {@code NotificationManager.notify(...)} methods. + *
+ * + *
+     * Notification notif = new NotificationCompat.Builder(mContext)
+     *         .setContentTitle("New mail from " + sender.toString())
+     *         .setContentText(subject)
+     *         .setSmallIcon(R.drawable.new_mail)
+     *         .extend(new NotificationCompat.WearableExtender()
+     *                 .setContentIcon(R.drawable.new_mail))
+     *         .build();
+     * NotificationManagerCompat.from(mContext).notify(0, notif);
+ * + *

Wearable extensions can be accessed on an existing notification by using the + * {@code WearableExtender(Notification)} constructor, + * and then using the {@code get} methods to access values. + * + *

+     * NotificationCompat.WearableExtender wearableExtender =
+     *         new NotificationCompat.WearableExtender(notification);
+     * List<Notification> pages = wearableExtender.getPages();
+ */ + public static final class WearableExtender implements Extender { + /** + * Sentinel value for an action index that is unset. + */ + public static final int UNSET_ACTION_INDEX = -1; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification with + * default sizing. + *

For custom display notifications created using {@link #setDisplayIntent}, + * the default is {@link #SIZE_LARGE}. All other notifications size automatically based + * on their content. + */ + public static final int SIZE_DEFAULT = 0; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * with an extra small size. + *

This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_XSMALL = 1; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * with a small size. + *

This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_SMALL = 2; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * with a medium size. + *

This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_MEDIUM = 3; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * with a large size. + *

This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_LARGE = 4; + + /** + * Size value for use with {@link #setCustomSizePreset} to show this notification + * full screen. + *

This value is only applicable for custom display notifications created using + * {@link #setDisplayIntent}. + */ + public static final int SIZE_FULL_SCREEN = 5; + + /** + * Sentinel value for use with {@link #setHintScreenTimeout} to keep the screen on for a + * short amount of time when this notification is displayed on the screen. This + * is the default value. + */ + public static final int SCREEN_TIMEOUT_SHORT = 0; + + /** + * Sentinel value for use with {@link #setHintScreenTimeout} to keep the screen on + * for a longer amount of time when this notification is displayed on the screen. + */ + public static final int SCREEN_TIMEOUT_LONG = -1; + + /** Notification extra which contains wearable extensions */ + private static final String EXTRA_WEARABLE_EXTENSIONS = "android.wearable.EXTENSIONS"; + + // Keys within EXTRA_WEARABLE_EXTENSIONS for wearable options. + private static final String KEY_ACTIONS = "actions"; + private static final String KEY_FLAGS = "flags"; + private static final String KEY_DISPLAY_INTENT = "displayIntent"; + private static final String KEY_PAGES = "pages"; + private static final String KEY_BACKGROUND = "background"; + private static final String KEY_CONTENT_ICON = "contentIcon"; + private static final String KEY_CONTENT_ICON_GRAVITY = "contentIconGravity"; + private static final String KEY_CONTENT_ACTION_INDEX = "contentActionIndex"; + private static final String KEY_CUSTOM_SIZE_PRESET = "customSizePreset"; + private static final String KEY_CUSTOM_CONTENT_HEIGHT = "customContentHeight"; + private static final String KEY_GRAVITY = "gravity"; + private static final String KEY_HINT_SCREEN_TIMEOUT = "hintScreenTimeout"; + + // Flags bitwise-ored to mFlags + private static final int FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE = 0x1; + private static final int FLAG_HINT_HIDE_ICON = 1 << 1; + private static final int FLAG_HINT_SHOW_BACKGROUND_ONLY = 1 << 2; + private static final int FLAG_START_SCROLL_BOTTOM = 1 << 3; + private static final int FLAG_HINT_AVOID_BACKGROUND_CLIPPING = 1 << 4; + + // Default value for flags integer + private static final int DEFAULT_FLAGS = FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE; + + private static final int DEFAULT_CONTENT_ICON_GRAVITY = GravityCompat.END; + private static final int DEFAULT_GRAVITY = Gravity.BOTTOM; + + private ArrayList mActions = new ArrayList(); + private int mFlags = DEFAULT_FLAGS; + private PendingIntent mDisplayIntent; + private ArrayList mPages = new ArrayList(); + private Bitmap mBackground; + private int mContentIcon; + private int mContentIconGravity = DEFAULT_CONTENT_ICON_GRAVITY; + private int mContentActionIndex = UNSET_ACTION_INDEX; + private int mCustomSizePreset = SIZE_DEFAULT; + private int mCustomContentHeight; + private int mGravity = DEFAULT_GRAVITY; + private int mHintScreenTimeout; + + /** + * Create a {@link NotificationCompat.WearableExtender} with default + * options. + */ + public WearableExtender() { + } + + public WearableExtender(Notification notif) { + Bundle extras = getExtras(notif); + Bundle wearableBundle = extras != null ? extras.getBundle(EXTRA_WEARABLE_EXTENSIONS) + : null; + if (wearableBundle != null) { + Action[] actions = IMPL.getActionsFromParcelableArrayList( + wearableBundle.getParcelableArrayList(KEY_ACTIONS)); + if (actions != null) { + Collections.addAll(mActions, actions); + } + + mFlags = wearableBundle.getInt(KEY_FLAGS, DEFAULT_FLAGS); + mDisplayIntent = wearableBundle.getParcelable(KEY_DISPLAY_INTENT); + + Notification[] pages = getNotificationArrayFromBundle( + wearableBundle, KEY_PAGES); + if (pages != null) { + Collections.addAll(mPages, pages); + } + + mBackground = wearableBundle.getParcelable(KEY_BACKGROUND); + mContentIcon = wearableBundle.getInt(KEY_CONTENT_ICON); + mContentIconGravity = wearableBundle.getInt(KEY_CONTENT_ICON_GRAVITY, + DEFAULT_CONTENT_ICON_GRAVITY); + mContentActionIndex = wearableBundle.getInt(KEY_CONTENT_ACTION_INDEX, + UNSET_ACTION_INDEX); + mCustomSizePreset = wearableBundle.getInt(KEY_CUSTOM_SIZE_PRESET, + SIZE_DEFAULT); + mCustomContentHeight = wearableBundle.getInt(KEY_CUSTOM_CONTENT_HEIGHT); + mGravity = wearableBundle.getInt(KEY_GRAVITY, DEFAULT_GRAVITY); + mHintScreenTimeout = wearableBundle.getInt(KEY_HINT_SCREEN_TIMEOUT); + } + } + + /** + * Apply wearable extensions to a notification that is being built. This is typically + * called by the {@link NotificationCompat.Builder#extend} method of + * {@link NotificationCompat.Builder}. + */ + @Override + public NotificationCompat.Builder extend(NotificationCompat.Builder builder) { + Bundle wearableBundle = new Bundle(); + + if (!mActions.isEmpty()) { + wearableBundle.putParcelableArrayList(KEY_ACTIONS, + IMPL.getParcelableArrayListForActions(mActions.toArray( + new Action[mActions.size()]))); + } + if (mFlags != DEFAULT_FLAGS) { + wearableBundle.putInt(KEY_FLAGS, mFlags); + } + if (mDisplayIntent != null) { + wearableBundle.putParcelable(KEY_DISPLAY_INTENT, mDisplayIntent); + } + if (!mPages.isEmpty()) { + wearableBundle.putParcelableArray(KEY_PAGES, mPages.toArray( + new Notification[mPages.size()])); + } + if (mBackground != null) { + wearableBundle.putParcelable(KEY_BACKGROUND, mBackground); + } + if (mContentIcon != 0) { + wearableBundle.putInt(KEY_CONTENT_ICON, mContentIcon); + } + if (mContentIconGravity != DEFAULT_CONTENT_ICON_GRAVITY) { + wearableBundle.putInt(KEY_CONTENT_ICON_GRAVITY, mContentIconGravity); + } + if (mContentActionIndex != UNSET_ACTION_INDEX) { + wearableBundle.putInt(KEY_CONTENT_ACTION_INDEX, + mContentActionIndex); + } + if (mCustomSizePreset != SIZE_DEFAULT) { + wearableBundle.putInt(KEY_CUSTOM_SIZE_PRESET, mCustomSizePreset); + } + if (mCustomContentHeight != 0) { + wearableBundle.putInt(KEY_CUSTOM_CONTENT_HEIGHT, mCustomContentHeight); + } + if (mGravity != DEFAULT_GRAVITY) { + wearableBundle.putInt(KEY_GRAVITY, mGravity); + } + if (mHintScreenTimeout != 0) { + wearableBundle.putInt(KEY_HINT_SCREEN_TIMEOUT, mHintScreenTimeout); + } + + builder.getExtras().putBundle(EXTRA_WEARABLE_EXTENSIONS, wearableBundle); + return builder; + } + + @Override + public WearableExtender clone() { + WearableExtender that = new WearableExtender(); + that.mActions = new ArrayList(this.mActions); + that.mFlags = this.mFlags; + that.mDisplayIntent = this.mDisplayIntent; + that.mPages = new ArrayList(this.mPages); + that.mBackground = this.mBackground; + that.mContentIcon = this.mContentIcon; + that.mContentIconGravity = this.mContentIconGravity; + that.mContentActionIndex = this.mContentActionIndex; + that.mCustomSizePreset = this.mCustomSizePreset; + that.mCustomContentHeight = this.mCustomContentHeight; + that.mGravity = this.mGravity; + that.mHintScreenTimeout = this.mHintScreenTimeout; + return that; + } + + /** + * Add a wearable action to this notification. + * + *

When wearable actions are added using this method, the set of actions that + * show on a wearable device splits from devices that only show actions added + * using {@link NotificationCompat.Builder#addAction}. This allows for customization + * of which actions display on different devices. + * + * @param action the action to add to this notification + * @return this object for method chaining + * @see NotificationCompat.Action + */ + public WearableExtender addAction(Action action) { + mActions.add(action); + return this; + } + + /** + * Adds wearable actions to this notification. + * + *

When wearable actions are added using this method, the set of actions that + * show on a wearable device splits from devices that only show actions added + * using {@link NotificationCompat.Builder#addAction}. This allows for customization + * of which actions display on different devices. + * + * @param actions the actions to add to this notification + * @return this object for method chaining + * @see NotificationCompat.Action + */ + public WearableExtender addActions(List actions) { + mActions.addAll(actions); + return this; + } + + /** + * Clear all wearable actions present on this builder. + * @return this object for method chaining. + * @see #addAction + */ + public WearableExtender clearActions() { + mActions.clear(); + return this; + } + + /** + * Get the wearable actions present on this notification. + */ + public List getActions() { + return mActions; + } + + /** + * Set an intent to launch inside of an activity view when displaying + * this notification. The {@link PendingIntent} provided should be for an activity. + * + *

+         * Intent displayIntent = new Intent(context, MyDisplayActivity.class);
+         * PendingIntent displayPendingIntent = PendingIntent.getActivity(context,
+         *         0, displayIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+         * Notification notif = new NotificationCompat.Builder(context)
+         *         .extend(new NotificationCompat.WearableExtender()
+         *                 .setDisplayIntent(displayPendingIntent)
+         *                 .setCustomSizePreset(NotificationCompat.WearableExtender.SIZE_MEDIUM))
+         *         .build();
+ * + *

The activity to launch needs to allow embedding, must be exported, and + * should have an empty task affinity. It is also recommended to use the device + * default light theme. + * + *

Example AndroidManifest.xml entry: + *

+         * <activity android:name="com.example.MyDisplayActivity"
+         *     android:exported="true"
+         *     android:allowEmbedded="true"
+         *     android:taskAffinity=""
+         *     android:theme="@android:style/Theme.DeviceDefault.Light" />
+ * + * @param intent the {@link PendingIntent} for an activity + * @return this object for method chaining + * @see NotificationCompat.WearableExtender#getDisplayIntent + */ + public WearableExtender setDisplayIntent(PendingIntent intent) { + mDisplayIntent = intent; + return this; + } + + /** + * Get the intent to launch inside of an activity view when displaying this + * notification. This {@code PendingIntent} should be for an activity. + */ + public PendingIntent getDisplayIntent() { + return mDisplayIntent; + } + + /** + * Add an additional page of content to display with this notification. The current + * notification forms the first page, and pages added using this function form + * subsequent pages. This field can be used to separate a notification into multiple + * sections. + * + * @param page the notification to add as another page + * @return this object for method chaining + * @see NotificationCompat.WearableExtender#getPages + */ + public WearableExtender addPage(Notification page) { + mPages.add(page); + return this; + } + + /** + * Add additional pages of content to display with this notification. The current + * notification forms the first page, and pages added using this function form + * subsequent pages. This field can be used to separate a notification into multiple + * sections. + * + * @param pages a list of notifications + * @return this object for method chaining + * @see NotificationCompat.WearableExtender#getPages + */ + public WearableExtender addPages(List pages) { + mPages.addAll(pages); + return this; + } + + /** + * Clear all additional pages present on this builder. + * @return this object for method chaining. + * @see #addPage + */ + public WearableExtender clearPages() { + mPages.clear(); + return this; + } + + /** + * Get the array of additional pages of content for displaying this notification. The + * current notification forms the first page, and elements within this array form + * subsequent pages. This field can be used to separate a notification into multiple + * sections. + * @return the pages for this notification + */ + public List getPages() { + return mPages; + } + + /** + * Set a background image to be displayed behind the notification content. + * Contrary to the {@link NotificationCompat.BigPictureStyle}, this background + * will work with any notification style. + * + * @param background the background bitmap + * @return this object for method chaining + * @see NotificationCompat.WearableExtender#getBackground + */ + public WearableExtender setBackground(Bitmap background) { + mBackground = background; + return this; + } + + /** + * Get a background image to be displayed behind the notification content. + * Contrary to the {@link NotificationCompat.BigPictureStyle}, this background + * will work with any notification style. + * + * @return the background image + * @see NotificationCompat.WearableExtender#setBackground + */ + public Bitmap getBackground() { + return mBackground; + } + + /** + * Set an icon that goes with the content of this notification. + */ + public WearableExtender setContentIcon(int icon) { + mContentIcon = icon; + return this; + } + + /** + * Get an icon that goes with the content of this notification. + */ + public int getContentIcon() { + return mContentIcon; + } + + /** + * Set the gravity that the content icon should have within the notification display. + * Supported values include {@link android.view.Gravity#START} and + * {@link android.view.Gravity#END}. The default value is {@link android.view.Gravity#END}. + * @see #setContentIcon + */ + public WearableExtender setContentIconGravity(int contentIconGravity) { + mContentIconGravity = contentIconGravity; + return this; + } + + /** + * Get the gravity that the content icon should have within the notification display. + * Supported values include {@link android.view.Gravity#START} and + * {@link android.view.Gravity#END}. The default value is {@link android.view.Gravity#END}. + * @see #getContentIcon + */ + public int getContentIconGravity() { + return mContentIconGravity; + } + + /** + * Set an action from this notification's actions to be clickable with the content of + * this notification. This action will no longer display separately from the + * notification's content. + * + *

For notifications with multiple pages, child pages can also have content actions + * set, although the list of available actions comes from the main notification and not + * from the child page's notification. + * + * @param actionIndex The index of the action to hoist onto the current notification page. + * If wearable actions were added to the main notification, this index + * will apply to that list, otherwise it will apply to the regular + * actions list. + */ + public WearableExtender setContentAction(int actionIndex) { + mContentActionIndex = actionIndex; + return this; + } + + /** + * Get the index of the notification action, if any, that was specified as being clickable + * with the content of this notification. This action will no longer display separately + * from the notification's content. + * + *

For notifications with multiple pages, child pages can also have content actions + * set, although the list of available actions comes from the main notification and not + * from the child page's notification. + * + *

If wearable specific actions were added to the main notification, this index will + * apply to that list, otherwise it will apply to the regular actions list. + * + * @return the action index or {@link #UNSET_ACTION_INDEX} if no action was selected. + */ + public int getContentAction() { + return mContentActionIndex; + } + + /** + * Set the gravity that this notification should have within the available viewport space. + * Supported values include {@link android.view.Gravity#TOP}, + * {@link android.view.Gravity#CENTER_VERTICAL} and {@link android.view.Gravity#BOTTOM}. + * The default value is {@link android.view.Gravity#BOTTOM}. + */ + public WearableExtender setGravity(int gravity) { + mGravity = gravity; + return this; + } + + /** + * Get the gravity that this notification should have within the available viewport space. + * Supported values include {@link android.view.Gravity#TOP}, + * {@link android.view.Gravity#CENTER_VERTICAL} and {@link android.view.Gravity#BOTTOM}. + * The default value is {@link android.view.Gravity#BOTTOM}. + */ + public int getGravity() { + return mGravity; + } + + /** + * Set the custom size preset for the display of this notification out of the available + * presets found in {@link NotificationCompat.WearableExtender}, e.g. + * {@link #SIZE_LARGE}. + *

Some custom size presets are only applicable for custom display notifications created + * using {@link NotificationCompat.WearableExtender#setDisplayIntent}. Check the + * documentation for the preset in question. See also + * {@link #setCustomContentHeight} and {@link #getCustomSizePreset}. + */ + public WearableExtender setCustomSizePreset(int sizePreset) { + mCustomSizePreset = sizePreset; + return this; + } + + /** + * Get the custom size preset for the display of this notification out of the available + * presets found in {@link NotificationCompat.WearableExtender}, e.g. + * {@link #SIZE_LARGE}. + *

Some custom size presets are only applicable for custom display notifications created + * using {@link #setDisplayIntent}. Check the documentation for the preset in question. + * See also {@link #setCustomContentHeight} and {@link #setCustomSizePreset}. + */ + public int getCustomSizePreset() { + return mCustomSizePreset; + } + + /** + * Set the custom height in pixels for the display of this notification's content. + *

This option is only available for custom display notifications created + * using {@link NotificationCompat.WearableExtender#setDisplayIntent}. See also + * {@link NotificationCompat.WearableExtender#setCustomSizePreset} and + * {@link #getCustomContentHeight}. + */ + public WearableExtender setCustomContentHeight(int height) { + mCustomContentHeight = height; + return this; + } + + /** + * Get the custom height in pixels for the display of this notification's content. + *

This option is only available for custom display notifications created + * using {@link #setDisplayIntent}. See also {@link #setCustomSizePreset} and + * {@link #setCustomContentHeight}. + */ + public int getCustomContentHeight() { + return mCustomContentHeight; + } + + /** + * Set whether the scrolling position for the contents of this notification should start + * at the bottom of the contents instead of the top when the contents are too long to + * display within the screen. Default is false (start scroll at the top). + */ + public WearableExtender setStartScrollBottom(boolean startScrollBottom) { + setFlag(FLAG_START_SCROLL_BOTTOM, startScrollBottom); + return this; + } + + /** + * Get whether the scrolling position for the contents of this notification should start + * at the bottom of the contents instead of the top when the contents are too long to + * display within the screen. Default is false (start scroll at the top). + */ + public boolean getStartScrollBottom() { + return (mFlags & FLAG_START_SCROLL_BOTTOM) != 0; + } + + /** + * Set whether the content intent is available when the wearable device is not connected + * to a companion device. The user can still trigger this intent when the wearable device + * is offline, but a visual hint will indicate that the content intent may not be available. + * Defaults to true. + */ + public WearableExtender setContentIntentAvailableOffline( + boolean contentIntentAvailableOffline) { + setFlag(FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE, contentIntentAvailableOffline); + return this; + } + + /** + * Get whether the content intent is available when the wearable device is not connected + * to a companion device. The user can still trigger this intent when the wearable device + * is offline, but a visual hint will indicate that the content intent may not be available. + * Defaults to true. + */ + public boolean getContentIntentAvailableOffline() { + return (mFlags & FLAG_CONTENT_INTENT_AVAILABLE_OFFLINE) != 0; + } + + /** + * Set a hint that this notification's icon should not be displayed. + * @param hintHideIcon {@code true} to hide the icon, {@code false} otherwise. + * @return this object for method chaining + */ + public WearableExtender setHintHideIcon(boolean hintHideIcon) { + setFlag(FLAG_HINT_HIDE_ICON, hintHideIcon); + return this; + } + + /** + * Get a hint that this notification's icon should not be displayed. + * @return {@code true} if this icon should not be displayed, false otherwise. + * The default value is {@code false} if this was never set. + */ + public boolean getHintHideIcon() { + return (mFlags & FLAG_HINT_HIDE_ICON) != 0; + } + + /** + * Set a visual hint that only the background image of this notification should be + * displayed, and other semantic content should be hidden. This hint is only applicable + * to sub-pages added using {@link #addPage}. + */ + public WearableExtender setHintShowBackgroundOnly(boolean hintShowBackgroundOnly) { + setFlag(FLAG_HINT_SHOW_BACKGROUND_ONLY, hintShowBackgroundOnly); + return this; + } + + /** + * Get a visual hint that only the background image of this notification should be + * displayed, and other semantic content should be hidden. This hint is only applicable + * to sub-pages added using {@link NotificationCompat.WearableExtender#addPage}. + */ + public boolean getHintShowBackgroundOnly() { + return (mFlags & FLAG_HINT_SHOW_BACKGROUND_ONLY) != 0; + } + + /** + * Set a hint that this notification's background should not be clipped if possible, + * and should instead be resized to fully display on the screen, retaining the aspect + * ratio of the image. This can be useful for images like barcodes or qr codes. + * @param hintAvoidBackgroundClipping {@code true} to avoid clipping if possible. + * @return this object for method chaining + */ + public WearableExtender setHintAvoidBackgroundClipping( + boolean hintAvoidBackgroundClipping) { + setFlag(FLAG_HINT_AVOID_BACKGROUND_CLIPPING, hintAvoidBackgroundClipping); + return this; + } + + /** + * Get a hint that this notification's background should not be clipped if possible, + * and should instead be resized to fully display on the screen, retaining the aspect + * ratio of the image. This can be useful for images like barcodes or qr codes. + * @return {@code true} if it's ok if the background is clipped on the screen, false + * otherwise. The default value is {@code false} if this was never set. + */ + public boolean getHintAvoidBackgroundClipping() { + return (mFlags & FLAG_HINT_AVOID_BACKGROUND_CLIPPING) != 0; + } + + /** + * Set a hint that the screen should remain on for at least this duration when + * this notification is displayed on the screen. + * @param timeout The requested screen timeout in milliseconds. Can also be either + * {@link #SCREEN_TIMEOUT_SHORT} or {@link #SCREEN_TIMEOUT_LONG}. + * @return this object for method chaining + */ + public WearableExtender setHintScreenTimeout(int timeout) { + mHintScreenTimeout = timeout; + return this; + } + + /** + * Get the duration, in milliseconds, that the screen should remain on for + * when this notification is displayed. + * @return the duration in milliseconds if > 0, or either one of the sentinel values + * {@link #SCREEN_TIMEOUT_SHORT} or {@link #SCREEN_TIMEOUT_LONG}. + */ + public int getHintScreenTimeout() { + return mHintScreenTimeout; + } + + private void setFlag(int mask, boolean value) { + if (value) { + mFlags |= mask; + } else { + mFlags &= ~mask; + } + } + } + + /** + *

Helper class to add Android Auto extensions to notifications. To create a notification + * with car extensions: + * + *

    + *
  1. Create an {@link NotificationCompat.Builder}, setting any desired + * properties. + *
  2. Create a {@link CarExtender}. + *
  3. Set car-specific properties using the {@code add} and {@code set} methods of + * {@link CarExtender}. + *
  4. Call {@link android.support.v4.app.NotificationCompat.Builder#extend(NotificationCompat.Extender)} + * to apply the extensions to a notification. + *
  5. Post the notification to the notification system with the + * {@code NotificationManagerCompat.notify(...)} methods and not the + * {@code NotificationManager.notify(...)} methods. + *
+ * + *
+     * Notification notification = new NotificationCompat.Builder(context)
+     *         ...
+     *         .extend(new CarExtender()
+     *                 .set*(...))
+     *         .build();
+     * 
+ * + *

Car extensions can be accessed on an existing notification by using the + * {@code CarExtender(Notification)} constructor, and then using the {@code get} methods + * to access values. + */ + public static final class CarExtender implements Extender { + private static final String TAG = "CarExtender"; + + private static final String EXTRA_CAR_EXTENDER = "android.car.EXTENSIONS"; + private static final String EXTRA_LARGE_ICON = "large_icon"; + private static final String EXTRA_CONVERSATION = "car_conversation"; + private static final String EXTRA_COLOR = "app_color"; + + private Bitmap mLargeIcon; + private UnreadConversation mUnreadConversation; + private int mColor = NotificationCompat.COLOR_DEFAULT; + + /** + * Create a {@link CarExtender} with default options. + */ + public CarExtender() { + } + + /** + * Create a {@link CarExtender} from the CarExtender options of an existing Notification. + * + * @param notif The notification from which to copy options. + */ + public CarExtender(Notification notif) { + if (Build.VERSION.SDK_INT < 21) { + return; + } + + Bundle carBundle = getExtras(notif)==null ? + null : getExtras(notif).getBundle(EXTRA_CAR_EXTENDER); + if (carBundle != null) { + mLargeIcon = carBundle.getParcelable(EXTRA_LARGE_ICON); + mColor = carBundle.getInt(EXTRA_COLOR, NotificationCompat.COLOR_DEFAULT); + + Bundle b = carBundle.getBundle(EXTRA_CONVERSATION); + mUnreadConversation = (UnreadConversation) IMPL.getUnreadConversationFromBundle( + b, UnreadConversation.FACTORY, RemoteInput.FACTORY); + } + } + + /** + * Apply car extensions to a notification that is being built. This is typically called by + * the {@link android.support.v4.app.NotificationCompat.Builder#extend(NotificationCompat.Extender)} + * method of {@link NotificationCompat.Builder}. + */ + @Override + public NotificationCompat.Builder extend(NotificationCompat.Builder builder) { + if (Build.VERSION.SDK_INT < 21) { + return builder; + } + + Bundle carExtensions = new Bundle(); + + if (mLargeIcon != null) { + carExtensions.putParcelable(EXTRA_LARGE_ICON, mLargeIcon); + } + if (mColor != NotificationCompat.COLOR_DEFAULT) { + carExtensions.putInt(EXTRA_COLOR, mColor); + } + + if (mUnreadConversation != null) { + Bundle b = IMPL.getBundleForUnreadConversation(mUnreadConversation); + carExtensions.putBundle(EXTRA_CONVERSATION, b); + } + + builder.getExtras().putBundle(EXTRA_CAR_EXTENDER, carExtensions); + return builder; + } + + /** + * Sets the accent color to use when Android Auto presents the notification. + * + * Android Auto uses the color set with {@link android.support.v4.app.NotificationCompat.Builder#setColor(int)} + * to accent the displayed notification. However, not all colors are acceptable in an + * automotive setting. This method can be used to override the color provided in the + * notification in such a situation. + */ + public CarExtender setColor(int color) { + mColor = color; + return this; + } + + /** + * Gets the accent color. + * + * @see setColor + */ + public int getColor() { + return mColor; + } + + /** + * Sets the large icon of the car notification. + * + * If no large icon is set in the extender, Android Auto will display the icon + * specified by {@link android.support.v4.app.NotificationCompat.Builder#setLargeIcon(android.graphics.Bitmap)} + * + * @param largeIcon The large icon to use in the car notification. + * @return This object for method chaining. + */ + public CarExtender setLargeIcon(Bitmap largeIcon) { + mLargeIcon = largeIcon; + return this; + } + + /** + * Gets the large icon used in this car notification, or null if no icon has been set. + * + * @return The large icon for the car notification. + * @see CarExtender#setLargeIcon + */ + public Bitmap getLargeIcon() { + return mLargeIcon; + } + + /** + * Sets the unread conversation in a message notification. + * + * @param unreadConversation The unread part of the conversation this notification conveys. + * @return This object for method chaining. + */ + public CarExtender setUnreadConversation(UnreadConversation unreadConversation) { + mUnreadConversation = unreadConversation; + return this; + } + + /** + * Returns the unread conversation conveyed by this notification. + * @see #setUnreadConversation(UnreadConversation) + */ + public UnreadConversation getUnreadConversation() { + return mUnreadConversation; + } + + /** + * A class which holds the unread messages from a conversation. + */ + public static class UnreadConversation extends NotificationCompatBase.UnreadConversation { + private final String[] mMessages; + private final RemoteInput mRemoteInput; + private final PendingIntent mReplyPendingIntent; + private final PendingIntent mReadPendingIntent; + private final String[] mParticipants; + private final long mLatestTimestamp; + + UnreadConversation(String[] messages, RemoteInput remoteInput, + PendingIntent replyPendingIntent, PendingIntent readPendingIntent, + String[] participants, long latestTimestamp) { + mMessages = messages; + mRemoteInput = remoteInput; + mReadPendingIntent = readPendingIntent; + mReplyPendingIntent = replyPendingIntent; + mParticipants = participants; + mLatestTimestamp = latestTimestamp; + } + + /** + * Gets the list of messages conveyed by this notification. + */ + @Override + public String[] getMessages() { + return mMessages; + } + + /** + * Gets the remote input that will be used to convey the response to a message list, or + * null if no such remote input exists. + */ + @Override + public RemoteInput getRemoteInput() { + return mRemoteInput; + } + + /** + * Gets the pending intent that will be triggered when the user replies to this + * notification. + */ + @Override + public PendingIntent getReplyPendingIntent() { + return mReplyPendingIntent; + } + + /** + * Gets the pending intent that Android Auto will send after it reads aloud all messages + * in this object's message list. + */ + @Override + public PendingIntent getReadPendingIntent() { + return mReadPendingIntent; + } + + /** + * Gets the participants in the conversation. + */ + @Override + public String[] getParticipants() { + return mParticipants; + } + + /** + * Gets the firs participant in the conversation. + */ + @Override + public String getParticipant() { + return mParticipants.length > 0 ? mParticipants[0] : null; + } + + /** + * Gets the timestamp of the conversation. + */ + @Override + public long getLatestTimestamp() { + return mLatestTimestamp; + } + + /** @hide */ + static final Factory FACTORY = new Factory() { + @Override + public UnreadConversation build( + String[] messages, RemoteInputCompatBase.RemoteInput remoteInput, + PendingIntent replyPendingIntent, PendingIntent readPendingIntent, + String[] participants, long latestTimestamp) { + return new UnreadConversation( + messages, (RemoteInput) remoteInput, replyPendingIntent, + readPendingIntent, + participants, latestTimestamp); + } + }; + + /** + * Builder class for {@link CarExtender.UnreadConversation} objects. + */ + public static class Builder { + private final List mMessages = new ArrayList(); + private final String mParticipant; + private RemoteInput mRemoteInput; + private PendingIntent mReadPendingIntent; + private PendingIntent mReplyPendingIntent; + private long mLatestTimestamp; + + /** + * Constructs a new builder for {@link CarExtender.UnreadConversation}. + * + * @param name The name of the other participant in the conversation. + */ + public Builder(String name) { + mParticipant = name; + } + + /** + * Appends a new unread message to the list of messages for this conversation. + * + * The messages should be added from oldest to newest. + * + * @param message The text of the new unread message. + * @return This object for method chaining. + */ + public Builder addMessage(String message) { + mMessages.add(message); + return this; + } + + /** + * Sets the pending intent and remote input which will convey the reply to this + * notification. + * + * @param pendingIntent The pending intent which will be triggered on a reply. + * @param remoteInput The remote input parcelable which will carry the reply. + * @return This object for method chaining. + * + * @see CarExtender.UnreadConversation#getRemoteInput + * @see CarExtender.UnreadConversation#getReplyPendingIntent + */ + public Builder setReplyAction( + PendingIntent pendingIntent, RemoteInput remoteInput) { + mRemoteInput = remoteInput; + mReplyPendingIntent = pendingIntent; + + return this; + } + + /** + * Sets the pending intent that will be sent once the messages in this notification + * are read. + * + * @param pendingIntent The pending intent to use. + * @return This object for method chaining. + */ + public Builder setReadPendingIntent(PendingIntent pendingIntent) { + mReadPendingIntent = pendingIntent; + return this; + } + + /** + * Sets the timestamp of the most recent message in an unread conversation. + * + * If a messaging notification has been posted by your application and has not + * yet been cancelled, posting a later notification with the same id and tag + * but without a newer timestamp may result in Android Auto not displaying a + * heads up notification for the later notification. + * + * @param timestamp The timestamp of the most recent message in the conversation. + * @return This object for method chaining. + */ + public Builder setLatestTimestamp(long timestamp) { + mLatestTimestamp = timestamp; + return this; + } + + /** + * Builds a new unread conversation object. + * + * @return The new unread conversation object. + */ + public UnreadConversation build() { + String[] messages = mMessages.toArray(new String[mMessages.size()]); + String[] participants = { mParticipant }; + return new UnreadConversation(messages, mRemoteInput, mReplyPendingIntent, + mReadPendingIntent, participants, mLatestTimestamp); + } + } + } + } + + + /** + * Get an array of Notification objects from a parcelable array bundle field. + * Update the bundle to have a typed array so fetches in the future don't need + * to do an array copy. + */ + private static Notification[] getNotificationArrayFromBundle(Bundle bundle, String key) { + Parcelable[] array = bundle.getParcelableArray(key); + if (array instanceof Notification[] || array == null) { + return (Notification[]) array; + } + Notification[] typedArray = new Notification[array.length]; + for (int i = 0; i < array.length; i++) { + typedArray[i] = (Notification) array[i]; + } + bundle.putParcelableArray(key, typedArray); + return typedArray; + } + + /** + * Gets the {@link Notification#extras} field from a notification in a backwards + * compatible manner. Extras field was supported from JellyBean (Api level 16) + * forwards. This function will return null on older api levels. + */ + public static Bundle getExtras(Notification notif) { + return IMPL.getExtras(notif); + } + + /** + * Get the number of actions in this notification in a backwards compatible + * manner. Actions were supported from JellyBean (Api level 16) forwards. + */ + public static int getActionCount(Notification notif) { + return IMPL.getActionCount(notif); + } + + /** + * Get an action on this notification in a backwards compatible + * manner. Actions were supported from JellyBean (Api level 16) forwards. + * @param notif The notification to inspect. + * @param actionIndex The index of the action to retrieve. + */ + public static Action getAction(Notification notif, int actionIndex) { + return IMPL.getAction(notif, actionIndex); + } + + /** + * Get the category of this notification in a backwards compatible + * manner. + * @param notif The notification to inspect. + */ + public static String getCategory(Notification notif) { + return IMPL.getCategory(notif); + } + + /** + * Get whether or not this notification is only relevant to the current device. + * + *

Some notifications can be bridged to other devices for remote display. + * If this hint is set, it is recommend that this notification not be bridged. + */ + public static boolean getLocalOnly(Notification notif) { + return IMPL.getLocalOnly(notif); + } + + /** + * Get the key used to group this notification into a cluster or stack + * with other notifications on devices which support such rendering. + */ + public static String getGroup(Notification notif) { + return IMPL.getGroup(notif); + } + + /** + * Get whether this notification to be the group summary for a group of notifications. + * Grouped notifications may display in a cluster or stack on devices which + * support such rendering. Requires a group key also be set using {@link Builder#setGroup}. + * @return Whether this notification is a group summary. + */ + public static boolean isGroupSummary(Notification notif) { + return IMPL.isGroupSummary(notif); + } + + /** + * Get a sort key that orders this notification among other notifications from the + * same package. This can be useful if an external sort was already applied and an app + * would like to preserve this. Notifications will be sorted lexicographically using this + * value, although providing different priorities in addition to providing sort key may + * cause this value to be ignored. + * + *

This sort key can also be used to order members of a notification group. See + * {@link Builder#setGroup}. + * + * @see String#compareTo(String) + */ + public static String getSortKey(Notification notif) { + return IMPL.getSortKey(notif); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi20.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi20.java new file mode 100644 index 00000000000..fcd75763c5e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi20.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.app.RemoteInput; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcelable; +import android.widget.RemoteViews; + +import java.util.ArrayList; + +class NotificationCompatApi20 { + public static class Builder implements NotificationBuilderWithBuilderAccessor, + NotificationBuilderWithActions { + private Notification.Builder b; + private Bundle mExtras; + + public Builder(Context context, Notification n, + CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, + RemoteViews tickerView, int number, + PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon, + int progressMax, int progress, boolean progressIndeterminate, boolean showWhen, + boolean useChronometer, int priority, CharSequence subText, boolean localOnly, + ArrayList people, Bundle extras, String groupKey, boolean groupSummary, + String sortKey) { + b = new Notification.Builder(context) + .setWhen(n.when) + .setShowWhen(showWhen) + .setSmallIcon(n.icon, n.iconLevel) + .setContent(n.contentView) + .setTicker(n.tickerText, tickerView) + .setSound(n.sound, n.audioStreamType) + .setVibrate(n.vibrate) + .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) + .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) + .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) + .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setDefaults(n.defaults) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setSubText(subText) + .setContentInfo(contentInfo) + .setContentIntent(contentIntent) + .setDeleteIntent(n.deleteIntent) + .setFullScreenIntent(fullScreenIntent, + (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) + .setLargeIcon(largeIcon) + .setNumber(number) + .setUsesChronometer(useChronometer) + .setPriority(priority) + .setProgress(progressMax, progress, progressIndeterminate) + .setLocalOnly(localOnly) + .setGroup(groupKey) + .setGroupSummary(groupSummary) + .setSortKey(sortKey); + mExtras = new Bundle(); + if (extras != null) { + mExtras.putAll(extras); + } + if (people != null && !people.isEmpty()) { + mExtras.putStringArray(Notification.EXTRA_PEOPLE, + people.toArray(new String[people.size()])); + } + } + + @Override + public void addAction(NotificationCompatBase.Action action) { + NotificationCompatApi20.addAction(b, action); + } + + @Override + public Notification.Builder getBuilder() { + return b; + } + + @Override + public Notification build() { + b.setExtras(mExtras); + return b.build(); + } + } + + public static void addAction(Notification.Builder b, NotificationCompatBase.Action action) { + Notification.Action.Builder actionBuilder = new Notification.Action.Builder( + action.getIcon(), action.getTitle(), action.getActionIntent()); + if (action.getRemoteInputs() != null) { + for (RemoteInput remoteInput : RemoteInputCompatApi20.fromCompat( + action.getRemoteInputs())) { + actionBuilder.addRemoteInput(remoteInput); + } + } + if (action.getExtras() != null) { + actionBuilder.addExtras(action.getExtras()); + } + b.addAction(actionBuilder.build()); + } + + public static NotificationCompatBase.Action getAction(Notification notif, + int actionIndex, NotificationCompatBase.Action.Factory actionFactory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + return getActionCompatFromAction(notif.actions[actionIndex], actionFactory, remoteInputFactory); + } + + private static NotificationCompatBase.Action getActionCompatFromAction( + Notification.Action action, NotificationCompatBase.Action.Factory actionFactory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + RemoteInputCompatBase.RemoteInput[] remoteInputs = RemoteInputCompatApi20.toCompat( + action.getRemoteInputs(), remoteInputFactory); + return actionFactory.build(action.icon, action.title, action.actionIntent, + action.getExtras(), remoteInputs); + } + + private static Notification.Action getActionFromActionCompat( + NotificationCompatBase.Action actionCompat) { + Notification.Action.Builder actionBuilder = new Notification.Action.Builder( + actionCompat.getIcon(), actionCompat.getTitle(), actionCompat.getActionIntent()) + .addExtras(actionCompat.getExtras()); + RemoteInputCompatBase.RemoteInput[] remoteInputCompats = actionCompat.getRemoteInputs(); + if (remoteInputCompats != null) { + RemoteInput[] remoteInputs = RemoteInputCompatApi20.fromCompat(remoteInputCompats); + for (RemoteInput remoteInput : remoteInputs) { + actionBuilder.addRemoteInput(remoteInput); + } + } + return actionBuilder.build(); + } + + /** + * Get a list of notification compat actions by parsing actions stored within a list of + * parcelables using the {@link Bundle#getParcelableArrayList} function in the same + * manner that framework code would do so. In API20, Using Action parcelable directly + * is correct. + */ + public static NotificationCompatBase.Action[] getActionsFromParcelableArrayList( + ArrayList parcelables, + NotificationCompatBase.Action.Factory actionFactory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + if (parcelables == null) { + return null; + } + NotificationCompatBase.Action[] actions = actionFactory.newArray(parcelables.size()); + for (int i = 0; i < actions.length; i++) { + Notification.Action action = (Notification.Action) parcelables.get(i); + actions[i] = getActionCompatFromAction(action, actionFactory, remoteInputFactory); + } + return actions; + } + + /** + * Get an array list of parcelables, suitable for {@link Bundle#putParcelableArrayList}, + * that matches what framework code would do to store an actions list in this way. In API20, + * action parcelables were directly placed as entries in the array list. + */ + public static ArrayList getParcelableArrayListForActions( + NotificationCompatBase.Action[] actions) { + if (actions == null) { + return null; + } + ArrayList parcelables = new ArrayList(actions.length); + for (NotificationCompatBase.Action action : actions) { + parcelables.add(getActionFromActionCompat(action)); + } + return parcelables; + } + + public static boolean getLocalOnly(Notification notif) { + return (notif.flags & Notification.FLAG_LOCAL_ONLY) != 0; + } + + public static String getGroup(Notification notif) { + return notif.getGroup(); + } + + public static boolean isGroupSummary(Notification notif) { + return (notif.flags & Notification.FLAG_GROUP_SUMMARY) != 0; + } + + public static String getSortKey(Notification notif) { + return notif.getSortKey(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi21.java new file mode 100644 index 00000000000..dbce1db52c0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatApi21.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcelable; +import android.widget.RemoteViews; + +import java.util.ArrayList; + +class NotificationCompatApi21 { + + public static final String CATEGORY_CALL = Notification.CATEGORY_CALL; + public static final String CATEGORY_MESSAGE = Notification.CATEGORY_MESSAGE; + public static final String CATEGORY_EMAIL = Notification.CATEGORY_EMAIL; + public static final String CATEGORY_EVENT = Notification.CATEGORY_EVENT; + public static final String CATEGORY_PROMO = Notification.CATEGORY_PROMO; + public static final String CATEGORY_ALARM = Notification.CATEGORY_ALARM; + public static final String CATEGORY_PROGRESS = Notification.CATEGORY_PROGRESS; + public static final String CATEGORY_SOCIAL = Notification.CATEGORY_SOCIAL; + public static final String CATEGORY_ERROR = Notification.CATEGORY_ERROR; + public static final String CATEGORY_TRANSPORT = Notification.CATEGORY_TRANSPORT; + public static final String CATEGORY_SYSTEM = Notification.CATEGORY_SYSTEM; + public static final String CATEGORY_SERVICE = Notification.CATEGORY_SERVICE; + public static final String CATEGORY_RECOMMENDATION = Notification.CATEGORY_RECOMMENDATION; + public static final String CATEGORY_STATUS = Notification.CATEGORY_STATUS; + + private static final String KEY_AUTHOR = "author"; + private static final String KEY_TEXT = "text"; + private static final String KEY_MESSAGES = "messages"; + private static final String KEY_REMOTE_INPUT = "remote_input"; + private static final String KEY_ON_REPLY = "on_reply"; + private static final String KEY_ON_READ = "on_read"; + private static final String KEY_PARTICIPANTS = "participants"; + private static final String KEY_TIMESTAMP = "timestamp"; + + public static class Builder implements NotificationBuilderWithBuilderAccessor, + NotificationBuilderWithActions { + private Notification.Builder b; + + public Builder(Context context, Notification n, + CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, + RemoteViews tickerView, int number, + PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon, + int progressMax, int progress, boolean progressIndeterminate, boolean showWhen, + boolean useChronometer, int priority, CharSequence subText, boolean localOnly, + String category, ArrayList people, Bundle extras, int color, + int visibility, Notification publicVersion, String groupKey, boolean groupSummary, + String sortKey) { + b = new Notification.Builder(context) + .setWhen(n.when) + .setShowWhen(showWhen) + .setSmallIcon(n.icon, n.iconLevel) + .setContent(n.contentView) + .setTicker(n.tickerText, tickerView) + .setSound(n.sound, n.audioStreamType) + .setVibrate(n.vibrate) + .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) + .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) + .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) + .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setDefaults(n.defaults) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setSubText(subText) + .setContentInfo(contentInfo) + .setContentIntent(contentIntent) + .setDeleteIntent(n.deleteIntent) + .setFullScreenIntent(fullScreenIntent, + (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) + .setLargeIcon(largeIcon) + .setNumber(number) + .setUsesChronometer(useChronometer) + .setPriority(priority) + .setProgress(progressMax, progress, progressIndeterminate) + .setLocalOnly(localOnly) + .setExtras(extras) + .setGroup(groupKey) + .setGroupSummary(groupSummary) + .setSortKey(sortKey) + .setCategory(category) + .setColor(color) + .setVisibility(visibility) + .setPublicVersion(publicVersion); + for (String person: people) { + b.addPerson(person); + } + } + + @Override + public void addAction(NotificationCompatBase.Action action) { + NotificationCompatApi20.addAction(b, action); + } + + @Override + public Notification.Builder getBuilder() { + return b; + } + + @Override + public Notification build() { + return b.build(); + } + } + + public static String getCategory(Notification notif) { + return notif.category; + } + + static Bundle getBundleForUnreadConversation(NotificationCompatBase.UnreadConversation uc) { + if (uc == null) { + return null; + } + Bundle b = new Bundle(); + String author = null; + if (uc.getParticipants() != null && uc.getParticipants().length > 1) { + author = uc.getParticipants()[0]; + } + Parcelable[] messages = new Parcelable[uc.getMessages().length]; + for (int i = 0; i < messages.length; i++) { + Bundle m = new Bundle(); + m.putString(KEY_TEXT, uc.getMessages()[i]); + m.putString(KEY_AUTHOR, author); + messages[i] = m; + } + b.putParcelableArray(KEY_MESSAGES, messages); + RemoteInputCompatBase.RemoteInput remoteInput = uc.getRemoteInput(); + if (remoteInput != null) { + b.putParcelable(KEY_REMOTE_INPUT, fromCompatRemoteInput(remoteInput)); + } + b.putParcelable(KEY_ON_REPLY, uc.getReplyPendingIntent()); + b.putParcelable(KEY_ON_READ, uc.getReadPendingIntent()); + b.putStringArray(KEY_PARTICIPANTS, uc.getParticipants()); + b.putLong(KEY_TIMESTAMP, uc.getLatestTimestamp()); + return b; + } + + static NotificationCompatBase.UnreadConversation getUnreadConversationFromBundle( + Bundle b, NotificationCompatBase.UnreadConversation.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + if (b == null) { + return null; + } + Parcelable[] parcelableMessages = b.getParcelableArray(KEY_MESSAGES); + String[] messages = null; + if (parcelableMessages != null) { + String[] tmp = new String[parcelableMessages.length]; + boolean success = true; + for (int i = 0; i < tmp.length; i++) { + if (!(parcelableMessages[i] instanceof Bundle)) { + success = false; + break; + } + tmp[i] = ((Bundle) parcelableMessages[i]).getString(KEY_TEXT); + if (tmp[i] == null) { + success = false; + break; + } + } + if (success) { + messages = tmp; + } else { + return null; + } + } + + PendingIntent onRead = b.getParcelable(KEY_ON_READ); + PendingIntent onReply = b.getParcelable(KEY_ON_REPLY); + + android.app.RemoteInput remoteInput = b.getParcelable(KEY_REMOTE_INPUT); + + String[] participants = b.getStringArray(KEY_PARTICIPANTS); + if (participants == null || participants.length != 1) { + return null; + } + + + return factory.build( + messages, + remoteInput != null ? toCompatRemoteInput(remoteInput, remoteInputFactory) : null, + onReply, + onRead, + participants, b.getLong(KEY_TIMESTAMP)); + } + + private static android.app.RemoteInput fromCompatRemoteInput( + RemoteInputCompatBase.RemoteInput src) { + return new android.app.RemoteInput.Builder(src.getResultKey()) + .setLabel(src.getLabel()) + .setChoices(src.getChoices()) + .setAllowFreeFormInput(src.getAllowFreeFormInput()) + .addExtras(src.getExtras()) + .build(); + } + + private static RemoteInputCompatBase.RemoteInput toCompatRemoteInput( + android.app.RemoteInput remoteInput, + RemoteInputCompatBase.RemoteInput.Factory factory) { + return factory.build(remoteInput.getResultKey(), + remoteInput.getLabel(), + remoteInput.getChoices(), + remoteInput.getAllowFreeFormInput(), + remoteInput.getExtras()); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatBase.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatBase.java new file mode 100644 index 00000000000..777a57f6ddb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatBase.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.PendingIntent; +import android.os.Bundle; + +/** + * @hide + */ +public class NotificationCompatBase { + + public static abstract class Action { + public abstract int getIcon(); + public abstract CharSequence getTitle(); + public abstract PendingIntent getActionIntent(); + public abstract Bundle getExtras(); + public abstract RemoteInputCompatBase.RemoteInput[] getRemoteInputs(); + + public interface Factory { + Action build(int icon, CharSequence title, PendingIntent actionIntent, + Bundle extras, RemoteInputCompatBase.RemoteInput[] remoteInputs); + public Action[] newArray(int length); + } + } + + public static abstract class UnreadConversation { + abstract String[] getParticipants(); + abstract String getParticipant(); + abstract String[] getMessages(); + abstract RemoteInputCompatBase.RemoteInput getRemoteInput(); + abstract PendingIntent getReplyPendingIntent(); + abstract PendingIntent getReadPendingIntent(); + abstract long getLatestTimestamp(); + + public interface Factory { + UnreadConversation build(String[] messages, + RemoteInputCompatBase.RemoteInput remoteInput, + PendingIntent replyPendingIntent, PendingIntent readPendingIntent, + String[] participants, long latestTimestamp); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatExtras.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatExtras.java new file mode 100644 index 00000000000..6a2ee932cd2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatExtras.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +/** + * Well-known extras used by {@link NotificationCompat} for backwards compatibility. + */ +public final class NotificationCompatExtras { + /** + * Extras key used internally by {@link NotificationCompat} to store the value of + * the {@link android.app.Notification#FLAG_LOCAL_ONLY} field before it was available. + * If possible, use {@link NotificationCompat#getLocalOnly} to access this field. + */ + public static final String EXTRA_LOCAL_ONLY = NotificationCompatJellybean.EXTRA_LOCAL_ONLY; + + /** + * Extras key used internally by {@link NotificationCompat} to store the value set + * by {@link android.app.Notification.Builder#setGroup} before it was available. + * If possible, use {@link NotificationCompat#getGroup} to access this value. + */ + public static final String EXTRA_GROUP_KEY = NotificationCompatJellybean.EXTRA_GROUP_KEY; + + /** + * Extras key used internally by {@link NotificationCompat} to store the value set + * by {@link android.app.Notification.Builder#setGroupSummary} before it was available. + * If possible, use {@link NotificationCompat#isGroupSummary} to access this value. + */ + public static final String EXTRA_GROUP_SUMMARY = + NotificationCompatJellybean.EXTRA_GROUP_SUMMARY; + + /** + * Extras key used internally by {@link NotificationCompat} to store the value set + * by {@link android.app.Notification.Builder#setSortKey} before it was available. + * If possible, use {@link NotificationCompat#getSortKey} to access this value. + */ + public static final String EXTRA_SORT_KEY = NotificationCompatJellybean.EXTRA_SORT_KEY; + + /** + * Extras key used internally by {@link NotificationCompat} to store the value of + * the {@link android.app.Notification.Action#extras} field before it was available. + * If possible, use {@link NotificationCompat#getAction} to access this field. + */ + public static final String EXTRA_ACTION_EXTRAS = + NotificationCompatJellybean.EXTRA_ACTION_EXTRAS; + + /** + * Extras key used internally by {@link NotificationCompat} to store the value of + * the {@link android.app.Notification.Action#getRemoteInputs} before the field + * was available. + * If possible, use {@link NotificationCompat.Action#getRemoteInputs to access this field. + */ + public static final String EXTRA_REMOTE_INPUTS = + NotificationCompatJellybean.EXTRA_REMOTE_INPUTS; + + private NotificationCompatExtras() {} +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatGingerbread.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatGingerbread.java new file mode 100644 index 00000000000..8537ab79bf7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatGingerbread.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; + +class NotificationCompatGingerbread { + public static Notification add(Notification notification, Context context, + CharSequence contentTitle, CharSequence contentText, PendingIntent contentIntent, + PendingIntent fullScreenIntent) { + notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent); + notification.fullScreenIntent = fullScreenIntent; + return notification; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatHoneycomb.java new file mode 100644 index 00000000000..3048e91f93b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatHoneycomb.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.widget.RemoteViews; + +class NotificationCompatHoneycomb { + static Notification add(Context context, Notification n, + CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, + RemoteViews tickerView, int number, + PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon) { + Notification.Builder b = new Notification.Builder(context) + .setWhen(n.when) + .setSmallIcon(n.icon, n.iconLevel) + .setContent(n.contentView) + .setTicker(n.tickerText, tickerView) + .setSound(n.sound, n.audioStreamType) + .setVibrate(n.vibrate) + .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) + .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) + .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) + .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setDefaults(n.defaults) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setContentInfo(contentInfo) + .setContentIntent(contentIntent) + .setDeleteIntent(n.deleteIntent) + .setFullScreenIntent(fullScreenIntent, + (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) + .setLargeIcon(largeIcon) + .setNumber(number); + + return b.getNotification(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatIceCreamSandwich.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatIceCreamSandwich.java new file mode 100644 index 00000000000..0842451f072 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatIceCreamSandwich.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.widget.RemoteViews; + +class NotificationCompatIceCreamSandwich { + + public static class Builder implements NotificationBuilderWithBuilderAccessor { + + private Notification.Builder b; + + public Builder(Context context, Notification n, CharSequence contentTitle, + CharSequence contentText, CharSequence contentInfo, RemoteViews tickerView, + int number, PendingIntent contentIntent, PendingIntent fullScreenIntent, + Bitmap largeIcon, int progressMax, int progress, boolean progressIndeterminate) { + b = new Notification.Builder(context) + .setWhen(n.when) + .setSmallIcon(n.icon, n.iconLevel) + .setContent(n.contentView) + .setTicker(n.tickerText, tickerView) + .setSound(n.sound, n.audioStreamType) + .setVibrate(n.vibrate) + .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) + .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) + .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) + .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setDefaults(n.defaults) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setContentInfo(contentInfo) + .setContentIntent(contentIntent) + .setDeleteIntent(n.deleteIntent) + .setFullScreenIntent(fullScreenIntent, + (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) + .setLargeIcon(largeIcon) + .setNumber(number) + .setProgress(progressMax, progress, progressIndeterminate); + } + + @Override + public Notification.Builder getBuilder() { + return b; + } + + @Override + public Notification build() { + return b.getNotification(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatJellybean.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatJellybean.java new file mode 100644 index 00000000000..6f54c4b816a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatJellybean.java @@ -0,0 +1,406 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.Log; +import android.util.SparseArray; +import android.widget.RemoteViews; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +class NotificationCompatJellybean { + public static final String TAG = "NotificationCompat"; + + // Extras keys used for Jellybean SDK and above. + static final String EXTRA_LOCAL_ONLY = "android.support.localOnly"; + static final String EXTRA_ACTION_EXTRAS = "android.support.actionExtras"; + static final String EXTRA_REMOTE_INPUTS = "android.support.remoteInputs"; + static final String EXTRA_GROUP_KEY = "android.support.groupKey"; + static final String EXTRA_GROUP_SUMMARY = "android.support.isGroupSummary"; + static final String EXTRA_SORT_KEY = "android.support.sortKey"; + static final String EXTRA_USE_SIDE_CHANNEL = "android.support.useSideChannel"; + + // Bundle keys for storing action fields in a bundle + private static final String KEY_ICON = "icon"; + private static final String KEY_TITLE = "title"; + private static final String KEY_ACTION_INTENT = "actionIntent"; + private static final String KEY_EXTRAS = "extras"; + private static final String KEY_REMOTE_INPUTS = "remoteInputs"; + + private static final Object sExtrasLock = new Object(); + private static Field sExtrasField; + private static boolean sExtrasFieldAccessFailed; + + private static final Object sActionsLock = new Object(); + private static Class sActionClass; + private static Field sActionsField; + private static Field sActionIconField; + private static Field sActionTitleField; + private static Field sActionIntentField; + private static boolean sActionsAccessFailed; + + public static class Builder implements NotificationBuilderWithBuilderAccessor, + NotificationBuilderWithActions { + private Notification.Builder b; + private final Bundle mExtras; + private List mActionExtrasList = new ArrayList(); + + public Builder(Context context, Notification n, + CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, + RemoteViews tickerView, int number, + PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon, + int progressMax, int progress, boolean progressIndeterminate, + boolean useChronometer, int priority, CharSequence subText, boolean localOnly, + Bundle extras, String groupKey, boolean groupSummary, String sortKey) { + b = new Notification.Builder(context) + .setWhen(n.when) + .setSmallIcon(n.icon, n.iconLevel) + .setContent(n.contentView) + .setTicker(n.tickerText, tickerView) + .setSound(n.sound, n.audioStreamType) + .setVibrate(n.vibrate) + .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) + .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) + .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) + .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setDefaults(n.defaults) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setSubText(subText) + .setContentInfo(contentInfo) + .setContentIntent(contentIntent) + .setDeleteIntent(n.deleteIntent) + .setFullScreenIntent(fullScreenIntent, + (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) + .setLargeIcon(largeIcon) + .setNumber(number) + .setUsesChronometer(useChronometer) + .setPriority(priority) + .setProgress(progressMax, progress, progressIndeterminate); + mExtras = new Bundle(); + if (extras != null) { + mExtras.putAll(extras); + } + if (localOnly) { + mExtras.putBoolean(EXTRA_LOCAL_ONLY, true); + } + if (groupKey != null) { + mExtras.putString(EXTRA_GROUP_KEY, groupKey); + if (groupSummary) { + mExtras.putBoolean(EXTRA_GROUP_SUMMARY, true); + } else { + mExtras.putBoolean(EXTRA_USE_SIDE_CHANNEL, true); + } + } + if (sortKey != null) { + mExtras.putString(EXTRA_SORT_KEY, sortKey); + } + } + + @Override + public void addAction(NotificationCompatBase.Action action) { + mActionExtrasList.add(writeActionAndGetExtras(b, action)); + } + + @Override + public Notification.Builder getBuilder() { + return b; + } + + public Notification build() { + Notification notif = b.build(); + // Merge in developer provided extras, but let the values already set + // for keys take precedence. + Bundle extras = getExtras(notif); + Bundle mergeBundle = new Bundle(mExtras); + for (String key : mExtras.keySet()) { + if (extras.containsKey(key)) { + mergeBundle.remove(key); + } + } + extras.putAll(mergeBundle); + SparseArray actionExtrasMap = buildActionExtrasMap(mActionExtrasList); + if (actionExtrasMap != null) { + // Add the action extras sparse array if any action was added with extras. + getExtras(notif).putSparseParcelableArray(EXTRA_ACTION_EXTRAS, actionExtrasMap); + } + return notif; + } + } + + public static void addBigTextStyle(NotificationBuilderWithBuilderAccessor b, + CharSequence bigContentTitle, boolean useSummary, + CharSequence summaryText, CharSequence bigText) { + Notification.BigTextStyle style = new Notification.BigTextStyle(b.getBuilder()) + .setBigContentTitle(bigContentTitle) + .bigText(bigText); + if (useSummary) { + style.setSummaryText(summaryText); + } + } + + public static void addBigPictureStyle(NotificationBuilderWithBuilderAccessor b, + CharSequence bigContentTitle, boolean useSummary, + CharSequence summaryText, Bitmap bigPicture, Bitmap bigLargeIcon, + boolean bigLargeIconSet) { + Notification.BigPictureStyle style = new Notification.BigPictureStyle(b.getBuilder()) + .setBigContentTitle(bigContentTitle) + .bigPicture(bigPicture); + if (bigLargeIconSet) { + style.bigLargeIcon(bigLargeIcon); + } + if (useSummary) { + style.setSummaryText(summaryText); + } + } + + public static void addInboxStyle(NotificationBuilderWithBuilderAccessor b, + CharSequence bigContentTitle, boolean useSummary, + CharSequence summaryText, ArrayList texts) { + Notification.InboxStyle style = new Notification.InboxStyle(b.getBuilder()) + .setBigContentTitle(bigContentTitle); + if (useSummary) { + style.setSummaryText(summaryText); + } + for (CharSequence text: texts) { + style.addLine(text); + } + } + + /** Return an SparseArray for action extras or null if none was needed. */ + public static SparseArray buildActionExtrasMap(List actionExtrasList) { + SparseArray actionExtrasMap = null; + for (int i = 0, count = actionExtrasList.size(); i < count; i++) { + Bundle actionExtras = actionExtrasList.get(i); + if (actionExtras != null) { + if (actionExtrasMap == null) { + actionExtrasMap = new SparseArray(); + } + actionExtrasMap.put(i, actionExtras); + } + } + return actionExtrasMap; + } + + /** + * Get the extras Bundle from a notification using reflection. Extras were present in + * Jellybean notifications, but the field was private until KitKat. + */ + public static Bundle getExtras(Notification notif) { + synchronized (sExtrasLock) { + if (sExtrasFieldAccessFailed) { + return null; + } + try { + if (sExtrasField == null) { + Field extrasField = Notification.class.getDeclaredField("extras"); + if (!Bundle.class.isAssignableFrom(extrasField.getType())) { + Log.e(TAG, "Notification.extras field is not of type Bundle"); + sExtrasFieldAccessFailed = true; + return null; + } + extrasField.setAccessible(true); + sExtrasField = extrasField; + } + Bundle extras = (Bundle) sExtrasField.get(notif); + if (extras == null) { + extras = new Bundle(); + sExtrasField.set(notif, extras); + } + return extras; + } catch (IllegalAccessException e) { + Log.e(TAG, "Unable to access notification extras", e); + } catch (NoSuchFieldException e) { + Log.e(TAG, "Unable to access notification extras", e); + } + sExtrasFieldAccessFailed = true; + return null; + } + } + + public static NotificationCompatBase.Action readAction( + NotificationCompatBase.Action.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory, int icon, + CharSequence title, PendingIntent actionIntent, Bundle extras) { + RemoteInputCompatBase.RemoteInput[] remoteInputs = null; + if (extras != null) { + remoteInputs = RemoteInputCompatJellybean.fromBundleArray( + BundleUtil.getBundleArrayFromBundle(extras, EXTRA_REMOTE_INPUTS), + remoteInputFactory); + } + return factory.build(icon, title, actionIntent, extras, remoteInputs); + } + + public static Bundle writeActionAndGetExtras( + Notification.Builder builder, NotificationCompatBase.Action action) { + builder.addAction(action.getIcon(), action.getTitle(), action.getActionIntent()); + Bundle actionExtras = new Bundle(action.getExtras()); + if (action.getRemoteInputs() != null) { + actionExtras.putParcelableArray(EXTRA_REMOTE_INPUTS, + RemoteInputCompatJellybean.toBundleArray(action.getRemoteInputs())); + } + return actionExtras; + } + + public static int getActionCount(Notification notif) { + synchronized (sActionsLock) { + Object[] actionObjects = getActionObjectsLocked(notif); + return actionObjects != null ? actionObjects.length : 0; + } + } + + public static NotificationCompatBase.Action getAction(Notification notif, int actionIndex, + NotificationCompatBase.Action.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + synchronized (sActionsLock) { + try { + Object actionObject = getActionObjectsLocked(notif)[actionIndex]; + Bundle actionExtras = null; + Bundle extras = getExtras(notif); + if (extras != null) { + SparseArray actionExtrasMap = extras.getSparseParcelableArray( + EXTRA_ACTION_EXTRAS); + if (actionExtrasMap != null) { + actionExtras = actionExtrasMap.get(actionIndex); + } + } + return readAction(factory, remoteInputFactory, + sActionIconField.getInt(actionObject), + (CharSequence) sActionTitleField.get(actionObject), + (PendingIntent) sActionIntentField.get(actionObject), + actionExtras); + } catch (IllegalAccessException e) { + Log.e(TAG, "Unable to access notification actions", e); + sActionsAccessFailed = true; + } + } + return null; + } + + private static Object[] getActionObjectsLocked(Notification notif) { + synchronized (sActionsLock) { + if (!ensureActionReflectionReadyLocked()) { + return null; + } + try { + return (Object[]) sActionsField.get(notif); + } catch (IllegalAccessException e) { + Log.e(TAG, "Unable to access notification actions", e); + sActionsAccessFailed = true; + return null; + } + } + } + + private static boolean ensureActionReflectionReadyLocked() { + if (sActionsAccessFailed) { + return false; + } + try { + if (sActionsField == null) { + sActionClass = Class.forName("android.app.Notification$Action"); + sActionIconField = sActionClass.getDeclaredField("icon"); + sActionTitleField = sActionClass.getDeclaredField("title"); + sActionIntentField = sActionClass.getDeclaredField("actionIntent"); + sActionsField = Notification.class.getDeclaredField("actions"); + sActionsField.setAccessible(true); + } + } catch (ClassNotFoundException e) { + Log.e(TAG, "Unable to access notification actions", e); + sActionsAccessFailed = true; + } catch (NoSuchFieldException e) { + Log.e(TAG, "Unable to access notification actions", e); + sActionsAccessFailed = true; + } + return !sActionsAccessFailed; + } + + public static NotificationCompatBase.Action[] getActionsFromParcelableArrayList( + ArrayList parcelables, + NotificationCompatBase.Action.Factory actionFactory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + if (parcelables == null) { + return null; + } + NotificationCompatBase.Action[] actions = actionFactory.newArray(parcelables.size()); + for (int i = 0; i < actions.length; i++) { + actions[i] = getActionFromBundle((Bundle) parcelables.get(i), + actionFactory, remoteInputFactory); + } + return actions; + } + + private static NotificationCompatBase.Action getActionFromBundle(Bundle bundle, + NotificationCompatBase.Action.Factory actionFactory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + return actionFactory.build( + bundle.getInt(KEY_ICON), + bundle.getCharSequence(KEY_TITLE), + bundle.getParcelable(KEY_ACTION_INTENT), + bundle.getBundle(KEY_EXTRAS), + RemoteInputCompatJellybean.fromBundleArray( + BundleUtil.getBundleArrayFromBundle(bundle, KEY_REMOTE_INPUTS), + remoteInputFactory)); + } + + public static ArrayList getParcelableArrayListForActions( + NotificationCompatBase.Action[] actions) { + if (actions == null) { + return null; + } + ArrayList parcelables = new ArrayList(actions.length); + for (NotificationCompatBase.Action action : actions) { + parcelables.add(getBundleForAction(action)); + } + return parcelables; + } + + private static Bundle getBundleForAction(NotificationCompatBase.Action action) { + Bundle bundle = new Bundle(); + bundle.putInt(KEY_ICON, action.getIcon()); + bundle.putCharSequence(KEY_TITLE, action.getTitle()); + bundle.putParcelable(KEY_ACTION_INTENT, action.getActionIntent()); + bundle.putBundle(KEY_EXTRAS, action.getExtras()); + bundle.putParcelableArray(KEY_REMOTE_INPUTS, RemoteInputCompatJellybean.toBundleArray( + action.getRemoteInputs())); + return bundle; + } + + public static boolean getLocalOnly(Notification notif) { + return getExtras(notif).getBoolean(EXTRA_LOCAL_ONLY); + } + + public static String getGroup(Notification n) { + return getExtras(n).getString(EXTRA_GROUP_KEY); + } + + public static boolean isGroupSummary(Notification n) { + return getExtras(n).getBoolean(EXTRA_GROUP_SUMMARY); + } + + public static String getSortKey(Notification n) { + return getExtras(n).getString(EXTRA_SORT_KEY); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatKitKat.java new file mode 100644 index 00000000000..e2fd3b70317 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatKitKat.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.util.SparseArray; +import android.widget.RemoteViews; + +import java.util.ArrayList; +import java.util.List; + +class NotificationCompatKitKat { + public static class Builder implements NotificationBuilderWithBuilderAccessor, + NotificationBuilderWithActions { + private Notification.Builder b; + private Bundle mExtras; + private List mActionExtrasList = new ArrayList(); + + public Builder(Context context, Notification n, + CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo, + RemoteViews tickerView, int number, + PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon, + int progressMax, int progress, boolean progressIndeterminate, boolean showWhen, + boolean useChronometer, int priority, CharSequence subText, boolean localOnly, + ArrayList people, Bundle extras, String groupKey, boolean groupSummary, + String sortKey) { + b = new Notification.Builder(context) + .setWhen(n.when) + .setShowWhen(showWhen) + .setSmallIcon(n.icon, n.iconLevel) + .setContent(n.contentView) + .setTicker(n.tickerText, tickerView) + .setSound(n.sound, n.audioStreamType) + .setVibrate(n.vibrate) + .setLights(n.ledARGB, n.ledOnMS, n.ledOffMS) + .setOngoing((n.flags & Notification.FLAG_ONGOING_EVENT) != 0) + .setOnlyAlertOnce((n.flags & Notification.FLAG_ONLY_ALERT_ONCE) != 0) + .setAutoCancel((n.flags & Notification.FLAG_AUTO_CANCEL) != 0) + .setDefaults(n.defaults) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setSubText(subText) + .setContentInfo(contentInfo) + .setContentIntent(contentIntent) + .setDeleteIntent(n.deleteIntent) + .setFullScreenIntent(fullScreenIntent, + (n.flags & Notification.FLAG_HIGH_PRIORITY) != 0) + .setLargeIcon(largeIcon) + .setNumber(number) + .setUsesChronometer(useChronometer) + .setPriority(priority) + .setProgress(progressMax, progress, progressIndeterminate); + mExtras = new Bundle(); + if (extras != null) { + mExtras.putAll(extras); + } + if (people != null && !people.isEmpty()) { + mExtras.putStringArray(Notification.EXTRA_PEOPLE, + people.toArray(new String[people.size()])); + } + if (localOnly) { + mExtras.putBoolean(NotificationCompatJellybean.EXTRA_LOCAL_ONLY, true); + } + if (groupKey != null) { + mExtras.putString(NotificationCompatJellybean.EXTRA_GROUP_KEY, groupKey); + if (groupSummary) { + mExtras.putBoolean(NotificationCompatJellybean.EXTRA_GROUP_SUMMARY, true); + } else { + mExtras.putBoolean(NotificationCompatJellybean.EXTRA_USE_SIDE_CHANNEL, true); + } + } + if (sortKey != null) { + mExtras.putString(NotificationCompatJellybean.EXTRA_SORT_KEY, sortKey); + } + } + + @Override + public void addAction(NotificationCompatBase.Action action) { + mActionExtrasList.add(NotificationCompatJellybean.writeActionAndGetExtras(b, action)); + } + + @Override + public Notification.Builder getBuilder() { + return b; + } + + @Override + public Notification build() { + SparseArray actionExtrasMap = NotificationCompatJellybean.buildActionExtrasMap( + mActionExtrasList); + if (actionExtrasMap != null) { + // Add the action extras sparse array if any action was added with extras. + mExtras.putSparseParcelableArray( + NotificationCompatJellybean.EXTRA_ACTION_EXTRAS, actionExtrasMap); + } + b.setExtras(mExtras); + return b.build(); + } + } + + public static Bundle getExtras(Notification notif) { + return notif.extras; + } + + public static int getActionCount(Notification notif) { + return notif.actions != null ? notif.actions.length : 0; + } + + public static NotificationCompatBase.Action getAction(Notification notif, + int actionIndex, NotificationCompatBase.Action.Factory factory, + RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) { + Notification.Action action = notif.actions[actionIndex]; + Bundle actionExtras = null; + SparseArray actionExtrasMap = notif.extras.getSparseParcelableArray( + NotificationCompatJellybean.EXTRA_ACTION_EXTRAS); + if (actionExtrasMap != null) { + actionExtras = actionExtrasMap.get(actionIndex); + } + return NotificationCompatJellybean.readAction(factory, remoteInputFactory, + action.icon, action.title, action.actionIntent, actionExtras); + } + + public static boolean getLocalOnly(Notification notif) { + return notif.extras.getBoolean(NotificationCompatJellybean.EXTRA_LOCAL_ONLY); + } + + public static String getGroup(Notification notif) { + return notif.extras.getString(NotificationCompatJellybean.EXTRA_GROUP_KEY); + } + + public static boolean isGroupSummary(Notification notif) { + return notif.extras.getBoolean(NotificationCompatJellybean.EXTRA_GROUP_SUMMARY); + } + + public static String getSortKey(Notification notif) { + return notif.extras.getString(NotificationCompatJellybean.EXTRA_SORT_KEY); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatSideChannelService.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatSideChannelService.java new file mode 100644 index 00000000000..ee4c1e69b03 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationCompatSideChannelService.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.Service; +import android.content.Intent; +import android.os.Build; +import android.os.IBinder; +import android.os.RemoteException; + +/** + * Abstract service to receive side channel notifications sent from + * {@link android.support.v4.app.NotificationManagerCompat}. + * + *

To receive side channel notifications, extend this service and register it in your + * android manifest with an intent filter for the BIND_NOTIFICATION_SIDE_CHANNEL action. + * Note: you must also have an enabled + * {@link android.service.notification.NotificationListenerService} within your package. + * + *

Example AndroidManifest.xml addition: + *

+ * <service android:name="com.example.NotificationSideChannelService">
+ *     <intent-filter>
+ *         <action android:name="android.support.BIND_NOTIFICATION_SIDE_CHANNEL" />
+ *     </intent-filter>
+ * </service>
+ * + */ +public abstract class NotificationCompatSideChannelService extends Service { + @Override + public IBinder onBind(Intent intent) { + if (intent.getAction().equals(NotificationManagerCompat.ACTION_BIND_SIDE_CHANNEL)) { + // Block side channel service connections if the current sdk has no need for + // side channeling. + if (Build.VERSION.SDK_INT > NotificationManagerCompat.MAX_SIDE_CHANNEL_SDK_VERSION) { + return null; + } + return new NotificationSideChannelStub(); + } + return null; + } + + /** + * Handle a side-channeled notification being posted. + */ + public abstract void notify(String packageName, int id, String tag, Notification notification); + + /** + * Handle a side-channelled notification being cancelled. + */ + public abstract void cancel(String packageName, int id, String tag); + + /** + * Handle the side-channelled cancelling of all notifications for a package. + */ + public abstract void cancelAll(String packageName); + + private class NotificationSideChannelStub extends INotificationSideChannel.Stub { + @Override + public void notify(String packageName, int id, String tag, Notification notification) + throws RemoteException { + checkPermission(getCallingUid(), packageName); + long idToken = clearCallingIdentity(); + try { + NotificationCompatSideChannelService.this.notify(packageName, id, tag, notification); + } finally { + restoreCallingIdentity(idToken); + } + } + + @Override + public void cancel(String packageName, int id, String tag) throws RemoteException { + checkPermission(getCallingUid(), packageName); + long idToken = clearCallingIdentity(); + try { + NotificationCompatSideChannelService.this.cancel(packageName, id, tag); + } finally { + restoreCallingIdentity(idToken); + } + } + + @Override + public void cancelAll(String packageName) { + checkPermission(getCallingUid(), packageName); + long idToken = clearCallingIdentity(); + try { + NotificationCompatSideChannelService.this.cancelAll(packageName); + } finally { + restoreCallingIdentity(idToken); + } + } + } + + private void checkPermission(int callingUid, String packageName) { + for (String validPackage : getPackageManager().getPackagesForUid(callingUid)) { + if (validPackage.equals(packageName)) { + return; + } + } + throw new SecurityException("NotificationSideChannelService: Uid " + callingUid + + " is not authorized for package " + packageName); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompat.java new file mode 100644 index 00000000000..a8480071871 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompat.java @@ -0,0 +1,628 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.os.Build; +import android.os.Bundle; +import android.os.DeadObjectException; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Message; +import android.os.RemoteException; +import android.provider.Settings; +import android.util.Log; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Compatibility library for NotificationManager with fallbacks for older platforms. + * + *

To use this class, call the static function {@link #from} to get a + * {@link NotificationManagerCompat} object, and then call one of its + * methods to post or cancel notifications. + */ +public class NotificationManagerCompat { + private static final String TAG = "NotifManCompat"; + + /** + * Notification extras key: if set to true, the posted notification should use + * the side channel for delivery instead of using notification manager. + */ + public static final String EXTRA_USE_SIDE_CHANNEL = + NotificationCompatJellybean.EXTRA_USE_SIDE_CHANNEL; + + /** + * Intent action to register for on a service to receive side channel + * notifications. The listening service must be in the same package as an enabled + * {@link android.service.notification.NotificationListenerService}. + */ + public static final String ACTION_BIND_SIDE_CHANNEL = + "android.support.BIND_NOTIFICATION_SIDE_CHANNEL"; + + /** + * Maximum sdk build version which needs support for side channeled notifications. + * Currently the only needed use is for side channeling group children before KITKAT_WATCH. + */ + static final int MAX_SIDE_CHANNEL_SDK_VERSION = 19; + + /** Base time delay for a side channel listener queue retry. */ + private static final int SIDE_CHANNEL_RETRY_BASE_INTERVAL_MS = 1000; + /** Maximum retries for a side channel listener before dropping tasks. */ + private static final int SIDE_CHANNEL_RETRY_MAX_COUNT = 6; + /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */ + private static final String SETTING_ENABLED_NOTIFICATION_LISTENERS = + "enabled_notification_listeners"; + private static final int SIDE_CHANNEL_BIND_FLAGS; + + /** Cache of enabled notification listener components */ + private static final Object sEnabledNotificationListenersLock = new Object(); + /** Guarded by {@link #sEnabledNotificationListenersLock} */ + private static String sEnabledNotificationListeners; + /** Guarded by {@link #sEnabledNotificationListenersLock} */ + private static Set sEnabledNotificationListenerPackages = new HashSet(); + + private final Context mContext; + private final NotificationManager mNotificationManager; + /** Lock for mutable static fields */ + private static final Object sLock = new Object(); + /** Guarded by {@link #sLock} */ + private static SideChannelManager sSideChannelManager; + + /** Get a {@link NotificationManagerCompat} instance for a provided context. */ + public static NotificationManagerCompat from(Context context) { + return new NotificationManagerCompat(context); + } + + private NotificationManagerCompat(Context context) { + mContext = context; + mNotificationManager = (NotificationManager) mContext.getSystemService( + Context.NOTIFICATION_SERVICE); + } + + private static final Impl IMPL; + + interface Impl { + void cancelNotification(NotificationManager notificationManager, String tag, int id); + + void postNotification(NotificationManager notificationManager, String tag, int id, + Notification notification); + + int getSideChannelBindFlags(); + } + + static class ImplBase implements Impl { + @Override + public void cancelNotification(NotificationManager notificationManager, String tag, + int id) { + notificationManager.cancel(id); + } + + @Override + public void postNotification(NotificationManager notificationManager, String tag, int id, + Notification notification) { + notificationManager.notify(id, notification); + } + + @Override + public int getSideChannelBindFlags() { + return Service.BIND_AUTO_CREATE; + } + } + + static class ImplEclair extends ImplBase { + @Override + public void cancelNotification(NotificationManager notificationManager, String tag, + int id) { + NotificationManagerCompatEclair.cancelNotification(notificationManager, tag, id); + } + + @Override + public void postNotification(NotificationManager notificationManager, String tag, int id, + Notification notification) { + NotificationManagerCompatEclair.postNotification(notificationManager, tag, id, + notification); + } + } + + static class ImplIceCreamSandwich extends ImplEclair { + @Override + public int getSideChannelBindFlags() { + return NotificationManagerCompatIceCreamSandwich.SIDE_CHANNEL_BIND_FLAGS; + } + } + + static { + if (Build.VERSION.SDK_INT >= 14) { + IMPL = new ImplIceCreamSandwich(); + } else if (Build.VERSION.SDK_INT >= 5) { + IMPL = new ImplEclair(); + } else { + IMPL = new ImplBase(); + } + SIDE_CHANNEL_BIND_FLAGS = IMPL.getSideChannelBindFlags(); + } + + /** + * Cancel a previously shown notification. + * @param id the ID of the notification + */ + public void cancel(int id) { + cancel(null, id); + } + + /** + * Cancel a previously shown notification. + * @param tag the string identifier of the notification. + * @param id the ID of the notification + */ + public void cancel(String tag, int id) { + IMPL.cancelNotification(mNotificationManager, tag, id); + if (Build.VERSION.SDK_INT <= MAX_SIDE_CHANNEL_SDK_VERSION) { + pushSideChannelQueue(new CancelTask(mContext.getPackageName(), id, tag)); + } + } + + /** Cancel all previously shown notifications. */ + public void cancelAll() { + mNotificationManager.cancelAll(); + if (Build.VERSION.SDK_INT <= MAX_SIDE_CHANNEL_SDK_VERSION) { + pushSideChannelQueue(new CancelTask(mContext.getPackageName())); + } + } + + /** + * Post a notification to be shown in the status bar, stream, etc. + * @param id the ID of the notification + * @param notification the notification to post to the system + */ + public void notify(int id, Notification notification) { + notify(null, id, notification); + } + + /** + * Post a notification to be shown in the status bar, stream, etc. + * @param tag the string identifier for a notification. Can be {@code null}. + * @param id the ID of the notification. The pair (tag, id) must be unique within your app. + * @param notification the notification to post to the system + */ + public void notify(String tag, int id, Notification notification) { + if (useSideChannelForNotification(notification)) { + pushSideChannelQueue(new NotifyTask(mContext.getPackageName(), id, tag, notification)); + // Cancel this notification in notification manager if it just transitioned to being + // side channelled. + IMPL.cancelNotification(mNotificationManager, tag, id); + } else { + IMPL.postNotification(mNotificationManager, tag, id, notification); + } + } + + /** + * Get the set of packages that have an enabled notification listener component within them. + */ + public static Set getEnabledListenerPackages(Context context) { + final String enabledNotificationListeners = Settings.Secure.getString( + context.getContentResolver(), + SETTING_ENABLED_NOTIFICATION_LISTENERS); + // Parse the string again if it is different from the last time this method was called. + if (enabledNotificationListeners != null + && !enabledNotificationListeners.equals(sEnabledNotificationListeners)) { + final String[] components = enabledNotificationListeners.split(":"); + Set packageNames = new HashSet(components.length); + for (String component : components) { + ComponentName componentName = ComponentName.unflattenFromString(component); + if (componentName != null) { + packageNames.add(componentName.getPackageName()); + } + } + synchronized (sEnabledNotificationListenersLock) { + sEnabledNotificationListenerPackages = packageNames; + sEnabledNotificationListeners = enabledNotificationListeners; + } + } + return sEnabledNotificationListenerPackages; + } + + /** + * Returns true if this notification should use the side channel for delivery. + */ + private static boolean useSideChannelForNotification(Notification notification) { + Bundle extras = NotificationCompat.getExtras(notification); + return extras != null && extras.getBoolean(EXTRA_USE_SIDE_CHANNEL); + } + + /** + * Push a notification task for distribution to notification side channels. + */ + private void pushSideChannelQueue(Task task) { + synchronized (sLock) { + if (sSideChannelManager == null) { + sSideChannelManager = new SideChannelManager(mContext.getApplicationContext()); + } + } + sSideChannelManager.queueTask(task); + } + + /** + * Helper class to manage a queue of pending tasks to send to notification side channel + * listeners. + */ + private static class SideChannelManager implements Handler.Callback, ServiceConnection { + private static final int MSG_QUEUE_TASK = 0; + private static final int MSG_SERVICE_CONNECTED = 1; + private static final int MSG_SERVICE_DISCONNECTED = 2; + private static final int MSG_RETRY_LISTENER_QUEUE = 3; + + private static final String KEY_BINDER = "binder"; + + private final Context mContext; + private final HandlerThread mHandlerThread; + private final Handler mHandler; + private final Map mRecordMap = + new HashMap(); + private Set mCachedEnabledPackages = new HashSet(); + + public SideChannelManager(Context context) { + mContext = context; + mHandlerThread = new HandlerThread("NotificationManagerCompat"); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper(), this); + } + + /** + * Queue a new task to be sent to all listeners. This function can be called + * from any thread. + */ + public void queueTask(Task task) { + mHandler.obtainMessage(MSG_QUEUE_TASK, task).sendToTarget(); + } + + @Override + public boolean handleMessage(Message msg) { + switch (msg.what) { + case MSG_QUEUE_TASK: + handleQueueTask((Task) msg.obj); + return true; + case MSG_SERVICE_CONNECTED: + ServiceConnectedEvent event = (ServiceConnectedEvent) msg.obj; + handleServiceConnected(event.componentName, event.iBinder); + return true; + case MSG_SERVICE_DISCONNECTED: + handleServiceDisconnected((ComponentName) msg.obj); + return true; + case MSG_RETRY_LISTENER_QUEUE: + handleRetryListenerQueue((ComponentName) msg.obj); + return true; + } + return false; + } + + private void handleQueueTask(Task task) { + updateListenerMap(); + for (ListenerRecord record : mRecordMap.values()) { + record.taskQueue.add(task); + processListenerQueue(record); + } + } + + private void handleServiceConnected(ComponentName componentName, IBinder iBinder) { + ListenerRecord record = mRecordMap.get(componentName); + if (record != null) { + record.service = INotificationSideChannel.Stub.asInterface(iBinder); + record.retryCount = 0; + processListenerQueue(record); + } + } + + private void handleServiceDisconnected(ComponentName componentName) { + ListenerRecord record = mRecordMap.get(componentName); + if (record != null) { + ensureServiceUnbound(record); + } + } + + private void handleRetryListenerQueue(ComponentName componentName) { + ListenerRecord record = mRecordMap.get(componentName); + if (record != null) { + processListenerQueue(record); + } + } + + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Connected to service " + componentName); + } + mHandler.obtainMessage(MSG_SERVICE_CONNECTED, + new ServiceConnectedEvent(componentName, iBinder)) + .sendToTarget(); + } + + @Override + public void onServiceDisconnected(ComponentName componentName) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Disconnected from service " + componentName); + } + mHandler.obtainMessage(MSG_SERVICE_DISCONNECTED, componentName).sendToTarget(); + } + + /** + * Check the current list of enabled listener packages and update the records map + * accordingly. + */ + private void updateListenerMap() { + Set enabledPackages = getEnabledListenerPackages(mContext); + if (enabledPackages.equals(mCachedEnabledPackages)) { + // Short-circuit when the list of enabled packages has not changed. + return; + } + mCachedEnabledPackages = enabledPackages; + List resolveInfos = mContext.getPackageManager().queryIntentServices( + new Intent().setAction(ACTION_BIND_SIDE_CHANNEL), PackageManager.GET_SERVICES); + Set enabledComponents = new HashSet(); + for (ResolveInfo resolveInfo : resolveInfos) { + if (!enabledPackages.contains(resolveInfo.serviceInfo.packageName)) { + continue; + } + ComponentName componentName = new ComponentName( + resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name); + if (resolveInfo.serviceInfo.permission != null) { + Log.w(TAG, "Permission present on component " + componentName + + ", not adding listener record."); + continue; + } + enabledComponents.add(componentName); + } + // Ensure all enabled components have a record in the listener map. + for (ComponentName componentName : enabledComponents) { + if (!mRecordMap.containsKey(componentName)) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Adding listener record for " + componentName); + } + mRecordMap.put(componentName, new ListenerRecord(componentName)); + } + } + // Remove listener records that are no longer for enabled components. + Iterator> it = + mRecordMap.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = it.next(); + if (!enabledComponents.contains(entry.getKey())) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Removing listener record for " + entry.getKey()); + } + ensureServiceUnbound(entry.getValue()); + it.remove(); + } + } + } + + /** + * Ensure we are already attempting to bind to a service, or start a new binding if not. + * @return Whether the service bind attempt was successful. + */ + private boolean ensureServiceBound(ListenerRecord record) { + if (record.bound) { + return true; + } + Intent intent = new Intent(ACTION_BIND_SIDE_CHANNEL).setComponent(record.componentName); + record.bound = mContext.bindService(intent, this, SIDE_CHANNEL_BIND_FLAGS); + if (record.bound) { + record.retryCount = 0; + } else { + Log.w(TAG, "Unable to bind to listener " + record.componentName); + mContext.unbindService(this); + } + return record.bound; + } + + /** + * Ensure we have unbound from a service. + */ + private void ensureServiceUnbound(ListenerRecord record) { + if (record.bound) { + mContext.unbindService(this); + record.bound = false; + } + record.service = null; + } + + /** + * Schedule a delayed retry to communicate with a listener service. + * After a maximum number of attempts (with exponential back-off), start + * dropping pending tasks for this listener. + */ + private void scheduleListenerRetry(ListenerRecord record) { + if (mHandler.hasMessages(MSG_RETRY_LISTENER_QUEUE, record.componentName)) { + return; + } + record.retryCount++; + if (record.retryCount > SIDE_CHANNEL_RETRY_MAX_COUNT) { + Log.w(TAG, "Giving up on delivering " + record.taskQueue.size() + " tasks to " + + record.componentName + " after " + record.retryCount + " retries"); + record.taskQueue.clear(); + return; + } + int delayMs = SIDE_CHANNEL_RETRY_BASE_INTERVAL_MS * (1 << (record.retryCount - 1)); + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Scheduling retry for " + delayMs + " ms"); + } + Message msg = mHandler.obtainMessage(MSG_RETRY_LISTENER_QUEUE, record.componentName); + mHandler.sendMessageDelayed(msg, delayMs); + } + + /** + * Perform a processing step for a listener. First check the bind state, then attempt + * to flush the task queue, and if an error is encountered, schedule a retry. + */ + private void processListenerQueue(ListenerRecord record) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Processing component " + record.componentName + ", " + + record.taskQueue.size() + " queued tasks"); + } + if (record.taskQueue.isEmpty()) { + return; + } + if (!ensureServiceBound(record) || record.service == null) { + // Ensure bind has started and that a service interface is ready to use. + scheduleListenerRetry(record); + return; + } + // Attempt to flush all items in the task queue. + while (true) { + Task task = record.taskQueue.peek(); + if (task == null) { + break; + } + try { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Sending task " + task); + } + task.send(record.service); + record.taskQueue.remove(); + } catch (DeadObjectException e) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + Log.d(TAG, "Remote service has died: " + record.componentName); + } + break; + } catch (RemoteException e) { + Log.w(TAG, "RemoteException communicating with " + record.componentName, e); + break; + } + } + if (!record.taskQueue.isEmpty()) { + // Some tasks were not sent, meaning an error was encountered, schedule a retry. + scheduleListenerRetry(record); + } + } + + /** A per-side-channel-service listener state record */ + private static class ListenerRecord { + public final ComponentName componentName; + /** Whether the service is currently bound to. */ + public boolean bound = false; + /** The service stub provided by onServiceConnected */ + public INotificationSideChannel service; + /** Queue of pending tasks to send to this listener service */ + public LinkedList taskQueue = new LinkedList(); + /** Number of retries attempted while connecting to this listener service */ + public int retryCount = 0; + + public ListenerRecord(ComponentName componentName) { + this.componentName = componentName; + } + } + } + + private static class ServiceConnectedEvent { + final ComponentName componentName; + final IBinder iBinder; + + public ServiceConnectedEvent(ComponentName componentName, + final IBinder iBinder) { + this.componentName = componentName; + this.iBinder = iBinder; + } + } + + private interface Task { + public void send(INotificationSideChannel service) throws RemoteException; + } + + private static class NotifyTask implements Task { + final String packageName; + final int id; + final String tag; + final Notification notif; + + public NotifyTask(String packageName, int id, String tag, Notification notif) { + this.packageName = packageName; + this.id = id; + this.tag = tag; + this.notif = notif; + } + + @Override + public void send(INotificationSideChannel service) throws RemoteException { + service.notify(packageName, id, tag, notif); + } + + public String toString() { + StringBuilder sb = new StringBuilder("NotifyTask["); + sb.append("packageName:").append(packageName); + sb.append(", id:").append(id); + sb.append(", tag:").append(tag); + sb.append("]"); + return sb.toString(); + } + } + + private static class CancelTask implements Task { + final String packageName; + final int id; + final String tag; + final boolean all; + + public CancelTask(String packageName) { + this.packageName = packageName; + this.id = 0; + this.tag = null; + this.all = true; + } + + public CancelTask(String packageName, int id, String tag) { + this.packageName = packageName; + this.id = id; + this.tag = tag; + this.all = false; + } + + @Override + public void send(INotificationSideChannel service) throws RemoteException { + if (all) { + service.cancelAll(packageName); + } else { + service.cancel(packageName, id, tag); + } + } + + public String toString() { + StringBuilder sb = new StringBuilder("CancelTask["); + sb.append("packageName:").append(packageName); + sb.append(", id:").append(id); + sb.append(", tag:").append(tag); + sb.append(", all:").append(all); + sb.append("]"); + return sb.toString(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatEclair.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatEclair.java new file mode 100644 index 00000000000..45d96e4b664 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatEclair.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Notification; +import android.app.NotificationManager; + +class NotificationManagerCompatEclair { + static void cancelNotification(NotificationManager notificationManager, String tag, + int id) { + notificationManager.cancel(tag, id); + } + + public static void postNotification(NotificationManager notificationManager, String tag, int id, + Notification notification) { + notificationManager.notify(tag, id, notification); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatIceCreamSandwich.java b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatIceCreamSandwich.java new file mode 100644 index 00000000000..f08817920ce --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/NotificationManagerCompatIceCreamSandwich.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Service; + +class NotificationManagerCompatIceCreamSandwich { + static final int SIDE_CHANNEL_BIND_FLAGS = Service.BIND_AUTO_CREATE + | Service.BIND_WAIVE_PRIORITY; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInput.java b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInput.java new file mode 100644 index 00000000000..38ecad092dd --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInput.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.content.Intent; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; + +/** + * Helper for using the {@link android.app.RemoteInput} API + * introduced after API level 4 in a backwards compatible fashion. + */ +public class RemoteInput extends RemoteInputCompatBase.RemoteInput { + private static final String TAG = "RemoteInput"; + + /** Label used to denote the clip data type used for remote input transport */ + public static final String RESULTS_CLIP_LABEL = RemoteInputCompatJellybean.RESULTS_CLIP_LABEL; + + /** Extra added to a clip data intent object to hold the results bundle. */ + public static final String EXTRA_RESULTS_DATA = RemoteInputCompatJellybean.EXTRA_RESULTS_DATA; + + private final String mResultKey; + private final CharSequence mLabel; + private final CharSequence[] mChoices; + private final boolean mAllowFreeFormInput; + private final Bundle mExtras; + + RemoteInput(String resultKey, CharSequence label, CharSequence[] choices, + boolean allowFreeFormInput, Bundle extras) { + this.mResultKey = resultKey; + this.mLabel = label; + this.mChoices = choices; + this.mAllowFreeFormInput = allowFreeFormInput; + this.mExtras = extras; + } + + /** + * Get the key that the result of this input will be set in from the Bundle returned by + * {@link #getResultsFromIntent} when the {@link android.app.PendingIntent} is sent. + */ + public String getResultKey() { + return mResultKey; + } + + /** + * Get the label to display to users when collecting this input. + */ + public CharSequence getLabel() { + return mLabel; + } + + /** + * Get possible input choices. This can be {@code null} if there are no choices to present. + */ + public CharSequence[] getChoices() { + return mChoices; + } + + /** + * Get whether or not users can provide an arbitrary value for + * input. If you set this to {@code false}, users must select one of the + * choices in {@link #getChoices}. An {@link IllegalArgumentException} is thrown + * if you set this to false and {@link #getChoices} returns {@code null} or empty. + */ + public boolean getAllowFreeFormInput() { + return mAllowFreeFormInput; + } + + /** + * Get additional metadata carried around with this remote input. + */ + public Bundle getExtras() { + return mExtras; + } + + /** + * Builder class for {@link android.support.v4.app.RemoteInput} objects. + */ + public static final class Builder { + private final String mResultKey; + private CharSequence mLabel; + private CharSequence[] mChoices; + private boolean mAllowFreeFormInput = true; + private Bundle mExtras = new Bundle(); + + /** + * Create a builder object for {@link android.support.v4.app.RemoteInput} objects. + * @param resultKey the Bundle key that refers to this input when collected from the user + */ + public Builder(String resultKey) { + if (resultKey == null) { + throw new IllegalArgumentException("Result key can't be null"); + } + mResultKey = resultKey; + } + + /** + * Set a label to be displayed to the user when collecting this input. + * @param label The label to show to users when they input a response. + * @return this object for method chaining + */ + public Builder setLabel(CharSequence label) { + mLabel = label; + return this; + } + + /** + * Specifies choices available to the user to satisfy this input. + * @param choices an array of pre-defined choices for users input. + * You must provide a non-null and non-empty array if + * you disabled free form input using {@link #setAllowFreeFormInput}. + * @return this object for method chaining + */ + public Builder setChoices(CharSequence[] choices) { + mChoices = choices; + return this; + } + + /** + * Specifies whether the user can provide arbitrary values. + * + * @param allowFreeFormInput The default is {@code true}. + * If you specify {@code false}, you must provide a non-null + * and non-empty array to {@link #setChoices} or an + * {@link IllegalArgumentException} is thrown. + * @return this object for method chaining + */ + public Builder setAllowFreeFormInput(boolean allowFreeFormInput) { + mAllowFreeFormInput = allowFreeFormInput; + return this; + } + + /** + * Merge additional metadata into this builder. + * + *

Values within the Bundle will replace existing extras values in this Builder. + * + * @see RemoteInput#getExtras + */ + public Builder addExtras(Bundle extras) { + if (extras != null) { + mExtras.putAll(extras); + } + return this; + } + + /** + * Get the metadata Bundle used by this Builder. + * + *

The returned Bundle is shared with this Builder. + */ + public Bundle getExtras() { + return mExtras; + } + + /** + * Combine all of the options that have been set and return a new + * {@link android.support.v4.app.RemoteInput} object. + */ + public RemoteInput build() { + return new RemoteInput(mResultKey, mLabel, mChoices, mAllowFreeFormInput, mExtras); + } + } + + /** + * Get the remote input results bundle from an intent. The returned Bundle will + * contain a key/value for every result key populated by remote input collector. + * Use the {@link Bundle#getCharSequence(String)} method to retrieve a value. + * @param intent The intent object that fired in response to an action or content intent + * which also had one or more remote input requested. + */ + public static Bundle getResultsFromIntent(Intent intent) { + return IMPL.getResultsFromIntent(intent); + } + + /** + * Populate an intent object with the results gathered from remote input. This method + * should only be called by remote input collection services when sending results to a + * pending intent. + * @param remoteInputs The remote inputs for which results are being provided + * @param intent The intent to add remote inputs to. The {@link android.content.ClipData} + * field of the intent will be modified to contain the results. + * @param results A bundle holding the remote input results. This bundle should + * be populated with keys matching the result keys specified in + * {@code remoteInputs} with values being the result per key. + */ + public static void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, + Bundle results) { + IMPL.addResultsToIntent(remoteInputs, intent, results); + } + + private static final Impl IMPL; + + interface Impl { + Bundle getResultsFromIntent(Intent intent); + void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, + Bundle results); + } + + static class ImplBase implements Impl { + @Override + public Bundle getResultsFromIntent(Intent intent) { + Log.w(TAG, "RemoteInput is only supported from API Level 16"); + return null; + } + + @Override + public void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results) { + Log.w(TAG, "RemoteInput is only supported from API Level 16"); + } + } + + static class ImplJellybean implements Impl { + @Override + public Bundle getResultsFromIntent(Intent intent) { + return RemoteInputCompatJellybean.getResultsFromIntent(intent); + } + + @Override + public void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results) { + RemoteInputCompatJellybean.addResultsToIntent(remoteInputs, intent, results); + } + } + + static class ImplApi20 implements Impl { + @Override + public Bundle getResultsFromIntent(Intent intent) { + return RemoteInputCompatApi20.getResultsFromIntent(intent); + } + + @Override + public void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results) { + RemoteInputCompatApi20.addResultsToIntent(remoteInputs, intent, results); + } + } + + static { + if (Build.VERSION.SDK_INT >= 20) { + IMPL = new ImplApi20(); + } else if (Build.VERSION.SDK_INT >= 16) { + IMPL = new ImplJellybean(); + } else { + IMPL = new ImplBase(); + } + } + + /** @hide */ + public static final Factory FACTORY = new Factory() { + @Override + public RemoteInput build(String resultKey, + CharSequence label, CharSequence[] choices, boolean allowFreeFormInput, + Bundle extras) { + return new RemoteInput(resultKey, label, choices, allowFreeFormInput, extras); + } + + @Override + public RemoteInput[] newArray(int size) { + return new RemoteInput[size]; + } + }; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatApi20.java b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatApi20.java new file mode 100644 index 00000000000..5f302f1fdb3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatApi20.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.RemoteInput; +import android.content.Intent; +import android.os.Bundle; + +class RemoteInputCompatApi20 { + static RemoteInputCompatBase.RemoteInput[] toCompat(RemoteInput[] srcArray, + RemoteInputCompatBase.RemoteInput.Factory factory) { + if (srcArray == null) { + return null; + } + RemoteInputCompatBase.RemoteInput[] result = factory.newArray(srcArray.length); + for (int i = 0; i < srcArray.length; i++) { + RemoteInput src = srcArray[i]; + result[i] = factory.build(src.getResultKey(), src.getLabel(), src.getChoices(), + src.getAllowFreeFormInput(), src.getExtras()); + } + return result; + } + + static RemoteInput[] fromCompat(RemoteInputCompatBase.RemoteInput[] srcArray) { + if (srcArray == null) { + return null; + } + RemoteInput[] result = new RemoteInput[srcArray.length]; + for (int i = 0; i < srcArray.length; i++) { + RemoteInputCompatBase.RemoteInput src = srcArray[i]; + result[i] = new RemoteInput.Builder(src.getResultKey()) + .setLabel(src.getLabel()) + .setChoices(src.getChoices()) + .setAllowFreeFormInput(src.getAllowFreeFormInput()) + .addExtras(src.getExtras()) + .build(); + } + return result; + } + + static Bundle getResultsFromIntent(Intent intent) { + return RemoteInput.getResultsFromIntent(intent); + } + + static void addResultsToIntent(RemoteInputCompatBase.RemoteInput[] remoteInputs, + Intent intent, Bundle results) { + RemoteInput.addResultsToIntent(fromCompat(remoteInputs), intent, results); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatBase.java b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatBase.java new file mode 100644 index 00000000000..24493366307 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatBase.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.os.Bundle; + +class RemoteInputCompatBase { + + public static abstract class RemoteInput { + protected abstract String getResultKey(); + protected abstract CharSequence getLabel(); + protected abstract CharSequence[] getChoices(); + protected abstract boolean getAllowFreeFormInput(); + protected abstract Bundle getExtras(); + + public interface Factory { + public RemoteInput build(String resultKey, CharSequence label, + CharSequence[] choices, boolean allowFreeFormInput, Bundle extras); + public RemoteInput[] newArray(int length); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatJellybean.java b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatJellybean.java new file mode 100644 index 00000000000..36f59819e1f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/RemoteInputCompatJellybean.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Intent; +import android.os.Bundle; + +class RemoteInputCompatJellybean { + /** Label used to denote the clip data type used for remote input transport */ + public static final String RESULTS_CLIP_LABEL = "android.remoteinput.results"; + + /** Extra added to a clip data intent object to hold the results bundle. */ + public static final String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData"; + + private static final String KEY_RESULT_KEY = "resultKey"; + private static final String KEY_LABEL = "label"; + private static final String KEY_CHOICES = "choices"; + private static final String KEY_ALLOW_FREE_FORM_INPUT = "allowFreeFormInput"; + private static final String KEY_EXTRAS = "extras"; + + static RemoteInputCompatBase.RemoteInput fromBundle(Bundle data, + RemoteInputCompatBase.RemoteInput.Factory factory) { + return factory.build(data.getString(KEY_RESULT_KEY), + data.getCharSequence(KEY_LABEL), + data.getCharSequenceArray(KEY_CHOICES), + data.getBoolean(KEY_ALLOW_FREE_FORM_INPUT), + data.getBundle(KEY_EXTRAS)); + } + + static Bundle toBundle(RemoteInputCompatBase.RemoteInput remoteInput) { + Bundle data = new Bundle(); + data.putString(KEY_RESULT_KEY, remoteInput.getResultKey()); + data.putCharSequence(KEY_LABEL, remoteInput.getLabel()); + data.putCharSequenceArray(KEY_CHOICES, remoteInput.getChoices()); + data.putBoolean(KEY_ALLOW_FREE_FORM_INPUT, remoteInput.getAllowFreeFormInput()); + data.putBundle(KEY_EXTRAS, remoteInput.getExtras()); + return data; + } + + static RemoteInputCompatBase.RemoteInput[] fromBundleArray(Bundle[] bundles, + RemoteInputCompatBase.RemoteInput.Factory factory) { + if (bundles == null) { + return null; + } + RemoteInputCompatBase.RemoteInput[] remoteInputs = factory.newArray(bundles.length); + for (int i = 0; i < bundles.length; i++) { + remoteInputs[i] = fromBundle(bundles[i], factory); + } + return remoteInputs; + } + + static Bundle[] toBundleArray(RemoteInputCompatBase.RemoteInput[] remoteInputs) { + if (remoteInputs == null) { + return null; + } + Bundle[] bundles = new Bundle[remoteInputs.length]; + for (int i = 0; i < remoteInputs.length; i++) { + bundles[i] = toBundle(remoteInputs[i]); + } + return bundles; + } + + static Bundle getResultsFromIntent(Intent intent) { + ClipData clipData = intent.getClipData(); + if (clipData == null) { + return null; + } + ClipDescription clipDescription = clipData.getDescription(); + if (!clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) { + return null; + } + if (clipDescription.getLabel().equals(RESULTS_CLIP_LABEL)) { + return clipData.getItemAt(0).getIntent().getExtras().getParcelable(EXTRA_RESULTS_DATA); + } + return null; + } + + static void addResultsToIntent(RemoteInputCompatBase.RemoteInput[] remoteInputs, Intent intent, + Bundle results) { + Bundle resultsBundle = new Bundle(); + for (RemoteInputCompatBase.RemoteInput remoteInput : remoteInputs) { + Object result = results.get(remoteInput.getResultKey()); + if (result instanceof CharSequence) { + resultsBundle.putCharSequence(remoteInput.getResultKey(), (CharSequence) result); + } + } + Intent clipIntent = new Intent(); + clipIntent.putExtra(EXTRA_RESULTS_DATA, resultsBundle); + intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipIntent)); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ServiceCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/ServiceCompat.java new file mode 100644 index 00000000000..f644501c49e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ServiceCompat.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +/** + * Helper for accessing features in {@link android.app.Service} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ServiceCompat { + + private ServiceCompat() { + /* Hide constructor */ + } + + /** + * Constant to return from {@link android.app.Service#onStartCommand}: if this + * service's process is killed while it is started (after returning from + * {@link android.app.Service#onStartCommand}), then leave it in the started + * state but don't retain this delivered intent. Later the system will try to + * re-create the service. Because it is in the started state, it will + * guarantee to call {@link android.app.Service#onStartCommand} after creating + * the new service instance; if there are not any pending start commands to be + * delivered to the service, it will be called with a null intent + * object, so you must take care to check for this. + * + *

This mode makes sense for things that will be explicitly started + * and stopped to run for arbitrary periods of time, such as a service + * performing background music playback. + */ + public static final int START_STICKY = 1; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompat.java b/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompat.java new file mode 100644 index 00000000000..87ebc492ef6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompat.java @@ -0,0 +1,957 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.support.annotation.StringRes; +import android.support.v4.content.IntentCompat; +import android.support.v4.view.MenuItemCompat; +import android.text.Html; +import android.text.Spanned; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; + +import java.util.ArrayList; + +/** + * Extra helper functionality for sharing data between activities. + * + * ShareCompat provides functionality to extend the {@link Intent#ACTION_SEND}/ + * {@link Intent#ACTION_SEND_MULTIPLE} protocol and support retrieving more info + * about the activity that invoked a social sharing action. + * + * {@link IntentBuilder} provides helper functions for constructing a sharing + * intent that always includes data about the calling activity and app. + * This lets the called activity provide attribution for the app that shared + * content. Constructing an intent this way can be done in a method-chaining style. + * To obtain an IntentBuilder with info about your calling activity, use the static + * method {@link IntentBuilder#from(Activity)}. + * + * {@link IntentReader} provides helper functions for parsing the defined extras + * within an {@link Intent#ACTION_SEND} or {@link Intent#ACTION_SEND_MULTIPLE} intent + * used to launch an activity. You can also obtain a Drawable for the caller's + * application icon and the application's localized label (the app's human-readable name). + * Social apps that enable sharing content are encouraged to use this information + * to call out the app that the content was shared from. + */ +public class ShareCompat { + /** + * Intent extra that stores the name of the calling package for an ACTION_SEND intent. + * When an activity is started using startActivityForResult this is redundant info. + * (It is also provided by {@link Activity#getCallingPackage()}.) + * + * Instead of using this constant directly, consider using {@link #getCallingPackage(Activity)} + * or {@link IntentReader#getCallingPackage()}. + */ + public static final String EXTRA_CALLING_PACKAGE = + "android.support.v4.app.EXTRA_CALLING_PACKAGE"; + + /** + * Intent extra that stores the {@link ComponentName} of the calling activity for + * an ACTION_SEND intent. + */ + public static final String EXTRA_CALLING_ACTIVITY = + "android.support.v4.app.EXTRA_CALLING_ACTIVITY"; + + /** + * Compatibility shims for sharing operations + */ + interface ShareCompatImpl { + void configureMenuItem(MenuItem item, IntentBuilder shareIntent); + String escapeHtml(CharSequence text); + } + + static class ShareCompatImplBase implements ShareCompatImpl { + public void configureMenuItem(MenuItem item, IntentBuilder shareIntent) { + item.setIntent(shareIntent.createChooserIntent()); + } + + public String escapeHtml(CharSequence text) { + StringBuilder out = new StringBuilder(); + withinStyle(out, text, 0, text.length()); + return out.toString(); + } + + private static void withinStyle(StringBuilder out, CharSequence text, + int start, int end) { + for (int i = start; i < end; i++) { + char c = text.charAt(i); + + if (c == '<') { + out.append("<"); + } else if (c == '>') { + out.append(">"); + } else if (c == '&') { + out.append("&"); + } else if (c > 0x7E || c < ' ') { + out.append("&#" + ((int) c) + ";"); + } else if (c == ' ') { + while (i + 1 < end && text.charAt(i + 1) == ' ') { + out.append(" "); + i++; + } + + out.append(' '); + } else { + out.append(c); + } + } + } + } + + static class ShareCompatImplICS extends ShareCompatImplBase { + public void configureMenuItem(MenuItem item, IntentBuilder shareIntent) { + ShareCompatICS.configureMenuItem(item, shareIntent.getActivity(), + shareIntent.getIntent()); + if (shouldAddChooserIntent(item)) { + item.setIntent(shareIntent.createChooserIntent()); + } + } + + boolean shouldAddChooserIntent(MenuItem item) { + return !item.hasSubMenu(); + } + } + + static class ShareCompatImplJB extends ShareCompatImplICS { + public String escapeHtml(CharSequence html) { + return ShareCompatJB.escapeHtml(html); + } + + @Override + boolean shouldAddChooserIntent(MenuItem item) { + return false; + } + } + + private static ShareCompatImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 16) { + IMPL = new ShareCompatImplJB(); + } else if (Build.VERSION.SDK_INT >= 14) { + IMPL = new ShareCompatImplICS(); + } else { + IMPL = new ShareCompatImplBase(); + } + } + + /** + * Retrieve the name of the package that launched calledActivity from a share intent. + * Apps that provide social sharing functionality can use this to provide attribution + * for the app that shared the content. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @param calledActivity Current activity that was launched to share content + * @return Name of the calling package + */ + public static String getCallingPackage(Activity calledActivity) { + String result = calledActivity.getCallingPackage(); + if (result == null) { + result = calledActivity.getIntent().getStringExtra(EXTRA_CALLING_PACKAGE); + } + return result; + } + + /** + * Retrieve the ComponentName of the activity that launched calledActivity from a share intent. + * Apps that provide social sharing functionality can use this to provide attribution + * for the app that shared the content. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @param calledActivity Current activity that was launched to share content + * @return ComponentName of the calling activity + */ + public static ComponentName getCallingActivity(Activity calledActivity) { + ComponentName result = calledActivity.getCallingActivity(); + if (result == null) { + result = calledActivity.getIntent().getParcelableExtra(EXTRA_CALLING_ACTIVITY); + } + return result; + } + + /** + * Configure a {@link MenuItem} to act as a sharing action. + * + *

If the app is running on API level 14 or higher (Android 4.0/Ice Cream Sandwich) + * this method will configure a ShareActionProvider to provide a more robust UI + * for selecting the target of the share. History will be tracked for each calling + * activity in a file named with the prefix ".sharecompat_" in the application's + * private data directory. If the application wishes to set this MenuItem to show + * as an action in the Action Bar it should use + * {@link MenuItemCompat#setShowAsAction(MenuItem, int)} to request that behavior + * in addition to calling this method.

+ * + *

If the app is running on an older platform version this method will configure + * a standard activity chooser dialog for the menu item.

+ * + *

During the calling activity's lifecycle, if data within the share intent must + * change the app should change that state in one of several ways:

+ *
    + *
  • Call {@link ActivityCompat#invalidateOptionsMenu(Activity)}. If the app is running + * on API level 11 or above and uses the Action Bar its menu will be recreated and rebuilt. + * If not, the activity will receive a call to {@link Activity#onPrepareOptionsMenu(Menu)} + * the next time the user presses the menu key to open the options menu panel. The activity + * can then call configureMenuItem again with a new or altered IntentBuilder to reconfigure + * the share menu item.
  • + *
  • Keep a reference to the MenuItem object for the share item once it has been created + * and call configureMenuItem to update the associated sharing intent as needed.
  • + *
+ * + * @param item MenuItem to configure for sharing + * @param shareIntent IntentBuilder with data about the content to share + */ + public static void configureMenuItem(MenuItem item, IntentBuilder shareIntent) { + IMPL.configureMenuItem(item, shareIntent); + } + + /** + * Configure a menu item to act as a sharing action. + * + * @param menu Menu containing the item to use for sharing + * @param menuItemId ID of the share item within menu + * @param shareIntent IntentBuilder with data about the content to share + * @see #configureMenuItem(MenuItem, IntentBuilder) + */ + public static void configureMenuItem(Menu menu, int menuItemId, IntentBuilder shareIntent) { + MenuItem item = menu.findItem(menuItemId); + if (item == null) { + throw new IllegalArgumentException("Could not find menu item with id " + menuItemId + + " in the supplied menu"); + } + configureMenuItem(item, shareIntent); + } + + /** + * IntentBuilder is a helper for constructing {@link Intent#ACTION_SEND} and + * {@link Intent#ACTION_SEND_MULTIPLE} sharing intents and starting activities + * to share content. The ComponentName and package name of the calling activity + * will be included. + */ + public static class IntentBuilder { + private Activity mActivity; + private Intent mIntent; + private CharSequence mChooserTitle; + private ArrayList mToAddresses; + private ArrayList mCcAddresses; + private ArrayList mBccAddresses; + + private ArrayList mStreams; + + /** + * Create a new IntentBuilder for launching a sharing action from launchingActivity. + * + * @param launchingActivity Activity that the share will be launched from + * @return a new IntentBuilder instance + */ + public static IntentBuilder from(Activity launchingActivity) { + return new IntentBuilder(launchingActivity); + } + + private IntentBuilder(Activity launchingActivity) { + mActivity = launchingActivity; + mIntent = new Intent().setAction(Intent.ACTION_SEND); + mIntent.putExtra(EXTRA_CALLING_PACKAGE, launchingActivity.getPackageName()); + mIntent.putExtra(EXTRA_CALLING_ACTIVITY, launchingActivity.getComponentName()); + mIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + } + + /** + * Retrieve the Intent as configured so far by the IntentBuilder. This Intent + * is suitable for use in a ShareActionProvider or chooser dialog. + * + *

To create an intent that will launch the activity chooser so that the user + * may select a target for the share, see {@link #createChooserIntent()}. + * + * @return The current Intent being configured by this builder + */ + public Intent getIntent() { + if (mToAddresses != null) { + combineArrayExtra(Intent.EXTRA_EMAIL, mToAddresses); + mToAddresses = null; + } + if (mCcAddresses != null) { + combineArrayExtra(Intent.EXTRA_CC, mCcAddresses); + mCcAddresses = null; + } + if (mBccAddresses != null) { + combineArrayExtra(Intent.EXTRA_BCC, mBccAddresses); + mBccAddresses = null; + } + + // Check if we need to change the action. + boolean needsSendMultiple = mStreams != null && mStreams.size() > 1; + boolean isSendMultiple = mIntent.getAction().equals(Intent.ACTION_SEND_MULTIPLE); + + if (!needsSendMultiple && isSendMultiple) { + // Change back to a single send action; place the first stream into the + // intent for single sharing. + mIntent.setAction(Intent.ACTION_SEND); + if (mStreams != null && !mStreams.isEmpty()) { + mIntent.putExtra(Intent.EXTRA_STREAM, mStreams.get(0)); + } else { + mIntent.removeExtra(Intent.EXTRA_STREAM); + } + mStreams = null; + } + + if (needsSendMultiple && !isSendMultiple) { + // Change to a multiple send action; place the relevant ArrayList into the + // intent for multiple sharing. + mIntent.setAction(Intent.ACTION_SEND_MULTIPLE); + if (mStreams != null && !mStreams.isEmpty()) { + mIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, mStreams); + } else { + mIntent.removeExtra(Intent.EXTRA_STREAM); + } + } + + return mIntent; + } + + Activity getActivity() { + return mActivity; + } + + private void combineArrayExtra(String extra, ArrayList add) { + String[] currentAddresses = mIntent.getStringArrayExtra(extra); + int currentLength = currentAddresses != null ? currentAddresses.length : 0; + String[] finalAddresses = new String[currentLength + add.size()]; + add.toArray(finalAddresses); + if (currentAddresses != null) { + System.arraycopy(currentAddresses, 0, finalAddresses, add.size(), currentLength); + } + mIntent.putExtra(extra, finalAddresses); + } + + private void combineArrayExtra(String extra, String[] add) { + // Add any items still pending + Intent intent = getIntent(); + String[] old = intent.getStringArrayExtra(extra); + int oldLength = old != null ? old.length : 0; + String[] result = new String[oldLength + add.length]; + if (old != null) System.arraycopy(old, 0, result, 0, oldLength); + System.arraycopy(add, 0, result, oldLength, add.length); + intent.putExtra(extra, result); + } + + /** + * Create an Intent that will launch the standard Android activity chooser, + * allowing the user to pick what activity/app on the system should handle + * the share. + * + * @return A chooser Intent for the currently configured sharing action + */ + public Intent createChooserIntent() { + return Intent.createChooser(getIntent(), mChooserTitle); + } + + /** + * Start a chooser activity for the current share intent. + * + *

Note that under most circumstances you should use + * {@link ShareCompat#configureMenuItem(MenuItem, IntentBuilder) + * ShareCompat.configureMenuItem()} to add a Share item to the menu while + * presenting a detail view of the content to be shared instead + * of invoking this directly.

+ */ + public void startChooser() { + mActivity.startActivity(createChooserIntent()); + } + + /** + * Set the title that will be used for the activity chooser for this share. + * + * @param title Title string + * @return This IntentBuilder for method chaining + */ + public IntentBuilder setChooserTitle(CharSequence title) { + mChooserTitle = title; + return this; + } + + /** + * Set the title that will be used for the activity chooser for this share. + * + * @param resId Resource ID of the title string to use + * @return This IntentBuilder for method chaining + */ + public IntentBuilder setChooserTitle(@StringRes int resId) { + return setChooserTitle(mActivity.getText(resId)); + } + + /** + * Set the type of data being shared + * + * @param mimeType mimetype of the shared data + * @return This IntentBuilder for method chaining + * @see Intent#setType(String) + */ + public IntentBuilder setType(String mimeType) { + mIntent.setType(mimeType); + return this; + } + + /** + * Set the literal text data to be sent as part of the share. + * This may be a styled CharSequence. + * + * @param text Text to share + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_TEXT + */ + public IntentBuilder setText(CharSequence text) { + mIntent.putExtra(Intent.EXTRA_TEXT, text); + return this; + } + + /** + * Set an HTML string to be sent as part of the share. + * If {@link Intent#EXTRA_TEXT EXTRA_TEXT} has not already been supplied, + * a styled version of the supplied HTML text will be added as EXTRA_TEXT as + * parsed by {@link android.text.Html#fromHtml(String) Html.fromHtml}. + * + * @param htmlText A string containing HTML markup as a richer version of the text + * provided by EXTRA_TEXT. + * @return This IntentBuilder for method chaining + * @see #setText(CharSequence) + */ + public IntentBuilder setHtmlText(String htmlText) { + mIntent.putExtra(IntentCompat.EXTRA_HTML_TEXT, htmlText); + if (!mIntent.hasExtra(Intent.EXTRA_TEXT)) { + // Supply a default if EXTRA_TEXT isn't set + setText(Html.fromHtml(htmlText)); + } + return this; + } + + /** + * Set a stream URI to the data that should be shared. + * + *

This replaces all currently set stream URIs and will produce a single-stream + * ACTION_SEND intent.

+ * + * @param streamUri URI of the stream to share + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_STREAM + */ + public IntentBuilder setStream(Uri streamUri) { + if (!mIntent.getAction().equals(Intent.ACTION_SEND)) { + mIntent.setAction(Intent.ACTION_SEND); + } + mStreams = null; + mIntent.putExtra(Intent.EXTRA_STREAM, streamUri); + return this; + } + + /** + * Add a stream URI to the data that should be shared. If this is not the first + * stream URI added the final intent constructed will become an ACTION_SEND_MULTIPLE + * intent. Not all apps will handle both ACTION_SEND and ACTION_SEND_MULTIPLE. + * + * @param streamUri URI of the stream to share + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_STREAM + * @see Intent#ACTION_SEND + * @see Intent#ACTION_SEND_MULTIPLE + */ + public IntentBuilder addStream(Uri streamUri) { + Uri currentStream = mIntent.getParcelableExtra(Intent.EXTRA_STREAM); + if (currentStream == null) { + return setStream(streamUri); + } + if (mStreams == null) { + mStreams = new ArrayList(); + } + if (currentStream != null) { + mIntent.removeExtra(Intent.EXTRA_STREAM); + mStreams.add(currentStream); + } + mStreams.add(streamUri); + return this; + } + + /** + * Set an array of email addresses as recipients of this share. + * This replaces all current "to" recipients that have been set so far. + * + * @param addresses Email addresses to send to + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_EMAIL + */ + public IntentBuilder setEmailTo(String[] addresses) { + if (mToAddresses != null) { + mToAddresses = null; + } + mIntent.putExtra(Intent.EXTRA_EMAIL, addresses); + return this; + } + + /** + * Add an email address to be used in the "to" field of the final Intent. + * + * @param address Email address to send to + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_EMAIL + */ + public IntentBuilder addEmailTo(String address) { + if (mToAddresses == null) { + mToAddresses = new ArrayList(); + } + mToAddresses.add(address); + return this; + } + + /** + * Add an array of email addresses to be used in the "to" field of the final Intent. + * + * @param addresses Email addresses to send to + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_EMAIL + */ + public IntentBuilder addEmailTo(String[] addresses) { + combineArrayExtra(Intent.EXTRA_EMAIL, addresses); + return this; + } + + /** + * Set an array of email addresses to CC on this share. + * This replaces all current "CC" recipients that have been set so far. + * + * @param addresses Email addresses to CC on the share + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_CC + */ + public IntentBuilder setEmailCc(String[] addresses) { + mIntent.putExtra(Intent.EXTRA_CC, addresses); + return this; + } + + /** + * Add an email address to be used in the "cc" field of the final Intent. + * + * @param address Email address to CC + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_CC + */ + public IntentBuilder addEmailCc(String address) { + if (mCcAddresses == null) { + mCcAddresses = new ArrayList(); + } + mCcAddresses.add(address); + return this; + } + + /** + * Add an array of email addresses to be used in the "cc" field of the final Intent. + * + * @param addresses Email addresses to CC + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_CC + */ + public IntentBuilder addEmailCc(String[] addresses) { + combineArrayExtra(Intent.EXTRA_CC, addresses); + return this; + } + + /** + * Set an array of email addresses to BCC on this share. + * This replaces all current "BCC" recipients that have been set so far. + * + * @param addresses Email addresses to BCC on the share + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_BCC + */ + public IntentBuilder setEmailBcc(String[] addresses) { + mIntent.putExtra(Intent.EXTRA_BCC, addresses); + return this; + } + + /** + * Add an email address to be used in the "bcc" field of the final Intent. + * + * @param address Email address to BCC + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_BCC + */ + public IntentBuilder addEmailBcc(String address) { + if (mBccAddresses == null) { + mBccAddresses = new ArrayList(); + } + mBccAddresses.add(address); + return this; + } + + /** + * Add an array of email addresses to be used in the "bcc" field of the final Intent. + * + * @param addresses Email addresses to BCC + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_BCC + */ + public IntentBuilder addEmailBcc(String[] addresses) { + combineArrayExtra(Intent.EXTRA_BCC, addresses); + return this; + } + + /** + * Set a subject heading for this share; useful for sharing via email. + * + * @param subject Subject heading for this share + * @return This IntentBuilder for method chaining + * @see Intent#EXTRA_SUBJECT + */ + public IntentBuilder setSubject(String subject) { + mIntent.putExtra(Intent.EXTRA_SUBJECT, subject); + return this; + } + } + + /** + * IntentReader is a helper for reading the data contained within a sharing (ACTION_SEND) + * Intent. It provides methods to parse standard elements included with a share + * in addition to extra metadata about the app that shared the content. + * + *

Social sharing apps are encouraged to provide attribution for the app that shared + * the content. IntentReader offers access to the application label, calling activity info, + * and application icon of the app that shared the content. This data may have been provided + * voluntarily by the calling app and should always be displayed to the user before submission + * for manual verification. The user should be offered the option to omit this information + * from shared posts if desired.

+ * + *

Activities that intend to receive sharing intents should configure an intent-filter + * to accept {@link Intent#ACTION_SEND} intents ("android.intent.action.SEND") and optionally + * accept {@link Intent#ACTION_SEND_MULTIPLE} ("android.intent.action.SEND_MULTIPLE") if + * the activity is equipped to handle multiple data streams.

+ */ + public static class IntentReader { + private static final String TAG = "IntentReader"; + + private Activity mActivity; + private Intent mIntent; + private String mCallingPackage; + private ComponentName mCallingActivity; + + private ArrayList mStreams; + + /** + * Get an IntentReader for parsing and interpreting the sharing intent + * used to start the given activity. + * + * @param activity Activity that was started to share content + * @return IntentReader for parsing sharing data + */ + public static IntentReader from(Activity activity) { + return new IntentReader(activity); + } + + private IntentReader(Activity activity) { + mActivity = activity; + mIntent = activity.getIntent(); + mCallingPackage = ShareCompat.getCallingPackage(activity); + mCallingActivity = ShareCompat.getCallingActivity(activity); + } + + /** + * Returns true if the activity this reader was obtained for was + * started with an {@link Intent#ACTION_SEND} or {@link Intent#ACTION_SEND_MULTIPLE} + * sharing Intent. + * + * @return true if the activity was started with an ACTION_SEND + * or ACTION_SEND_MULTIPLE Intent + */ + public boolean isShareIntent() { + final String action = mIntent.getAction(); + return Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action); + } + + /** + * Returns true if the activity this reader was obtained for was started with an + * {@link Intent#ACTION_SEND} intent and contains a single shared item. + * The shared content should be obtained using either the {@link #getText()} + * or {@link #getStream()} methods depending on the type of content shared. + * + * @return true if the activity was started with an ACTION_SEND intent + */ + public boolean isSingleShare() { + return Intent.ACTION_SEND.equals(mIntent.getAction()); + } + + /** + * Returns true if the activity this reader was obtained for was started with an + * {@link Intent#ACTION_SEND_MULTIPLE} intent. The Intent may contain more than + * one stream item. + * + * @return true if the activity was started with an ACTION_SEND_MULTIPLE intent + */ + public boolean isMultipleShare() { + return Intent.ACTION_SEND_MULTIPLE.equals(mIntent.getAction()); + } + + /** + * Get the mimetype of the data shared to this activity. + * + * @return mimetype of the shared data + * @see Intent#getType() + */ + public String getType() { + return mIntent.getType(); + } + + /** + * Get the literal text shared with the target activity. + * + * @return Literal shared text or null if none was supplied + * @see Intent#EXTRA_TEXT + */ + public CharSequence getText() { + return mIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); + } + + /** + * Get the styled HTML text shared with the target activity. + * If no HTML text was supplied but {@link Intent#EXTRA_TEXT} contained + * styled text, it will be converted to HTML if possible and returned. + * If the text provided by {@link Intent#EXTRA_TEXT} was not styled text, + * it will be escaped by {@link android.text.Html#escapeHtml(CharSequence)} + * and returned. If no text was provided at all, this method will return null. + * + * @return Styled text provided by the sender as HTML. + */ + public String getHtmlText() { + String result = mIntent.getStringExtra(IntentCompat.EXTRA_HTML_TEXT); + if (result == null) { + CharSequence text = getText(); + if (text instanceof Spanned) { + result = Html.toHtml((Spanned) text); + } else if (text != null) { + result = IMPL.escapeHtml(text); + } + } + return result; + } + + /** + * Get a URI referring to a data stream shared with the target activity. + * + *

This call will fail if the share intent contains multiple stream items. + * If {@link #isMultipleShare()} returns true the application should use + * {@link #getStream(int)} and {@link #getStreamCount()} to retrieve the + * included stream items.

+ * + * @return A URI referring to a data stream to be shared or null if one was not supplied + * @see Intent#EXTRA_STREAM + */ + public Uri getStream() { + return mIntent.getParcelableExtra(Intent.EXTRA_STREAM); + } + + /** + * Get the URI of a stream item shared with the target activity. + * Index should be in the range [0-getStreamCount()). + * + * @param index Index of text item to retrieve + * @return Requested stream item URI + * @see Intent#EXTRA_STREAM + * @see Intent#ACTION_SEND_MULTIPLE + */ + public Uri getStream(int index) { + if (mStreams == null && isMultipleShare()) { + mStreams = mIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + } + if (mStreams != null) { + return mStreams.get(index); + } + if (index == 0) { + return mIntent.getParcelableExtra(Intent.EXTRA_STREAM); + } + throw new IndexOutOfBoundsException("Stream items available: " + getStreamCount() + + " index requested: " + index); + } + + /** + * Return the number of stream items shared. The return value will be 0 or 1 if + * this was an {@link Intent#ACTION_SEND} intent, or 0 or more if it was an + * {@link Intent#ACTION_SEND_MULTIPLE} intent. + * + * @return Count of text items contained within the Intent + */ + public int getStreamCount() { + if (mStreams == null && isMultipleShare()) { + mStreams = mIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM); + } + if (mStreams != null) { + return mStreams.size(); + } + return mIntent.hasExtra(Intent.EXTRA_STREAM) ? 1 : 0; + } + + /** + * Get an array of Strings, each an email address to share to. + * + * @return An array of email addresses or null if none were supplied. + * @see Intent#EXTRA_EMAIL + */ + public String[] getEmailTo() { + return mIntent.getStringArrayExtra(Intent.EXTRA_EMAIL); + } + + /** + * Get an array of Strings, each an email address to CC on this share. + * + * @return An array of email addresses or null if none were supplied. + * @see Intent#EXTRA_CC + */ + public String[] getEmailCc() { + return mIntent.getStringArrayExtra(Intent.EXTRA_CC); + } + + /** + * Get an array of Strings, each an email address to BCC on this share. + * + * @return An array of email addresses or null if none were supplied. + * @see Intent#EXTRA_BCC + */ + public String[] getEmailBcc() { + return mIntent.getStringArrayExtra(Intent.EXTRA_BCC); + } + + /** + * Get a subject heading for this share; useful when sharing via email. + * + * @return The subject heading for this share or null if one was not supplied. + * @see Intent#EXTRA_SUBJECT + */ + public String getSubject() { + return mIntent.getStringExtra(Intent.EXTRA_SUBJECT); + } + + /** + * Get the name of the package that invoked this sharing intent. If the activity + * was not started for a result, IntentBuilder will read this from extra metadata placed + * in the Intent by ShareBuilder. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @return Name of the package that started this activity or null if unknown + * @see Activity#getCallingPackage() + * @see ShareCompat#EXTRA_CALLING_PACKAGE + */ + public String getCallingPackage() { + return mCallingPackage; + } + + /** + * Get the {@link ComponentName} of the Activity that invoked this sharing intent. + * If the target sharing activity was not started for a result, IntentBuilder will read + * this from extra metadata placed in the intent by ShareBuilder. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @return ComponentName of the calling Activity or null if unknown + * @see Activity#getCallingActivity() + * @see ShareCompat#EXTRA_CALLING_ACTIVITY + */ + public ComponentName getCallingActivity() { + return mCallingActivity; + } + + /** + * Get the icon of the calling activity as a Drawable if data about + * the calling activity is available. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @return The calling Activity's icon or null if unknown + */ + public Drawable getCallingActivityIcon() { + if (mCallingActivity == null) return null; + + PackageManager pm = mActivity.getPackageManager(); + try { + return pm.getActivityIcon(mCallingActivity); + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not retrieve icon for calling activity", e); + } + return null; + } + + /** + * Get the icon of the calling application as a Drawable if data + * about the calling package is available. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @return The calling application's icon or null if unknown + */ + public Drawable getCallingApplicationIcon() { + if (mCallingPackage == null) return null; + + PackageManager pm = mActivity.getPackageManager(); + try { + return pm.getApplicationIcon(mCallingPackage); + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not retrieve icon for calling application", e); + } + return null; + } + + /** + * Get the human-readable label (title) of the calling application if + * data about the calling package is available. + * + *

Note: This data may have been provided voluntarily by the calling + * application. As such it should not be trusted for accuracy in the context of + * security or verification.

+ * + * @return The calling application's label or null if unknown + */ + public CharSequence getCallingApplicationLabel() { + if (mCallingPackage == null) return null; + + PackageManager pm = mActivity.getPackageManager(); + try { + return pm.getApplicationLabel(pm.getApplicationInfo(mCallingPackage, 0)); + } catch (NameNotFoundException e) { + Log.e(TAG, "Could not retrieve label for calling application", e); + } + return null; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatICS.java b/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatICS.java new file mode 100644 index 00000000000..0f2ff352bc8 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatICS.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.content.Intent; +import android.view.ActionProvider; +import android.view.MenuItem; +import android.widget.ShareActionProvider; + +class ShareCompatICS { + private static final String HISTORY_FILENAME_PREFIX = ".sharecompat_"; + + public static void configureMenuItem(MenuItem item, Activity callingActivity, Intent intent) { + ActionProvider itemProvider = item.getActionProvider(); + ShareActionProvider provider = null; + if (!(itemProvider instanceof ShareActionProvider)) { + provider = new ShareActionProvider(callingActivity); + } else { + provider = (ShareActionProvider) itemProvider; + } + provider.setShareHistoryFileName(HISTORY_FILENAME_PREFIX + + callingActivity.getClass().getName()); + provider.setShareIntent(intent); + item.setActionProvider(provider); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatJB.java b/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatJB.java new file mode 100644 index 00000000000..8a4de97af8f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/ShareCompatJB.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.text.Html; + +class ShareCompatJB { + public static String escapeHtml(CharSequence html) { + return Html.escapeHtml(html); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/SharedElementCallback.java b/mobile/google-android-support-v4/src/android/support/v4/app/SharedElementCallback.java new file mode 100644 index 00000000000..7c0de86d5a7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/SharedElementCallback.java @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.app; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcelable; +import android.view.View; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; + +import java.util.List; +import java.util.Map; + +/** + * Listener provided in + * {@link FragmentActivity#setEnterSharedElementCallback(SharedElementCallback)} and + * {@link FragmentActivity#setExitSharedElementCallback(SharedElementCallback)} + * to monitor the Activity transitions. The events can be used to customize Activity + * Transition behavior. + */ +public abstract class SharedElementCallback { + private Matrix mTempMatrix; + private static int MAX_IMAGE_SIZE = (1024 * 1024); + private static final String BUNDLE_SNAPSHOT_BITMAP = "sharedElement:snapshot:bitmap"; + private static final String BUNDLE_SNAPSHOT_IMAGE_SCALETYPE = "sharedElement:snapshot:imageScaleType"; + private static final String BUNDLE_SNAPSHOT_IMAGE_MATRIX = "sharedElement:snapshot:imageMatrix"; + + /** + * Called immediately after the start state is set for the shared element. + * The shared element will start at the size and position of the shared element + * in the launching Activity or Fragment. + * + * @param sharedElementNames The names of the shared elements that were accepted into + * the View hierarchy. + * @param sharedElements The shared elements that are part of the View hierarchy. + * @param sharedElementSnapshots The Views containing snap shots of the shared element + * from the launching Window. These elements will not + * be part of the scene, but will be positioned relative + * to the Window decor View. This list is null for Fragment + * Transitions. + */ + public void onSharedElementStart(List sharedElementNames, + List sharedElements, List sharedElementSnapshots) {} + + /** + * Called after the end state is set for the shared element, but before the end state + * is captured by the shared element transition. + *

+ * Any customization done in + * {@link #onSharedElementStart(java.util.List, java.util.List, java.util.List)} + * may need to be modified to the final state of the shared element if it is not + * automatically corrected by layout. For example, rotation or scale will not + * be affected by layout and if changed in {@link #onSharedElementStart(java.util.List, + * java.util.List, java.util.List)}, it will also have to be set here again to correct + * the end state. + *

+ * + * @param sharedElementNames The names of the shared elements that were accepted into + * the View hierarchy. + * @param sharedElements The shared elements that are part of the View hierarchy. + * @param sharedElementSnapshots The Views containing snap shots of the shared element + * from the launching Window. These elements will not + * be part of the scene, but will be positioned relative + * to the Window decor View. This list will be null for + * Fragment Transitions. + */ + public void onSharedElementEnd(List sharedElementNames, + List sharedElements, List sharedElementSnapshots) {} + + /** + * Called after {@link #onMapSharedElements(java.util.List, java.util.Map)} when + * transferring shared elements in. Any shared elements that have no mapping will be in + * rejectedSharedElements. The elements remaining in + * rejectedSharedElements will be transitioned out of the Scene. If a + * View is removed from rejectedSharedElements, it must be handled by the + * SharedElementListener. + *

+ * Views in rejectedSharedElements will have their position and size set to the + * position of the calling shared element, relative to the Window decor View and contain + * snapshots of the View from the calling Activity or Fragment. This + * view may be safely added to the decor View's overlay to remain in position. + *

+ *

This method is not called for Fragment Transitions. All rejected shared elements + * will be handled by the exit transition.

+ * + * @param rejectedSharedElements Views containing visual information of shared elements + * that are not part of the entering scene. These Views + * are positioned relative to the Window decor View. A + * View removed from this list will not be transitioned + * automatically. + */ + public void onRejectSharedElements(List rejectedSharedElements) {} + + /** + * Lets the SharedElementCallback adjust the mapping of shared element names to + * Views. + * + * @param names The names of all shared elements transferred from the calling Activity + * or Fragment in the order they were provided. + * @param sharedElements The mapping of shared element names to Views. The best guess + * will be filled into sharedElements based on the transitionNames. + */ + public void onMapSharedElements(List names, Map sharedElements) {} + + + /** + * Creates a snapshot of a shared element to be used by the remote Activity and reconstituted + * with {@link #onCreateSnapshotView(android.content.Context, android.os.Parcelable)}. A + * null return value will mean that the remote Activity will have a null snapshot View in + * {@link #onSharedElementStart(java.util.List, java.util.List, java.util.List)} and + * {@link #onSharedElementEnd(java.util.List, java.util.List, java.util.List)}. + * + *

This is not called for Fragment Transitions.

+ * + * @param sharedElement The shared element View to create a snapshot for. + * @param viewToGlobalMatrix A matrix containing a transform from the view to the screen + * coordinates. + * @param screenBounds The bounds of shared element in screen coordinate space. This is + * the bounds of the view with the viewToGlobalMatrix applied. + * @return A snapshot to send to the remote Activity to be reconstituted with + * {@link #onCreateSnapshotView(android.content.Context, android.os.Parcelable)} and passed + * into {@link #onSharedElementStart(java.util.List, java.util.List, java.util.List)} and + * {@link #onSharedElementEnd(java.util.List, java.util.List, java.util.List)}. + */ + public Parcelable onCaptureSharedElementSnapshot(View sharedElement, Matrix viewToGlobalMatrix, + RectF screenBounds) { + if (sharedElement instanceof ImageView) { + ImageView imageView = ((ImageView) sharedElement); + Drawable d = imageView.getDrawable(); + Drawable bg = imageView.getBackground(); + if (d != null && bg == null) { + Bitmap bitmap = createDrawableBitmap(d); + if (bitmap != null) { + Bundle bundle = new Bundle(); + bundle.putParcelable(BUNDLE_SNAPSHOT_BITMAP, bitmap); + bundle.putString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE, + imageView.getScaleType().toString()); + if (imageView.getScaleType() == ScaleType.MATRIX) { + Matrix matrix = imageView.getImageMatrix(); + float[] values = new float[9]; + matrix.getValues(values); + bundle.putFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX, values); + } + return bundle; + } + } + } + int bitmapWidth = Math.round(screenBounds.width()); + int bitmapHeight = Math.round(screenBounds.height()); + Bitmap bitmap = null; + if (bitmapWidth > 0 && bitmapHeight > 0) { + float scale = Math.min(1f, ((float)MAX_IMAGE_SIZE) / (bitmapWidth * bitmapHeight)); + bitmapWidth *= scale; + bitmapHeight *= scale; + if (mTempMatrix == null) { + mTempMatrix = new Matrix(); + } + mTempMatrix.set(viewToGlobalMatrix); + mTempMatrix.postTranslate(-screenBounds.left, -screenBounds.top); + mTempMatrix.postScale(scale, scale); + bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + canvas.concat(mTempMatrix); + sharedElement.draw(canvas); + } + return bitmap; + } + + /** + * Get a copy of bitmap of given drawable. + */ + private static Bitmap createDrawableBitmap(Drawable drawable) { + int width = drawable.getIntrinsicWidth(); + int height = drawable.getIntrinsicHeight(); + if (width <= 0 || height <= 0) { + return null; + } + float scale = Math.min(1f, ((float)MAX_IMAGE_SIZE) / (width * height)); + if (drawable instanceof BitmapDrawable && scale == 1f) { + // return same bitmap if scale down not needed + return ((BitmapDrawable) drawable).getBitmap(); + } + int bitmapWidth = (int) (width * scale); + int bitmapHeight = (int) (height * scale); + Bitmap bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + Rect existingBounds = drawable.getBounds(); + int left = existingBounds.left; + int top = existingBounds.top; + int right = existingBounds.right; + int bottom = existingBounds.bottom; + drawable.setBounds(0, 0, bitmapWidth, bitmapHeight); + drawable.draw(canvas); + drawable.setBounds(left, top, right, bottom); + return bitmap; + } + + /** + * Reconstitutes a snapshot View from a Parcelable returned in + * {@link #onCaptureSharedElementSnapshot(android.view.View, android.graphics.Matrix, + * android.graphics.RectF)} to be used in {@link #onSharedElementStart(java.util.List, + * java.util.List, java.util.List)} and {@link #onSharedElementEnd(java.util.List, + * java.util.List, java.util.List)}. The returned View will be sized and positioned after + * this call so that it is ready to be added to the decor View's overlay. + * + *

This is not called for Fragment Transitions.

+ * + * @param context The Context used to create the snapshot View. + * @param snapshot The Parcelable returned by {@link #onCaptureSharedElementSnapshot( + * android.view.View, android.graphics.Matrix, android.graphics.RectF)}. + * @return A View to be sent in {@link #onSharedElementStart(java.util.List, java.util.List, + * java.util.List)} and {@link #onSharedElementEnd(java.util.List, java.util.List, + * java.util.List)}. A null value will produce a null snapshot value for those two methods. + */ + public View onCreateSnapshotView(Context context, Parcelable snapshot) { + ImageView view = null; + if (snapshot instanceof Bundle) { + Bundle bundle = (Bundle) snapshot; + Bitmap bitmap = (Bitmap) bundle.getParcelable(BUNDLE_SNAPSHOT_BITMAP); + if (bitmap == null) { + return null; + } + ImageView imageView = new ImageView(context); + view = imageView; + imageView.setImageBitmap(bitmap); + imageView.setScaleType( + ScaleType.valueOf(bundle.getString(BUNDLE_SNAPSHOT_IMAGE_SCALETYPE))); + if (imageView.getScaleType() == ScaleType.MATRIX) { + float[] values = bundle.getFloatArray(BUNDLE_SNAPSHOT_IMAGE_MATRIX); + Matrix matrix = new Matrix(); + matrix.setValues(values); + imageView.setImageMatrix(matrix); + } + } else if (snapshot instanceof Bitmap) { + Bitmap bitmap = (Bitmap) snapshot; + view = new ImageView(context); + view.setImageBitmap(bitmap); + } + return view; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/SuperNotCalledException.java b/mobile/google-android-support-v4/src/android/support/v4/app/SuperNotCalledException.java new file mode 100644 index 00000000000..0313eb996d6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/SuperNotCalledException.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.util.AndroidRuntimeException; + +final class SuperNotCalledException extends AndroidRuntimeException { + public SuperNotCalledException(String msg) { + super(msg); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilder.java b/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilder.java new file mode 100644 index 00000000000..9146872cd97 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilder.java @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.content.ContextCompat; +import android.support.v4.content.IntentCompat; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Iterator; + +/** + * Utility class for constructing synthetic back stacks for cross-task navigation + * on Android 3.0 and newer. + * + *

In API level 11 (Android 3.0/Honeycomb) the recommended conventions for + * app navigation using the back key changed. The back key's behavior is local + * to the current task and does not capture navigation across different tasks. + * Navigating across tasks and easily reaching the previous task is accomplished + * through the "recents" UI, accessible through the software-provided Recents key + * on the navigation or system bar. On devices with the older hardware button configuration + * the recents UI can be accessed with a long press on the Home key.

+ * + *

When crossing from one task stack to another post-Android 3.0, + * the application should synthesize a back stack/history for the new task so that + * the user may navigate out of the new task and back to the Launcher by repeated + * presses of the back key. Back key presses should not navigate across task stacks.

+ * + *

TaskStackBuilder provides a backward-compatible way to obey the correct conventions + * around cross-task navigation on the device's version of the platform. On devices running + * Android 3.0 or newer, calls to the {@link #startActivities()} method or sending the + * {@link PendingIntent} generated by {@link #getPendingIntent(int, int)} will construct + * the synthetic back stack as prescribed. On devices running older versions of the platform, + * these same calls will invoke the topmost activity in the supplied stack, ignoring + * the rest of the synthetic stack and allowing the back key to navigate back to the previous + * task.

+ * + *
+ *

About Navigation

+ * For more detailed information about tasks, the back stack, and navigation design guidelines, + * please read + * Tasks and Back Stack + * from the developer guide and Navigation + * from the design guide. + *
+ */ +public class TaskStackBuilder implements Iterable { + private static final String TAG = "TaskStackBuilder"; + + public interface SupportParentable { + Intent getSupportParentActivityIntent(); + } + + interface TaskStackBuilderImpl { + PendingIntent getPendingIntent(Context context, Intent[] intents, int requestCode, + int flags, Bundle options); + } + + static class TaskStackBuilderImplBase implements TaskStackBuilderImpl { + public PendingIntent getPendingIntent(Context context, Intent[] intents, int requestCode, + int flags, Bundle options) { + Intent topIntent = new Intent(intents[intents.length - 1]); + topIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + return PendingIntent.getActivity(context, requestCode, topIntent, flags); + } + } + + static class TaskStackBuilderImplHoneycomb implements TaskStackBuilderImpl { + public PendingIntent getPendingIntent(Context context, Intent[] intents, int requestCode, + int flags, Bundle options) { + intents[0] = new Intent(intents[0]).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + IntentCompat.FLAG_ACTIVITY_CLEAR_TASK | + IntentCompat.FLAG_ACTIVITY_TASK_ON_HOME); + return TaskStackBuilderHoneycomb.getActivitiesPendingIntent(context, requestCode, + intents, flags); + } + } + + static class TaskStackBuilderImplJellybean implements TaskStackBuilderImpl { + public PendingIntent getPendingIntent(Context context, Intent[] intents, int requestCode, + int flags, Bundle options) { + intents[0] = new Intent(intents[0]).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + IntentCompat.FLAG_ACTIVITY_CLEAR_TASK | + IntentCompat.FLAG_ACTIVITY_TASK_ON_HOME); + return TaskStackBuilderJellybean.getActivitiesPendingIntent(context, requestCode, + intents, flags, options); + } + } + + private static final TaskStackBuilderImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 11) { + IMPL = new TaskStackBuilderImplHoneycomb(); + } else { + IMPL = new TaskStackBuilderImplBase(); + } + } + + private final ArrayList mIntents = new ArrayList(); + private final Context mSourceContext; + + private TaskStackBuilder(Context a) { + mSourceContext = a; + } + + /** + * Return a new TaskStackBuilder for launching a fresh task stack consisting + * of a series of activities. + * + * @param context The context that will launch the new task stack or generate a PendingIntent + * @return A new TaskStackBuilder + */ + public static TaskStackBuilder create(Context context) { + return new TaskStackBuilder(context); + } + + /** + * Return a new TaskStackBuilder for launching a fresh task stack consisting + * of a series of activities. + * + * @param context The context that will launch the new task stack or generate a PendingIntent + * @return A new TaskStackBuilder + * + * @deprecated use {@link #create(Context)} instead + */ + public static TaskStackBuilder from(Context context) { + return create(context); + } + + /** + * Add a new Intent to the task stack. The most recently added Intent will invoke + * the Activity at the top of the final task stack. + * + * @param nextIntent Intent for the next Activity in the synthesized task stack + * @return This TaskStackBuilder for method chaining + */ + public TaskStackBuilder addNextIntent(Intent nextIntent) { + mIntents.add(nextIntent); + return this; + } + + /** + * Add a new Intent with the resolved chain of parents for the target activity to + * the task stack. + * + *

This is equivalent to calling {@link #addParentStack(ComponentName) addParentStack} + * with the resolved ComponentName of nextIntent (if it can be resolved), followed by + * {@link #addNextIntent(Intent) addNextIntent} with nextIntent.

+ * + * @param nextIntent Intent for the topmost Activity in the synthesized task stack. + * Its chain of parents as specified in the manifest will be added. + * @return This TaskStackBuilder for method chaining. + */ + public TaskStackBuilder addNextIntentWithParentStack(Intent nextIntent) { + ComponentName target = nextIntent.getComponent(); + if (target == null) { + target = nextIntent.resolveActivity(mSourceContext.getPackageManager()); + } + if (target != null) { + addParentStack(target); + } + addNextIntent(nextIntent); + return this; + } + + /** + * Add the activity parent chain as specified by manifest <meta-data> elements + * to the task stack builder. + * + * @param sourceActivity All parents of this activity will be added + * @return This TaskStackBuilder for method chaining + */ + public TaskStackBuilder addParentStack(Activity sourceActivity) { + Intent parent = null; + if (sourceActivity instanceof SupportParentable) { + parent = ((SupportParentable) sourceActivity).getSupportParentActivityIntent(); + } + if (parent == null) { + parent = NavUtils.getParentActivityIntent(sourceActivity); + } + + if (parent != null) { + // We have the actual parent intent, build the rest from static metadata + // then add the direct parent intent to the end. + ComponentName target = parent.getComponent(); + if (target == null) { + target = parent.resolveActivity(mSourceContext.getPackageManager()); + } + addParentStack(target); + addNextIntent(parent); + } + return this; + } + + /** + * Add the activity parent chain as specified by manifest <meta-data> elements + * to the task stack builder. + * + * @param sourceActivityClass All parents of this activity will be added + * @return This TaskStackBuilder for method chaining + */ + public TaskStackBuilder addParentStack(Class sourceActivityClass) { + return addParentStack(new ComponentName(mSourceContext, sourceActivityClass)); + } + + /** + * Add the activity parent chain as specified by manifest <meta-data> elements + * to the task stack builder. + * + * @param sourceActivityName Must specify an Activity component. All parents of + * this activity will be added + * @return This TaskStackBuilder for method chaining + */ + public TaskStackBuilder addParentStack(ComponentName sourceActivityName) { + final int insertAt = mIntents.size(); + try { + Intent parent = NavUtils.getParentActivityIntent(mSourceContext, sourceActivityName); + while (parent != null) { + mIntents.add(insertAt, parent); + parent = NavUtils.getParentActivityIntent(mSourceContext, parent.getComponent()); + } + } catch (NameNotFoundException e) { + Log.e(TAG, "Bad ComponentName while traversing activity parent metadata"); + throw new IllegalArgumentException(e); + } + return this; + } + + /** + * @return the number of intents added so far. + */ + public int getIntentCount() { + return mIntents.size(); + } + + /** + * Get the intent at the specified index. + * Useful if you need to modify the flags or extras of an intent that was previously added, + * for example with {@link #addParentStack(Activity)}. + * + * @param index Index from 0-getIntentCount() + * @return the intent at position index + * + * @deprecated Renamed to editIntentAt to better reflect intended usage + */ + public Intent getIntent(int index) { + return editIntentAt(index); + } + + /** + * Return the intent at the specified index for modification. + * Useful if you need to modify the flags or extras of an intent that was previously added, + * for example with {@link #addParentStack(Activity)}. + * + * @param index Index from 0-getIntentCount() + * @return the intent at position index + */ + public Intent editIntentAt(int index) { + return mIntents.get(index); + } + + /** + * @deprecated Use editIntentAt instead + */ + public Iterator iterator() { + return mIntents.iterator(); + } + + /** + * Start the task stack constructed by this builder. The Context used to obtain + * this builder must be an Activity. + * + *

On devices that do not support API level 11 or higher the topmost activity + * will be started as a new task. On devices that do support API level 11 or higher + * the new task stack will be created in its entirety.

+ */ + public void startActivities() { + startActivities(null); + } + + /** + * Start the task stack constructed by this builder. The Context used to obtain + * this builder must be an Activity. + * + *

On devices that do not support API level 11 or higher the topmost activity + * will be started as a new task. On devices that do support API level 11 or higher + * the new task stack will be created in its entirety.

+ * + * @param options Additional options for how the Activity should be started. + * See {@link android.content.Context#startActivity(Intent, Bundle) + */ + public void startActivities(Bundle options) { + if (mIntents.isEmpty()) { + throw new IllegalStateException( + "No intents added to TaskStackBuilder; cannot startActivities"); + } + + Intent[] intents = mIntents.toArray(new Intent[mIntents.size()]); + intents[0] = new Intent(intents[0]).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + IntentCompat.FLAG_ACTIVITY_CLEAR_TASK | + IntentCompat.FLAG_ACTIVITY_TASK_ON_HOME); + if (!ContextCompat.startActivities(mSourceContext, intents, options)) { + Intent topIntent = new Intent(intents[intents.length - 1]); + topIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mSourceContext.startActivity(topIntent); + } + } + + /** + * Obtain a {@link PendingIntent} for launching the task constructed by this builder so far. + * + * @param requestCode Private request code for the sender + * @param flags May be {@link PendingIntent#FLAG_ONE_SHOT}, + * {@link PendingIntent#FLAG_NO_CREATE}, {@link PendingIntent#FLAG_CANCEL_CURRENT}, + * {@link PendingIntent#FLAG_UPDATE_CURRENT}, or any of the flags supported by + * {@link Intent#fillIn(Intent, int)} to control which unspecified parts of the + * intent that can be supplied when the actual send happens. + * @return The obtained PendingIntent + */ + public PendingIntent getPendingIntent(int requestCode, int flags) { + return getPendingIntent(requestCode, flags, null); + } + + /** + * Obtain a {@link PendingIntent} for launching the task constructed by this builder so far. + * + * @param requestCode Private request code for the sender + * @param flags May be {@link PendingIntent#FLAG_ONE_SHOT}, + * {@link PendingIntent#FLAG_NO_CREATE}, {@link PendingIntent#FLAG_CANCEL_CURRENT}, + * {@link PendingIntent#FLAG_UPDATE_CURRENT}, or any of the flags supported by + * {@link Intent#fillIn(Intent, int)} to control which unspecified parts of the + * intent that can be supplied when the actual send happens. + * @param options Additional options for how the Activity should be started. + * See {@link android.content.Context#startActivity(Intent, Bundle) + * @return The obtained PendingIntent + */ + public PendingIntent getPendingIntent(int requestCode, int flags, Bundle options) { + if (mIntents.isEmpty()) { + throw new IllegalStateException( + "No intents added to TaskStackBuilder; cannot getPendingIntent"); + } + + Intent[] intents = mIntents.toArray(new Intent[mIntents.size()]); + intents[0] = new Intent(intents[0]).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + IntentCompat.FLAG_ACTIVITY_CLEAR_TASK | + IntentCompat.FLAG_ACTIVITY_TASK_ON_HOME); + // Appropriate flags will be added by the call below. + return IMPL.getPendingIntent(mSourceContext, intents, requestCode, flags, options); + } + + /** + * Return an array containing the intents added to this builder. The intent at the + * root of the task stack will appear as the first item in the array and the + * intent at the top of the stack will appear as the last item. + * + * @return An array containing the intents added to this builder. + */ + public Intent[] getIntents() { + Intent[] intents = new Intent[mIntents.size()]; + if (intents.length == 0) return intents; + + intents[0] = new Intent(mIntents.get(0)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | + IntentCompat.FLAG_ACTIVITY_CLEAR_TASK | + IntentCompat.FLAG_ACTIVITY_TASK_ON_HOME); + for (int i = 1; i < intents.length; i++) { + intents[i] = new Intent(mIntents.get(i)); + } + return intents; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderHoneycomb.java new file mode 100644 index 00000000000..8bb602e4b69 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderHoneycomb.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; + +/** + * Implementation of TaskStackBuilder that can call Honeycomb APIs. + */ +class TaskStackBuilderHoneycomb { + public static PendingIntent getActivitiesPendingIntent(Context context, int requestCode, + Intent[] intents, int flags) { + return PendingIntent.getActivities(context, requestCode, intents, flags); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderJellybean.java b/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderJellybean.java new file mode 100644 index 00000000000..8b79b1be3df --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/TaskStackBuilderJellybean.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.app; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +class TaskStackBuilderJellybean { + + public static PendingIntent getActivitiesPendingIntent(Context context, int requestCode, + Intent[] intents, int flags, Bundle options) { + return PendingIntent.getActivities(context, requestCode, intents, flags, options); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/app/package.html b/mobile/google-android-support-v4/src/android/support/v4/app/package.html new file mode 100644 index 00000000000..02d1b79ce8c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/app/package.html @@ -0,0 +1,8 @@ + + +Support android.app classes to assist with development of applications for +android API level 4 or later. The main features here are backwards-compatible +versions of {@link android.support.v4.app.FragmentManager} and +{@link android.support.v4.app.LoaderManager}. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/AsyncTaskLoader.java b/mobile/google-android-support-v4/src/android/support/v4/content/AsyncTaskLoader.java new file mode 100644 index 00000000000..22fe3a223a6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/AsyncTaskLoader.java @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.Context; +import android.os.Handler; +import android.os.SystemClock; +import android.support.v4.util.TimeUtils; +import android.util.Log; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.concurrent.CountDownLatch; + +/** + * Static library support version of the framework's {@link android.content.AsyncTaskLoader}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public abstract class AsyncTaskLoader extends Loader { + static final String TAG = "AsyncTaskLoader"; + static final boolean DEBUG = false; + + final class LoadTask extends ModernAsyncTask implements Runnable { + + D result; + boolean waiting; + + private CountDownLatch done = new CountDownLatch(1); + + /* Runs on a worker thread */ + @Override + protected D doInBackground(Void... params) { + if (DEBUG) Log.v(TAG, this + " >>> doInBackground"); + result = AsyncTaskLoader.this.onLoadInBackground(); + if (DEBUG) Log.v(TAG, this + " <<< doInBackground"); + return result; + } + + /* Runs on the UI thread */ + @Override + protected void onPostExecute(D data) { + if (DEBUG) Log.v(TAG, this + " onPostExecute"); + try { + AsyncTaskLoader.this.dispatchOnLoadComplete(this, data); + } finally { + done.countDown(); + } + } + + @Override + protected void onCancelled() { + if (DEBUG) Log.v(TAG, this + " onCancelled"); + try { + AsyncTaskLoader.this.dispatchOnCancelled(this, result); + } finally { + done.countDown(); + } + } + + @Override + public void run() { + waiting = false; + AsyncTaskLoader.this.executePendingTask(); + } + } + + volatile LoadTask mTask; + volatile LoadTask mCancellingTask; + + long mUpdateThrottle; + long mLastLoadCompleteTime = -10000; + Handler mHandler; + + public AsyncTaskLoader(Context context) { + super(context); + } + + /** + * Set amount to throttle updates by. This is the minimum time from + * when the last {@link #onLoadInBackground()} call has completed until + * a new load is scheduled. + * + * @param delayMS Amount of delay, in milliseconds. + */ + public void setUpdateThrottle(long delayMS) { + mUpdateThrottle = delayMS; + if (delayMS != 0) { + mHandler = new Handler(); + } + } + + @Override + protected void onForceLoad() { + super.onForceLoad(); + cancelLoad(); + mTask = new LoadTask(); + if (DEBUG) Log.v(TAG, "Preparing load: mTask=" + mTask); + executePendingTask(); + } + + /** + * Attempt to cancel the current load task. See {@link android.os.AsyncTask#cancel(boolean)} + * for more info. Must be called on the main thread of the process. + * + *

Cancelling is not an immediate operation, since the load is performed + * in a background thread. If there is currently a load in progress, this + * method requests that the load be cancelled, and notes this is the case; + * once the background thread has completed its work its remaining state + * will be cleared. If another load request comes in during this time, + * it will be held until the cancelled load is complete. + * + * @return Returns false if the task could not be cancelled, + * typically because it has already completed normally, or + * because {@link #startLoading()} hasn't been called; returns + * true otherwise. + */ + public boolean cancelLoad() { + if (DEBUG) Log.v(TAG, "cancelLoad: mTask=" + mTask); + if (mTask != null) { + if (mCancellingTask != null) { + // There was a pending task already waiting for a previous + // one being canceled; just drop it. + if (DEBUG) Log.v(TAG, + "cancelLoad: still waiting for cancelled task; dropping next"); + if (mTask.waiting) { + mTask.waiting = false; + mHandler.removeCallbacks(mTask); + } + mTask = null; + return false; + } else if (mTask.waiting) { + // There is a task, but it is waiting for the time it should + // execute. We can just toss it. + if (DEBUG) Log.v(TAG, "cancelLoad: task is waiting, dropping it"); + mTask.waiting = false; + mHandler.removeCallbacks(mTask); + mTask = null; + return false; + } else { + boolean cancelled = mTask.cancel(false); + if (DEBUG) Log.v(TAG, "cancelLoad: cancelled=" + cancelled); + if (cancelled) { + mCancellingTask = mTask; + } + mTask = null; + return cancelled; + } + } + return false; + } + + /** + * Called if the task was canceled before it was completed. Gives the class a chance + * to properly dispose of the result. + */ + public void onCanceled(D data) { + } + + void executePendingTask() { + if (mCancellingTask == null && mTask != null) { + if (mTask.waiting) { + mTask.waiting = false; + mHandler.removeCallbacks(mTask); + } + if (mUpdateThrottle > 0) { + long now = SystemClock.uptimeMillis(); + if (now < (mLastLoadCompleteTime+mUpdateThrottle)) { + // Not yet time to do another load. + if (DEBUG) Log.v(TAG, "Waiting until " + + (mLastLoadCompleteTime+mUpdateThrottle) + + " to execute: " + mTask); + mTask.waiting = true; + mHandler.postAtTime(mTask, mLastLoadCompleteTime+mUpdateThrottle); + return; + } + } + if (DEBUG) Log.v(TAG, "Executing: " + mTask); + mTask.executeOnExecutor(ModernAsyncTask.THREAD_POOL_EXECUTOR, (Void[]) null); + } + } + + void dispatchOnCancelled(LoadTask task, D data) { + onCanceled(data); + if (mCancellingTask == task) { + if (DEBUG) Log.v(TAG, "Cancelled task is now canceled!"); + rollbackContentChanged(); + mLastLoadCompleteTime = SystemClock.uptimeMillis(); + mCancellingTask = null; + executePendingTask(); + } + } + + void dispatchOnLoadComplete(LoadTask task, D data) { + if (mTask != task) { + if (DEBUG) Log.v(TAG, "Load complete of old task, trying to cancel"); + dispatchOnCancelled(task, data); + } else { + if (isAbandoned()) { + // This cursor has been abandoned; just cancel the new data. + onCanceled(data); + } else { + commitContentChanged(); + mLastLoadCompleteTime = SystemClock.uptimeMillis(); + mTask = null; + if (DEBUG) Log.v(TAG, "Delivering result"); + deliverResult(data); + } + } + } + + /** + */ + public abstract D loadInBackground(); + + /** + * Called on a worker thread to perform the actual load. Implementations should not deliver the + * result directly, but should return them from this method, which will eventually end up + * calling {@link #deliverResult} on the UI thread. If implementations need to process + * the results on the UI thread they may override {@link #deliverResult} and do so + * there. + * + * @return Implementations must return the result of their load operation. + */ + protected D onLoadInBackground() { + return loadInBackground(); + } + + /** + * Locks the current thread until the loader completes the current load + * operation. Returns immediately if there is no load operation running. + * Should not be called from the UI thread: calling it from the UI + * thread would cause a deadlock. + *

+ * Use for testing only. Never call this from a UI thread. + * + * @hide + */ + public void waitForLoader() { + LoadTask task = mTask; + if (task != null) { + try { + task.done.await(); + } catch (InterruptedException e) { + // Ignore + } + } + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(prefix, fd, writer, args); + if (mTask != null) { + writer.print(prefix); writer.print("mTask="); writer.print(mTask); + writer.print(" waiting="); writer.println(mTask.waiting); + } + if (mCancellingTask != null) { + writer.print(prefix); writer.print("mCancellingTask="); writer.print(mCancellingTask); + writer.print(" waiting="); writer.println(mCancellingTask.waiting); + } + if (mUpdateThrottle != 0) { + writer.print(prefix); writer.print("mUpdateThrottle="); + TimeUtils.formatDuration(mUpdateThrottle, writer); + writer.print(" mLastLoadCompleteTime="); + TimeUtils.formatDuration(mLastLoadCompleteTime, + SystemClock.uptimeMillis(), writer); + writer.println(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompat.java b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompat.java new file mode 100644 index 00000000000..5c632dd5a51 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompat.java @@ -0,0 +1,389 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Environment; +import android.os.StatFs; +import android.support.v4.os.EnvironmentCompat; +import android.util.Log; + +import java.io.File; + +/** + * Helper for accessing features in {@link android.content.Context} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ContextCompat { + private static final String TAG = "ContextCompat"; + + private static final String DIR_ANDROID = "Android"; + private static final String DIR_DATA = "data"; + private static final String DIR_OBB = "obb"; + private static final String DIR_FILES = "files"; + private static final String DIR_CACHE = "cache"; + + /** + * Start a set of activities as a synthesized task stack, if able. + * + *

In API level 11 (Android 3.0/Honeycomb) the recommended conventions for + * app navigation using the back key changed. The back key's behavior is local + * to the current task and does not capture navigation across different tasks. + * Navigating across tasks and easily reaching the previous task is accomplished + * through the "recents" UI, accessible through the software-provided Recents key + * on the navigation or system bar. On devices with the older hardware button configuration + * the recents UI can be accessed with a long press on the Home key.

+ * + *

When crossing from one task stack to another post-Android 3.0, + * the application should synthesize a back stack/history for the new task so that + * the user may navigate out of the new task and back to the Launcher by repeated + * presses of the back key. Back key presses should not navigate across task stacks.

+ * + *

startActivities provides a mechanism for constructing a synthetic task stack of + * multiple activities. If the underlying API is not available on the system this method + * will return false.

+ * + * @param context Start activities using this activity as the starting context + * @param intents Array of intents defining the activities that will be started. The element + * length-1 will correspond to the top activity on the resulting task stack. + * @return true if the underlying API was available and the call was successful, false otherwise + */ + public static boolean startActivities(Context context, Intent[] intents) { + return startActivities(context, intents, null); + } + + /** + * Start a set of activities as a synthesized task stack, if able. + * + *

In API level 11 (Android 3.0/Honeycomb) the recommended conventions for + * app navigation using the back key changed. The back key's behavior is local + * to the current task and does not capture navigation across different tasks. + * Navigating across tasks and easily reaching the previous task is accomplished + * through the "recents" UI, accessible through the software-provided Recents key + * on the navigation or system bar. On devices with the older hardware button configuration + * the recents UI can be accessed with a long press on the Home key.

+ * + *

When crossing from one task stack to another post-Android 3.0, + * the application should synthesize a back stack/history for the new task so that + * the user may navigate out of the new task and back to the Launcher by repeated + * presses of the back key. Back key presses should not navigate across task stacks.

+ * + *

startActivities provides a mechanism for constructing a synthetic task stack of + * multiple activities. If the underlying API is not available on the system this method + * will return false.

+ * + * @param context Start activities using this activity as the starting context + * @param intents Array of intents defining the activities that will be started. The element + * length-1 will correspond to the top activity on the resulting task stack. + * @param options Additional options for how the Activity should be started. + * See {@link android.content.Context#startActivity(Intent, Bundle) + * @return true if the underlying API was available and the call was successful, false otherwise + */ + public static boolean startActivities(Context context, Intent[] intents, + Bundle options) { + final int version = Build.VERSION.SDK_INT; + if (version >= 16) { + ContextCompatJellybean.startActivities(context, intents, options); + return true; + } else if (version >= 11) { + ContextCompatHoneycomb.startActivities(context, intents); + return true; + } + return false; + } + + /** + * Returns absolute paths to application-specific directories on all + * external storage devices where the application's OBB files (if there are + * any) can be found. Note if the application does not have any OBB files, + * these directories may not exist. + *

+ * This is like {@link Context#getFilesDir()} in that these files will be + * deleted when the application is uninstalled, however there are some + * important differences: + *

    + *
  • External files are not always available: they will disappear if the + * user mounts the external storage on a computer or removes it. + *
  • There is no security enforced with these files. + *
+ *

+ * External storage devices returned here are considered a permanent part of + * the device, including both emulated external storage and physical media + * slots, such as SD cards in a battery compartment. The returned paths do + * not include transient devices, such as USB flash drives. + *

+ * An application may store data on any or all of the returned devices. For + * example, an app may choose to store large files on the device with the + * most available space, as measured by {@link StatFs}. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#KITKAT}, no permissions + * are required to write to the returned paths; they're always accessible to + * the calling app. Before then, + * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} is required to + * write. Write access outside of these paths on secondary external storage + * devices is not available. To request external storage access in a + * backwards compatible way, consider using {@code android:maxSdkVersion} + * like this: + * + *

<uses-permission
+     *     android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+     *     android:maxSdkVersion="18" />
+ *

+ * The first path returned is the same as {@link Context#getObbDir()}. + * Returned paths may be {@code null} if a storage device is unavailable. + * + * @see Context#getObbDir() + * @see EnvironmentCompat#getStorageState(File) + */ + public static File[] getObbDirs(Context context) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return ContextCompatKitKat.getObbDirs(context); + } else { + final File single; + if (version >= 11) { + single = ContextCompatHoneycomb.getObbDir(context); + } else { + single = buildPath(Environment.getExternalStorageDirectory(), DIR_ANDROID, DIR_OBB, + context.getPackageName()); + } + return new File[] { single }; + } + } + + /** + * Returns absolute paths to application-specific directories on all + * external storage devices where the application can place persistent files + * it owns. These files are internal to the application, and not typically + * visible to the user as media. + *

+ * This is like {@link Context#getFilesDir()} in that these files will be + * deleted when the application is uninstalled, however there are some + * important differences: + *

    + *
  • External files are not always available: they will disappear if the + * user mounts the external storage on a computer or removes it. + *
  • There is no security enforced with these files. + *
+ *

+ * External storage devices returned here are considered a permanent part of + * the device, including both emulated external storage and physical media + * slots, such as SD cards in a battery compartment. The returned paths do + * not include transient devices, such as USB flash drives. + *

+ * An application may store data on any or all of the returned devices. For + * example, an app may choose to store large files on the device with the + * most available space, as measured by {@link StatFs}. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#KITKAT}, no permissions + * are required to write to the returned paths; they're always accessible to + * the calling app. Before then, + * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} is required to + * write. Write access outside of these paths on secondary external storage + * devices is not available. To request external storage access in a + * backwards compatible way, consider using {@code android:maxSdkVersion} + * like this: + * + *

<uses-permission
+     *     android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+     *     android:maxSdkVersion="18" />
+ *

+ * The first path returned is the same as + * {@link Context#getExternalFilesDir(String)}. Returned paths may be + * {@code null} if a storage device is unavailable. + * + * @see Context#getExternalFilesDir(String) + * @see EnvironmentCompat#getStorageState(File) + */ + public static File[] getExternalFilesDirs(Context context, String type) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return ContextCompatKitKat.getExternalFilesDirs(context, type); + } else { + final File single; + if (version >= 8) { + single = ContextCompatFroyo.getExternalFilesDir(context, type); + } else { + single = buildPath(Environment.getExternalStorageDirectory(), DIR_ANDROID, DIR_DATA, + context.getPackageName(), DIR_FILES, type); + } + return new File[] { single }; + } + } + + /** + * Returns absolute paths to application-specific directories on all + * external storage devices where the application can place cache files it + * owns. These files are internal to the application, and not typically + * visible to the user as media. + *

+ * This is like {@link Context#getCacheDir()} in that these files will be + * deleted when the application is uninstalled, however there are some + * important differences: + *

    + *
  • External files are not always available: they will disappear if the + * user mounts the external storage on a computer or removes it. + *
  • There is no security enforced with these files. + *
+ *

+ * External storage devices returned here are considered a permanent part of + * the device, including both emulated external storage and physical media + * slots, such as SD cards in a battery compartment. The returned paths do + * not include transient devices, such as USB flash drives. + *

+ * An application may store data on any or all of the returned devices. For + * example, an app may choose to store large files on the device with the + * most available space, as measured by {@link StatFs}. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#KITKAT}, no permissions + * are required to write to the returned paths; they're always accessible to + * the calling app. Before then, + * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} is required to + * write. Write access outside of these paths on secondary external storage + * devices is not available. To request external storage access in a + * backwards compatible way, consider using {@code android:maxSdkVersion} + * like this: + * + *

<uses-permission
+     *     android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+     *     android:maxSdkVersion="18" />
+ *

+ * The first path returned is the same as + * {@link Context#getExternalCacheDir()}. Returned paths may be {@code null} + * if a storage device is unavailable. + * + * @see Context#getExternalCacheDir() + * @see EnvironmentCompat#getStorageState(File) + */ + public static File[] getExternalCacheDirs(Context context) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return ContextCompatKitKat.getExternalCacheDirs(context); + } else { + final File single; + if (version >= 8) { + single = ContextCompatFroyo.getExternalCacheDir(context); + } else { + single = buildPath(Environment.getExternalStorageDirectory(), DIR_ANDROID, DIR_DATA, + context.getPackageName(), DIR_CACHE); + } + return new File[] { single }; + } + } + + private static File buildPath(File base, String... segments) { + File cur = base; + for (String segment : segments) { + if (cur == null) { + cur = new File(segment); + } else if (segment != null) { + cur = new File(cur, segment); + } + } + return cur; + } + + /** + * Return a drawable object associated with a particular resource ID. + *

+ * Starting in {@link android.os.Build.VERSION_CODES#LOLLIPOP}, the returned + * drawable will be styled for the specified Context's theme. + * + * @param id The desired resource identifier, as generated by the aapt tool. + * This integer encodes the package, type, and resource entry. + * The value 0 is an invalid identifier. + * @return Drawable An object that can be used to draw this resource. + */ + public static final Drawable getDrawable(Context context, int id) { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return ContextCompatApi21.getDrawable(context, id); + } else { + return context.getResources().getDrawable(id); + } + } + + /** + * Returns the absolute path to the directory on the filesystem similar to + * {@link Context#getFilesDir()}. The difference is that files placed under this + * directory will be excluded from automatic backup to remote storage on + * devices running {@link android.os.Build.VERSION_CODES#LOLLIPOP} or later. See + * {@link android.app.backup.BackupAgent BackupAgent} for a full discussion + * of the automatic backup mechanism in Android. + * + *

No permissions are required to read or write to the returned path, since this + * path is internal storage. + * + * @return The path of the directory holding application files that will not be + * automatically backed up to remote storage. + * + * @see android.content.Context.getFilesDir + */ + public final File getNoBackupFilesDir(Context context) { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return ContextCompatApi21.getNoBackupFilesDir(context); + } else { + ApplicationInfo appInfo = context.getApplicationInfo(); + return createFilesDir(new File(appInfo.dataDir, "no_backup")); + } + } + + /** + * Returns the absolute path to the application specific cache directory on + * the filesystem designed for storing cached code. On devices running + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} or later, the system will delete + * any files stored in this location both when your specific application is + * upgraded, and when the entire platform is upgraded. + *

+ * This location is optimal for storing compiled or optimized code generated + * by your application at runtime. + *

+ * Apps require no extra permissions to read or write to the returned path, + * since this path lives in their private storage. + * + * @return The path of the directory holding application code cache files. + */ + public final File getCodeCacheDir(Context context) { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return ContextCompatApi21.getCodeCacheDir(context); + } else { + ApplicationInfo appInfo = context.getApplicationInfo(); + return createFilesDir(new File(appInfo.dataDir, "code_cache")); + } + } + + private synchronized static File createFilesDir(File file) { + if (!file.exists()) { + if (!file.mkdirs()) { + if (file.exists()) { + // spurious failure; probably racing with another process for this app + return file; + } + Log.w(TAG, "Unable to create files subdir " + file.getPath()); + return null; + } + } + return file; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatApi21.java new file mode 100644 index 00000000000..cf93924db41 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatApi21.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.Context; +import android.graphics.drawable.Drawable; + +import java.io.File; + +class ContextCompatApi21 { + public static Drawable getDrawable(Context context, int id) { + return context.getDrawable(id); + } + + public static File getNoBackupFilesDir(Context context) { + return context.getNoBackupFilesDir(); + } + + public static File getCodeCacheDir(Context context) { + return context.getCodeCacheDir(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatFroyo.java b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatFroyo.java new file mode 100644 index 00000000000..1fc2f8d8f5d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatFroyo.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.Context; + +import java.io.File; + +class ContextCompatFroyo { + public static File getExternalCacheDir(Context context) { + return context.getExternalCacheDir(); + } + + public static File getExternalFilesDir(Context context, String type) { + return context.getExternalFilesDir(type); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatHoneycomb.java new file mode 100644 index 00000000000..9abc97d3208 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatHoneycomb.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.Context; +import android.content.Intent; + +import java.io.File; + +/** + * Implementation of context compatibility that can call Honeycomb APIs. + */ +class ContextCompatHoneycomb { + + static void startActivities(Context context, Intent[] intents) { + context.startActivities(intents); + } + + public static File getObbDir(Context context) { + return context.getObbDir(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatJellybean.java b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatJellybean.java new file mode 100644 index 00000000000..c353b96308f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatJellybean.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +class ContextCompatJellybean { + + public static void startActivities(Context context, Intent[] intents, Bundle options) { + context.startActivities(intents, options); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatKitKat.java new file mode 100644 index 00000000000..6b58827bf4f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/ContextCompatKitKat.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.Context; + +import java.io.File; + +class ContextCompatKitKat { + public static File[] getExternalCacheDirs(Context context) { + return context.getExternalCacheDirs(); + } + + public static File[] getExternalFilesDirs(Context context, String type) { + return context.getExternalFilesDirs(type); + } + + public static File[] getObbDirs(Context context) { + return context.getObbDirs(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/CursorLoader.java b/mobile/google-android-support-v4/src/android/support/v4/content/CursorLoader.java new file mode 100644 index 00000000000..980e7d96f02 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/CursorLoader.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.net.Uri; + +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.Arrays; + +/** + * Static library support version of the framework's {@link android.content.CursorLoader}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class CursorLoader extends AsyncTaskLoader { + final ForceLoadContentObserver mObserver; + + Uri mUri; + String[] mProjection; + String mSelection; + String[] mSelectionArgs; + String mSortOrder; + + Cursor mCursor; + + /* Runs on a worker thread */ + @Override + public Cursor loadInBackground() { + Cursor cursor = getContext().getContentResolver().query(mUri, mProjection, mSelection, + mSelectionArgs, mSortOrder); + if (cursor != null) { + // Ensure the cursor window is filled + cursor.getCount(); + cursor.registerContentObserver(mObserver); + } + return cursor; + } + + /* Runs on the UI thread */ + @Override + public void deliverResult(Cursor cursor) { + if (isReset()) { + // An async query came in while the loader is stopped + if (cursor != null) { + cursor.close(); + } + return; + } + Cursor oldCursor = mCursor; + mCursor = cursor; + + if (isStarted()) { + super.deliverResult(cursor); + } + + if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) { + oldCursor.close(); + } + } + + /** + * Creates an empty unspecified CursorLoader. You must follow this with + * calls to {@link #setUri(Uri)}, {@link #setSelection(String)}, etc + * to specify the query to perform. + */ + public CursorLoader(Context context) { + super(context); + mObserver = new ForceLoadContentObserver(); + } + + /** + * Creates a fully-specified CursorLoader. See + * {@link android.content.ContentResolver#query(Uri, String[], String, String[], String) + * ContentResolver.query()} for documentation on the meaning of the + * parameters. These will be passed as-is to that call. + */ + public CursorLoader(Context context, Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + super(context); + mObserver = new ForceLoadContentObserver(); + mUri = uri; + mProjection = projection; + mSelection = selection; + mSelectionArgs = selectionArgs; + mSortOrder = sortOrder; + } + + /** + * Starts an asynchronous load of the contacts list data. When the result is ready the callbacks + * will be called on the UI thread. If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. + * + * Must be called from the UI thread + */ + @Override + protected void onStartLoading() { + if (mCursor != null) { + deliverResult(mCursor); + } + if (takeContentChanged() || mCursor == null) { + forceLoad(); + } + } + + /** + * Must be called from the UI thread + */ + @Override + protected void onStopLoading() { + // Attempt to cancel the current load task if possible. + cancelLoad(); + } + + @Override + public void onCanceled(Cursor cursor) { + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + + @Override + protected void onReset() { + super.onReset(); + + // Ensure the loader is stopped + onStopLoading(); + + if (mCursor != null && !mCursor.isClosed()) { + mCursor.close(); + } + mCursor = null; + } + + public Uri getUri() { + return mUri; + } + + public void setUri(Uri uri) { + mUri = uri; + } + + public String[] getProjection() { + return mProjection; + } + + public void setProjection(String[] projection) { + mProjection = projection; + } + + public String getSelection() { + return mSelection; + } + + public void setSelection(String selection) { + mSelection = selection; + } + + public String[] getSelectionArgs() { + return mSelectionArgs; + } + + public void setSelectionArgs(String[] selectionArgs) { + mSelectionArgs = selectionArgs; + } + + public String getSortOrder() { + return mSortOrder; + } + + public void setSortOrder(String sortOrder) { + mSortOrder = sortOrder; + } + + @Override + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + super.dump(prefix, fd, writer, args); + writer.print(prefix); writer.print("mUri="); writer.println(mUri); + writer.print(prefix); writer.print("mProjection="); + writer.println(Arrays.toString(mProjection)); + writer.print(prefix); writer.print("mSelection="); writer.println(mSelection); + writer.print(prefix); writer.print("mSelectionArgs="); + writer.println(Arrays.toString(mSelectionArgs)); + writer.print(prefix); writer.print("mSortOrder="); writer.println(mSortOrder); + writer.print(prefix); writer.print("mCursor="); writer.println(mCursor); + writer.print(prefix); writer.print("mContentChanged="); writer.println(mContentChanged); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/FileProvider.java b/mobile/google-android-support-v4/src/android/support/v4/content/FileProvider.java new file mode 100644 index 00000000000..515ce1cd38a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/FileProvider.java @@ -0,0 +1,773 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.START_TAG; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ProviderInfo; +import android.content.res.XmlResourceParser; +import android.database.Cursor; +import android.database.MatrixCursor; +import android.net.Uri; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.provider.OpenableColumns; +import android.text.TextUtils; +import android.webkit.MimeTypeMap; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * FileProvider is a special subclass of {@link ContentProvider} that facilitates secure sharing + * of files associated with an app by creating a content:// {@link Uri} for a file + * instead of a file:/// {@link Uri}. + *

+ * A content URI allows you to grant read and write access using + * temporary access permissions. When you create an {@link Intent} containing + * a content URI, in order to send the content URI + * to a client app, you can also call {@link Intent#setFlags(int) Intent.setFlags()} to add + * permissions. These permissions are available to the client app for as long as the stack for + * a receiving {@link android.app.Activity} is active. For an {@link Intent} going to a + * {@link android.app.Service}, the permissions are available as long as the + * {@link android.app.Service} is running. + *

+ * In comparison, to control access to a file:/// {@link Uri} you have to modify the + * file system permissions of the underlying file. The permissions you provide become available to + * any app, and remain in effect until you change them. This level of access is + * fundamentally insecure. + *

+ * The increased level of file access security offered by a content URI + * makes FileProvider a key part of Android's security infrastructure. + *

+ * This overview of FileProvider includes the following topics: + *

+ *
    + *
  1. Defining a FileProvider
  2. + *
  3. Specifying Available Files
  4. + *
  5. Retrieving the Content URI for a File
  6. + *
  7. Granting Temporary Permissions to a URI
  8. + *
  9. Serving a Content URI to Another App
  10. + *
+ *

Defining a FileProvider

+ *

+ * Since the default functionality of FileProvider includes content URI generation for files, you + * don't need to define a subclass in code. Instead, you can include a FileProvider in your app + * by specifying it entirely in XML. To specify the FileProvider component itself, add a + * <provider> + * element to your app manifest. Set the android:name attribute to + * android.support.v4.content.FileProvider. Set the android:authorities + * attribute to a URI authority based on a domain you control; for example, if you control the + * domain mydomain.com you should use the authority + * com.mydomain.fileprovider. Set the android:exported attribute to + * false; the FileProvider does not need to be public. Set the + * android:grantUriPermissions attribute to true, to allow you + * to grant temporary access to files. For example: + *

+ *<manifest>
+ *    ...
+ *    <application>
+ *        ...
+ *        <provider
+ *            android:name="android.support.v4.content.FileProvider"
+ *            android:authorities="com.mydomain.fileprovider"
+ *            android:exported="false"
+ *            android:grantUriPermissions="true">
+ *            ...
+ *        </provider>
+ *        ...
+ *    </application>
+ *</manifest>
+ *

+ * If you want to override any of the default behavior of FileProvider methods, extend + * the FileProvider class and use the fully-qualified class name in the android:name + * attribute of the <provider> element. + *

Specifying Available Files

+ * A FileProvider can only generate a content URI for files in directories that you specify + * beforehand. To specify a directory, specify the its storage area and path in XML, using child + * elements of the <paths> element. + * For example, the following paths element tells FileProvider that you intend to + * request content URIs for the images/ subdirectory of your private file area. + *
+ *<paths xmlns:android="http://schemas.android.com/apk/res/android">
+ *    <files-path name="my_images" path="images/"/>
+ *    ...
+ *</paths>
+ *
+ *

+ * The <paths> element must contain one or more of the following child elements: + *

+ *
+ *
+ *
+ *<files-path name="name" path="path" />
+ *
+ *
+ *
+ * Represents files in the files/ subdirectory of your app's internal storage + * area. This subdirectory is the same as the value returned by {@link Context#getFilesDir() + * Context.getFilesDir()}. + *
+ *
+ *<external-path name="name" path="path" />
+ *
+ *
+ *
+ * Represents files in the root of your app's external storage area. The path + * {@link Context#getExternalFilesDir(String) Context.getExternalFilesDir()} returns the + * files/ subdirectory of this this root. + *
+ *
+ *
+ *<cache-path name="name" path="path" />
+ *
+ *
+ *
+ * Represents files in the cache subdirectory of your app's internal storage area. The root path + * of this subdirectory is the same as the value returned by {@link Context#getCacheDir() + * getCacheDir()}. + *
+ *
+ *

+ * These child elements all use the same attributes: + *

+ *
+ *
+ * name="name" + *
+ *
+ * A URI path segment. To enforce security, this value hides the name of the subdirectory + * you're sharing. The subdirectory name for this value is contained in the + * path attribute. + *
+ *
+ * path="path" + *
+ *
+ * The subdirectory you're sharing. While the name attribute is a URI path + * segment, the path value is an actual subdirectory name. Notice that the + * value refers to a subdirectory, not an individual file or files. You can't + * share a single file by its file name, nor can you specify a subset of files using + * wildcards. + *
+ *
+ *

+ * You must specify a child element of <paths> for each directory that contains + * files for which you want content URIs. For example, these XML elements specify two directories: + *

+ *<paths xmlns:android="http://schemas.android.com/apk/res/android">
+ *    <files-path name="my_images" path="images/"/>
+ *    <files-path name="my_docs" path="docs/"/>
+ *</paths>
+ *
+ *

+ * Put the <paths> element and its children in an XML file in your project. + * For example, you can add them to a new file called res/xml/file_paths.xml. + * To link this file to the FileProvider, add a + * <meta-data> element + * as a child of the <provider> element that defines the FileProvider. Set the + * <meta-data> element's "android:name" attribute to + * android.support.FILE_PROVIDER_PATHS. Set the element's "android:resource" attribute + * to @xml/file_paths (notice that you don't specify the .xml + * extension). For example: + *

+ *<provider
+ *    android:name="android.support.v4.content.FileProvider"
+ *    android:authorities="com.mydomain.fileprovider"
+ *    android:exported="false"
+ *    android:grantUriPermissions="true">
+ *    <meta-data
+ *        android:name="android.support.FILE_PROVIDER_PATHS"
+ *        android:resource="@xml/file_paths" />
+ *</provider>
+ *
+ *

Generating the Content URI for a File

+ *

+ * To share a file with another app using a content URI, your app has to generate the content URI. + * To generate the content URI, create a new {@link File} for the file, then pass the {@link File} + * to {@link #getUriForFile(Context, String, File) getUriForFile()}. You can send the content URI + * returned by {@link #getUriForFile(Context, String, File) getUriForFile()} to another app in an + * {@link android.content.Intent}. The client app that receives the content URI can open the file + * and access its contents by calling + * {@link android.content.ContentResolver#openFileDescriptor(Uri, String) + * ContentResolver.openFileDescriptor} to get a {@link ParcelFileDescriptor}. + *

+ * For example, suppose your app is offering files to other apps with a FileProvider that has the + * authority com.mydomain.fileprovider. To get a content URI for the file + * default_image.jpg in the images/ subdirectory of your internal storage + * add the following code: + *

+ *File imagePath = new File(Context.getFilesDir(), "images");
+ *File newFile = new File(imagePath, "default_image.jpg");
+ *Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
+ *
+ * As a result of the previous snippet, + * {@link #getUriForFile(Context, String, File) getUriForFile()} returns the content URI + * content://com.mydomain.fileprovider/my_images/default_image.jpg. + *

Granting Temporary Permissions to a URI

+ * To grant an access permission to a content URI returned from + * {@link #getUriForFile(Context, String, File) getUriForFile()}, do one of the following: + *
    + *
  • + * Call the method + * {@link Context#grantUriPermission(String, Uri, int) + * Context.grantUriPermission(package, Uri, mode_flags)} for the content:// + * {@link Uri}, using the desired mode flags. This grants temporary access permission for the + * content URI to the specified package, according to the value of the + * the mode_flags parameter, which you can set to + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}, {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} + * or both. The permission remains in effect until you revoke it by calling + * {@link Context#revokeUriPermission(Uri, int) revokeUriPermission()} or until the device + * reboots. + *
  • + *
  • + * Put the content URI in an {@link Intent} by calling {@link Intent#setData(Uri) setData()}. + *
  • + *
  • + * Next, call the method {@link Intent#setFlags(int) Intent.setFlags()} with either + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} or + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} or both. + *
  • + *
  • + * Finally, send the {@link Intent} to + * another app. Most often, you do this by calling + * {@link android.app.Activity#setResult(int, android.content.Intent) setResult()}. + *

    + * Permissions granted in an {@link Intent} remain in effect while the stack of the receiving + * {@link android.app.Activity} is active. When the stack finishes, the permissions are + * automatically removed. Permissions granted to one {@link android.app.Activity} in a client + * app are automatically extended to other components of that app. + *

    + *
  • + *
+ *

Serving a Content URI to Another App

+ *

+ * There are a variety of ways to serve the content URI for a file to a client app. One common way + * is for the client app to start your app by calling + * {@link android.app.Activity#startActivityForResult(Intent, int, Bundle) startActivityResult()}, + * which sends an {@link Intent} to your app to start an {@link android.app.Activity} in your app. + * In response, your app can immediately return a content URI to the client app or present a user + * interface that allows the user to pick a file. In the latter case, once the user picks the file + * your app can return its content URI. In both cases, your app returns the content URI in an + * {@link Intent} sent via {@link android.app.Activity#setResult(int, Intent) setResult()}. + *

+ *

+ * You can also put the content URI in a {@link android.content.ClipData} object and then add the + * object to an {@link Intent} you send to a client app. To do this, call + * {@link Intent#setClipData(ClipData) Intent.setClipData()}. When you use this approach, you can + * add multiple {@link android.content.ClipData} objects to the {@link Intent}, each with its own + * content URI. When you call {@link Intent#setFlags(int) Intent.setFlags()} on the {@link Intent} + * to set temporary access permissions, the same permissions are applied to all of the content + * URIs. + *

+ *

+ * Note: The {@link Intent#setClipData(ClipData) Intent.setClipData()} method is + * only available in platform version 16 (Android 4.1) and later. If you want to maintain + * compatibility with previous versions, you should send one content URI at a time in the + * {@link Intent}. Set the action to {@link Intent#ACTION_SEND} and put the URI in data by calling + * {@link Intent#setData setData()}. + *

+ *

More Information

+ *

+ * To learn more about FileProvider, see the Android training class + * Sharing Files Securely with URIs. + *

+ */ +public class FileProvider extends ContentProvider { + private static final String[] COLUMNS = { + OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }; + + private static final String + META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS"; + + private static final String TAG_ROOT_PATH = "root-path"; + private static final String TAG_FILES_PATH = "files-path"; + private static final String TAG_CACHE_PATH = "cache-path"; + private static final String TAG_EXTERNAL = "external-path"; + + private static final String ATTR_NAME = "name"; + private static final String ATTR_PATH = "path"; + + private static final File DEVICE_ROOT = new File("/"); + + // @GuardedBy("sCache") + private static HashMap sCache = new HashMap(); + + private PathStrategy mStrategy; + + /** + * The default FileProvider implementation does not need to be initialized. If you want to + * override this method, you must provide your own subclass of FileProvider. + */ + @Override + public boolean onCreate() { + return true; + } + + /** + * After the FileProvider is instantiated, this method is called to provide the system with + * information about the provider. + * + * @param context A {@link Context} for the current component. + * @param info A {@link ProviderInfo} for the new provider. + */ + @Override + public void attachInfo(Context context, ProviderInfo info) { + super.attachInfo(context, info); + + // Sanity check our security + if (info.exported) { + throw new SecurityException("Provider must not be exported"); + } + if (!info.grantUriPermissions) { + throw new SecurityException("Provider must grant uri permissions"); + } + + mStrategy = getPathStrategy(context, info.authority); + } + + /** + * Return a content URI for a given {@link File}. Specific temporary + * permissions for the content URI can be set with + * {@link Context#grantUriPermission(String, Uri, int)}, or added + * to an {@link Intent} by calling {@link Intent#setData(Uri) setData()} and then + * {@link Intent#setFlags(int) setFlags()}; in both cases, the applicable flags are + * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and + * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. A FileProvider can only return a + * content {@link Uri} for file paths defined in their <paths> + * meta-data element. See the Class Overview for more information. + * + * @param context A {@link Context} for the current component. + * @param authority The authority of a {@link FileProvider} defined in a + * {@code <provider>} element in your app's manifest. + * @param file A {@link File} pointing to the filename for which you want a + * content {@link Uri}. + * @return A content URI for the file. + * @throws IllegalArgumentException When the given {@link File} is outside + * the paths supported by the provider. + */ + public static Uri getUriForFile(Context context, String authority, File file) { + final PathStrategy strategy = getPathStrategy(context, authority); + return strategy.getUriForFile(file); + } + + /** + * Use a content URI returned by + * {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file + * managed by the FileProvider. + * FileProvider reports the column names defined in {@link android.provider.OpenableColumns}: + *
    + *
  • {@link android.provider.OpenableColumns#DISPLAY_NAME}
  • + *
  • {@link android.provider.OpenableColumns#SIZE}
  • + *
+ * For more information, see + * {@link ContentProvider#query(Uri, String[], String, String[], String) + * ContentProvider.query()}. + * + * @param uri A content URI returned by {@link #getUriForFile}. + * @param projection The list of columns to put into the {@link Cursor}. If null all columns are + * included. + * @param selection Selection criteria to apply. If null then all data that matches the content + * URI is returned. + * @param selectionArgs An array of {@link java.lang.String}, containing arguments to bind to + * the selection parameter. The query method scans selection from left to + * right and iterates through selectionArgs, replacing the current "?" character in + * selection with the value at the current position in selectionArgs. The + * values are bound to selection as {@link java.lang.String} values. + * @param sortOrder A {@link java.lang.String} containing the column name(s) on which to sort + * the resulting {@link Cursor}. + * @return A {@link Cursor} containing the results of the query. + * + */ + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + // ContentProvider has already checked granted permissions + final File file = mStrategy.getFileForUri(uri); + + if (projection == null) { + projection = COLUMNS; + } + + String[] cols = new String[projection.length]; + Object[] values = new Object[projection.length]; + int i = 0; + for (String col : projection) { + if (OpenableColumns.DISPLAY_NAME.equals(col)) { + cols[i] = OpenableColumns.DISPLAY_NAME; + values[i++] = file.getName(); + } else if (OpenableColumns.SIZE.equals(col)) { + cols[i] = OpenableColumns.SIZE; + values[i++] = file.length(); + } + } + + cols = copyOf(cols, i); + values = copyOf(values, i); + + final MatrixCursor cursor = new MatrixCursor(cols, 1); + cursor.addRow(values); + return cursor; + } + + /** + * Returns the MIME type of a content URI returned by + * {@link #getUriForFile(Context, String, File) getUriForFile()}. + * + * @param uri A content URI returned by + * {@link #getUriForFile(Context, String, File) getUriForFile()}. + * @return If the associated file has an extension, the MIME type associated with that + * extension; otherwise application/octet-stream. + */ + @Override + public String getType(Uri uri) { + // ContentProvider has already checked granted permissions + final File file = mStrategy.getFileForUri(uri); + + final int lastDot = file.getName().lastIndexOf('.'); + if (lastDot >= 0) { + final String extension = file.getName().substring(lastDot + 1); + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mime != null) { + return mime; + } + } + + return "application/octet-stream"; + } + + /** + * By default, this method throws an {@link java.lang.UnsupportedOperationException}. You must + * subclass FileProvider if you want to provide different functionality. + */ + @Override + public Uri insert(Uri uri, ContentValues values) { + throw new UnsupportedOperationException("No external inserts"); + } + + /** + * By default, this method throws an {@link java.lang.UnsupportedOperationException}. You must + * subclass FileProvider if you want to provide different functionality. + */ + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + throw new UnsupportedOperationException("No external updates"); + } + + /** + * Deletes the file associated with the specified content URI, as + * returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this + * method does not throw an {@link java.io.IOException}; you must check its return value. + * + * @param uri A content URI for a file, as returned by + * {@link #getUriForFile(Context, String, File) getUriForFile()}. + * @param selection Ignored. Set to {@code null}. + * @param selectionArgs Ignored. Set to {@code null}. + * @return 1 if the delete succeeds; otherwise, 0. + */ + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + // ContentProvider has already checked granted permissions + final File file = mStrategy.getFileForUri(uri); + return file.delete() ? 1 : 0; + } + + /** + * By default, FileProvider automatically returns the + * {@link ParcelFileDescriptor} for a file associated with a content:// + * {@link Uri}. To get the {@link ParcelFileDescriptor}, call + * {@link android.content.ContentResolver#openFileDescriptor(Uri, String) + * ContentResolver.openFileDescriptor}. + * + * To override this method, you must provide your own subclass of FileProvider. + * + * @param uri A content URI associated with a file, as returned by + * {@link #getUriForFile(Context, String, File) getUriForFile()}. + * @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and + * write access, or "rwt" for read and write access that truncates any existing file. + * @return A new {@link ParcelFileDescriptor} with which you can access the file. + */ + @Override + public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { + // ContentProvider has already checked granted permissions + final File file = mStrategy.getFileForUri(uri); + final int fileMode = modeToMode(mode); + return ParcelFileDescriptor.open(file, fileMode); + } + + /** + * Return {@link PathStrategy} for given authority, either by parsing or + * returning from cache. + */ + private static PathStrategy getPathStrategy(Context context, String authority) { + PathStrategy strat; + synchronized (sCache) { + strat = sCache.get(authority); + if (strat == null) { + try { + strat = parsePathStrategy(context, authority); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e); + } catch (XmlPullParserException e) { + throw new IllegalArgumentException( + "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e); + } + sCache.put(authority, strat); + } + } + return strat; + } + + /** + * Parse and return {@link PathStrategy} for given authority as defined in + * {@link #META_DATA_FILE_PROVIDER_PATHS} {@code <meta-data>}. + * + * @see #getPathStrategy(Context, String) + */ + private static PathStrategy parsePathStrategy(Context context, String authority) + throws IOException, XmlPullParserException { + final SimplePathStrategy strat = new SimplePathStrategy(authority); + + final ProviderInfo info = context.getPackageManager() + .resolveContentProvider(authority, PackageManager.GET_META_DATA); + final XmlResourceParser in = info.loadXmlMetaData( + context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS); + if (in == null) { + throw new IllegalArgumentException( + "Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data"); + } + + int type; + while ((type = in.next()) != END_DOCUMENT) { + if (type == START_TAG) { + final String tag = in.getName(); + + final String name = in.getAttributeValue(null, ATTR_NAME); + String path = in.getAttributeValue(null, ATTR_PATH); + + File target = null; + if (TAG_ROOT_PATH.equals(tag)) { + target = buildPath(DEVICE_ROOT, path); + } else if (TAG_FILES_PATH.equals(tag)) { + target = buildPath(context.getFilesDir(), path); + } else if (TAG_CACHE_PATH.equals(tag)) { + target = buildPath(context.getCacheDir(), path); + } else if (TAG_EXTERNAL.equals(tag)) { + target = buildPath(Environment.getExternalStorageDirectory(), path); + } + + if (target != null) { + strat.addRoot(name, target); + } + } + } + + return strat; + } + + /** + * Strategy for mapping between {@link File} and {@link Uri}. + *

+ * Strategies must be symmetric so that mapping a {@link File} to a + * {@link Uri} and then back to a {@link File} points at the original + * target. + *

+ * Strategies must remain consistent across app launches, and not rely on + * dynamic state. This ensures that any generated {@link Uri} can still be + * resolved if your process is killed and later restarted. + * + * @see SimplePathStrategy + */ + interface PathStrategy { + /** + * Return a {@link Uri} that represents the given {@link File}. + */ + public Uri getUriForFile(File file); + + /** + * Return a {@link File} that represents the given {@link Uri}. + */ + public File getFileForUri(Uri uri); + } + + /** + * Strategy that provides access to files living under a narrow whitelist of + * filesystem roots. It will throw {@link SecurityException} if callers try + * accessing files outside the configured roots. + *

+ * For example, if configured with + * {@code addRoot("myfiles", context.getFilesDir())}, then + * {@code context.getFileStreamPath("foo.txt")} would map to + * {@code content://myauthority/myfiles/foo.txt}. + */ + static class SimplePathStrategy implements PathStrategy { + private final String mAuthority; + private final HashMap mRoots = new HashMap(); + + public SimplePathStrategy(String authority) { + mAuthority = authority; + } + + /** + * Add a mapping from a name to a filesystem root. The provider only offers + * access to files that live under configured roots. + */ + public void addRoot(String name, File root) { + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException("Name must not be empty"); + } + + try { + // Resolve to canonical path to keep path checking fast + root = root.getCanonicalFile(); + } catch (IOException e) { + throw new IllegalArgumentException( + "Failed to resolve canonical path for " + root, e); + } + + mRoots.put(name, root); + } + + @Override + public Uri getUriForFile(File file) { + String path; + try { + path = file.getCanonicalPath(); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to resolve canonical path for " + file); + } + + // Find the most-specific root path + Map.Entry mostSpecific = null; + for (Map.Entry root : mRoots.entrySet()) { + final String rootPath = root.getValue().getPath(); + if (path.startsWith(rootPath) && (mostSpecific == null + || rootPath.length() > mostSpecific.getValue().getPath().length())) { + mostSpecific = root; + } + } + + if (mostSpecific == null) { + throw new IllegalArgumentException( + "Failed to find configured root that contains " + path); + } + + // Start at first char of path under root + final String rootPath = mostSpecific.getValue().getPath(); + if (rootPath.endsWith("/")) { + path = path.substring(rootPath.length()); + } else { + path = path.substring(rootPath.length() + 1); + } + + // Encode the tag and path separately + path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/"); + return new Uri.Builder().scheme("content") + .authority(mAuthority).encodedPath(path).build(); + } + + @Override + public File getFileForUri(Uri uri) { + String path = uri.getEncodedPath(); + + final int splitIndex = path.indexOf('/', 1); + final String tag = Uri.decode(path.substring(1, splitIndex)); + path = Uri.decode(path.substring(splitIndex + 1)); + + final File root = mRoots.get(tag); + if (root == null) { + throw new IllegalArgumentException("Unable to find configured root for " + uri); + } + + File file = new File(root, path); + try { + file = file.getCanonicalFile(); + } catch (IOException e) { + throw new IllegalArgumentException("Failed to resolve canonical path for " + file); + } + + if (!file.getPath().startsWith(root.getPath())) { + throw new SecurityException("Resolved path jumped beyond configured root"); + } + + return file; + } + } + + /** + * Copied from ContentResolver.java + */ + private static int modeToMode(String mode) { + int modeBits; + if ("r".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_ONLY; + } else if ("w".equals(mode) || "wt".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_TRUNCATE; + } else if ("wa".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_APPEND; + } else if ("rw".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_WRITE + | ParcelFileDescriptor.MODE_CREATE; + } else if ("rwt".equals(mode)) { + modeBits = ParcelFileDescriptor.MODE_READ_WRITE + | ParcelFileDescriptor.MODE_CREATE + | ParcelFileDescriptor.MODE_TRUNCATE; + } else { + throw new IllegalArgumentException("Invalid mode: " + mode); + } + return modeBits; + } + + private static File buildPath(File base, String... segments) { + File cur = base; + for (String segment : segments) { + if (segment != null) { + cur = new File(cur, segment); + } + } + return cur; + } + + private static String[] copyOf(String[] original, int newLength) { + final String[] result = new String[newLength]; + System.arraycopy(original, 0, result, 0, newLength); + return result; + } + + private static Object[] copyOf(Object[] original, int newLength) { + final Object[] result = new Object[newLength]; + System.arraycopy(original, 0, result, 0, newLength); + return result; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompat.java b/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompat.java new file mode 100644 index 00000000000..11d333443cc --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompat.java @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.Build; + +/** + * Helper for accessing features in {@link android.content.Intent} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class IntentCompat { + + interface IntentCompatImpl { + Intent makeMainActivity(ComponentName componentName); + Intent makeMainSelectorActivity(String selectorAction, String selectorCategory); + Intent makeRestartActivityTask(ComponentName mainActivity); + } + + static class IntentCompatImplBase implements IntentCompatImpl { + @Override + public Intent makeMainActivity(ComponentName componentName) { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setComponent(componentName); + intent.addCategory(Intent.CATEGORY_LAUNCHER); + return intent; + } + + @Override + public Intent makeMainSelectorActivity(String selectorAction, + String selectorCategory) { + // Before api 15 you couldn't set a selector intent. + // Fall back and just return an intent with the requested action/category, + // even though it won't be a proper "main" intent. + Intent intent = new Intent(selectorAction); + intent.addCategory(selectorCategory); + return intent; + } + + @Override + public Intent makeRestartActivityTask(ComponentName mainActivity) { + Intent intent = makeMainActivity(mainActivity); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK + | IntentCompat.FLAG_ACTIVITY_CLEAR_TASK); + return intent; + } + } + + static class IntentCompatImplHC extends IntentCompatImplBase { + @Override + public Intent makeMainActivity(ComponentName componentName) { + return IntentCompatHoneycomb.makeMainActivity(componentName); + } + @Override + public Intent makeRestartActivityTask(ComponentName componentName) { + return IntentCompatHoneycomb.makeRestartActivityTask(componentName); + } + } + + static class IntentCompatImplIcsMr1 extends IntentCompatImplHC { + @Override + public Intent makeMainSelectorActivity(String selectorAction, String selectorCategory) { + return IntentCompatIcsMr1.makeMainSelectorActivity(selectorAction, selectorCategory); + } + } + + private static final IntentCompatImpl IMPL; + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 15) { + IMPL = new IntentCompatImplIcsMr1(); + } else if (version >= 11) { + IMPL = new IntentCompatImplHC(); + } else { + IMPL = new IntentCompatImplBase(); + } + } + + private IntentCompat() { + /* Hide constructor */ + } + + /** + * Broadcast Action: Resources for a set of packages (which were + * previously unavailable) are currently + * available since the media on which they exist is available. + * The extra data {@link #EXTRA_CHANGED_PACKAGE_LIST} contains a + * list of packages whose availability changed. + * The extra data {@link #EXTRA_CHANGED_UID_LIST} contains a + * list of uids of packages whose availability changed. + * Note that the + * packages in this list do not receive this broadcast. + * The specified set of packages are now available on the system. + *

Includes the following extras: + *

    + *
  • {@link #EXTRA_CHANGED_PACKAGE_LIST} is the set of packages + * whose resources(were previously unavailable) are currently available. + * {@link #EXTRA_CHANGED_UID_LIST} is the set of uids of the + * packages whose resources(were previously unavailable) + * are currently available. + *
+ * + *

This is a protected intent that can only be sent + * by the system. + */ + public static final String ACTION_EXTERNAL_APPLICATIONS_AVAILABLE = + "android.intent.action.EXTERNAL_APPLICATIONS_AVAILABLE"; + + /** + * Broadcast Action: Resources for a set of packages are currently + * unavailable since the media on which they exist is unavailable. + * The extra data {@link #EXTRA_CHANGED_PACKAGE_LIST} contains a + * list of packages whose availability changed. + * The extra data {@link #EXTRA_CHANGED_UID_LIST} contains a + * list of uids of packages whose availability changed. + * The specified set of packages can no longer be + * launched and are practically unavailable on the system. + *

Inclues the following extras: + *

    + *
  • {@link #EXTRA_CHANGED_PACKAGE_LIST} is the set of packages + * whose resources are no longer available. + * {@link #EXTRA_CHANGED_UID_LIST} is the set of packages + * whose resources are no longer available. + *
+ * + *

This is a protected intent that can only be sent + * by the system. + */ + public static final String ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE = + "android.intent.action.EXTERNAL_APPLICATIONS_UNAVAILABLE"; + + /** + * This field is part of + * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_AVAILABLE}, + * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE} + * and contains a string array of all of the components that have changed. + */ + public static final String EXTRA_CHANGED_PACKAGE_LIST = + "android.intent.extra.changed_package_list"; + + /** + * This field is part of + * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_AVAILABLE}, + * {@link android.content.Intent#ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE} + * and contains an integer array of uids of all of the components + * that have changed. + */ + public static final String EXTRA_CHANGED_UID_LIST = + "android.intent.extra.changed_uid_list"; + + /** + * A constant String that is associated with the Intent, used with + * {@link android.content.Intent#ACTION_SEND} to supply an alternative to + * {@link android.content.Intent#EXTRA_TEXT} + * as HTML formatted text. Note that you must also supply + * {@link android.content.Intent#EXTRA_TEXT}. + */ + public static final String EXTRA_HTML_TEXT = "android.intent.extra.HTML_TEXT"; + + /** + * If set in an Intent passed to {@link Context#startActivity Context.startActivity()}, + * this flag will cause a newly launching task to be placed on top of the current + * home activity task (if there is one). That is, pressing back from the task + * will always return the user to home even if that was not the last activity they + * saw. This can only be used in conjunction with + * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK}. + */ + public static final int FLAG_ACTIVITY_TASK_ON_HOME = 0x00004000; + + /** + * If set in an Intent passed to {@link Context#startActivity Context.startActivity()}, + * this flag will cause any existing task that would be associated with the + * activity to be cleared before the activity is started. That is, the activity + * becomes the new root of an otherwise empty task, and any old activities + * are finished. This can only be used in conjunction with + * {@link android.content.Intent#FLAG_ACTIVITY_NEW_TASK}. + * + *

This flag will only be obeyed on devices supporting API 11 or higher.

+ */ + public static final int FLAG_ACTIVITY_CLEAR_TASK = 0x00008000; + + /** + * Create an intent to launch the main (root) activity of a task. This + * is the Intent that is started when the application's is launched from + * Home. For anything else that wants to launch an application in the + * same way, it is important that they use an Intent structured the same + * way, and can use this function to ensure this is the case. + * + *

The returned Intent has the given Activity component as its explicit + * component, {@link Intent#ACTION_MAIN ACTION_MAIN} as its action, and includes the + * category {@link Intent#CATEGORY_LAUNCHER CATEGORY_LAUNCHER}. This does not have + * {@link Intent#FLAG_ACTIVITY_NEW_TASK FLAG_ACTIVITY_NEW_TASK} set, + * though typically you will want to do that through {@link Intent#addFlags(int) addFlags(int)} + * on the returned Intent. + * + * @param mainActivity The main activity component that this Intent will + * launch. + * @return Returns a newly created Intent that can be used to launch the + * activity as a main application entry. + * + * @see Intent#setClass + * @see Intent#setComponent + */ + public static Intent makeMainActivity(ComponentName mainActivity) { + return IMPL.makeMainActivity(mainActivity); + } + + + /** + * Make an Intent for the main activity of an application, without + * specifying a specific activity to run but giving a selector to find + * the activity. This results in a final Intent that is structured + * the same as when the application is launched from + * Home. For anything else that wants to launch an application in the + * same way, it is important that they use an Intent structured the same + * way, and can use this function to ensure this is the case. + * + *

The returned Intent has {@link Intent#ACTION_MAIN} as its action, and includes the + * category {@link Intent#CATEGORY_LAUNCHER}. This does not have + * {@link Intent#FLAG_ACTIVITY_NEW_TASK} set, though typically you will want + * to do that through {@link Intent#addFlags(int)} on the returned Intent. + * + * @param selectorAction The action name of the Intent's selector. + * @param selectorCategory The name of a category to add to the Intent's + * selector. + * @return Returns a newly created Intent that can be used to launch the + * activity as a main application entry. + * + * @see #setSelector(Intent) + */ + public static Intent makeMainSelectorActivity(String selectorAction, + String selectorCategory) { + return IMPL.makeMainSelectorActivity(selectorAction, selectorCategory); + } + + /** + * Make an Intent that can be used to re-launch an application's task + * in its base state. This is like {@link #makeMainActivity(ComponentName)}, + * but also sets the flags {@link Intent#FLAG_ACTIVITY_NEW_TASK} and + * {@link IntentCompat#FLAG_ACTIVITY_CLEAR_TASK}. + * + * @param mainActivity The activity component that is the root of the + * task; this is the activity that has been published in the application's + * manifest as the main launcher icon. + * + * @return Returns a newly created Intent that can be used to relaunch the + * activity's task in its root state. + */ + public static Intent makeRestartActivityTask(ComponentName mainActivity) { + return IMPL.makeRestartActivityTask(mainActivity); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatHoneycomb.java new file mode 100644 index 00000000000..6356898184b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatHoneycomb.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.ComponentName; +import android.content.Intent; + +class IntentCompatHoneycomb { + public static Intent makeMainActivity(ComponentName mainActivity) { + return Intent.makeMainActivity(mainActivity); + } + + public static Intent makeRestartActivityTask(ComponentName mainActivity) { + return Intent.makeRestartActivityTask(mainActivity); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatIcsMr1.java b/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatIcsMr1.java new file mode 100644 index 00000000000..0fa2c433530 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/IntentCompatIcsMr1.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.Intent; + +class IntentCompatIcsMr1 { + + public static Intent makeMainSelectorActivity(String selectorAction, String selectorCategory) { + return Intent.makeMainSelectorActivity(selectorAction, selectorCategory); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/Loader.java b/mobile/google-android-support-v4/src/android/support/v4/content/Loader.java new file mode 100644 index 00000000000..bd01b0a2033 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/Loader.java @@ -0,0 +1,405 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.support.v4.util.DebugUtils; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +/** + * Static library support version of the framework's {@link android.content.Loader}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class Loader { + int mId; + OnLoadCompleteListener mListener; + Context mContext; + boolean mStarted = false; + boolean mAbandoned = false; + boolean mReset = true; + boolean mContentChanged = false; + boolean mProcessingChange = false; + + /** + * An implementation of a ContentObserver that takes care of connecting + * it to the Loader to have the loader re-load its data when the observer + * is told it has changed. You do not normally need to use this yourself; + * it is used for you by {@link android.support.v4.content.CursorLoader} + * to take care of executing an update when the cursor's backing data changes. + */ + public final class ForceLoadContentObserver extends ContentObserver { + public ForceLoadContentObserver() { + super(new Handler()); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + onContentChanged(); + } + } + + /** + * Interface that is implemented to discover when a Loader has finished + * loading its data. You do not normally need to implement this yourself; + * it is used in the implementation of {@link android.support.v4.app.LoaderManager} + * to find out when a Loader it is managing has completed so that this can + * be reported to its client. This interface should only be used if a + * Loader is not being used in conjunction with LoaderManager. + */ + public interface OnLoadCompleteListener { + /** + * Called on the thread that created the Loader when the load is complete. + * + * @param loader the loader that completed the load + * @param data the result of the load + */ + public void onLoadComplete(Loader loader, D data); + } + + /** + * Stores away the application context associated with context. Since Loaders can be used + * across multiple activities it's dangerous to store the context directly. + * + * @param context used to retrieve the application context. + */ + public Loader(Context context) { + mContext = context.getApplicationContext(); + } + + /** + * Sends the result of the load to the registered listener. Should only be called by subclasses. + * + * Must be called from the process's main thread. + * + * @param data the result of the load + */ + public void deliverResult(D data) { + if (mListener != null) { + mListener.onLoadComplete(this, data); + } + } + + /** + * @return an application context retrieved from the Context passed to the constructor. + */ + public Context getContext() { + return mContext; + } + + /** + * @return the ID of this loader + */ + public int getId() { + return mId; + } + + /** + * Registers a class that will receive callbacks when a load is complete. + * The callback will be called on the process's main thread so it's safe to + * pass the results to widgets. + * + *

Must be called from the process's main thread. + */ + public void registerListener(int id, OnLoadCompleteListener listener) { + if (mListener != null) { + throw new IllegalStateException("There is already a listener registered"); + } + mListener = listener; + mId = id; + } + + /** + * Remove a listener that was previously added with {@link #registerListener}. + * + * Must be called from the process's main thread. + */ + public void unregisterListener(OnLoadCompleteListener listener) { + if (mListener == null) { + throw new IllegalStateException("No listener register"); + } + if (mListener != listener) { + throw new IllegalArgumentException("Attempting to unregister the wrong listener"); + } + mListener = null; + } + + /** + * Return whether this load has been started. That is, its {@link #startLoading()} + * has been called and no calls to {@link #stopLoading()} or + * {@link #reset()} have yet been made. + */ + public boolean isStarted() { + return mStarted; + } + + /** + * Return whether this loader has been abandoned. In this state, the + * loader must not report any new data, and must keep + * its last reported data valid until it is finally reset. + */ + public boolean isAbandoned() { + return mAbandoned; + } + + /** + * Return whether this load has been reset. That is, either the loader + * has not yet been started for the first time, or its {@link #reset()} + * has been called. + */ + public boolean isReset() { + return mReset; + } + + /** + * Starts an asynchronous load of the Loader's data. When the result + * is ready the callbacks will be called on the process's main thread. + * If a previous load has been completed and is still valid + * the result may be passed to the callbacks immediately. + * The loader will monitor the source of + * the data set and may deliver future callbacks if the source changes. + * Calling {@link #stopLoading} will stop the delivery of callbacks. + * + *

This updates the Loader's internal state so that + * {@link #isStarted()} and {@link #isReset()} will return the correct + * values, and then calls the implementation's {@link #onStartLoading()}. + * + *

Must be called from the process's main thread. + */ + public final void startLoading() { + mStarted = true; + mReset = false; + mAbandoned = false; + onStartLoading(); + } + + /** + * Subclasses must implement this to take care of loading their data, + * as per {@link #startLoading()}. This is not called by clients directly, + * but as a result of a call to {@link #startLoading()}. + */ + protected void onStartLoading() { + } + + /** + * Force an asynchronous load. Unlike {@link #startLoading()} this will ignore a previously + * loaded data set and load a new one. This simply calls through to the + * implementation's {@link #onForceLoad()}. You generally should only call this + * when the loader is started -- that is, {@link #isStarted()} returns true. + * + *

Must be called from the process's main thread. + */ + public void forceLoad() { + onForceLoad(); + } + + /** + * Subclasses must implement this to take care of requests to {@link #forceLoad()}. + * This will always be called from the process's main thread. + */ + protected void onForceLoad() { + } + + /** + * Stops delivery of updates until the next time {@link #startLoading()} is called. + * Implementations should not invalidate their data at this point -- + * clients are still free to use the last data the loader reported. They will, + * however, typically stop reporting new data if the data changes; they can + * still monitor for changes, but must not report them to the client until and + * if {@link #startLoading()} is later called. + * + *

This updates the Loader's internal state so that + * {@link #isStarted()} will return the correct + * value, and then calls the implementation's {@link #onStopLoading()}. + * + *

Must be called from the process's main thread. + */ + public void stopLoading() { + mStarted = false; + onStopLoading(); + } + + /** + * Subclasses must implement this to take care of stopping their loader, + * as per {@link #stopLoading()}. This is not called by clients directly, + * but as a result of a call to {@link #stopLoading()}. + * This will always be called from the process's main thread. + */ + protected void onStopLoading() { + } + + /** + * Tell the Loader that it is being abandoned. This is called prior + * to {@link #reset} to have it retain its current data but not report + * any new data. + */ + public void abandon() { + mAbandoned = true; + onAbandon(); + } + + /** + * Subclasses implement this to take care of being abandoned. This is + * an optional intermediate state prior to {@link #onReset()} -- it means that + * the client is no longer interested in any new data from the loader, + * so the loader must not report any further updates. However, the + * loader must keep its last reported data valid until the final + * {@link #onReset()} happens. You can retrieve the current abandoned + * state with {@link #isAbandoned}. + */ + protected void onAbandon() { + } + + /** + * Resets the state of the Loader. The Loader should at this point free + * all of its resources, since it may never be called again; however, its + * {@link #startLoading()} may later be called at which point it must be + * able to start running again. + * + *

This updates the Loader's internal state so that + * {@link #isStarted()} and {@link #isReset()} will return the correct + * values, and then calls the implementation's {@link #onReset()}. + * + *

Must be called from the process's main thread. + */ + public void reset() { + onReset(); + mReset = true; + mStarted = false; + mAbandoned = false; + mContentChanged = false; + mProcessingChange = false; + } + + /** + * Subclasses must implement this to take care of resetting their loader, + * as per {@link #reset()}. This is not called by clients directly, + * but as a result of a call to {@link #reset()}. + * This will always be called from the process's main thread. + */ + protected void onReset() { + } + + /** + * Take the current flag indicating whether the loader's content had + * changed while it was stopped. If it had, true is returned and the + * flag is cleared. + */ + public boolean takeContentChanged() { + boolean res = mContentChanged; + mContentChanged = false; + mProcessingChange |= res; + return res; + } + + /** + * Commit that you have actually fully processed a content change that + * was returned by {@link #takeContentChanged}. This is for use with + * {@link #rollbackContentChanged()} to handle situations where a load + * is cancelled. Call this when you have completely processed a load + * without it being cancelled. + */ + public void commitContentChanged() { + mProcessingChange = false; + } + + /** + * Report that you have abandoned the processing of a content change that + * was returned by {@link #takeContentChanged()} and would like to rollback + * to the state where there is again a pending content change. This is + * to handle the case where a data load due to a content change has been + * canceled before its data was delivered back to the loader. + */ + public void rollbackContentChanged() { + if (mProcessingChange) { + mContentChanged = true; + } + } + + /** + * Called when {@link ForceLoadContentObserver} detects a change. The + * default implementation checks to see if the loader is currently started; + * if so, it simply calls {@link #forceLoad()}; otherwise, it sets a flag + * so that {@link #takeContentChanged()} returns true. + * + *

Must be called from the process's main thread. + */ + public void onContentChanged() { + if (mStarted) { + forceLoad(); + } else { + // This loader has been stopped, so we don't want to load + // new data right now... but keep track of it changing to + // refresh later if we start again. + mContentChanged = true; + } + } + + /** + * For debugging, converts an instance of the Loader's data class to + * a string that can be printed. Must handle a null data. + */ + public String dataToString(D data) { + StringBuilder sb = new StringBuilder(64); + DebugUtils.buildShortClassTag(data, sb); + sb.append("}"); + return sb.toString(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(64); + DebugUtils.buildShortClassTag(this, sb); + sb.append(" id="); + sb.append(mId); + sb.append("}"); + return sb.toString(); + } + + /** + * Print the Loader's state into the given stream. + * + * @param prefix Text to print at the front of each line. + * @param fd The raw file descriptor that the dump is being sent to. + * @param writer A PrintWriter to which the dump is to be set. + * @param args Additional arguments to the dump request. + */ + public void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { + writer.print(prefix); writer.print("mId="); writer.print(mId); + writer.print(" mListener="); writer.println(mListener); + if (mStarted || mContentChanged || mProcessingChange) { + writer.print(prefix); writer.print("mStarted="); writer.print(mStarted); + writer.print(" mContentChanged="); writer.print(mContentChanged); + writer.print(" mProcessingChange="); writer.println(mProcessingChange); + } + if (mAbandoned || mReset) { + writer.print(prefix); writer.print("mAbandoned="); writer.print(mAbandoned); + writer.print(" mReset="); writer.println(mReset); + } + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/LocalBroadcastManager.java b/mobile/google-android-support-v4/src/android/support/v4/content/LocalBroadcastManager.java new file mode 100644 index 00000000000..7a7c50f4eff --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/LocalBroadcastManager.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Set; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.Uri; +import android.os.Handler; +import android.os.Message; +import android.util.Log; + +/** + * Helper to register for and send broadcasts of Intents to local objects + * within your process. This is has a number of advantages over sending + * global broadcasts with {@link android.content.Context#sendBroadcast}: + *

    + *
  • You know that the data you are broadcasting won't leave your app, so + * don't need to worry about leaking private data. + *
  • It is not possible for other applications to send these broadcasts to + * your app, so you don't need to worry about having security holes they can + * exploit. + *
  • It is more efficient than sending a global broadcast through the + * system. + *
+ */ +public class LocalBroadcastManager { + private static class ReceiverRecord { + final IntentFilter filter; + final BroadcastReceiver receiver; + boolean broadcasting; + + ReceiverRecord(IntentFilter _filter, BroadcastReceiver _receiver) { + filter = _filter; + receiver = _receiver; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(128); + builder.append("Receiver{"); + builder.append(receiver); + builder.append(" filter="); + builder.append(filter); + builder.append("}"); + return builder.toString(); + } + } + + private static class BroadcastRecord { + final Intent intent; + final ArrayList receivers; + + BroadcastRecord(Intent _intent, ArrayList _receivers) { + intent = _intent; + receivers = _receivers; + } + } + + private static final String TAG = "LocalBroadcastManager"; + private static final boolean DEBUG = false; + + private final Context mAppContext; + + private final HashMap> mReceivers + = new HashMap>(); + private final HashMap> mActions + = new HashMap>(); + + private final ArrayList mPendingBroadcasts + = new ArrayList(); + + static final int MSG_EXEC_PENDING_BROADCASTS = 1; + + private final Handler mHandler; + + private static final Object mLock = new Object(); + private static LocalBroadcastManager mInstance; + + public static LocalBroadcastManager getInstance(Context context) { + synchronized (mLock) { + if (mInstance == null) { + mInstance = new LocalBroadcastManager(context.getApplicationContext()); + } + return mInstance; + } + } + + private LocalBroadcastManager(Context context) { + mAppContext = context; + mHandler = new Handler(context.getMainLooper()) { + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_EXEC_PENDING_BROADCASTS: + executePendingBroadcasts(); + break; + default: + super.handleMessage(msg); + } + } + }; + } + + /** + * Register a receive for any local broadcasts that match the given IntentFilter. + * + * @param receiver The BroadcastReceiver to handle the broadcast. + * @param filter Selects the Intent broadcasts to be received. + * + * @see #unregisterReceiver + */ + public void registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { + synchronized (mReceivers) { + ReceiverRecord entry = new ReceiverRecord(filter, receiver); + ArrayList filters = mReceivers.get(receiver); + if (filters == null) { + filters = new ArrayList(1); + mReceivers.put(receiver, filters); + } + filters.add(filter); + for (int i=0; i entries = mActions.get(action); + if (entries == null) { + entries = new ArrayList(1); + mActions.put(action, entries); + } + entries.add(entry); + } + } + } + + /** + * Unregister a previously registered BroadcastReceiver. All + * filters that have been registered for this BroadcastReceiver will be + * removed. + * + * @param receiver The BroadcastReceiver to unregister. + * + * @see #registerReceiver + */ + public void unregisterReceiver(BroadcastReceiver receiver) { + synchronized (mReceivers) { + ArrayList filters = mReceivers.remove(receiver); + if (filters == null) { + return; + } + for (int i=0; i receivers = mActions.get(action); + if (receivers != null) { + for (int k=0; k categories = intent.getCategories(); + + final boolean debug = DEBUG || + ((intent.getFlags() & Intent.FLAG_DEBUG_LOG_RESOLUTION) != 0); + if (debug) Log.v( + TAG, "Resolving type " + type + " scheme " + scheme + + " of intent " + intent); + + ArrayList entries = mActions.get(intent.getAction()); + if (entries != null) { + if (debug) Log.v(TAG, "Action list: " + entries); + + ArrayList receivers = null; + for (int i=0; i= 0) { + if (debug) Log.v(TAG, " Filter matched! match=0x" + + Integer.toHexString(match)); + if (receivers == null) { + receivers = new ArrayList(); + } + receivers.add(receiver); + receiver.broadcasting = true; + } else { + if (debug) { + String reason; + switch (match) { + case IntentFilter.NO_MATCH_ACTION: reason = "action"; break; + case IntentFilter.NO_MATCH_CATEGORY: reason = "category"; break; + case IntentFilter.NO_MATCH_DATA: reason = "data"; break; + case IntentFilter.NO_MATCH_TYPE: reason = "type"; break; + default: reason = "unknown reason"; break; + } + Log.v(TAG, " Filter did not match: " + reason); + } + } + } + + if (receivers != null) { + for (int i=0; iNote that for now this is not publicly available because it is not a + * complete implementation, only sufficient for the needs of + * {@link AsyncTaskLoader}. + */ +abstract class ModernAsyncTask { + private static final String LOG_TAG = "AsyncTask"; + + private static final int CORE_POOL_SIZE = 5; + private static final int MAXIMUM_POOL_SIZE = 128; + private static final int KEEP_ALIVE = 1; + + private static final ThreadFactory sThreadFactory = new ThreadFactory() { + private final AtomicInteger mCount = new AtomicInteger(1); + + public Thread newThread(Runnable r) { + return new Thread(r, "ModernAsyncTask #" + mCount.getAndIncrement()); + } + }; + + private static final BlockingQueue sPoolWorkQueue = + new LinkedBlockingQueue(10); + + /** + * An {@link Executor} that can be used to execute tasks in parallel. + */ + public static final Executor THREAD_POOL_EXECUTOR + = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, + TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory); + + private static final int MESSAGE_POST_RESULT = 0x1; + private static final int MESSAGE_POST_PROGRESS = 0x2; + + private static final InternalHandler sHandler = new InternalHandler(); + + private static volatile Executor sDefaultExecutor = THREAD_POOL_EXECUTOR; + private final WorkerRunnable mWorker; + private final FutureTask mFuture; + + private volatile Status mStatus = Status.PENDING; + + private final AtomicBoolean mTaskInvoked = new AtomicBoolean(); + + /** + * Indicates the current status of the task. Each status will be set only once + * during the lifetime of a task. + */ + public enum Status { + /** + * Indicates that the task has not been executed yet. + */ + PENDING, + /** + * Indicates that the task is running. + */ + RUNNING, + /** + * Indicates that {@link android.os.AsyncTask#onPostExecute(Object)} has finished. + */ + FINISHED, + } + + /** @hide Used to force static handler to be created. */ + public static void init() { + sHandler.getLooper(); + } + + /** @hide */ + public static void setDefaultExecutor(Executor exec) { + sDefaultExecutor = exec; + } + + /** + * Creates a new asynchronous task. This constructor must be invoked on the UI thread. + */ + public ModernAsyncTask() { + mWorker = new WorkerRunnable() { + public Result call() throws Exception { + mTaskInvoked.set(true); + + Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); + return postResult(doInBackground(mParams)); + } + }; + + mFuture = new FutureTask(mWorker) { + @Override + protected void done() { + try { + final Result result = get(); + + postResultIfNotInvoked(result); + } catch (InterruptedException e) { + android.util.Log.w(LOG_TAG, e); + } catch (ExecutionException e) { + throw new RuntimeException("An error occured while executing doInBackground()", + e.getCause()); + } catch (CancellationException e) { + postResultIfNotInvoked(null); + } catch (Throwable t) { + throw new RuntimeException("An error occured while executing " + + "doInBackground()", t); + } + } + }; + } + + private void postResultIfNotInvoked(Result result) { + final boolean wasTaskInvoked = mTaskInvoked.get(); + if (!wasTaskInvoked) { + postResult(result); + } + } + + private Result postResult(Result result) { + Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, + new AsyncTaskResult(this, result)); + message.sendToTarget(); + return result; + } + + /** + * Returns the current status of this task. + * + * @return The current status. + */ + public final Status getStatus() { + return mStatus; + } + + /** + * Override this method to perform a computation on a background thread. The + * specified parameters are the parameters passed to {@link #execute} + * by the caller of this task. + * + * This method can call {@link #publishProgress} to publish updates + * on the UI thread. + * + * @param params The parameters of the task. + * + * @return A result, defined by the subclass of this task. + * + * @see #onPreExecute() + * @see #onPostExecute + * @see #publishProgress + */ + protected abstract Result doInBackground(Params... params); + + /** + * Runs on the UI thread before {@link #doInBackground}. + * + * @see #onPostExecute + * @see #doInBackground + */ + protected void onPreExecute() { + } + + /** + *

Runs on the UI thread after {@link #doInBackground}. The + * specified result is the value returned by {@link #doInBackground}.

+ * + *

This method won't be invoked if the task was cancelled.

+ * + * @param result The result of the operation computed by {@link #doInBackground}. + * + * @see #onPreExecute + * @see #doInBackground + * @see #onCancelled(Object) + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onPostExecute(Result result) { + } + + /** + * Runs on the UI thread after {@link #publishProgress} is invoked. + * The specified values are the values passed to {@link #publishProgress}. + * + * @param values The values indicating progress. + * + * @see #publishProgress + * @see #doInBackground + */ + @SuppressWarnings({"UnusedDeclaration"}) + protected void onProgressUpdate(Progress... values) { + } + + /** + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + *

The default implementation simply invokes {@link #onCancelled()} and + * ignores the result. If you write your own implementation, do not call + * super.onCancelled(result).

+ * + * @param result The result, if any, computed in + * {@link #doInBackground(Object[])}, can be null + * + * @see #cancel(boolean) + * @see #isCancelled() + */ + @SuppressWarnings({"UnusedParameters"}) + protected void onCancelled(Result result) { + onCancelled(); + } + + /** + *

Applications should preferably override {@link #onCancelled(Object)}. + * This method is invoked by the default implementation of + * {@link #onCancelled(Object)}.

+ * + *

Runs on the UI thread after {@link #cancel(boolean)} is invoked and + * {@link #doInBackground(Object[])} has finished.

+ * + * @see #onCancelled(Object) + * @see #cancel(boolean) + * @see #isCancelled() + */ + protected void onCancelled() { + } + + /** + * Returns true if this task was cancelled before it completed + * normally. If you are calling {@link #cancel(boolean)} on the task, + * the value returned by this method should be checked periodically from + * {@link #doInBackground(Object[])} to end the task as soon as possible. + * + * @return true if task was cancelled before it completed + * + * @see #cancel(boolean) + */ + public final boolean isCancelled() { + return mFuture.isCancelled(); + } + + /** + *

Attempts to cancel execution of this task. This attempt will + * fail if the task has already completed, already been cancelled, + * or could not be cancelled for some other reason. If successful, + * and this task has not started when cancel is called, + * this task should never run. If the task has already started, + * then the mayInterruptIfRunning parameter determines + * whether the thread executing this task should be interrupted in + * an attempt to stop the task.

+ * + *

Calling this method will result in {@link #onCancelled(Object)} being + * invoked on the UI thread after {@link #doInBackground(Object[])} + * returns. Calling this method guarantees that {@link #onPostExecute(Object)} + * is never invoked. After invoking this method, you should check the + * value returned by {@link #isCancelled()} periodically from + * {@link #doInBackground(Object[])} to finish the task as early as + * possible.

+ * + * @param mayInterruptIfRunning true if the thread executing this + * task should be interrupted; otherwise, in-progress tasks are allowed + * to complete. + * + * @return false if the task could not be cancelled, + * typically because it has already completed normally; + * true otherwise + * + * @see #isCancelled() + * @see #onCancelled(Object) + */ + public final boolean cancel(boolean mayInterruptIfRunning) { + return mFuture.cancel(mayInterruptIfRunning); + } + + /** + * Waits if necessary for the computation to complete, and then + * retrieves its result. + * + * @return The computed result. + * + * @throws CancellationException If the computation was cancelled. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + */ + public final Result get() throws InterruptedException, ExecutionException { + return mFuture.get(); + } + + /** + * Waits if necessary for at most the given time for the computation + * to complete, and then retrieves its result. + * + * @param timeout Time to wait before cancelling the operation. + * @param unit The time unit for the timeout. + * + * @return The computed result. + * + * @throws CancellationException If the computation was cancelled. + * @throws ExecutionException If the computation threw an exception. + * @throws InterruptedException If the current thread was interrupted + * while waiting. + * @throws TimeoutException If the wait timed out. + */ + public final Result get(long timeout, TimeUnit unit) throws InterruptedException, + ExecutionException, TimeoutException { + return mFuture.get(timeout, unit); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + *

Note: this function schedules the task on a queue for a single background + * thread or pool of threads depending on the platform version. When first + * introduced, AsyncTasks were executed serially on a single background thread. + * Starting with {@link android.os.Build.VERSION_CODES#DONUT}, this was changed + * to a pool of threads allowing multiple tasks to operate in parallel. After + * {@link android.os.Build.VERSION_CODES#HONEYCOMB}, it is planned to change this + * back to a single thread to avoid common application errors caused + * by parallel execution. If you truly want parallel execution, you can use + * the {@link #executeOnExecutor} version of this method + * with {@link #THREAD_POOL_EXECUTOR}; however, see commentary there for warnings on + * its use. + * + *

This method must be invoked on the UI thread. + * + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link android.os.AsyncTask.Status#RUNNING} or + * {@link android.os.AsyncTask.Status#FINISHED}. + */ + public final ModernAsyncTask execute(Params... params) { + return executeOnExecutor(sDefaultExecutor, params); + } + + /** + * Executes the task with the specified parameters. The task returns + * itself (this) so that the caller can keep a reference to it. + * + *

This method is typically used with {@link #THREAD_POOL_EXECUTOR} to + * allow multiple tasks to run in parallel on a pool of threads managed by + * AsyncTask, however you can also use your own {@link Executor} for custom + * behavior. + * + *

Warning: Allowing multiple tasks to run in parallel from + * a thread pool is generally not what one wants, because the order + * of their operation is not defined. For example, if these tasks are used + * to modify any state in common (such as writing a file due to a button click), + * there are no guarantees on the order of the modifications. + * Without careful work it is possible in rare cases for the newer version + * of the data to be over-written by an older one, leading to obscure data + * loss and stability issues. + * + *

This method must be invoked on the UI thread. + * + * @param exec The executor to use. {@link #THREAD_POOL_EXECUTOR} is available as a + * convenient process-wide thread pool for tasks that are loosely coupled. + * @param params The parameters of the task. + * + * @return This instance of AsyncTask. + * + * @throws IllegalStateException If {@link #getStatus()} returns either + * {@link android.os.AsyncTask.Status#RUNNING} + * or {@link android.os.AsyncTask.Status#FINISHED}. + */ + public final ModernAsyncTask executeOnExecutor(Executor exec, + Params... params) { + if (mStatus != Status.PENDING) { + switch (mStatus) { + case RUNNING: + throw new IllegalStateException("Cannot execute task:" + + " the task is already running."); + case FINISHED: + throw new IllegalStateException("Cannot execute task:" + + " the task has already been executed " + + "(a task can be executed only once)"); + } + } + + mStatus = Status.RUNNING; + + onPreExecute(); + + mWorker.mParams = params; + exec.execute(mFuture); + + return this; + } + + /** + * Convenience version of {@link #execute(Object...)} for use with + * a simple Runnable object. + */ + public static void execute(Runnable runnable) { + sDefaultExecutor.execute(runnable); + } + + /** + * This method can be invoked from {@link #doInBackground} to + * publish updates on the UI thread while the background computation is + * still running. Each call to this method will trigger the execution of + * {@link #onProgressUpdate} on the UI thread. + * + * {@link #onProgressUpdate} will note be called if the task has been + * canceled. + * + * @param values The progress values to update the UI with. + * + * @see #onProgressUpdate + * @see #doInBackground + */ + protected final void publishProgress(Progress... values) { + if (!isCancelled()) { + sHandler.obtainMessage(MESSAGE_POST_PROGRESS, + new AsyncTaskResult(this, values)).sendToTarget(); + } + } + + private void finish(Result result) { + if (isCancelled()) { + onCancelled(result); + } else { + onPostExecute(result); + } + mStatus = Status.FINISHED; + } + + private static class InternalHandler extends Handler { + @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) + @Override + public void handleMessage(Message msg) { + AsyncTaskResult result = (AsyncTaskResult) msg.obj; + switch (msg.what) { + case MESSAGE_POST_RESULT: + // There is only one result + result.mTask.finish(result.mData[0]); + break; + case MESSAGE_POST_PROGRESS: + result.mTask.onProgressUpdate(result.mData); + break; + } + } + } + + private static abstract class WorkerRunnable implements Callable { + Params[] mParams; + } + + @SuppressWarnings({"RawUseOfParameterizedType"}) + private static class AsyncTaskResult { + final ModernAsyncTask mTask; + final Data[] mData; + + AsyncTaskResult(ModernAsyncTask task, Data... data) { + mTask = task; + mData = data; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/WakefulBroadcastReceiver.java b/mobile/google-android-support-v4/src/android/support/v4/content/WakefulBroadcastReceiver.java new file mode 100644 index 00000000000..9e41f22e826 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/WakefulBroadcastReceiver.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content; + +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.os.PowerManager; +import android.util.Log; +import android.util.SparseArray; + +/** + * Helper for the common pattern of implementing a {@link BroadcastReceiver} + * that receives a device wakeup event and then passes the work off + * to a {@link android.app.Service}, while ensuring that the + * device does not go back to sleep during the transition. + * + *

This class takes care of creating and managing a partial wake lock + * for you; you must request the {@link android.Manifest.permission#WAKE_LOCK} + * permission to use it.

+ * + *

Example

+ * + *

A {@link WakefulBroadcastReceiver} uses the method + * {@link WakefulBroadcastReceiver#startWakefulService startWakefulService()} + * to start the service that does the work. This method is comparable to + * {@link android.content.Context#startService startService()}, except that + * the {@link WakefulBroadcastReceiver} is holding a wake lock when the service + * starts. The intent that is passed with + * {@link WakefulBroadcastReceiver#startWakefulService startWakefulService()} + * holds an extra identifying the wake lock.

+ * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/content/SimpleWakefulReceiver.java complete} + * + *

The service (in this example, an {@link android.app.IntentService}) does + * some work. When it is finished, it releases the wake lock by calling + * {@link WakefulBroadcastReceiver#completeWakefulIntent + * completeWakefulIntent(intent)}. The intent it passes as a parameter + * is the same intent that the {@link WakefulBroadcastReceiver} originally + * passed in.

+ * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/content/SimpleWakefulService.java complete} + */ +public abstract class WakefulBroadcastReceiver extends BroadcastReceiver { + private static final String EXTRA_WAKE_LOCK_ID = "android.support.content.wakelockid"; + + private static final SparseArray mActiveWakeLocks + = new SparseArray(); + private static int mNextId = 1; + + /** + * Do a {@link android.content.Context#startService(android.content.Intent) + * Context.startService}, but holding a wake lock while the service starts. + * This will modify the Intent to hold an extra identifying the wake lock; + * when the service receives it in {@link android.app.Service#onStartCommand + * Service.onStartCommand}, it should pass back the Intent it receives there to + * {@link #completeWakefulIntent(android.content.Intent)} in order to release + * the wake lock. + * + * @param context The Context in which it operate. + * @param intent The Intent with which to start the service, as per + * {@link android.content.Context#startService(android.content.Intent) + * Context.startService}. + */ + public static ComponentName startWakefulService(Context context, Intent intent) { + synchronized (mActiveWakeLocks) { + int id = mNextId; + mNextId++; + if (mNextId <= 0) { + mNextId = 1; + } + + intent.putExtra(EXTRA_WAKE_LOCK_ID, id); + ComponentName comp = context.startService(intent); + if (comp == null) { + return null; + } + + PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE); + PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, + "wake:" + comp.flattenToShortString()); + wl.setReferenceCounted(false); + wl.acquire(60*1000); + mActiveWakeLocks.put(id, wl); + return comp; + } + } + + /** + * Finish the execution from a previous {@link #startWakefulService}. Any wake lock + * that was being held will now be released. + * + * @param intent The Intent as originally generated by {@link #startWakefulService}. + * @return Returns true if the intent is associated with a wake lock that is + * now released; returns false if there was no wake lock specified for it. + */ + public static boolean completeWakefulIntent(Intent intent) { + final int id = intent.getIntExtra(EXTRA_WAKE_LOCK_ID, 0); + if (id == 0) { + return false; + } + synchronized (mActiveWakeLocks) { + PowerManager.WakeLock wl = mActiveWakeLocks.get(id); + if (wl != null) { + wl.release(); + mActiveWakeLocks.remove(id); + return true; + } + // We return true whether or not we actually found the wake lock + // the return code is defined to indicate whether the Intent contained + // an identifier for a wake lock that it was supposed to match. + // We just log a warning here if there is no wake lock found, which could + // happen for example if this function is called twice on the same + // intent or the process is killed and restarted before processing the intent. + Log.w("WakefulBroadcastReceiver", "No active wake lock id #" + id); + return true; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/package.html b/mobile/google-android-support-v4/src/android/support/v4/content/package.html new file mode 100644 index 00000000000..33bf4b5ccd9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/package.html @@ -0,0 +1,10 @@ + + +Support android.content classes to assist with development of applications for +android API level 4 or later. The main features here are +{@link android.support.v4.content.Loader} and related classes and +{@link android.support.v4.content.LocalBroadcastManager} to +provide a cleaner implementation of broadcasts that don't need to go outside +of an app. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/pm/ActivityInfoCompat.java b/mobile/google-android-support-v4/src/android/support/v4/content/pm/ActivityInfoCompat.java new file mode 100644 index 00000000000..5c279236d50 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/pm/ActivityInfoCompat.java @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content.pm; + +/** + * Helper for accessing features in {@link android.content.pm.ActivityInfo} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ActivityInfoCompat { + + private ActivityInfoCompat() { + /* Hide constructor */ + } + + /** + * Bit in ActivityInfo#configChanges that indicates that the + * activity can itself handle the ui mode. Set from the + * {@link android.R.attr#configChanges} attribute. + */ + public static final int CONFIG_UI_MODE = 0x0200; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/pm/package.html b/mobile/google-android-support-v4/src/android/support/v4/content/pm/package.html new file mode 100644 index 00000000000..da850bda667 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/pm/package.html @@ -0,0 +1,6 @@ + + +Support android.content.pm classes to assist with development of applications for +android API level 4 or later. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompat.java b/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompat.java new file mode 100644 index 00000000000..252977b4a8a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompat.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content.res; + +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.content.res.Resources.Theme; +import android.graphics.drawable.Drawable; +import android.os.Build; + +/** + * Helper for accessing features in {@link android.content.res.Resources} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ResourcesCompat { + /** + * Return a drawable object associated with a particular resource ID and + * styled for the specified theme. Various types of objects will be + * returned depending on the underlying resource -- for example, a solid + * color, PNG image, scalable image, etc. + *

+ * Prior to API level 21, the theme will not be applied and this method + * simply calls through to {@link Resources#getDrawable(int)}. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @param theme The theme used to style the drawable attributes, may be + * {@code null}. + * @return Drawable An object that can be used to draw this resource. + * @throws NotFoundException Throws NotFoundException if the given ID does + * not exist. + */ + @SuppressWarnings("deprecation") + public static Drawable getDrawable(Resources res, int id, Theme theme) + throws NotFoundException { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return ResourcesCompatApi21.getDrawable(res, id, theme); + } else { + return res.getDrawable(id); + } + } + + + /** + * Return a drawable object associated with a particular resource ID for + * the given screen density in DPI and styled for the specified theme. + *

+ * Prior to API level 15, the theme and density will not be applied and + * this method simply calls through to {@link Resources#getDrawable(int)}. + *

+ * Prior to API level 21, the theme will not be applied and this method + * calls through to Resources.getDrawableForDensity(int, int). + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @param density The desired screen density indicated by the resource as + * found in {@link android.util.DisplayMetrics}. + * @param theme The theme used to style the drawable attributes, may be + * {@code null}. + * @return Drawable An object that can be used to draw this resource. + * @throws NotFoundException Throws NotFoundException if the given ID does + * not exist. + */ + @SuppressWarnings("deprecation") + public static Drawable getDrawableForDensity(Resources res, int id, int density, Theme theme) + throws NotFoundException { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return ResourcesCompatApi21.getDrawableForDensity(res, id, density, theme); + } else if (version >= 15) { + return ResourcesCompatIcsMr1.getDrawableForDensity(res, id, density); + } else { + return res.getDrawable(id); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatApi21.java new file mode 100644 index 00000000000..2272c02627e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatApi21.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content.res; + +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.content.res.Resources.Theme; +import android.graphics.drawable.Drawable; + +class ResourcesCompatApi21 { + public static Drawable getDrawable(Resources res, int id, Theme theme) + throws NotFoundException { + return res.getDrawable(id, theme); + } + + public static Drawable getDrawableForDensity(Resources res, int id, int density, Theme theme) + throws NotFoundException { + return res.getDrawableForDensity(id, density, theme); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatIcsMr1.java b/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatIcsMr1.java new file mode 100644 index 00000000000..8e14256bcab --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/content/res/ResourcesCompatIcsMr1.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.content.res; + +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.content.res.Resources.Theme; +import android.graphics.drawable.Drawable; + +class ResourcesCompatIcsMr1 { + public static Drawable getDrawableForDensity(Resources res, int id, int density) + throws NotFoundException { + return res.getDrawableForDensity(id, density); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/database/DatabaseUtilsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/database/DatabaseUtilsCompat.java new file mode 100644 index 00000000000..9a825e80f93 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/database/DatabaseUtilsCompat.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.database; + +import android.text.TextUtils; + +/** + * Helper for accessing features in {@link android.database.DatabaseUtils} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class DatabaseUtilsCompat { + + private DatabaseUtilsCompat() { + /* Hide constructor */ + } + + /** + * Concatenates two SQL WHERE clauses, handling empty or null values. + */ + public static String concatenateWhere(String a, String b) { + if (TextUtils.isEmpty(a)) { + return b; + } + if (TextUtils.isEmpty(b)) { + return a; + } + + return "(" + a + ") AND (" + b + ")"; + } + + /** + * Appends one set of selection args to another. This is useful when adding a selection + * argument to a user provided set. + */ + public static String[] appendSelectionArgs(String[] originalValues, String[] newValues) { + if (originalValues == null || originalValues.length == 0) { + return newValues; + } + String[] result = new String[originalValues.length + newValues.length ]; + System.arraycopy(originalValues, 0, result, 0, originalValues.length); + System.arraycopy(newValues, 0, result, originalValues.length, newValues.length); + return result; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/database/package.html b/mobile/google-android-support-v4/src/android/support/v4/database/package.html new file mode 100644 index 00000000000..25ac59aafbb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/database/package.html @@ -0,0 +1,6 @@ + + +Support android.database classes to assist with development of applications for +android API level 4 or later. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompat.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompat.java new file mode 100644 index 00000000000..5b1cb687d60 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompat.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.graphics; + +import android.graphics.Bitmap; + +/** + * Helper for accessing features in {@link android.graphics.Bitmap} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class BitmapCompat { + /** + * Interface for the full API. + */ + interface BitmapImpl { + public boolean hasMipMap(Bitmap bitmap); + public void setHasMipMap(Bitmap bitmap, boolean hasMipMap); + public int getAllocationByteCount(Bitmap bitmap); + } + + static class BaseBitmapImpl implements BitmapImpl { + @Override + public boolean hasMipMap(Bitmap bitmap) { + return false; + } + + @Override + public void setHasMipMap(Bitmap bitmap, boolean hasMipMap) { + } + + @Override + public int getAllocationByteCount(Bitmap bitmap) { + return bitmap.getRowBytes() * bitmap.getHeight(); + } + } + + static class HcMr1BitmapCompatImpl extends BaseBitmapImpl { + @Override + public int getAllocationByteCount(Bitmap bitmap) { + return BitmapCompatHoneycombMr1.getAllocationByteCount(bitmap); + } + } + + static class JbMr2BitmapCompatImpl extends HcMr1BitmapCompatImpl { + @Override + public boolean hasMipMap(Bitmap bitmap){ + return BitmapCompatJellybeanMR2.hasMipMap(bitmap); + } + + @Override + public void setHasMipMap(Bitmap bitmap, boolean hasMipMap) { + BitmapCompatJellybeanMR2.setHasMipMap(bitmap, hasMipMap); + } + } + + static class KitKatBitmapCompatImpl extends JbMr2BitmapCompatImpl { + @Override + public int getAllocationByteCount(Bitmap bitmap) { + return BitmapCompatKitKat.getAllocationByteCount(bitmap); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final BitmapImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 19) { + IMPL = new KitKatBitmapCompatImpl(); + } else if (version >= 18) { + IMPL = new JbMr2BitmapCompatImpl(); + } else if (version >= 12) { + IMPL = new HcMr1BitmapCompatImpl(); + } else { + IMPL = new BaseBitmapImpl(); + } + } + + public static boolean hasMipMap(Bitmap bitmap) { + return IMPL.hasMipMap(bitmap); + } + + public static void setHasMipMap(Bitmap bitmap, boolean hasMipMap) { + IMPL.setHasMipMap(bitmap, hasMipMap); + } + + /** + * Returns the size of the allocated memory used to store this bitmap's pixels in a backwards + * compatible way. + * + * @param bitmap the bitmap in which to return it's allocation size + * @return the allocation size in bytes + */ + public static int getAllocationByteCount(Bitmap bitmap) { + return IMPL.getAllocationByteCount(bitmap); + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatHoneycombMr1.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatHoneycombMr1.java new file mode 100644 index 00000000000..94358d5fffb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatHoneycombMr1.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.graphics; + +import android.graphics.Bitmap; + +/** + * Implementation of BitmapCompat that can use Honeycomb MR1 APIs. + */ +class BitmapCompatHoneycombMr1 { + + static int getAllocationByteCount(Bitmap bitmap) { + return bitmap.getByteCount(); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatJellybeanMR2.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatJellybeanMR2.java new file mode 100644 index 00000000000..21f0d49b53b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatJellybeanMR2.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.graphics; + +import android.graphics.Bitmap; + +class BitmapCompatJellybeanMR2 { + public static boolean hasMipMap(Bitmap bitmap) { + return bitmap.hasMipMap(); + } + + public static void setHasMipMap(Bitmap bitmap, boolean hasMipMap) { + bitmap.setHasMipMap(hasMipMap); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatKitKat.java new file mode 100644 index 00000000000..2d4a66eda88 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/BitmapCompatKitKat.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.graphics; + +import android.graphics.Bitmap; + +/** + * Implementation of BitmapCompat that can use KitKat APIs. + */ +class BitmapCompatKitKat { + + static int getAllocationByteCount(Bitmap bitmap) { + return bitmap.getAllocationByteCount(); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/ColorUtils.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/ColorUtils.java new file mode 100644 index 00000000000..aac809bceac --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/ColorUtils.java @@ -0,0 +1,297 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics; + +import android.graphics.Color; + +/** + * A set of color-related utility methods, building upon those available in {@code Color}. + */ +public class ColorUtils { + + private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10; + private static final int MIN_ALPHA_SEARCH_PRECISION = 10; + + private ColorUtils() {} + + /** + * Composite two potentially translucent colors over each other and returns the result. + */ + public static int compositeColors(int foreground, int background) { + int bgAlpha = Color.alpha(background); + int fgAlpha = Color.alpha(foreground); + int a = compositeAlpha(fgAlpha, bgAlpha); + + int r = compositeComponent(Color.red(foreground), fgAlpha, + Color.red(background), bgAlpha, a); + int g = compositeComponent(Color.green(foreground), fgAlpha, + Color.green(background), bgAlpha, a); + int b = compositeComponent(Color.blue(foreground), fgAlpha, + Color.blue(background), bgAlpha, a); + + return Color.argb(a, r, g, b); + } + + private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) { + return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF); + } + + private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) { + if (a == 0) return 0; + return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF); + } + + /** + * Returns the luminance of a color. + * + * Formula defined here: http://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef + */ + public static double calculateLuminance(int color) { + double red = Color.red(color) / 255d; + red = red < 0.03928 ? red / 12.92 : Math.pow((red + 0.055) / 1.055, 2.4); + + double green = Color.green(color) / 255d; + green = green < 0.03928 ? green / 12.92 : Math.pow((green + 0.055) / 1.055, 2.4); + + double blue = Color.blue(color) / 255d; + blue = blue < 0.03928 ? blue / 12.92 : Math.pow((blue + 0.055) / 1.055, 2.4); + + return (0.2126 * red) + (0.7152 * green) + (0.0722 * blue); + } + + /** + * Returns the contrast ratio between {@code foreground} and {@code background}. + * {@code background} must be opaque. + *

+ * Formula defined + * here. + */ + public static double calculateContrast(int foreground, int background) { + if (Color.alpha(background) != 255) { + throw new IllegalArgumentException("background can not be translucent"); + } + if (Color.alpha(foreground) < 255) { + // If the foreground is translucent, composite the foreground over the background + foreground = compositeColors(foreground, background); + } + + final double luminance1 = calculateLuminance(foreground) + 0.05; + final double luminance2 = calculateLuminance(background) + 0.05; + + // Now return the lighter luminance divided by the darker luminance + return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2); + } + + /** + * Calculates the minimum alpha value which can be applied to {@code foreground} so that would + * have a contrast value of at least {@code minContrastRatio} when compared to + * {@code background}. + * + * @param foreground the foreground color. + * @param background the background color. Should be opaque. + * @param minContrastRatio the minimum contrast ratio. + * @return the alpha value in the range 0-255, or -1 if no value could be calculated. + */ + public static int calculateMinimumAlpha(int foreground, int background, + float minContrastRatio) { + if (Color.alpha(background) != 255) { + throw new IllegalArgumentException("background can not be translucent"); + } + + // First lets check that a fully opaque foreground has sufficient contrast + int testForeground = setAlphaComponent(foreground, 255); + double testRatio = calculateContrast(testForeground, background); + if (testRatio < minContrastRatio) { + // Fully opaque foreground does not have sufficient contrast, return error + return -1; + } + + // Binary search to find a value with the minimum value which provides sufficient contrast + int numIterations = 0; + int minAlpha = 0; + int maxAlpha = 255; + + while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS && + (maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) { + final int testAlpha = (minAlpha + maxAlpha) / 2; + + testForeground = setAlphaComponent(foreground, testAlpha); + testRatio = calculateContrast(testForeground, background); + + if (testRatio < minContrastRatio) { + minAlpha = testAlpha; + } else { + maxAlpha = testAlpha; + } + + numIterations++; + } + + // Conservatively return the max of the range of possible alphas, which is known to pass. + return maxAlpha; + } + + /** + * Convert RGB components to HSL (hue-saturation-lightness). + *

    + *
  • hsl[0] is Hue [0 .. 360)
  • + *
  • hsl[1] is Saturation [0...1]
  • + *
  • hsl[2] is Lightness [0...1]
  • + *
+ * + * @param r red component value [0..255] + * @param g green component value [0..255] + * @param b blue component value [0..255] + * @param hsl 3 element array which holds the resulting HSL components. + */ + public static void RGBToHSL(int r, int g, int b, float[] hsl) { + final float rf = r / 255f; + final float gf = g / 255f; + final float bf = b / 255f; + + final float max = Math.max(rf, Math.max(gf, bf)); + final float min = Math.min(rf, Math.min(gf, bf)); + final float deltaMaxMin = max - min; + + float h, s; + float l = (max + min) / 2f; + + if (max == min) { + // Monochromatic + h = s = 0f; + } else { + if (max == rf) { + h = ((gf - bf) / deltaMaxMin) % 6f; + } else if (max == gf) { + h = ((bf - rf) / deltaMaxMin) + 2f; + } else { + h = ((rf - gf) / deltaMaxMin) + 4f; + } + + s = deltaMaxMin / (1f - Math.abs(2f * l - 1f)); + } + + h = (h * 60f) % 360f; + if (h < 0) { + h += 360f; + } + + hsl[0] = constrain(h, 0f, 360f); + hsl[1] = constrain(s, 0f, 1f); + hsl[2] = constrain(l, 0f, 1f); + } + + /** + * Convert the ARGB color to its HSL (hue-saturation-lightness) components. + *
    + *
  • hsl[0] is Hue [0 .. 360)
  • + *
  • hsl[1] is Saturation [0...1]
  • + *
  • hsl[2] is Lightness [0...1]
  • + *
+ * + * @param color the ARGB color to convert. The alpha component is ignored. + * @param hsl 3 element array which holds the resulting HSL components. + */ + public static void colorToHSL(int color, float[] hsl) { + RGBToHSL(Color.red(color), Color.green(color), Color.blue(color), hsl); + } + + /** + * Convert HSL (hue-saturation-lightness) components to a RGB color. + *
    + *
  • hsl[0] is Hue [0 .. 360)
  • + *
  • hsl[1] is Saturation [0...1]
  • + *
  • hsl[2] is Lightness [0...1]
  • + *
+ * If hsv values are out of range, they are pinned. + * + * @param hsl 3 element array which holds the input HSL components. + * @return the resulting RGB color + */ + public static int HSLToColor(float[] hsl) { + final float h = hsl[0]; + final float s = hsl[1]; + final float l = hsl[2]; + + final float c = (1f - Math.abs(2 * l - 1f)) * s; + final float m = l - 0.5f * c; + final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f)); + + final int hueSegment = (int) h / 60; + + int r = 0, g = 0, b = 0; + + switch (hueSegment) { + case 0: + r = Math.round(255 * (c + m)); + g = Math.round(255 * (x + m)); + b = Math.round(255 * m); + break; + case 1: + r = Math.round(255 * (x + m)); + g = Math.round(255 * (c + m)); + b = Math.round(255 * m); + break; + case 2: + r = Math.round(255 * m); + g = Math.round(255 * (c + m)); + b = Math.round(255 * (x + m)); + break; + case 3: + r = Math.round(255 * m); + g = Math.round(255 * (x + m)); + b = Math.round(255 * (c + m)); + break; + case 4: + r = Math.round(255 * (x + m)); + g = Math.round(255 * m); + b = Math.round(255 * (c + m)); + break; + case 5: + case 6: + r = Math.round(255 * (c + m)); + g = Math.round(255 * m); + b = Math.round(255 * (x + m)); + break; + } + + r = constrain(r, 0, 255); + g = constrain(g, 0, 255); + b = constrain(b, 0, 255); + + return Color.rgb(r, g, b); + } + + /** + * Set the alpha component of {@code color} to be {@code alpha}. + */ + public static int setAlphaComponent(int color, int alpha) { + if (alpha < 0 || alpha > 255) { + throw new IllegalArgumentException("alpha must be between 0 and 255."); + } + return (color & 0x00ffffff) | (alpha << 24); + } + + private static float constrain(float amount, float low, float high) { + return amount < low ? low : (amount > high ? high : amount); + } + + private static int constrain(int amount, int low, int high) { + return amount < low ? low : (amount > high ? high : amount); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompat.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompat.java new file mode 100644 index 00000000000..99c762f13b5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompat.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; + +/** + * Helper for accessing features in {@link android.graphics.drawable.Drawable} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class DrawableCompat { + /** + * Interface for the full API. + */ + interface DrawableImpl { + void jumpToCurrentState(Drawable drawable); + void setAutoMirrored(Drawable drawable, boolean mirrored); + boolean isAutoMirrored(Drawable drawable); + void setHotspot(Drawable drawable, float x, float y); + void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom); + void setTint(Drawable drawable, int tint); + void setTintList(Drawable drawable, ColorStateList tint); + void setTintMode(Drawable drawable, PorterDuff.Mode tintMode); + Drawable wrap(Drawable drawable); + } + + /** + * Interface implementation that doesn't use anything about v4 APIs. + */ + static class BaseDrawableImpl implements DrawableImpl { + @Override + public void jumpToCurrentState(Drawable drawable) { + } + + @Override + public void setAutoMirrored(Drawable drawable, boolean mirrored) { + } + + @Override + public boolean isAutoMirrored(Drawable drawable) { + return false; + } + + @Override + public void setHotspot(Drawable drawable, float x, float y) { + } + + @Override + public void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom) { + } + + @Override + public void setTint(Drawable drawable, int tint) { + DrawableCompatBase.setTint(drawable, tint); + } + + @Override + public void setTintList(Drawable drawable, ColorStateList tint) { + DrawableCompatBase.setTintList(drawable, tint); + } + + @Override + public void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) { + DrawableCompatBase.setTintMode(drawable, tintMode); + } + + @Override + public Drawable wrap(Drawable drawable) { + return DrawableCompatBase.wrapForTinting(drawable); + } + } + + /** + * Interface implementation for devices with at least v11 APIs. + */ + static class HoneycombDrawableImpl extends BaseDrawableImpl { + @Override + public void jumpToCurrentState(Drawable drawable) { + DrawableCompatHoneycomb.jumpToCurrentState(drawable); + } + + @Override + public Drawable wrap(Drawable drawable) { + return DrawableCompatHoneycomb.wrapForTinting(drawable); + } + } + + /** + * Interface implementation for devices with at least KitKat APIs. + */ + static class KitKatDrawableImpl extends HoneycombDrawableImpl { + @Override + public void setAutoMirrored(Drawable drawable, boolean mirrored) { + DrawableCompatKitKat.setAutoMirrored(drawable, mirrored); + } + + @Override + public boolean isAutoMirrored(Drawable drawable) { + return DrawableCompatKitKat.isAutoMirrored(drawable); + } + + @Override + public Drawable wrap(Drawable drawable) { + return DrawableCompatKitKat.wrapForTinting(drawable); + } + } + + /** + * Interface implementation for devices with at least L APIs. + */ + static class LollipopDrawableImpl extends KitKatDrawableImpl { + @Override + public void setHotspot(Drawable drawable, float x, float y) { + DrawableCompatLollipop.setHotspot(drawable, x, y); + } + + @Override + public void setHotspotBounds(Drawable drawable, int left, int top, int right, int bottom) { + DrawableCompatLollipop.setHotspotBounds(drawable, left, top, right, bottom); + } + + @Override + public void setTint(Drawable drawable, int tint) { + DrawableCompatLollipop.setTint(drawable, tint); + } + + @Override + public void setTintList(Drawable drawable, ColorStateList tint) { + DrawableCompatLollipop.setTintList(drawable, tint); + } + + @Override + public void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) { + DrawableCompatLollipop.setTintMode(drawable, tintMode); + } + + @Override + public Drawable wrap(Drawable drawable) { + return DrawableCompatLollipop.wrapForTinting(drawable); + } + } + + /** + * Interface implementation for devices with at least L APIs. + */ + static class LollipopMr1DrawableImpl extends LollipopDrawableImpl { + @Override + public Drawable wrap(Drawable drawable) { + return DrawableCompatApi22.wrapForTinting(drawable); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final DrawableImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 22) { + IMPL = new LollipopMr1DrawableImpl(); + } else if (version >= 21) { + IMPL = new LollipopDrawableImpl(); + } else if (version >= 19) { + IMPL = new KitKatDrawableImpl(); + } else if (version >= 11) { + IMPL = new HoneycombDrawableImpl(); + } else { + IMPL = new BaseDrawableImpl(); + } + } + + /** + * Call {@link Drawable#jumpToCurrentState() Drawable.jumpToCurrentState()}. + *

+ * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} + * device this method does nothing. + * + * @param drawable The Drawable against which to invoke the method. + */ + public static void jumpToCurrentState(Drawable drawable) { + IMPL.jumpToCurrentState(drawable); + } + + /** + * Set whether this Drawable is automatically mirrored when its layout + * direction is RTL (right-to left). See + * {@link android.util.LayoutDirection}. + *

+ * If running on a pre-{@link android.os.Build.VERSION_CODES#KITKAT} device + * this method does nothing. + * + * @param drawable The Drawable against which to invoke the method. + * @param mirrored Set to true if the Drawable should be mirrored, false if + * not. + */ + public static void setAutoMirrored(Drawable drawable, boolean mirrored) { + IMPL.setAutoMirrored(drawable, mirrored); + } + + /** + * Tells if this Drawable will be automatically mirrored when its layout + * direction is RTL right-to-left. See {@link android.util.LayoutDirection}. + *

+ * If running on a pre-{@link android.os.Build.VERSION_CODES#KITKAT} device + * this method returns false. + * + * @param drawable The Drawable against which to invoke the method. + * @return boolean Returns true if this Drawable will be automatically + * mirrored. + */ + public static boolean isAutoMirrored(Drawable drawable) { + return IMPL.isAutoMirrored(drawable); + } + + /** + * Specifies the hotspot's location within the drawable. + * + * @param drawable The Drawable against which to invoke the method. + * @param x The X coordinate of the center of the hotspot + * @param y The Y coordinate of the center of the hotspot + */ + public static void setHotspot(Drawable drawable, float x, float y) { + IMPL.setHotspot(drawable, x, y); + } + + /** + * Sets the bounds to which the hotspot is constrained, if they should be + * different from the drawable bounds. + * + * @param drawable The Drawable against which to invoke the method. + */ + public static void setHotspotBounds(Drawable drawable, int left, int top, + int right, int bottom) { + IMPL.setHotspotBounds(drawable, left, top, right, bottom); + } + + /** + * Specifies a tint for {@code drawable}. + * + * @param drawable The Drawable against which to invoke the method. + * @param tint Color to use for tinting this drawable + */ + public static void setTint(Drawable drawable, int tint) { + IMPL.setTint(drawable, tint); + } + + /** + * Specifies a tint for {@code drawable} as a color state list. + * + * @param drawable The Drawable against which to invoke the method. + * @param tint Color state list to use for tinting this drawable, or null to clear the tint + */ + public static void setTintList(Drawable drawable, ColorStateList tint) { + IMPL.setTintList(drawable, tint); + } + + /** + * Specifies a tint blending mode for {@code drawable}. + * + * @param drawable The Drawable against which to invoke the method. + * @param tintMode A Porter-Duff blending mode + */ + public static void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) { + IMPL.setTintMode(drawable, tintMode); + } + + /** + * Potentially wrap {@code drawable} so that it may be used for tinting across the + * different API levels, via the tinting methods in this class. + *

+ * If you need to get hold of the original {@link android.graphics.drawable.Drawable} again, + * you can use the value returned from {@link #unwrap(Drawable)}. + * + * @param drawable The Drawable to process + * @return A drawable capable of being tinted across all API levels. + * + * @see #setTint(Drawable, int) + * @see #setTintList(Drawable, ColorStateList) + * @see #setTintMode(Drawable, PorterDuff.Mode) + * @see #unwrap(Drawable) + */ + public static Drawable wrap(Drawable drawable) { + return IMPL.wrap(drawable); + } + + /** + * Unwrap {@code drawable} if it is the result of a call to {@link #wrap(Drawable)}. If + * the {@code drawable} is not the result of a call to {@link #wrap(Drawable)} then + * {@code drawable} is returned as-is. + * + * @param drawable The drawable to unwrap + * @return the unwrapped {@link Drawable} or {@code drawable} if it hasn't been wrapped. + * + * @see #wrap(Drawable) + */ + public static T unwrap(Drawable drawable) { + if (drawable instanceof DrawableWrapper) { + return (T) ((DrawableWrapper) drawable).getWrappedDrawable(); + } + return (T) drawable; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatApi22.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatApi22.java new file mode 100644 index 00000000000..bfd2bea1228 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatApi22.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.graphics.drawable.Drawable; + +/** + * Implementation of drawable compatibility that can call Lollipop-MR1 APIs. + */ +class DrawableCompatApi22 { + + public static Drawable wrapForTinting(Drawable drawable) { + // We don't need to wrap anything in Lollipop-MR1 + return drawable; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatBase.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatBase.java new file mode 100644 index 00000000000..48096185275 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatBase.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; + +/** + * Base implementation of drawable compatibility. + */ +class DrawableCompatBase { + + public static void setTint(Drawable drawable, int tint) { + if (drawable instanceof DrawableWrapper) { + ((DrawableWrapper) drawable).setTint(tint); + } + } + + public static void setTintList(Drawable drawable, ColorStateList tint) { + if (drawable instanceof DrawableWrapper) { + ((DrawableWrapper) drawable).setTintList(tint); + } + } + + public static void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) { + if (drawable instanceof DrawableWrapper) { + ((DrawableWrapper) drawable).setTintMode(tintMode); + } + } + + public static Drawable wrapForTinting(Drawable drawable) { + if (!(drawable instanceof DrawableWrapperDonut)) { + return new DrawableWrapperDonut(drawable); + } + return drawable; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java new file mode 100644 index 00000000000..def23ed5ce7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatHoneycomb.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; + +/** + * Implementation of drawable compatibility that can call Honeycomb APIs. + */ +class DrawableCompatHoneycomb { + + public static void jumpToCurrentState(Drawable drawable) { + drawable.jumpToCurrentState(); + } + + public static Drawable wrapForTinting(Drawable drawable) { + if (!(drawable instanceof DrawableWrapperHoneycomb)) { + return new DrawableWrapperHoneycomb(drawable); + } + return drawable; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatKitKat.java new file mode 100644 index 00000000000..b42c86b91fb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatKitKat.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; + +/** + * Implementation of drawable compatibility that can call KitKat APIs. + */ +class DrawableCompatKitKat { + public static void setAutoMirrored(Drawable drawable, boolean mirrored) { + drawable.setAutoMirrored(mirrored); + } + + public static boolean isAutoMirrored(Drawable drawable) { + return drawable.isAutoMirrored(); + } + + public static Drawable wrapForTinting(Drawable drawable) { + if (!(drawable instanceof DrawableWrapperKitKat)) { + return new DrawableWrapperKitKat(drawable); + } + return drawable; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatLollipop.java new file mode 100644 index 00000000000..e5388b9ea8b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableCompatLollipop.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; + +/** + * Implementation of drawable compatibility that can call L APIs. + */ +class DrawableCompatLollipop { + + public static void setHotspot(Drawable drawable, float x, float y) { + drawable.setHotspot(x, y); + } + + public static void setHotspotBounds(Drawable drawable, int left, int top, + int right, int bottom) { + drawable.setHotspotBounds( left, top, right, bottom); + } + + public static void setTint(Drawable drawable, int tint) { + if (drawable instanceof DrawableWrapperLollipop) { + // GradientDrawable on Lollipop does not support tinting, so we'll use our compatible + // functionality instead + DrawableCompatBase.setTint(drawable, tint); + } else { + // Else, we'll use the framework API + drawable.setTint(tint); + } + } + + public static void setTintList(Drawable drawable, ColorStateList tint) { + if (drawable instanceof DrawableWrapperLollipop) { + // GradientDrawable on Lollipop does not support tinting, so we'll use our compatible + // functionality instead + DrawableCompatBase.setTintList(drawable, tint); + } else { + // Else, we'll use the framework API + drawable.setTintList(tint); + } + } + + public static void setTintMode(Drawable drawable, PorterDuff.Mode tintMode) { + if (drawable instanceof DrawableWrapperLollipop) { + // GradientDrawable on Lollipop does not support tinting, so we'll use our compatible + // functionality instead + DrawableCompatBase.setTintMode(drawable, tintMode); + } else { + // Else, we'll use the framework API + drawable.setTintMode(tintMode); + } + } + + public static Drawable wrapForTinting(Drawable drawable) { + if (drawable instanceof GradientDrawable) { + // GradientDrawable on Lollipop does not support tinting, so we'll use our compatible + // functionality instead + return new DrawableWrapperLollipop(drawable); + } + return drawable; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapper.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapper.java new file mode 100644 index 00000000000..1073f344c60 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapper.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; + +/** + * Interface which allows a {@link android.graphics.drawable.Drawable} to receive tinting calls from + * {@code DrawableCompat}. + * + * @hide + */ +public interface DrawableWrapper { + + void setTint(int tint); + + void setTintList(ColorStateList tint); + + void setTintMode(PorterDuff.Mode tintMode); + + Drawable getWrappedDrawable(); + + void setWrappedDrawable(Drawable drawable); + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperDonut.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperDonut.java new file mode 100644 index 00000000000..e3672c8266e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperDonut.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.ColorStateList; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.Region; +import android.graphics.drawable.Drawable; + +/** + * Drawable which delegates all calls to it's wrapped {@link android.graphics.drawable.Drawable}. + *

+ * Also allows backward compatible tinting via a color or {@link ColorStateList}. + * This functionality is accessed via static methods in {@code DrawableCompat}. + */ +class DrawableWrapperDonut extends Drawable implements Drawable.Callback, DrawableWrapper { + + static final PorterDuff.Mode DEFAULT_MODE = PorterDuff.Mode.SRC_IN; + + private ColorStateList mTintList; + private PorterDuff.Mode mTintMode = DEFAULT_MODE; + + private int mCurrentColor; + private PorterDuff.Mode mCurrentMode; + private boolean mColorFilterSet; + + Drawable mDrawable; + + DrawableWrapperDonut(Drawable drawable) { + setWrappedDrawable(drawable); + } + + @Override + public void draw(Canvas canvas) { + mDrawable.draw(canvas); + } + + @Override + protected void onBoundsChange(Rect bounds) { + mDrawable.setBounds(bounds); + } + + @Override + public void setChangingConfigurations(int configs) { + mDrawable.setChangingConfigurations(configs); + } + + @Override + public int getChangingConfigurations() { + return mDrawable.getChangingConfigurations(); + } + + @Override + public void setDither(boolean dither) { + mDrawable.setDither(dither); + } + + @Override + public void setFilterBitmap(boolean filter) { + mDrawable.setFilterBitmap(filter); + } + + @Override + public void setAlpha(int alpha) { + mDrawable.setAlpha(alpha); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mDrawable.setColorFilter(cf); + } + + @Override + public boolean isStateful() { + return (mTintList != null && mTintList.isStateful()) || mDrawable.isStateful(); + } + + @Override + public boolean setState(final int[] stateSet) { + boolean handled = mDrawable.setState(stateSet); + handled = updateTint(stateSet) || handled; + return handled; + } + + @Override + public int[] getState() { + return mDrawable.getState(); + } + + @Override + public Drawable getCurrent() { + return mDrawable.getCurrent(); + } + + @Override + public boolean setVisible(boolean visible, boolean restart) { + return super.setVisible(visible, restart) || mDrawable.setVisible(visible, restart); + } + + @Override + public int getOpacity() { + return mDrawable.getOpacity(); + } + + @Override + public Region getTransparentRegion() { + return mDrawable.getTransparentRegion(); + } + + @Override + public int getIntrinsicWidth() { + return mDrawable.getIntrinsicWidth(); + } + + @Override + public int getIntrinsicHeight() { + return mDrawable.getIntrinsicHeight(); + } + + @Override + public int getMinimumWidth() { + return mDrawable.getMinimumWidth(); + } + + @Override + public int getMinimumHeight() { + return mDrawable.getMinimumHeight(); + } + + @Override + public boolean getPadding(Rect padding) { + return mDrawable.getPadding(padding); + } + + @Override + public Drawable mutate() { + Drawable wrapped = mDrawable; + Drawable mutated = wrapped.mutate(); + if (mutated != wrapped) { + // If mutate() returned a new instance, update our reference + setWrappedDrawable(mutated); + } + // We return ourselves, since only the wrapped drawable needs to mutate + return this; + } + + /** + * {@inheritDoc} + */ + public void invalidateDrawable(Drawable who) { + invalidateSelf(); + } + + /** + * {@inheritDoc} + */ + public void scheduleDrawable(Drawable who, Runnable what, long when) { + scheduleSelf(what, when); + } + + /** + * {@inheritDoc} + */ + public void unscheduleDrawable(Drawable who, Runnable what) { + unscheduleSelf(what); + } + + @Override + protected boolean onLevelChange(int level) { + return mDrawable.setLevel(level); + } + + @Override + public void setTint(int tint) { + setTintList(ColorStateList.valueOf(tint)); + } + + @Override + public void setTintList(ColorStateList tint) { + mTintList = tint; + updateTint(getState()); + } + + @Override + public void setTintMode(PorterDuff.Mode tintMode) { + mTintMode = tintMode; + updateTint(getState()); + } + + private boolean updateTint(int[] state) { + if (mTintList != null && mTintMode != null) { + final int color = mTintList.getColorForState(state, mTintList.getDefaultColor()); + final PorterDuff.Mode mode = mTintMode; + if (!mColorFilterSet || color != mCurrentColor || mode != mCurrentMode) { + setColorFilter(color, mode); + mCurrentColor = color; + mCurrentMode = mode; + mColorFilterSet = true; + return true; + } + } + return false; + } + + /** + * Returns the wrapped {@link Drawable} + */ + public Drawable getWrappedDrawable() { + return mDrawable; + } + + /** + * Sets the current wrapped {@link Drawable} + */ + public void setWrappedDrawable(Drawable drawable) { + if (mDrawable != null) { + mDrawable.setCallback(null); + } + + mDrawable = drawable; + + if (drawable != null) { + drawable.setCallback(this); + } + // Invalidate ourselves + invalidateSelf(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperHoneycomb.java new file mode 100644 index 00000000000..f9fd7d84fae --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperHoneycomb.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.graphics.drawable.Drawable; + +class DrawableWrapperHoneycomb extends DrawableWrapperDonut { + + DrawableWrapperHoneycomb(Drawable drawable) { + super(drawable); + } + + @Override + public void jumpToCurrentState() { + mDrawable.jumpToCurrentState(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperKitKat.java new file mode 100644 index 00000000000..dc70c6eeb3f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperKitKat.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.graphics.drawable.Drawable; + +class DrawableWrapperKitKat extends DrawableWrapperHoneycomb { + + DrawableWrapperKitKat(Drawable drawable) { + super(drawable); + } + + @Override + public void setAutoMirrored(boolean mirrored) { + mDrawable.setAutoMirrored(mirrored); + } + + @Override + public boolean isAutoMirrored() { + return mDrawable.isAutoMirrored(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperLollipop.java new file mode 100644 index 00000000000..1f15040056c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/DrawableWrapperLollipop.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.Resources; +import android.graphics.Outline; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +class DrawableWrapperLollipop extends DrawableWrapperKitKat { + + DrawableWrapperLollipop(Drawable drawable) { + super(drawable); + } + + @Override + public void setHotspot(float x, float y) { + mDrawable.setHotspot(x, y); + } + + @Override + public void setHotspotBounds(int left, int top, int right, int bottom) { + mDrawable.setHotspotBounds(left, top, right, bottom); + } + + @Override + public void getOutline(Outline outline) { + mDrawable.getOutline(outline); + } + + @Override + public void applyTheme(Resources.Theme t) { + mDrawable.applyTheme(t); + } + + @Override + public boolean canApplyTheme() { + return mDrawable.canApplyTheme(); + } + + @Override + public Rect getDirtyBounds() { + return mDrawable.getDirtyBounds(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java new file mode 100644 index 00000000000..7231a382124 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable.java @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.graphics.drawable; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.Drawable; +import android.util.DisplayMetrics; +import android.view.Gravity; + +/** + * A Drawable that wraps a bitmap and can be drawn with rounded corners. You can create a + * RoundedBitmapDrawable from a file path, an input stream, or from a + * {@link android.graphics.Bitmap} object. + *

+ * Also see the {@link android.graphics.Bitmap} class, which handles the management and + * transformation of raw bitmap graphics, and should be used when drawing to a + * {@link android.graphics.Canvas}. + *

+ */ +public abstract class RoundedBitmapDrawable extends Drawable { + private static final int DEFAULT_PAINT_FLAGS = + Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG; + Bitmap mBitmap; + private int mTargetDensity = DisplayMetrics.DENSITY_DEFAULT; + private int mGravity = Gravity.FILL; + private Paint mPaint = new Paint(DEFAULT_PAINT_FLAGS); + private BitmapShader mBitmapShader; + private float mCornerRadius; + + final Rect mDstRect = new Rect(); // Gravity.apply() sets this + final RectF mDstRectF = new RectF(); + + private boolean mApplyGravity = true; + + // These are scaled to match the target density. + private int mBitmapWidth; + private int mBitmapHeight; + + /** + * Returns the paint used to render this drawable. + */ + public final Paint getPaint() { + return mPaint; + } + + /** + * Returns the bitmap used by this drawable to render. May be null. + */ + public final Bitmap getBitmap() { + return mBitmap; + } + + private void computeBitmapSize() { + mBitmapWidth = mBitmap.getScaledWidth(mTargetDensity); + mBitmapHeight = mBitmap.getScaledHeight(mTargetDensity); + } + + /** + * Set the density scale at which this drawable will be rendered. This + * method assumes the drawable will be rendered at the same density as the + * specified canvas. + * + * @param canvas The Canvas from which the density scale must be obtained. + * + * @see android.graphics.Bitmap#setDensity(int) + * @see android.graphics.Bitmap#getDensity() + */ + public void setTargetDensity(Canvas canvas) { + setTargetDensity(canvas.getDensity()); + } + + /** + * Set the density scale at which this drawable will be rendered. + * + * @param metrics The DisplayMetrics indicating the density scale for this drawable. + * + * @see android.graphics.Bitmap#setDensity(int) + * @see android.graphics.Bitmap#getDensity() + */ + public void setTargetDensity(DisplayMetrics metrics) { + setTargetDensity(metrics.densityDpi); + } + + /** + * Set the density at which this drawable will be rendered. + * + * @param density The density scale for this drawable. + * + * @see android.graphics.Bitmap#setDensity(int) + * @see android.graphics.Bitmap#getDensity() + */ + public void setTargetDensity(int density) { + if (mTargetDensity != density) { + mTargetDensity = density == 0 ? DisplayMetrics.DENSITY_DEFAULT : density; + if (mBitmap != null) { + computeBitmapSize(); + } + invalidateSelf(); + } + } + + /** + * Get the gravity used to position/stretch the bitmap within its bounds. + * + * @return the gravity applied to the bitmap + * + * @see android.view.Gravity + */ + public int getGravity() { + return mGravity; + } + + /** + * Set the gravity used to position/stretch the bitmap within its bounds. + * + * @param gravity the gravity + * + * @see android.view.Gravity + */ + public void setGravity(int gravity) { + if (mGravity != gravity) { + mGravity = gravity; + mApplyGravity = true; + invalidateSelf(); + } + } + + /** + * Enables or disables the mipmap hint for this drawable's bitmap. + * See {@link Bitmap#setHasMipMap(boolean)} for more information. + * + * If the bitmap is null, or the current API version does not support setting a mipmap hint, + * calling this method has no effect. + * + * @param mipMap True if the bitmap should use mipmaps, false otherwise. + * + * @see #hasMipMap() + */ + public void setMipMap(boolean mipMap) { + throw new UnsupportedOperationException(); // must be overridden in subclasses + } + + /** + * Indicates whether the mipmap hint is enabled on this drawable's bitmap. + * + * @return True if the mipmap hint is set, false otherwise. If the bitmap + * is null, this method always returns false. + * + * @see #setMipMap(boolean) + */ + public boolean hasMipMap() { + throw new UnsupportedOperationException(); // must be overridden in subclasses + } + + /** + * Enables or disables anti-aliasing for this drawable. Anti-aliasing affects + * the edges of the bitmap only so it applies only when the drawable is rotated. + * + * @param aa True if the bitmap should be anti-aliased, false otherwise. + * + * @see #hasAntiAlias() + */ + public void setAntiAlias(boolean aa) { + mPaint.setAntiAlias(aa); + invalidateSelf(); + } + + /** + * Indicates whether anti-aliasing is enabled for this drawable. + * + * @return True if anti-aliasing is enabled, false otherwise. + * + * @see #setAntiAlias(boolean) + */ + public boolean hasAntiAlias() { + return mPaint.isAntiAlias(); + } + + @Override + public void setFilterBitmap(boolean filter) { + mPaint.setFilterBitmap(filter); + invalidateSelf(); + } + + @Override + public void setDither(boolean dither) { + mPaint.setDither(dither); + invalidateSelf(); + } + + void gravityCompatApply(int gravity, int bitmapWidth, int bitmapHeight, + Rect bounds, Rect outRect) { + throw new UnsupportedOperationException(); + } + + void updateDstRect() { + if (mApplyGravity) { + gravityCompatApply(mGravity, mBitmapWidth, mBitmapHeight, + getBounds(), mDstRect); + mDstRectF.set(mDstRect); + mApplyGravity = false; + } + } + + @Override + public void draw(Canvas canvas) { + final Bitmap bitmap = mBitmap; + if (bitmap == null) { + return; + } + + updateDstRect(); + + final Paint paint = mPaint; + final Shader shader = paint.getShader(); + if (shader == null) { + canvas.drawBitmap(bitmap, null, mDstRect, paint); + } else { + canvas.drawRoundRect(mDstRectF, mCornerRadius, mCornerRadius, paint); + } + } + + @Override + public void setAlpha(int alpha) { + final int oldAlpha = mPaint.getAlpha(); + if (alpha != oldAlpha) { + mPaint.setAlpha(alpha); + invalidateSelf(); + } + } + + public int getAlpha() { + return mPaint.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mPaint.setColorFilter(cf); + invalidateSelf(); + } + + public ColorFilter getColorFilter() { + return mPaint.getColorFilter(); + } + + /** + * Sets the corner radius to be applied when drawing the bitmap. + */ + public void setCornerRadius(float cornerRadius) { + if (isGreaterThanZero(cornerRadius)) { + mPaint.setShader(mBitmapShader); + } else { + mPaint.setShader(null); + } + mCornerRadius = cornerRadius; + } + + /** + * @return The corner radius applied when drawing the bitmap. + */ + public float getCornerRadius() { + return mCornerRadius; + } + + @Override + public int getIntrinsicWidth() { + return mBitmapWidth; + } + + @Override + public int getIntrinsicHeight() { + return mBitmapHeight; + } + + @Override + public int getOpacity() { + if (mGravity != Gravity.FILL) { + return PixelFormat.TRANSLUCENT; + } + Bitmap bm = mBitmap; + return (bm == null + || bm.hasAlpha() + || mPaint.getAlpha() < 255 + || isGreaterThanZero(mCornerRadius)) + ? PixelFormat.TRANSLUCENT : PixelFormat.OPAQUE; + } + + RoundedBitmapDrawable(Resources res, Bitmap bitmap) { + if (res != null) { + mTargetDensity = res.getDisplayMetrics().densityDpi; + } + + mBitmap = bitmap; + if (mBitmap != null) { + computeBitmapSize(); + mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + } else { + mBitmapWidth = mBitmapHeight = -1; + } + } + + private static boolean isGreaterThanZero(float toCompare) { + return Float.compare(toCompare, +0.0f) > 0; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java new file mode 100644 index 00000000000..081feea4e74 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawable21.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Outline; +import android.graphics.Rect; +import android.view.Gravity; +import android.view.View; + +class RoundedBitmapDrawable21 extends RoundedBitmapDrawable { + protected RoundedBitmapDrawable21(Resources res, Bitmap bitmap) { + super(res, bitmap); + } + + @Override + public void getOutline(Outline outline) { + updateDstRect(); + outline.setRoundRect(mDstRect, getCornerRadius()); + } + + @Override + public void setMipMap(boolean mipMap) { + if (mBitmap != null) { + mBitmap.setHasMipMap(mipMap); + invalidateSelf(); + } + } + + @Override + public boolean hasMipMap() { + return mBitmap != null && mBitmap.hasMipMap(); + } + + @Override + void gravityCompatApply(int gravity, int bitmapWidth, int bitmapHeight, + Rect bounds, Rect outRect) { + Gravity.apply(gravity, bitmapWidth, bitmapHeight, + bounds, outRect, View.LAYOUT_DIRECTION_LTR); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java new file mode 100644 index 00000000000..0376c988968 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/graphics/drawable/RoundedBitmapDrawableFactory.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.graphics.drawable; + +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Rect; +import android.os.Build; +import android.support.v4.graphics.BitmapCompat; +import android.support.v4.view.GravityCompat; +import android.support.v4.view.ViewCompat; +import android.util.Log; + +/** + * Constructs {@link RoundedBitmapDrawable RoundedBitmapDrawable} objects, + * either from Bitmaps directly, or from streams and files. + */ +public class RoundedBitmapDrawableFactory { + private static final String TAG = "RoundedBitmapDrawableFactory"; + + private static class DefaultRoundedBitmapDrawable extends RoundedBitmapDrawable { + DefaultRoundedBitmapDrawable(Resources res, Bitmap bitmap) { + super(res, bitmap); + } + + @Override + public void setMipMap(boolean mipMap) { + if (mBitmap != null) { + BitmapCompat.setHasMipMap(mBitmap, mipMap); + invalidateSelf(); + } + } + + @Override + public boolean hasMipMap() { + return mBitmap != null && BitmapCompat.hasMipMap(mBitmap); + } + + @Override + void gravityCompatApply(int gravity, int bitmapWidth, int bitmapHeight, + Rect bounds, Rect outRect) { + GravityCompat.apply(gravity, bitmapWidth, bitmapHeight, + bounds, outRect, ViewCompat.LAYOUT_DIRECTION_LTR); + } + } + + /** + * Returns a new drawable by creating it from a bitmap, setting initial target density based on + * the display metrics of the resources. + */ + public static RoundedBitmapDrawable create(Resources res, Bitmap bitmap) { + if (Build.VERSION.SDK_INT >= 21) { + return new RoundedBitmapDrawable21(res, bitmap); + } + return new DefaultRoundedBitmapDrawable(res, bitmap); + } + + /** + * Returns a new drawable, creating it by opening a given file path and decoding the bitmap. + */ + public static RoundedBitmapDrawable create(Resources res, + String filepath) { + final RoundedBitmapDrawable drawable = create(res, BitmapFactory.decodeFile(filepath)); + if (drawable.getBitmap() == null) { + Log.w(TAG, "BitmapDrawable cannot decode " + filepath); + } + return drawable; + } + + + /** + * Returns a new drawable, creating it by decoding a bitmap from the given input stream. + */ + public static RoundedBitmapDrawable create(Resources res, + java.io.InputStream is) { + final RoundedBitmapDrawable drawable = create(res, BitmapFactory.decodeStream(is)); + if (drawable.getBitmap() == null) { + Log.w(TAG, "BitmapDrawable cannot decode " + is); + } + return drawable; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerCompat.java new file mode 100644 index 00000000000..177c40adb03 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerCompat.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.hardware.display; + +import android.content.Context; +import android.view.Display; +import android.view.WindowManager; + +import java.util.WeakHashMap; + +/** + * Helper for accessing features in {@link android.hardware.display.DisplayManager} + * introduced after API level 4 in a backwards compatible fashion. + */ +public abstract class DisplayManagerCompat { + private static final WeakHashMap sInstances = + new WeakHashMap(); + + /** + * Display category: Presentation displays. + *

+ * This category can be used to identify secondary displays that are suitable for + * use as presentation displays. + *

+ * + * @see android.app.Presentation for information about presenting content + * on secondary displays. + * @see #getDisplays(String) + */ + public static final String DISPLAY_CATEGORY_PRESENTATION = + "android.hardware.display.category.PRESENTATION"; + + DisplayManagerCompat() { + } + + /** + * Gets an instance of the display manager given the context. + */ + public static DisplayManagerCompat getInstance(Context context) { + synchronized (sInstances) { + DisplayManagerCompat instance = sInstances.get(context); + if (instance == null) { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 17) { + instance = new JellybeanMr1Impl(context); + } else { + instance = new LegacyImpl(context); + } + sInstances.put(context, instance); + } + return instance; + } + } + + /** + * Gets information about a logical display. + * + * The display metrics may be adjusted to provide compatibility + * for legacy applications. + * + * @param displayId The logical display id. + * @return The display object, or null if there is no valid display with the given id. + */ + public abstract Display getDisplay(int displayId); + + /** + * Gets all currently valid logical displays. + * + * @return An array containing all displays. + */ + public abstract Display[] getDisplays(); + + /** + * Gets all currently valid logical displays of the specified category. + *

+ * When there are multiple displays in a category the returned displays are sorted + * of preference. For example, if the requested category is + * {@link #DISPLAY_CATEGORY_PRESENTATION} and there are multiple presentation displays + * then the displays are sorted so that the first display in the returned array + * is the most preferred presentation display. The application may simply + * use the first display or allow the user to choose. + *

+ * + * @param category The requested display category or null to return all displays. + * @return An array containing all displays sorted by order of preference. + * + * @see #DISPLAY_CATEGORY_PRESENTATION + */ + public abstract Display[] getDisplays(String category); + + private static class LegacyImpl extends DisplayManagerCompat { + private final WindowManager mWindowManager; + + public LegacyImpl(Context context) { + mWindowManager = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); + } + + @Override + public Display getDisplay(int displayId) { + Display display = mWindowManager.getDefaultDisplay(); + if (display.getDisplayId() == displayId) { + return display; + } + return null; + } + + @Override + public Display[] getDisplays() { + return new Display[] { mWindowManager.getDefaultDisplay() }; + } + + @Override + public Display[] getDisplays(String category) { + return category == null ? getDisplays() : new Display[0]; + } + } + + private static class JellybeanMr1Impl extends DisplayManagerCompat { + private final Object mDisplayManagerObj; + + public JellybeanMr1Impl(Context context) { + mDisplayManagerObj = DisplayManagerJellybeanMr1.getDisplayManager(context); + } + + @Override + public Display getDisplay(int displayId) { + return DisplayManagerJellybeanMr1.getDisplay(mDisplayManagerObj, displayId); + } + + @Override + public Display[] getDisplays() { + return DisplayManagerJellybeanMr1.getDisplays(mDisplayManagerObj); + } + + @Override + public Display[] getDisplays(String category) { + return DisplayManagerJellybeanMr1.getDisplays(mDisplayManagerObj, category); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerJellybeanMr1.java b/mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerJellybeanMr1.java new file mode 100644 index 00000000000..b44a2e8f780 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/hardware/display/DisplayManagerJellybeanMr1.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.hardware.display; + +import android.content.Context; +import android.view.Display; + +final class DisplayManagerJellybeanMr1 { + public static Object getDisplayManager(Context context) { + return context.getSystemService(Context.DISPLAY_SERVICE); + } + + public static Display getDisplay(Object displayManagerObj, int displayId) { + return ((android.hardware.display.DisplayManager)displayManagerObj).getDisplay(displayId); + } + + public static Display[] getDisplays(Object displayManagerObj) { + return ((android.hardware.display.DisplayManager)displayManagerObj).getDisplays(); + } + + public static Display[] getDisplays(Object displayManagerObj, String category) { + return ((android.hardware.display.DisplayManager)displayManagerObj).getDisplays(category); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenu.java b/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenu.java new file mode 100644 index 00000000000..2a04cd83b91 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenu.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.internal.view; + +/** + * Interface for managing the items in a menu. + * + * This version extends the one available in the framework to ensures that any necessary + * elements added in later versions of the framework, are available for all platforms. + * + * @see android.view.Menu + * @hide + */ +public interface SupportMenu extends android.view.Menu { + + /** + * This is the part of an order integer that the user can provide. + * + * @hide + */ + static final int USER_MASK = 0x0000ffff; + /** + * Bit shift of the user portion of the order integer. + * + * @hide + */ + static final int USER_SHIFT = 0; + + /** + * This is the part of an order integer that supplies the category of the item. + * + * @hide + */ + static final int CATEGORY_MASK = 0xffff0000; + /** + * Bit shift of the category portion of the order integer. + * + * @hide + */ + static final int CATEGORY_SHIFT = 16; +} + diff --git a/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenuItem.java b/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenuItem.java new file mode 100644 index 00000000000..0a1bccbb2c6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportMenuItem.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.internal.view; + +import android.support.v4.view.ActionProvider; +import android.support.v4.view.MenuItemCompat; +import android.view.MenuItem; +import android.view.View; + +/** + * Interface for direct access to a previously created menu item. + * + * This version extends the one available in the framework to ensures that any necessary + * elements added in later versions of the framework, are available for all platforms. + * + * @see android.view.MenuItem + * @hide + */ +public interface SupportMenuItem extends android.view.MenuItem { + /* + * These should be kept in sync with attrs.xml enum constants for showAsAction + */ + /** + * Never show this item as a button in an Action Bar. + */ + public static final int SHOW_AS_ACTION_NEVER = 0; + /** + * Show this item as a button in an Action Bar if the system decides there is room for it. + */ + public static final int SHOW_AS_ACTION_IF_ROOM = 1; + /** + * Always show this item as a button in an Action Bar. + * Use sparingly! If too many items are set to always show in the Action Bar it can + * crowd the Action Bar and degrade the user experience on devices with smaller screens. + * A good rule of thumb is to have no more than 2 items set to always show at a time. + */ + public static final int SHOW_AS_ACTION_ALWAYS = 2; + + /** + * When this item is in the action bar, always show it with a text label even if it also has an + * icon specified. + */ + public static final int SHOW_AS_ACTION_WITH_TEXT = 4; + + /** + * This item's action view collapses to a normal menu item. When expanded, the action view + * temporarily takes over a larger segment of its container. + */ + public static final int SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW = 8; + + /** + * Sets how this item should display in the presence of an Action Bar. The parameter actionEnum + * is a flag set. One of {@link #SHOW_AS_ACTION_ALWAYS}, {@link #SHOW_AS_ACTION_IF_ROOM}, or + * {@link #SHOW_AS_ACTION_NEVER} should be used, and you may optionally OR the value with {@link + * #SHOW_AS_ACTION_WITH_TEXT}. SHOW_AS_ACTION_WITH_TEXT requests that when the item is shown as + * an action, it should be shown with a text label. + * + * @param actionEnum How the item should display. One of {@link #SHOW_AS_ACTION_ALWAYS}, {@link + * #SHOW_AS_ACTION_IF_ROOM}, or {@link #SHOW_AS_ACTION_NEVER}. + * SHOW_AS_ACTION_NEVER is the default. + * @see android.app.ActionBar + * @see #setActionView(View) + */ + public void setShowAsAction(int actionEnum); + + /** + * Sets how this item should display in the presence of an Action Bar. + * The parameter actionEnum is a flag set. One of {@link #SHOW_AS_ACTION_ALWAYS}, + * {@link #SHOW_AS_ACTION_IF_ROOM}, or {@link #SHOW_AS_ACTION_NEVER} should + * be used, and you may optionally OR the value with {@link #SHOW_AS_ACTION_WITH_TEXT}. + * SHOW_AS_ACTION_WITH_TEXT requests that when the item is shown as an action, + * it should be shown with a text label. + * + *

Note: This method differs from {@link #setShowAsAction(int)} only in that it + * returns the current MenuItem instance for call chaining. + * + * @param actionEnum How the item should display. One of {@link #SHOW_AS_ACTION_ALWAYS}, {@link + * #SHOW_AS_ACTION_IF_ROOM}, or {@link #SHOW_AS_ACTION_NEVER}. + * SHOW_AS_ACTION_NEVER is the default. + * @return This MenuItem instance for call chaining. + * @see android.app.ActionBar + * @see #setActionView(View) + */ + public MenuItem setShowAsActionFlags(int actionEnum); + + /** + * Set an action view for this menu item. An action view will be displayed in place + * of an automatically generated menu item element in the UI when this item is shown + * as an action within a parent. + * + *

Note: Setting an action view overrides the action provider + * provider set via {@link #setSupportActionProvider(android.support.v4.view.ActionProvider)}.

+ * + * @param view View to use for presenting this item to the user. + * @return This Item so additional setters can be called. + * @see #setShowAsAction(int) + */ + public MenuItem setActionView(View view); + + /** + * Set an action view for this menu item. An action view will be displayed in place + * of an automatically generated menu item element in the UI when this item is shown + * as an action within a parent. + * + *

Note: Setting an action view overrides the action provider + * provider set via {@link #setSupportActionProvider(android.support.v4.view.ActionProvider)}.

+ * + * @param resId Layout resource to use for presenting this item to the user. + * @return This Item so additional setters can be called. + * @see #setShowAsAction(int) + */ + public MenuItem setActionView(int resId); + + /** + * Returns the currently set action view for this menu item. + * + * @return This item's action view + * @see #setActionView(View) + * @see #setShowAsAction(int) + */ + public View getActionView(); + + /** + * Sets the {@link android.support.v4.view.ActionProvider} responsible for creating an action view if + * the item is placed on the action bar. The provider also provides a default + * action invoked if the item is placed in the overflow menu. + * + *

Note: Setting an action provider overrides the action view + * set via {@link #setActionView(int)} or {@link #setActionView(View)}. + *

+ * + * @param actionProvider The action provider. + * @return This Item so additional setters can be called. + * @see android.support.v4.view.ActionProvider + */ + public SupportMenuItem setSupportActionProvider(ActionProvider actionProvider); + + /** + * Gets the {@link ActionProvider}. + * + * @return The action provider. + * @see ActionProvider + * @see #setSupportActionProvider(ActionProvider) + */ + public ActionProvider getSupportActionProvider(); + + /** + * Expand the action view associated with this menu item. The menu item must have an action view + * set, as well as the showAsAction flag {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. If a + * listener has been set using {@link #setSupportOnActionExpandListener(android.support.v4.view.MenuItemCompat.OnActionExpandListener)} + * it will have its {@link android.support.v4.view.MenuItemCompat.OnActionExpandListener#onMenuItemActionExpand(MenuItem)} method + * invoked. The listener may return false from this method to prevent expanding the action view. + * + * @return true if the action view was expanded, false otherwise. + */ + public boolean expandActionView(); + + /** + * Collapse the action view associated with this menu item. The menu item must have an action + * view set, as well as the showAsAction flag {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. If a + * listener has been set using {@link #setSupportOnActionExpandListener(android.support.v4.view.MenuItemCompat.OnActionExpandListener)} + * it will have its {@link android.support.v4.view.MenuItemCompat.OnActionExpandListener#onMenuItemActionCollapse(MenuItem)} method + * invoked. The listener may return false from this method to prevent collapsing the action + * view. + * + * @return true if the action view was collapsed, false otherwise. + */ + public boolean collapseActionView(); + + /** + * Returns true if this menu item's action view has been expanded. + * + * @return true if the item's action view is expanded, false otherwise. + * @see #expandActionView() + * @see #collapseActionView() + * @see #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW + * @see android.support.v4.view.MenuItemCompat.OnActionExpandListener + */ + public boolean isActionViewExpanded(); + + /** + * Set an {@link android.support.v4.view.MenuItemCompat.OnActionExpandListener} on this menu item to be notified when the associated + * action view is expanded or collapsed. The menu item must be configured to expand or collapse + * its action view using the flag {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. + * + * @param listener Listener that will respond to expand/collapse events + * @return This menu item instance for call chaining + */ + public SupportMenuItem setSupportOnActionExpandListener(MenuItemCompat.OnActionExpandListener listener); +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportSubMenu.java b/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportSubMenu.java new file mode 100644 index 00000000000..4f9bd3939c8 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/internal/view/SupportSubMenu.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.internal.view; + +/** + * Subclass of {@link SupportMenu} for sub menus. + * + * This version extends the one available in the framework to ensures that any necessary + * elements added in later versions of the framework, are available for all platforms. + * + * @see android.view.SubMenu + * @hide + */ +public interface SupportSubMenu extends SupportMenu, android.view.SubMenu { +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompat.java new file mode 100644 index 00000000000..39e3a2411f2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompat.java @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.media; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +/** + * A simple set of metadata for a media item suitable for display. This can be + * created using the Builder or retrieved from existing metadata using + * {@link MediaMetadataCompat#getDescription()}. + */ +public final class MediaDescriptionCompat implements Parcelable { + /** + * A unique persistent id for the content or null. + */ + private final String mMediaId; + /** + * A primary title suitable for display or null. + */ + private final CharSequence mTitle; + /** + * A subtitle suitable for display or null. + */ + private final CharSequence mSubtitle; + /** + * A description suitable for display or null. + */ + private final CharSequence mDescription; + /** + * A bitmap icon suitable for display or null. + */ + private final Bitmap mIcon; + /** + * A Uri for an icon suitable for display or null. + */ + private final Uri mIconUri; + /** + * Extras for opaque use by apps/system. + */ + private final Bundle mExtras; + + /** + * A cached copy of the equivalent framework object. + */ + private Object mDescriptionObj; + + private MediaDescriptionCompat(String mediaId, CharSequence title, CharSequence subtitle, + CharSequence description, Bitmap icon, Uri iconUri, Bundle extras) { + mMediaId = mediaId; + mTitle = title; + mSubtitle = subtitle; + mDescription = description; + mIcon = icon; + mIconUri = iconUri; + mExtras = extras; + } + + private MediaDescriptionCompat(Parcel in) { + mMediaId = in.readString(); + mTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mSubtitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mDescription = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mIcon = in.readParcelable(null); + mIconUri = in.readParcelable(null); + mExtras = in.readBundle(); + } + + /** + * Returns the media id or null. See + * {@link MediaMetadataCompat#METADATA_KEY_MEDIA_ID}. + */ + public String getMediaId() { + return mMediaId; + } + + /** + * Returns a title suitable for display or null. + * + * @return A title or null. + */ + public CharSequence getTitle() { + return mTitle; + } + + /** + * Returns a subtitle suitable for display or null. + * + * @return A subtitle or null. + */ + public CharSequence getSubtitle() { + return mSubtitle; + } + + /** + * Returns a description suitable for display or null. + * + * @return A description or null. + */ + public CharSequence getDescription() { + return mDescription; + } + + /** + * Returns a bitmap icon suitable for display or null. + * + * @return An icon or null. + */ + public Bitmap getIconBitmap() { + return mIcon; + } + + /** + * Returns a Uri for an icon suitable for display or null. + * + * @return An icon uri or null. + */ + public Uri getIconUri() { + return mIconUri; + } + + /** + * Returns any extras that were added to the description. + * + * @return A bundle of extras or null. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (Build.VERSION.SDK_INT < 21) { + dest.writeString(mMediaId); + TextUtils.writeToParcel(mTitle, dest, flags); + TextUtils.writeToParcel(mSubtitle, dest, flags); + TextUtils.writeToParcel(mDescription, dest, flags); + dest.writeParcelable(mIcon, flags); + dest.writeParcelable(mIconUri, flags); + dest.writeBundle(mExtras); + } else { + MediaDescriptionCompatApi21.writeToParcel(getMediaDescription(), dest, flags); + } + } + + @Override + public String toString() { + return mTitle + ", " + mSubtitle + ", " + mDescription; + } + + /** + * Gets the underlying framework {@link android.media.MediaDescription} + * object. + *

+ * This method is only supported on + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + *

+ * + * @return An equivalent {@link android.media.MediaDescription} object, or + * null if none. + */ + public Object getMediaDescription() { + if (mDescriptionObj != null || Build.VERSION.SDK_INT < 21) { + return mDescriptionObj; + } + Object bob = MediaDescriptionCompatApi21.Builder.newInstance(); + MediaDescriptionCompatApi21.Builder.setMediaId(bob, mMediaId); + MediaDescriptionCompatApi21.Builder.setTitle(bob, mTitle); + MediaDescriptionCompatApi21.Builder.setSubtitle(bob, mSubtitle); + MediaDescriptionCompatApi21.Builder.setDescription(bob, mDescription); + MediaDescriptionCompatApi21.Builder.setIconBitmap(bob, mIcon); + MediaDescriptionCompatApi21.Builder.setIconUri(bob, mIconUri); + MediaDescriptionCompatApi21.Builder.setExtras(bob, mExtras); + mDescriptionObj = MediaDescriptionCompatApi21.Builder.build(bob); + + return mDescriptionObj; + } + + /** + * Creates an instance from a framework + * {@link android.media.MediaDescription} object. + *

+ * This method is only supported on API 21+. + *

+ * + * @param descriptionObj A {@link android.media.MediaDescription} object, or + * null if none. + * @return An equivalent {@link MediaMetadataCompat} object, or null if + * none. + */ + public static MediaDescriptionCompat fromMediaDescription(Object descriptionObj) { + if (descriptionObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + Builder bob = new Builder(); + bob.setMediaId(MediaDescriptionCompatApi21.getMediaId(descriptionObj)); + bob.setTitle(MediaDescriptionCompatApi21.getTitle(descriptionObj)); + bob.setSubtitle(MediaDescriptionCompatApi21.getSubtitle(descriptionObj)); + bob.setDescription(MediaDescriptionCompatApi21.getDescription(descriptionObj)); + bob.setIconBitmap(MediaDescriptionCompatApi21.getIconBitmap(descriptionObj)); + bob.setIconUri(MediaDescriptionCompatApi21.getIconUri(descriptionObj)); + bob.setExtras(MediaDescriptionCompatApi21.getExtras(descriptionObj)); + MediaDescriptionCompat descriptionCompat = bob.build(); + descriptionCompat.mDescriptionObj = descriptionObj; + + return descriptionCompat; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public MediaDescriptionCompat createFromParcel(Parcel in) { + if (Build.VERSION.SDK_INT < 21) { + return new MediaDescriptionCompat(in); + } else { + return fromMediaDescription(MediaDescriptionCompatApi21.fromParcel(in)); + } + } + + @Override + public MediaDescriptionCompat[] newArray(int size) { + return new MediaDescriptionCompat[size]; + } + }; + + /** + * Builder for {@link MediaDescriptionCompat} objects. + */ + public static final class Builder { + private String mMediaId; + private CharSequence mTitle; + private CharSequence mSubtitle; + private CharSequence mDescription; + private Bitmap mIcon; + private Uri mIconUri; + private Bundle mExtras; + + /** + * Creates an initially empty builder. + */ + public Builder() { + } + + /** + * Sets the media id. + * + * @param mediaId The unique id for the item or null. + * @return this + */ + public Builder setMediaId(String mediaId) { + mMediaId = mediaId; + return this; + } + + /** + * Sets the title. + * + * @param title A title suitable for display to the user or null. + * @return this + */ + public Builder setTitle(CharSequence title) { + mTitle = title; + return this; + } + + /** + * Sets the subtitle. + * + * @param subtitle A subtitle suitable for display to the user or null. + * @return this + */ + public Builder setSubtitle(CharSequence subtitle) { + mSubtitle = subtitle; + return this; + } + + /** + * Sets the description. + * + * @param description A description suitable for display to the user or + * null. + * @return this + */ + public Builder setDescription(CharSequence description) { + mDescription = description; + return this; + } + + /** + * Sets the icon. + * + * @param icon A {@link Bitmap} icon suitable for display to the user or + * null. + * @return this + */ + public Builder setIconBitmap(Bitmap icon) { + mIcon = icon; + return this; + } + + /** + * Sets the icon uri. + * + * @param iconUri A {@link Uri} for an icon suitable for display to the + * user or null. + * @return this + */ + public Builder setIconUri(Uri iconUri) { + mIconUri = iconUri; + return this; + } + + /** + * Sets a bundle of extras. + * + * @param extras The extras to include with this description or null. + * @return this + */ + public Builder setExtras(Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Creates a {@link MediaDescriptionCompat} instance with the specified + * fields. + * + * @return A MediaDescriptionCompat instance. + */ + public MediaDescriptionCompat build() { + return new MediaDescriptionCompat(mMediaId, mTitle, mSubtitle, mDescription, mIcon, + mIconUri, mExtras); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompatApi21.java new file mode 100644 index 00000000000..991515acccb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/MediaDescriptionCompatApi21.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.media; + +import android.graphics.Bitmap; +import android.media.MediaDescription; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcel; + +public class MediaDescriptionCompatApi21 { + + public static String getMediaId(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getMediaId(); + } + + public static CharSequence getTitle(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getTitle(); + } + + public static CharSequence getSubtitle(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getSubtitle(); + } + + public static CharSequence getDescription(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getDescription(); + } + + public static Bitmap getIconBitmap(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getIconBitmap(); + } + + public static Uri getIconUri(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getIconUri(); + } + + public static Bundle getExtras(Object descriptionObj) { + return ((MediaDescription) descriptionObj).getExtras(); + } + + public static void writeToParcel(Object descriptionObj, Parcel dest, int flags) { + ((MediaDescription) descriptionObj).writeToParcel(dest, flags); + } + + public static Object fromParcel(Parcel in) { + return MediaDescription.CREATOR.createFromParcel(in); + } + + public static class Builder { + public static Object newInstance() { + return new MediaDescription.Builder(); + } + + + public static void setMediaId(Object builderObj, String mediaId) { + ((MediaDescription.Builder)builderObj).setMediaId(mediaId); + } + + public static void setTitle(Object builderObj, CharSequence title) { + ((MediaDescription.Builder)builderObj).setTitle(title); + } + + public static void setSubtitle(Object builderObj, CharSequence subtitle) { + ((MediaDescription.Builder)builderObj).setSubtitle(subtitle); + } + + public static void setDescription(Object builderObj, CharSequence description) { + ((MediaDescription.Builder)builderObj).setDescription(description); + } + + public static void setIconBitmap(Object builderObj, Bitmap iconBitmap) { + ((MediaDescription.Builder)builderObj).setIconBitmap(iconBitmap); + } + + public static void setIconUri(Object builderObj, Uri iconUri) { + ((MediaDescription.Builder)builderObj).setIconUri(iconUri); + } + + public static void setExtras(Object builderObj, Bundle extras) { + ((MediaDescription.Builder)builderObj).setExtras(extras); + } + + public static Object build(Object builderObj) { + return ((MediaDescription.Builder) builderObj).build(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.aidl new file mode 100644 index 00000000000..6d36b97eca3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.support.v4.media; + +parcelable MediaMetadataCompat; diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.java new file mode 100644 index 00000000000..38074807053 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompat.java @@ -0,0 +1,746 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.media; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.util.ArrayMap; +import android.text.TextUtils; +import android.util.Log; + +import java.util.Set; + +/** + * Contains metadata about an item, such as the title, artist, etc. + */ +public final class MediaMetadataCompat implements Parcelable { + private static final String TAG = "MediaMetadata"; + + /** + * The title of the media. + */ + public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE"; + + /** + * The artist of the media. + */ + public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST"; + + /** + * The duration of the media in ms. A negative duration indicates that the + * duration is unknown (or infinite). + */ + public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION"; + + /** + * The album title for the media. + */ + public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM"; + + /** + * The author of the media. + */ + public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR"; + + /** + * The writer of the media. + */ + public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER"; + + /** + * The composer of the media. + */ + public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER"; + + /** + * The compilation status of the media. + */ + public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION"; + + /** + * The date the media was created or published as TODO determine format. + */ + public static final String METADATA_KEY_DATE = "android.media.metadata.DATE"; + + /** + * The year the media was created or published as a long. + */ + public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR"; + + /** + * The genre of the media. + */ + public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE"; + + /** + * The track number for the media. + */ + public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER"; + + /** + * The number of tracks in the media's original source. + */ + public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS"; + + /** + * The disc number for the media's original source. + */ + public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER"; + + /** + * The artist for the album of the media's original source. + */ + public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST"; + + /** + * The artwork for the media as a {@link Bitmap}. + */ + public static final String METADATA_KEY_ART = "android.media.metadata.ART"; + + /** + * The artwork for the media as a Uri style String. + */ + public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI"; + + /** + * The artwork for the album of the media's original source as a + * {@link Bitmap}. + */ + public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART"; + + /** + * The artwork for the album of the media's original source as a Uri style + * String. + */ + public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI"; + + /** + * The user's rating for the media. + * + * @see RatingCompat + */ + public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING"; + + /** + * The overall rating for the media. + * + * @see RatingCompat + */ + public static final String METADATA_KEY_RATING = "android.media.metadata.RATING"; + + /** + * A title that is suitable for display to the user. This will generally be + * the same as {@link #METADATA_KEY_TITLE} but may differ for some formats. + * When displaying media described by this metadata this should be preferred + * if present. + */ + public static final String METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE"; + + /** + * A subtitle that is suitable for display to the user. When displaying a + * second line for media described by this metadata this should be preferred + * to other fields if present. + */ + public static final String METADATA_KEY_DISPLAY_SUBTITLE + = "android.media.metadata.DISPLAY_SUBTITLE"; + + /** + * A description that is suitable for display to the user. When displaying + * more information for media described by this metadata this should be + * preferred to other fields if present. + */ + public static final String METADATA_KEY_DISPLAY_DESCRIPTION + = "android.media.metadata.DISPLAY_DESCRIPTION"; + + /** + * An icon or thumbnail that is suitable for display to the user. When + * displaying an icon for media described by this metadata this should be + * preferred to other fields if present. This must be a {@link Bitmap}. + */ + public static final String METADATA_KEY_DISPLAY_ICON + = "android.media.metadata.DISPLAY_ICON"; + + /** + * An icon or thumbnail that is suitable for display to the user. When + * displaying more information for media described by this metadata the + * display description should be preferred to other fields when present. + * This must be a Uri style String. + */ + public static final String METADATA_KEY_DISPLAY_ICON_URI + = "android.media.metadata.DISPLAY_ICON_URI"; + + /** + * A String key for identifying the content. This value is specific to the + * service providing the content. If used, this should be a persistent + * unique key for the underlying content. + */ + public static final String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID"; + + private static final int METADATA_TYPE_LONG = 0; + private static final int METADATA_TYPE_TEXT = 1; + private static final int METADATA_TYPE_BITMAP = 2; + private static final int METADATA_TYPE_RATING = 3; + private static final ArrayMap METADATA_KEYS_TYPE; + + static { + METADATA_KEYS_TYPE = new ArrayMap(); + METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_COMPILATION, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING); + METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_TITLE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_SUBTITLE, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON, METADATA_TYPE_BITMAP); + METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON_URI, METADATA_TYPE_TEXT); + METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_ID, METADATA_TYPE_TEXT); + } + + private static final String[] PREFERRED_DESCRIPTION_ORDER = { + METADATA_KEY_TITLE, + METADATA_KEY_ARTIST, + METADATA_KEY_ALBUM, + METADATA_KEY_ALBUM_ARTIST, + METADATA_KEY_WRITER, + METADATA_KEY_AUTHOR, + METADATA_KEY_COMPOSER + }; + + private static final String[] PREFERRED_BITMAP_ORDER = { + METADATA_KEY_DISPLAY_ICON, + METADATA_KEY_ART, + METADATA_KEY_ALBUM_ART + }; + + private static final String[] PREFERRED_URI_ORDER = { + METADATA_KEY_DISPLAY_ICON_URI, + METADATA_KEY_ART_URI, + METADATA_KEY_ALBUM_ART_URI + }; + + private final Bundle mBundle; + private Object mMetadataObj; + private MediaDescriptionCompat mDescription; + + private MediaMetadataCompat(Bundle bundle) { + mBundle = new Bundle(bundle); + } + + private MediaMetadataCompat(Parcel in) { + mBundle = in.readBundle(); + } + + /** + * Returns true if the given key is contained in the metadata + * + * @param key a String key + * @return true if the key exists in this metadata, false otherwise + */ + public boolean containsKey(String key) { + return mBundle.containsKey(key); + } + + /** + * Returns the value associated with the given key, or null if no mapping of + * the desired type exists for the given key or a null value is explicitly + * associated with the key. + * + * @param key The key the value is stored under + * @return a CharSequence value, or null + */ + public CharSequence getText(String key) { + return mBundle.getCharSequence(key); + } + + /** + * Returns the value associated with the given key, or null if no mapping of + * the desired type exists for the given key or a null value is explicitly + * associated with the key. + * + * @param key The key the value is stored under + * @return a String value, or null + */ + public String getString(String key) { + CharSequence text = mBundle.getCharSequence(key); + if (text != null) { + return text.toString(); + } + return null; + } + + /** + * Returns the value associated with the given key, or 0L if no long exists + * for the given key. + * + * @param key The key the value is stored under + * @return a long value + */ + public long getLong(String key) { + return mBundle.getLong(key, 0); + } + + /** + * Return a {@link RatingCompat} for the given key or null if no rating exists for + * the given key. + * + * @param key The key the value is stored under + * @return A {@link RatingCompat} or null + */ + public RatingCompat getRating(String key) { + RatingCompat rating = null; + try { + rating = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.w(TAG, "Failed to retrieve a key as Rating.", e); + } + return rating; + } + + /** + * Return a {@link Bitmap} for the given key or null if no bitmap exists for + * the given key. + * + * @param key The key the value is stored under + * @return A {@link Bitmap} or null + */ + public Bitmap getBitmap(String key) { + Bitmap bmp = null; + try { + bmp = mBundle.getParcelable(key); + } catch (Exception e) { + // ignore, value was not a bitmap + Log.w(TAG, "Failed to retrieve a key as Bitmap.", e); + } + return bmp; + } + + /** + * Returns a simple description of this metadata for display purposes. + * + * @return A simple description of this metadata. + */ + public MediaDescriptionCompat getDescription() { + if (mDescription != null) { + return mDescription; + } + + String mediaId = getString(METADATA_KEY_MEDIA_ID); + + CharSequence[] text = new CharSequence[3]; + Bitmap icon = null; + Uri iconUri = null; + + // First handle the case where display data is set already + CharSequence displayText = getText(METADATA_KEY_DISPLAY_TITLE); + if (!TextUtils.isEmpty(displayText)) { + // If they have a display title use only display data, otherwise use + // our best bets + text[0] = displayText; + text[1] = getText(METADATA_KEY_DISPLAY_SUBTITLE); + text[2] = getText(METADATA_KEY_DISPLAY_DESCRIPTION); + } else { + // Use whatever fields we can + int textIndex = 0; + int keyIndex = 0; + while (textIndex < text.length && keyIndex < PREFERRED_DESCRIPTION_ORDER.length) { + CharSequence next = getText(PREFERRED_DESCRIPTION_ORDER[keyIndex++]); + if (!TextUtils.isEmpty(next)) { + // Fill in the next empty bit of text + text[textIndex++] = next; + } + } + } + + // Get the best art bitmap we can find + for (int i = 0; i < PREFERRED_BITMAP_ORDER.length; i++) { + Bitmap next = getBitmap(PREFERRED_BITMAP_ORDER[i]); + if (next != null) { + icon = next; + break; + } + } + + // Get the best Uri we can find + for (int i = 0; i < PREFERRED_URI_ORDER.length; i++) { + String next = getString(PREFERRED_URI_ORDER[i]); + if (!TextUtils.isEmpty(next)) { + iconUri = Uri.parse(next); + break; + } + } + + MediaDescriptionCompat.Builder bob = new MediaDescriptionCompat.Builder(); + bob.setMediaId(mediaId); + bob.setTitle(text[0]); + bob.setSubtitle(text[1]); + bob.setDescription(text[2]); + bob.setIconBitmap(icon); + bob.setIconUri(iconUri); + mDescription = bob.build(); + + return mDescription; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeBundle(mBundle); + } + + /** + * Get the number of fields in this metadata. + * + * @return The number of fields in the metadata. + */ + public int size() { + return mBundle.size(); + } + + /** + * Returns a Set containing the Strings used as keys in this metadata. + * + * @return a Set of String keys + */ + public Set keySet() { + return mBundle.keySet(); + } + + /** + * Gets the bundle backing the metadata object. This is available to support + * backwards compatibility. Apps should not modify the bundle directly. + * + * @return The Bundle backing this metadata. + */ + public Bundle getBundle() { + return mBundle; + } + + /** + * Creates an instance from a framework {@link android.media.MediaMetadata} + * object. + *

+ * This method is only supported on + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + *

+ * + * @param metadataObj A {@link android.media.MediaMetadata} object, or null + * if none. + * @return An equivalent {@link MediaMetadataCompat} object, or null if + * none. + */ + public static MediaMetadataCompat fromMediaMetadata(Object metadataObj) { + if (metadataObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + Builder builder = new Builder(); + for (String key : MediaMetadataCompatApi21.keySet(metadataObj)) { + Integer type = METADATA_KEYS_TYPE.get(key); + if (type != null) { + switch (type) { + case METADATA_TYPE_BITMAP: + builder.putBitmap(key, + MediaMetadataCompatApi21.getBitmap(metadataObj, key)); + break; + case METADATA_TYPE_LONG: + builder.putLong(key, + MediaMetadataCompatApi21.getLong(metadataObj, key)); + break; + case METADATA_TYPE_RATING: + builder.putRating(key, RatingCompat.fromRating( + MediaMetadataCompatApi21.getRating(metadataObj, key))); + break; + case METADATA_TYPE_TEXT: + builder.putText(key, + MediaMetadataCompatApi21.getText(metadataObj, key)); + break; + } + } + } + MediaMetadataCompat metadata = builder.build(); + metadata.mMetadataObj = metadataObj; + return metadata; + } + + /** + * Gets the underlying framework {@link android.media.MediaMetadata} object. + *

+ * This method is only supported on + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + *

+ * + * @return An equivalent {@link android.media.MediaMetadata} object, or null + * if none. + */ + public Object getMediaMetadata() { + if (mMetadataObj != null || Build.VERSION.SDK_INT < 21) { + return mMetadataObj; + } + + Object builderObj = MediaMetadataCompatApi21.Builder.newInstance(); + for (String key : keySet()) { + Integer type = METADATA_KEYS_TYPE.get(key); + if (type != null) { + switch (type) { + case METADATA_TYPE_BITMAP: + MediaMetadataCompatApi21.Builder.putBitmap(builderObj, key, + getBitmap(key)); + break; + case METADATA_TYPE_LONG: + MediaMetadataCompatApi21.Builder.putLong(builderObj, key, + getLong(key)); + break; + case METADATA_TYPE_RATING: + MediaMetadataCompatApi21.Builder.putRating(builderObj, key, + getRating(key).getRating()); + break; + case METADATA_TYPE_TEXT: + MediaMetadataCompatApi21.Builder.putText(builderObj, key, + getText(key)); + break; + } + } + } + mMetadataObj = MediaMetadataCompatApi21.Builder.build(builderObj); + return mMetadataObj; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public MediaMetadataCompat createFromParcel(Parcel in) { + return new MediaMetadataCompat(in); + } + + @Override + public MediaMetadataCompat[] newArray(int size) { + return new MediaMetadataCompat[size]; + } + }; + + /** + * Use to build MediaMetadata objects. The system defined metadata keys must + * use the appropriate data type. + */ + public static final class Builder { + private final Bundle mBundle; + + /** + * Create an empty Builder. Any field that should be included in the + * {@link MediaMetadataCompat} must be added. + */ + public Builder() { + mBundle = new Bundle(); + } + + /** + * Create a Builder using a {@link MediaMetadataCompat} instance to set the + * initial values. All fields in the source metadata will be included in + * the new metadata. Fields can be overwritten by adding the same key. + * + * @param source + */ + public Builder(MediaMetadataCompat source) { + mBundle = new Bundle(source.mBundle); + } + + /** + * Put a CharSequence value into the metadata. Custom keys may be used, + * but if the METADATA_KEYs defined in this class are used they may only + * be one of the following: + *
    + *
  • {@link #METADATA_KEY_TITLE}
  • + *
  • {@link #METADATA_KEY_ARTIST}
  • + *
  • {@link #METADATA_KEY_ALBUM}
  • + *
  • {@link #METADATA_KEY_AUTHOR}
  • + *
  • {@link #METADATA_KEY_WRITER}
  • + *
  • {@link #METADATA_KEY_COMPOSER}
  • + *
  • {@link #METADATA_KEY_DATE}
  • + *
  • {@link #METADATA_KEY_GENRE}
  • + *
  • {@link #METADATA_KEY_ALBUM_ARTIST}
  • + *
  • {@link #METADATA_KEY_ART_URI}
  • + *
  • {@link #METADATA_KEY_ALBUM_ART_URI}
  • + *
  • {@link #METADATA_KEY_DISPLAY_TITLE}
  • + *
  • {@link #METADATA_KEY_DISPLAY_SUBTITLE}
  • + *
  • {@link #METADATA_KEY_DISPLAY_DESCRIPTION}
  • + *
  • {@link #METADATA_KEY_DISPLAY_ICON_URI}
  • + *
+ * + * @param key The key for referencing this value + * @param value The CharSequence value to store + * @return The Builder to allow chaining + */ + public Builder putText(String key, CharSequence value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a CharSequence"); + } + } + mBundle.putCharSequence(key, value); + return this; + } + + /** + * Put a String value into the metadata. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + *
    + *
  • {@link #METADATA_KEY_TITLE}
  • + *
  • {@link #METADATA_KEY_ARTIST}
  • + *
  • {@link #METADATA_KEY_ALBUM}
  • + *
  • {@link #METADATA_KEY_AUTHOR}
  • + *
  • {@link #METADATA_KEY_WRITER}
  • + *
  • {@link #METADATA_KEY_COMPOSER}
  • + *
  • {@link #METADATA_KEY_DATE}
  • + *
  • {@link #METADATA_KEY_GENRE}
  • + *
  • {@link #METADATA_KEY_ALBUM_ARTIST}
  • + *
  • {@link #METADATA_KEY_ART_URI}
  • + *
  • {@link #METADATA_KEY_ALBUM_ART_URI}
  • + *
  • {@link #METADATA_KEY_DISPLAY_TITLE}
  • + *
  • {@link #METADATA_KEY_DISPLAY_SUBTITLE}
  • + *
  • {@link #METADATA_KEY_DISPLAY_DESCRIPTION}
  • + *
  • {@link #METADATA_KEY_DISPLAY_ICON_URI}
  • + *
+ * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putString(String key, String value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a String"); + } + } + mBundle.putCharSequence(key, value); + return this; + } + + /** + * Put a long value into the metadata. Custom keys may be used, but if + * the METADATA_KEYs defined in this class are used they may only be one + * of the following: + *
    + *
  • {@link #METADATA_KEY_DURATION}
  • + *
  • {@link #METADATA_KEY_TRACK_NUMBER}
  • + *
  • {@link #METADATA_KEY_NUM_TRACKS}
  • + *
  • {@link #METADATA_KEY_DISC_NUMBER}
  • + *
  • {@link #METADATA_KEY_YEAR}
  • + *
+ * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putLong(String key, long value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a long"); + } + } + mBundle.putLong(key, value); + return this; + } + + /** + * Put a {@link RatingCompat} into the metadata. Custom keys may be used, but + * if the METADATA_KEYs defined in this class are used they may only be + * one of the following: + *
    + *
  • {@link #METADATA_KEY_RATING}
  • + *
  • {@link #METADATA_KEY_USER_RATING}
  • + *
+ * + * @param key The key for referencing this value + * @param value The String value to store + * @return The Builder to allow chaining + */ + public Builder putRating(String key, RatingCompat value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a Rating"); + } + } + mBundle.putParcelable(key, value); + return this; + } + + /** + * Put a {@link Bitmap} into the metadata. Custom keys may be used, but + * if the METADATA_KEYs defined in this class are used they may only be + * one of the following: + *
    + *
  • {@link #METADATA_KEY_ART}
  • + *
  • {@link #METADATA_KEY_ALBUM_ART}
  • + *
  • {@link #METADATA_KEY_DISPLAY_ICON}
  • + *
+ * + * @param key The key for referencing this value + * @param value The Bitmap to store + * @return The Builder to allow chaining + */ + public Builder putBitmap(String key, Bitmap value) { + if (METADATA_KEYS_TYPE.containsKey(key)) { + if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) { + throw new IllegalArgumentException("The " + key + + " key cannot be used to put a Bitmap"); + } + } + mBundle.putParcelable(key, value); + return this; + } + + /** + * Creates a {@link MediaMetadataCompat} instance with the specified fields. + * + * @return The new MediaMetadata instance + */ + public MediaMetadataCompat build() { + return new MediaMetadataCompat(mBundle); + } + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompatApi21.java new file mode 100644 index 00000000000..eddcf769445 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/MediaMetadataCompatApi21.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media; + +import android.graphics.Bitmap; +import android.media.MediaMetadata; +import android.media.Rating; + +import java.util.Set; + +class MediaMetadataCompatApi21 { + public static Set keySet(Object metadataObj) { + return ((MediaMetadata)metadataObj).keySet(); + } + + public static Bitmap getBitmap(Object metadataObj, String key) { + return ((MediaMetadata)metadataObj).getBitmap(key); + } + + public static long getLong(Object metadataObj, String key) { + return ((MediaMetadata)metadataObj).getLong(key); + } + + public static Object getRating(Object metadataObj, String key) { + return ((MediaMetadata)metadataObj).getRating(key); + } + + public static CharSequence getText(Object metadataObj, String key) { + return ((MediaMetadata) metadataObj).getText(key); + } + + public static class Builder { + public static Object newInstance() { + return new MediaMetadata.Builder(); + } + + public static void putBitmap(Object builderObj, String key, Bitmap value) { + ((MediaMetadata.Builder)builderObj).putBitmap(key, value); + } + + public static void putLong(Object builderObj, String key, long value) { + ((MediaMetadata.Builder)builderObj).putLong(key, value); + } + + public static void putRating(Object builderObj, String key, Object ratingObj) { + ((MediaMetadata.Builder)builderObj).putRating(key, (Rating)ratingObj); + } + + public static void putText(Object builderObj, String key, CharSequence value) { + ((MediaMetadata.Builder) builderObj).putText(key, value); + } + + public static void putString(Object builderObj, String key, String value) { + ((MediaMetadata.Builder) builderObj).putString(key, value); + } + + public static Object build(Object builderObj) { + return ((MediaMetadata.Builder)builderObj).build(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.aidl new file mode 100644 index 00000000000..223fd5c96f7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.support.v4.media; + +parcelable RatingCompat; diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.java new file mode 100644 index 00000000000..c7da6ec4a03 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompat.java @@ -0,0 +1,370 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media; + +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Log; + +/** + * A class to encapsulate rating information used as content metadata. + * A rating is defined by its rating style (see {@link #RATING_HEART}, + * {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, + * {@link #RATING_5_STARS} or {@link #RATING_PERCENTAGE}) and the actual rating value (which may + * be defined as "unrated"), both of which are defined when the rating instance is constructed + * through one of the factory methods. + */ +public final class RatingCompat implements Parcelable { + private final static String TAG = "Rating"; + + /** + * Indicates a rating style is not supported. A Rating will never have this + * type, but can be used by other classes to indicate they do not support + * Rating. + */ + public final static int RATING_NONE = 0; + + /** + * A rating style with a single degree of rating, "heart" vs "no heart". Can be used to + * indicate the content referred to is a favorite (or not). + */ + public final static int RATING_HEART = 1; + + /** + * A rating style for "thumb up" vs "thumb down". + */ + public final static int RATING_THUMB_UP_DOWN = 2; + + /** + * A rating style with 0 to 3 stars. + */ + public final static int RATING_3_STARS = 3; + + /** + * A rating style with 0 to 4 stars. + */ + public final static int RATING_4_STARS = 4; + + /** + * A rating style with 0 to 5 stars. + */ + public final static int RATING_5_STARS = 5; + + /** + * A rating style expressed as a percentage. + */ + public final static int RATING_PERCENTAGE = 6; + + private final static float RATING_NOT_RATED = -1.0f; + + private final int mRatingStyle; + private final float mRatingValue; + + private Object mRatingObj; // framework Rating object + + private RatingCompat(int ratingStyle, float rating) { + mRatingStyle = ratingStyle; + mRatingValue = rating; + } + + @Override + public String toString() { + return "Rating:style=" + mRatingStyle + " rating=" + + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue)); + } + + @Override + public int describeContents() { + return mRatingStyle; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mRatingStyle); + dest.writeFloat(mRatingValue); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + /** + * Rebuilds a Rating previously stored with writeToParcel(). + * @param p Parcel object to read the Rating from + * @return a new Rating created from the data in the parcel + */ + @Override + public RatingCompat createFromParcel(Parcel p) { + return new RatingCompat(p.readInt(), p.readFloat()); + } + + @Override + public RatingCompat[] newArray(int size) { + return new RatingCompat[size]; + } + }; + + /** + * Return a Rating instance with no rating. + * Create and return a new Rating instance with no rating known for the given + * rating style. + * @param ratingStyle one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN}, + * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS}, + * or {@link #RATING_PERCENTAGE}. + * @return null if an invalid rating style is passed, a new Rating instance otherwise. + */ + public static RatingCompat newUnratedRating(int ratingStyle) { + switch(ratingStyle) { + case RATING_HEART: + case RATING_THUMB_UP_DOWN: + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + case RATING_PERCENTAGE: + return new RatingCompat(ratingStyle, RATING_NOT_RATED); + default: + return null; + } + } + + /** + * Return a Rating instance with a heart-based rating. + * Create and return a new Rating instance with a rating style of {@link #RATING_HEART}, + * and a heart-based rating. + * @param hasHeart true for a "heart selected" rating, false for "heart unselected". + * @return a new Rating instance. + */ + public static RatingCompat newHeartRating(boolean hasHeart) { + return new RatingCompat(RATING_HEART, hasHeart ? 1.0f : 0.0f); + } + + /** + * Return a Rating instance with a thumb-based rating. + * Create and return a new Rating instance with a {@link #RATING_THUMB_UP_DOWN} + * rating style, and a "thumb up" or "thumb down" rating. + * @param thumbIsUp true for a "thumb up" rating, false for "thumb down". + * @return a new Rating instance. + */ + public static RatingCompat newThumbRating(boolean thumbIsUp) { + return new RatingCompat(RATING_THUMB_UP_DOWN, thumbIsUp ? 1.0f : 0.0f); + } + + /** + * Return a Rating instance with a star-based rating. + * Create and return a new Rating instance with one of the star-base rating styles + * and the given integer or fractional number of stars. Non integer values can for instance + * be used to represent an average rating value, which might not be an integer number of stars. + * @param starRatingStyle one of {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, + * {@link #RATING_5_STARS}. + * @param starRating a number ranging from 0.0f to 3.0f, 4.0f or 5.0f according to + * the rating style. + * @return null if the rating style is invalid, or the rating is out of range, + * a new Rating instance otherwise. + */ + public static RatingCompat newStarRating(int starRatingStyle, float starRating) { + float maxRating = -1.0f; + switch(starRatingStyle) { + case RATING_3_STARS: + maxRating = 3.0f; + break; + case RATING_4_STARS: + maxRating = 4.0f; + break; + case RATING_5_STARS: + maxRating = 5.0f; + break; + default: + Log.e(TAG, "Invalid rating style (" + starRatingStyle + ") for a star rating"); + return null; + } + if ((starRating < 0.0f) || (starRating > maxRating)) { + Log.e(TAG, "Trying to set out of range star-based rating"); + return null; + } + return new RatingCompat(starRatingStyle, starRating); + } + + /** + * Return a Rating instance with a percentage-based rating. + * Create and return a new Rating instance with a {@link #RATING_PERCENTAGE} + * rating style, and a rating of the given percentage. + * @param percent the value of the rating + * @return null if the rating is out of range, a new Rating instance otherwise. + */ + public static RatingCompat newPercentageRating(float percent) { + if ((percent < 0.0f) || (percent > 100.0f)) { + Log.e(TAG, "Invalid percentage-based rating value"); + return null; + } else { + return new RatingCompat(RATING_PERCENTAGE, percent); + } + } + + /** + * Return whether there is a rating value available. + * @return true if the instance was not created with {@link #newUnratedRating(int)}. + */ + public boolean isRated() { + return mRatingValue >= 0.0f; + } + + /** + * Return the rating style. + * @return one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN}, + * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS}, + * or {@link #RATING_PERCENTAGE}. + */ + public int getRatingStyle() { + return mRatingStyle; + } + + /** + * Return whether the rating is "heart selected". + * @return true if the rating is "heart selected", false if the rating is "heart unselected", + * if the rating style is not {@link #RATING_HEART} or if it is unrated. + */ + public boolean hasHeart() { + if (mRatingStyle != RATING_HEART) { + return false; + } else { + return (mRatingValue == 1.0f); + } + } + + /** + * Return whether the rating is "thumb up". + * @return true if the rating is "thumb up", false if the rating is "thumb down", + * if the rating style is not {@link #RATING_THUMB_UP_DOWN} or if it is unrated. + */ + public boolean isThumbUp() { + if (mRatingStyle != RATING_THUMB_UP_DOWN) { + return false; + } else { + return (mRatingValue == 1.0f); + } + } + + /** + * Return the star-based rating value. + * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is + * not star-based, or if it is unrated. + */ + public float getStarRating() { + switch (mRatingStyle) { + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + if (isRated()) { + return mRatingValue; + } + default: + return -1.0f; + } + } + + /** + * Return the percentage-based rating value. + * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is + * not percentage-based, or if it is unrated. + */ + public float getPercentRating() { + if ((mRatingStyle != RATING_PERCENTAGE) || !isRated()) { + return -1.0f; + } else { + return mRatingValue; + } + } + + /** + * Creates an instance from a framework {@link android.media.Rating} object. + *

+ * This method is only supported on API 21+. + *

+ * + * @param ratingObj A {@link android.media.Rating} object, or null if none. + * @return An equivalent {@link RatingCompat} object, or null if none. + */ + public static RatingCompat fromRating(Object ratingObj) { + if (ratingObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + final int ratingStyle = RatingCompatApi21.getRatingStyle(ratingObj); + final RatingCompat rating; + if (RatingCompatApi21.isRated(ratingObj)) { + switch (ratingStyle) { + case RATING_HEART: + rating = newHeartRating(RatingCompatApi21.hasHeart(ratingObj)); + break; + case RATING_THUMB_UP_DOWN: + rating = newThumbRating(RatingCompatApi21.isThumbUp(ratingObj)); + break; + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + rating = newStarRating(ratingStyle, + RatingCompatApi21.getStarRating(ratingObj)); + break; + case RATING_PERCENTAGE: + rating = newPercentageRating(RatingCompatApi21.getPercentRating(ratingObj)); + break; + default: + return null; + } + } else { + rating = newUnratedRating(ratingStyle); + } + rating.mRatingObj = ratingObj; + return rating; + } + + /** + * Gets the underlying framework {@link android.media.Rating} object. + *

+ * This method is only supported on API 21+. + *

+ * + * @return An equivalent {@link android.media.Rating} object, or null if none. + */ + public Object getRating() { + if (mRatingObj != null || Build.VERSION.SDK_INT < 21) { + return mRatingObj; + } + + if (isRated()) { + switch (mRatingStyle) { + case RATING_HEART: + mRatingObj = RatingCompatApi21.newHeartRating(hasHeart()); + break; + case RATING_THUMB_UP_DOWN: + mRatingObj = RatingCompatApi21.newThumbRating(isThumbUp()); + break; + case RATING_3_STARS: + case RATING_4_STARS: + case RATING_5_STARS: + mRatingObj = RatingCompatApi21.newStarRating(mRatingStyle, getStarRating()); + break; + case RATING_PERCENTAGE: + mRatingObj = RatingCompatApi21.newPercentageRating(getPercentRating()); + default: + return null; + } + } else { + mRatingObj = RatingCompatApi21.newUnratedRating(mRatingStyle); + } + return mRatingObj; + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompatApi21.java new file mode 100644 index 00000000000..6c8b9b2b4f4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/RatingCompatApi21.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media; + +import android.media.Rating; + +class RatingCompatApi21 { + public static Object newUnratedRating(int ratingStyle) { + return Rating.newUnratedRating(ratingStyle); + } + + public static Object newHeartRating(boolean hasHeart) { + return Rating.newHeartRating(hasHeart); + } + + public static Object newThumbRating(boolean thumbIsUp) { + return Rating.newThumbRating(thumbIsUp); + } + + public static Object newStarRating(int starRatingStyle, float starRating) { + return Rating.newStarRating(starRatingStyle, starRating); + } + + public static Object newPercentageRating(float percent) { + return Rating.newPercentageRating(percent); + } + + public static boolean isRated(Object ratingObj) { + return ((Rating)ratingObj).isRated(); + } + + public static int getRatingStyle(Object ratingObj) { + return ((Rating)ratingObj).getRatingStyle(); + } + + public static boolean hasHeart(Object ratingObj) { + return ((Rating)ratingObj).hasHeart(); + } + + public static boolean isThumbUp(Object ratingObj) { + return ((Rating)ratingObj).isThumbUp(); + } + + public static float getStarRating(Object ratingObj) { + return ((Rating)ratingObj).getStarRating(); + } + + public static float getPercentRating(Object ratingObj) { + return ((Rating)ratingObj).getPercentRating(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/TransportController.java b/mobile/google-android-support-v4/src/android/support/v4/media/TransportController.java new file mode 100644 index 00000000000..b92a4a10f81 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/TransportController.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media; + +/** + * Base interface to controlling a media transport. This is the + * interface for implementing things like on-screen controls: it + * allows them to request changes in playback, retrieve the current + * playback state, and monitor for changes to the playback state. + */ +public abstract class TransportController { + /** + * Start listening to changes in playback state. + */ + public abstract void registerStateListener(TransportStateListener listener); + + /** + * Stop listening to changes in playback state. + */ + public abstract void unregisterStateListener(TransportStateListener listener); + + /** + * Request that the player start its playback at its current position. + */ + public abstract void startPlaying(); + + /** + * Request that the player pause its playback and stay at its current position. + */ + public abstract void pausePlaying(); + + /** + * Request that the player stop its playback; it may clear its state in whatever + * way is appropriate. + */ + public abstract void stopPlaying(); + + /** + * Retrieve the total duration of the media stream, in milliseconds. + */ + public abstract long getDuration(); + + /** + * Retrieve the current playback location in the media stream, in milliseconds. + */ + public abstract long getCurrentPosition(); + + /** + * Move to a new location in the media stream. + * @param pos Position to move to, in milliseconds. + */ + public abstract void seekTo(long pos); + + /** + * Return whether the player is currently playing its stream. + */ + public abstract boolean isPlaying(); + + /** + * Retrieve amount, in percentage (0-100), that the media stream has been buffered + * on to the local device. Return 100 if the stream is always local. + */ + public abstract int getBufferPercentage(); + + /** + * Retrieve the flags for the media transport control buttons that this transport supports. + * Result is a combination of the following flags: + * {@link TransportMediator#FLAG_KEY_MEDIA_PREVIOUS}, + * {@link TransportMediator#FLAG_KEY_MEDIA_REWIND}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PLAY}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PLAY_PAUSE}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PAUSE}, + * {@link TransportMediator#FLAG_KEY_MEDIA_STOP}, + * {@link TransportMediator#FLAG_KEY_MEDIA_FAST_FORWARD}, + * {@link TransportMediator#FLAG_KEY_MEDIA_NEXT} + */ + public abstract int getTransportControlFlags(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediator.java b/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediator.java new file mode 100644 index 00000000000..16bf94b558e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediator.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media; + +import android.app.Activity; +import android.content.Context; +import android.media.AudioManager; +import android.os.Build; +import android.support.v4.view.KeyEventCompat; +import android.view.KeyEvent; +import android.view.View; + +import java.util.ArrayList; + +/** + * Helper for implementing a media transport control (with play, pause, skip, and + * other media actions). Takes care of both key events and advanced features + * like {@link android.media.RemoteControlClient}. This class is intended to + * serve as an intermediary between transport controls (whether they be on-screen + * controls, hardware buttons, remote controls) and the actual player. The player + * is represented by a single {@link TransportPerformer} that must be supplied to + * this class. On-screen controls that want to control and show the state of the + * player should do this through calls to the {@link TransportController} interface. + * + *

Here is a simple but fairly complete sample of a video player that is built + * around this class. Note that the MediaController class used here is not the one + * included in the standard Android framework, but a custom implementation. Real + * applications often implement their own transport controls, or you can copy the + * implementation here out of Support4Demos.

+ * + * {@sample development/samples/Support4Demos/src/com/example/android/supportv4/media/TransportControllerActivity.java + * complete} + */ +public class TransportMediator extends TransportController { + final Context mContext; + final TransportPerformer mCallbacks; + final AudioManager mAudioManager; + final View mView; + final Object mDispatcherState; + final TransportMediatorJellybeanMR2 mController; + final ArrayList mListeners + = new ArrayList(); + final TransportMediatorCallback mTransportKeyCallback + = new TransportMediatorCallback() { + @Override + public void handleKey(KeyEvent key) { + key.dispatch(mKeyEventCallback); + } + @Override + public void handleAudioFocusChange(int focusChange) { + mCallbacks.onAudioFocusChange(focusChange); + } + + @Override + public long getPlaybackPosition() { + return mCallbacks.onGetCurrentPosition(); + } + + @Override + public void playbackPositionUpdate(long newPositionMs) { + mCallbacks.onSeekTo(newPositionMs); + } + }; + + /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_PLAY KeyEvent.KEYCODE_MEDIA_PLAY} */ + public static final int KEYCODE_MEDIA_PLAY = 126; + /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_PAUSE KeyEvent.KEYCODE_MEDIA_PAUSE} */ + public static final int KEYCODE_MEDIA_PAUSE = 127; + /** Synonym for {@link KeyEvent#KEYCODE_MEDIA_RECORD KeyEvent.KEYCODE_MEDIA_RECORD} */ + public static final int KEYCODE_MEDIA_RECORD = 130; + + /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PREVIOUS + * RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS */ + public final static int FLAG_KEY_MEDIA_PREVIOUS = 1 << 0; + /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_REWIND + * RemoteControlClient.FLAG_KEY_MEDIA_REWIND */ + public final static int FLAG_KEY_MEDIA_REWIND = 1 << 1; + /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY + * RemoteControlClient.FLAG_KEY_MEDIA_PLAY */ + public final static int FLAG_KEY_MEDIA_PLAY = 1 << 2; + /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PLAY_PAUSE + * RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE */ + public final static int FLAG_KEY_MEDIA_PLAY_PAUSE = 1 << 3; + /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_PAUSE + * RemoteControlClient.FLAG_KEY_MEDIA_PAUSE */ + public final static int FLAG_KEY_MEDIA_PAUSE = 1 << 4; + /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_STOP + * RemoteControlClient.FLAG_KEY_MEDIA_STOP */ + public final static int FLAG_KEY_MEDIA_STOP = 1 << 5; + /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_FAST_FORWARD + * RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD */ + public final static int FLAG_KEY_MEDIA_FAST_FORWARD = 1 << 6; + /** Synonym for {@link android.media.RemoteControlClient#FLAG_KEY_MEDIA_NEXT + * RemoteControlClient.FLAG_KEY_MEDIA_NEXT */ + public final static int FLAG_KEY_MEDIA_NEXT = 1 << 7; + + static boolean isMediaKey(int keyCode) { + switch (keyCode) { + case KEYCODE_MEDIA_PLAY: + case KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { + return true; + } + } + return false; + } + + final KeyEvent.Callback mKeyEventCallback = new KeyEvent.Callback() { + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + return isMediaKey(keyCode) ? mCallbacks.onMediaButtonDown(keyCode, event) : false; + } + + public boolean onKeyLongPress(int keyCode, KeyEvent event) { + return false; + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + return isMediaKey(keyCode) ? mCallbacks.onMediaButtonUp(keyCode, event) : false; + } + + @Override + public boolean onKeyMultiple(int keyCode, int count, KeyEvent event) { + return false; + } + }; + + public TransportMediator(Activity activity, TransportPerformer callbacks) { + this(activity, null, callbacks); + } + + public TransportMediator(View view, TransportPerformer callbacks) { + this(null, view, callbacks); + } + + private TransportMediator(Activity activity, View view, TransportPerformer callbacks) { + mContext = activity != null ? activity : view.getContext(); + mCallbacks = callbacks; + mAudioManager = (AudioManager)mContext.getSystemService(Context.AUDIO_SERVICE); + mView = activity != null ? activity.getWindow().getDecorView() : view; + mDispatcherState = KeyEventCompat.getKeyDispatcherState(mView); + if (Build.VERSION.SDK_INT >= 18) { // JellyBean MR2 + mController = new TransportMediatorJellybeanMR2(mContext, mAudioManager, + mView, mTransportKeyCallback); + } else { + mController = null; + } + } + + /** + * Return the {@link android.media.RemoteControlClient} associated with this transport. + * This returns a generic Object since the RemoteControlClient is not availble before + * {@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH}. Further, this class + * will not use RemoteControlClient in its implementation until + * {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}. You should always check for + * null here and not do anything with the RemoteControlClient if none is given; this + * way you don't need to worry about the current platform API version. + * + *

Note that this class takes possession of the + * {@link android.media.RemoteControlClient.OnGetPlaybackPositionListener} and + * {@link android.media.RemoteControlClient.OnPlaybackPositionUpdateListener} callbacks; + * you will interact with these through + * {@link TransportPerformer#onGetCurrentPosition() TransportPerformer.onGetCurrentPosition} and + * {@link TransportPerformer#onSeekTo TransportPerformer.onSeekTo}, respectively.

+ */ + public Object getRemoteControlClient() { + return mController != null ? mController.getRemoteControlClient() : null; + } + + /** + * Must call from {@link Activity#dispatchKeyEvent Activity.dispatchKeyEvent} to give + * the transport an opportunity to intercept media keys. Any such keys will show up + * in {@link TransportPerformer}. + * @param event + */ + public boolean dispatchKeyEvent(KeyEvent event) { + return KeyEventCompat.dispatch(event, mKeyEventCallback, mDispatcherState, this); + } + + public void registerStateListener(TransportStateListener listener) { + mListeners.add(listener); + } + + public void unregisterStateListener(TransportStateListener listener) { + mListeners.remove(listener); + } + + private TransportStateListener[] getListeners() { + if (mListeners.size() <= 0) { + return null; + } + TransportStateListener listeners[] = new TransportStateListener[mListeners.size()]; + mListeners.toArray(listeners); + return listeners; + } + + private void reportPlayingChanged() { + TransportStateListener[] listeners = getListeners(); + if (listeners != null) { + for (TransportStateListener listener : listeners) { + listener.onPlayingChanged(this); + } + } + } + + private void reportTransportControlsChanged() { + TransportStateListener[] listeners = getListeners(); + if (listeners != null) { + for (TransportStateListener listener : listeners) { + listener.onTransportControlsChanged(this); + } + } + } + + private void pushControllerState() { + if (mController != null) { + mController.refreshState(mCallbacks.onIsPlaying(), + mCallbacks.onGetCurrentPosition(), + mCallbacks.onGetTransportControlFlags()); + } + } + + public void refreshState() { + pushControllerState(); + reportPlayingChanged(); + reportTransportControlsChanged(); + } + + /** + * Move the controller into the playing state. This updates the remote control + * client to indicate it is playing, and takes audio focus for the app. + */ + @Override + public void startPlaying() { + if (mController != null) { + mController.startPlaying(); + } + mCallbacks.onStart(); + pushControllerState(); + reportPlayingChanged(); + } + + /** + * Move the controller into the paused state. This updates the remote control + * client to indicate it is paused, but keeps audio focus. + */ + @Override + public void pausePlaying() { + if (mController != null) { + mController.pausePlaying(); + } + mCallbacks.onPause(); + pushControllerState(); + reportPlayingChanged(); + } + + /** + * Move the controller into the stopped state. This updates the remote control + * client to indicate it is stopped, and removes audio focus from the app. + */ + @Override + public void stopPlaying() { + if (mController != null) { + mController.stopPlaying(); + } + mCallbacks.onStop(); + pushControllerState(); + reportPlayingChanged(); + } + + @Override + public long getDuration() { + return mCallbacks.onGetDuration(); + } + + @Override + public long getCurrentPosition() { + return mCallbacks.onGetCurrentPosition(); + } + + @Override + public void seekTo(long pos) { + mCallbacks.onSeekTo(pos); + } + + @Override + public boolean isPlaying() { + return mCallbacks.onIsPlaying(); + } + + @Override + public int getBufferPercentage() { + return mCallbacks.onGetBufferPercentage(); + } + + /** + * Retrieves the flags for the media transport control buttons that this transport supports. + * Result is a combination of the following flags: + * {@link #FLAG_KEY_MEDIA_PREVIOUS}, + * {@link #FLAG_KEY_MEDIA_REWIND}, + * {@link #FLAG_KEY_MEDIA_PLAY}, + * {@link #FLAG_KEY_MEDIA_PLAY_PAUSE}, + * {@link #FLAG_KEY_MEDIA_PAUSE}, + * {@link #FLAG_KEY_MEDIA_STOP}, + * {@link #FLAG_KEY_MEDIA_FAST_FORWARD}, + * {@link #FLAG_KEY_MEDIA_NEXT} + */ + public int getTransportControlFlags() { + return mCallbacks.onGetTransportControlFlags(); + } + + /** + * Optionally call when no longer using the TransportController. Its resources + * will also be automatically cleaned up when your activity/view is detached from + * its window, so you don't normally need to call this explicitly. + */ + public void destroy() { + mController.destroy(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorCallback.java b/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorCallback.java new file mode 100644 index 00000000000..7dae7dc1461 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorCallback.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media; + +import android.view.KeyEvent; + +interface TransportMediatorCallback { + public void handleKey(KeyEvent key); + public void handleAudioFocusChange(int focusChange); + public long getPlaybackPosition(); + public void playbackPositionUpdate(long newPositionMs); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorJellybeanMR2.java b/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorJellybeanMR2.java new file mode 100644 index 00000000000..38b6778f23e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/TransportMediatorJellybeanMR2.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media; + +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.media.AudioManager; +import android.media.RemoteControlClient; +import android.util.Log; +import android.view.KeyEvent; +import android.view.View; +import android.view.ViewTreeObserver; + +class TransportMediatorJellybeanMR2 + implements RemoteControlClient.OnGetPlaybackPositionListener, + RemoteControlClient.OnPlaybackPositionUpdateListener { + final Context mContext; + final AudioManager mAudioManager; + final View mTargetView; + final TransportMediatorCallback mTransportCallback; + final String mReceiverAction; + final IntentFilter mReceiverFilter; + final Intent mIntent; + final ViewTreeObserver.OnWindowAttachListener mWindowAttachListener = + new ViewTreeObserver.OnWindowAttachListener() { + @Override + public void onWindowAttached() { + windowAttached(); + } + @Override + public void onWindowDetached() { + windowDetached(); + } + }; + final ViewTreeObserver.OnWindowFocusChangeListener mWindowFocusListener = + new ViewTreeObserver.OnWindowFocusChangeListener() { + @Override + public void onWindowFocusChanged(boolean hasFocus) { + if (hasFocus) gainFocus(); + else loseFocus(); + } + }; + final BroadcastReceiver mMediaButtonReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + try { + KeyEvent event = (KeyEvent)intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT); + mTransportCallback.handleKey(event); + } catch (ClassCastException e) { + Log.w("TransportController", e); + } + } + }; + AudioManager.OnAudioFocusChangeListener mAudioFocusChangeListener + = new AudioManager.OnAudioFocusChangeListener() { + @Override + public void onAudioFocusChange(int focusChange) { + mTransportCallback.handleAudioFocusChange(focusChange); + } + }; + + PendingIntent mPendingIntent; + RemoteControlClient mRemoteControl; + boolean mFocused; + int mPlayState = 0; + boolean mAudioFocused; + + public TransportMediatorJellybeanMR2(Context context, AudioManager audioManager, + View view, TransportMediatorCallback transportCallback) { + mContext = context; + mAudioManager = audioManager; + mTargetView = view; + mTransportCallback = transportCallback; + mReceiverAction = context.getPackageName() + ":transport:" + System.identityHashCode(this); + mIntent = new Intent(mReceiverAction); + mIntent.setPackage(context.getPackageName()); + mReceiverFilter = new IntentFilter(); + mReceiverFilter.addAction(mReceiverAction); + mTargetView.getViewTreeObserver().addOnWindowAttachListener(mWindowAttachListener); + mTargetView.getViewTreeObserver().addOnWindowFocusChangeListener(mWindowFocusListener); + } + + public Object getRemoteControlClient() { + return mRemoteControl; + } + + public void destroy() { + windowDetached(); + mTargetView.getViewTreeObserver().removeOnWindowAttachListener(mWindowAttachListener); + mTargetView.getViewTreeObserver().removeOnWindowFocusChangeListener(mWindowFocusListener); + } + + void windowAttached() { + mContext.registerReceiver(mMediaButtonReceiver, mReceiverFilter); + mPendingIntent = PendingIntent.getBroadcast(mContext, 0, mIntent, + PendingIntent.FLAG_CANCEL_CURRENT); + mRemoteControl = new RemoteControlClient(mPendingIntent); + mRemoteControl.setOnGetPlaybackPositionListener(this); + mRemoteControl.setPlaybackPositionUpdateListener(this); + } + + void gainFocus() { + if (!mFocused) { + mFocused = true; + mAudioManager.registerMediaButtonEventReceiver(mPendingIntent); + mAudioManager.registerRemoteControlClient(mRemoteControl); + if (mPlayState == RemoteControlClient.PLAYSTATE_PLAYING) { + takeAudioFocus(); + } + } + } + + void takeAudioFocus() { + if (!mAudioFocused) { + mAudioFocused = true; + mAudioManager.requestAudioFocus(mAudioFocusChangeListener, + AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + } + } + + public void startPlaying() { + if (mPlayState != RemoteControlClient.PLAYSTATE_PLAYING) { + mPlayState = RemoteControlClient.PLAYSTATE_PLAYING; + mRemoteControl.setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + } + if (mFocused) { + takeAudioFocus(); + } + } + + @Override + public long onGetPlaybackPosition() { + return mTransportCallback.getPlaybackPosition(); + } + + @Override + public void onPlaybackPositionUpdate(long newPositionMs) { + mTransportCallback.playbackPositionUpdate(newPositionMs); + } + + public void refreshState(boolean playing, long position, int transportControls) { + if (mRemoteControl != null) { + mRemoteControl.setPlaybackState(playing ? RemoteControlClient.PLAYSTATE_PLAYING + : RemoteControlClient.PLAYSTATE_STOPPED, position, playing ? 1 : 0); + mRemoteControl.setTransportControlFlags(transportControls); + } + } + + public void pausePlaying() { + if (mPlayState == RemoteControlClient.PLAYSTATE_PLAYING) { + mPlayState = RemoteControlClient.PLAYSTATE_PAUSED; + mRemoteControl.setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); + } + dropAudioFocus(); + } + + public void stopPlaying() { + if (mPlayState != RemoteControlClient.PLAYSTATE_STOPPED) { + mPlayState = RemoteControlClient.PLAYSTATE_STOPPED; + mRemoteControl.setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + } + dropAudioFocus(); + } + + void dropAudioFocus() { + if (mAudioFocused) { + mAudioFocused = false; + mAudioManager.abandonAudioFocus(mAudioFocusChangeListener); + } + } + + void loseFocus() { + dropAudioFocus(); + if (mFocused) { + mFocused = false; + mAudioManager.unregisterRemoteControlClient(mRemoteControl); + mAudioManager.unregisterMediaButtonEventReceiver(mPendingIntent); + } + } + + void windowDetached() { + loseFocus(); + if (mPendingIntent != null) { + mContext.unregisterReceiver(mMediaButtonReceiver); + mPendingIntent.cancel(); + mPendingIntent = null; + mRemoteControl = null; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/TransportPerformer.java b/mobile/google-android-support-v4/src/android/support/v4/media/TransportPerformer.java new file mode 100644 index 00000000000..2d3afc9fe2f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/TransportPerformer.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media; + +import android.os.SystemClock; +import android.view.KeyEvent; + +/** + * Implemented by the playback side of the media system, to respond to + * requests to perform actions and to retrieve its current state. These + * requests may either come from key events dispatched directly to your UI, or + * events sent over a media button event receiver that this class keeps active + * while your window is in focus. + */ +public abstract class TransportPerformer { + /** + * Request to start playback on the media, resuming from whatever current state + * (position etc) it is in. + */ + public abstract void onStart(); + + /** + * Request to pause playback of the media, staying at the current playback position + * and other state so a later call to {@link #onStart()} will resume at the same place. + */ + public abstract void onPause(); + + /** + * Request to completely stop playback of the media, clearing whatever state the + * player thinks is appropriate. + */ + public abstract void onStop(); + + /** + * Request to return the duration of the current media, in milliseconds. + */ + public abstract long onGetDuration(); + + /** + * Request to return the current playback position, in milliseconds. + */ + public abstract long onGetCurrentPosition(); + + /** + * Request to move the current playback position. + * @param pos New position to move to, in milliseconds. + */ + public abstract void onSeekTo(long pos); + + /** + * Request to find out whether the player is currently playing its media. + */ + public abstract boolean onIsPlaying(); + + /** + * Request to find out how much of the media has been buffered on the local device. + * @return Return a percentage (0-100) indicating how much of the total data + * has been buffered. The default implementation returns 100, meaning the content + * is always on the local device. + */ + public int onGetBufferPercentage() { + return 100; + } + + /** + * Retrieves the flags for the media transport control buttons that this transport supports. + * Result is a combination of the following flags: + * {@link TransportMediator#FLAG_KEY_MEDIA_PREVIOUS}, + * {@link TransportMediator#FLAG_KEY_MEDIA_REWIND}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PLAY}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PLAY_PAUSE}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PAUSE}, + * {@link TransportMediator#FLAG_KEY_MEDIA_STOP}, + * {@link TransportMediator#FLAG_KEY_MEDIA_FAST_FORWARD}, + * {@link TransportMediator#FLAG_KEY_MEDIA_NEXT} + * + *

The default implementation returns: + * {@link TransportMediator#FLAG_KEY_MEDIA_PLAY}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PLAY_PAUSE}, + * {@link TransportMediator#FLAG_KEY_MEDIA_PAUSE}, and + * {@link TransportMediator#FLAG_KEY_MEDIA_STOP}

+ */ + public int onGetTransportControlFlags() { + return TransportMediator.FLAG_KEY_MEDIA_PLAY + | TransportMediator.FLAG_KEY_MEDIA_PLAY_PAUSE + | TransportMediator.FLAG_KEY_MEDIA_PAUSE + | TransportMediator.FLAG_KEY_MEDIA_STOP; + } + + /** + * Report that a media button has been pressed. This is like + * {@link android.view.KeyEvent.Callback#onKeyDown(int, android.view.KeyEvent)} but + * will only deliver media keys. The default implementation handles these keys: + *
    + *
  • KEYCODE_MEDIA_PLAY: call {@link #onStart}
  • + *
  • KEYCODE_MEDIA_PAUSE: call {@link #onPause}
  • + *
  • KEYCODE_MEDIA_STOP: call {@link #onStop}
  • + *
  • KEYCODE_MEDIA_PLAY_PAUSE and KEYCODE_HEADSETHOOK: call {@link #onPause} + * if {@link #onIsPlaying()} returns true, otherwise call {@link #onStart}
  • + *
+ * @param keyCode The code of the media key. + * @param event The full key event. + * @return Indicate whether the key has been consumed. The default + * implementation always returns true. This only matters for keys + * being dispatched here from + * {@link TransportMediator#dispatchKeyEvent(android.view.KeyEvent) + * TransportController.dispatchKeyEvent}, and determines whether the key + * continues on to its default key handling (which for media keys means + * being delivered to the current media remote control, which should + * be us). + */ + public boolean onMediaButtonDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case TransportMediator.KEYCODE_MEDIA_PLAY: + onStart(); + return true; + case TransportMediator.KEYCODE_MEDIA_PAUSE: + onPause(); + return true; + case KeyEvent.KEYCODE_MEDIA_STOP: + onStop(); + return true; + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_HEADSETHOOK: + if (onIsPlaying()) { + onPause(); + } else { + onStart(); + } + } + return true; + } + + /** + * Report that a media button has been released. This is like + * {@link KeyEvent.Callback#onKeyUp(int, android.view.KeyEvent)} but + * will only deliver media keys. The default implementation does nothing. + * @param keyCode The code of the media key. + * @param event The full key event. + * @return Indicate whether the key has been consumed. The default + * implementation always returns true. This only matters for keys + * being dispatched here from + * {@link TransportMediator#dispatchKeyEvent(android.view.KeyEvent) + * TransportController.dispatchKeyEvent}, and determines whether the key + * continues on to its default key handling (which for media keys means + * being delivered to the current media remote control, which should + * be us). + */ + public boolean onMediaButtonUp(int keyCode, KeyEvent event) { + return true; + } + + // Copy constants from framework since we can't link to them. + static final int AUDIOFOCUS_GAIN = 1; + static final int AUDIOFOCUS_GAIN_TRANSIENT = 2; + static final int AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK = 3; + static final int AUDIOFOCUS_LOSS = -1 * AUDIOFOCUS_GAIN; + static final int AUDIOFOCUS_LOSS_TRANSIENT = -1 * AUDIOFOCUS_GAIN_TRANSIENT; + static final int AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK = + -1 * AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK; + + /** + * Report that audio focus has changed on the app. This only happens if + * you have indicated you have started playing with + * {@link TransportMediator#startPlaying TransportController.startPlaying}, + * which takes audio focus for you. + * @param focusChange The type of focus change, as per + * {@link android.media.AudioManager.OnAudioFocusChangeListener#onAudioFocusChange(int) + * OnAudioFocusChangeListener.onAudioFocusChange}. The default implementation will + * deliver a {@link KeyEvent#KEYCODE_MEDIA_STOP} + * when receiving {@link android.media.AudioManager#AUDIOFOCUS_LOSS}. + */ + public void onAudioFocusChange(int focusChange) { + int keyCode = 0; + switch (focusChange) { + case AUDIOFOCUS_LOSS: + // This will cause us to stop playback, which means we drop audio focus + // so we will not get any further audio focus gain. + keyCode = TransportMediator.KEYCODE_MEDIA_PAUSE; + break; + } + if (keyCode != 0) { + final long now = SystemClock.uptimeMillis(); + onMediaButtonDown(keyCode, new KeyEvent(now, now, KeyEvent.ACTION_DOWN, keyCode, 0)); + onMediaButtonUp(keyCode, new KeyEvent(now, now, KeyEvent.ACTION_UP, keyCode, 0)); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/TransportStateListener.java b/mobile/google-android-support-v4/src/android/support/v4/media/TransportStateListener.java new file mode 100644 index 00000000000..384cca26010 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/TransportStateListener.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media; + +/** + * A listener for playback changes that can be registered with + * {@link TransportController}. + */ +public class TransportStateListener { + /** + * The play state of the transport changed. Use + * {@link android.support.v4.media.TransportController#isPlaying() + * TransportController.isPlaying()} to determine the new state. + */ + public void onPlayingChanged(TransportController controller) { + } + + /** + * The available controls of the transport changed. Use + * {@link TransportController#getTransportControlFlags()} + * TransportController.getTransportControlFlags()} to determine the new state. + */ + public void onTransportControlsChanged(TransportController controller) { + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompat.java new file mode 100644 index 00000000000..2efa7a611d0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompat.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media; + +import android.os.Build; +import android.support.v4.media.session.MediaSessionCompat; + +/** + * Handles requests to adjust or set the volume on a session. This is also used + * to push volume updates back to the session after a request has been handled. + * You can set a volume provider on a session by calling + * {@link MediaSessionCompat#setPlaybackToRemote}. + */ +public abstract class VolumeProviderCompat { + /** + * The volume is fixed and can not be modified. Requests to change volume + * should be ignored. + */ + public static final int VOLUME_CONTROL_FIXED = 0; + + /** + * The volume control uses relative adjustment via + * {@link #onAdjustVolume(int)}. Attempts to set the volume to a specific + * value should be ignored. + */ + public static final int VOLUME_CONTROL_RELATIVE = 1; + + /** + * The volume control uses an absolute value. It may be adjusted using + * {@link #onAdjustVolume(int)} or set directly using + * {@link #onSetVolumeTo(int)}. + */ + public static final int VOLUME_CONTROL_ABSOLUTE = 2; + + private final int mControlType; + private final int mMaxVolume; + private int mCurrentVolume; + private Callback mCallback; + + private Object mVolumeProviderObj; + + /** + * Create a new volume provider for handling volume events. You must specify + * the type of volume control and the maximum volume that can be used. + * + * @param volumeControl The method for controlling volume that is used by + * this provider. + * @param maxVolume The maximum allowed volume. + * @param currentVolume The current volume. + */ + public VolumeProviderCompat(int volumeControl, int maxVolume, int currentVolume) { + mControlType = volumeControl; + mMaxVolume = maxVolume; + mCurrentVolume = currentVolume; + } + + /** + * Get the current volume of the provider. + * + * @return The current volume. + */ + public final int getCurrentVolume() { + return mCurrentVolume; + } + + /** + * Get the volume control type that this volume provider uses. + * + * @return The volume control type for this volume provider + */ + public final int getVolumeControl() { + return mControlType; + } + + /** + * Get the maximum volume this provider allows. + * + * @return The max allowed volume. + */ + public final int getMaxVolume() { + return mMaxVolume; + } + + /** + * Set the current volume and notify the system that the volume has been + * changed. + * + * @param currentVolume The current volume of the output. + */ + public final void setCurrentVolume(int currentVolume) { + if (mCallback != null) { + mCallback.onVolumeChanged(this); + } + } + + /** + * Override to handle requests to set the volume of the current output. + * + * @param volume The volume to set the output to. + */ + public void onSetVolumeTo(int volume) { + } + + /** + * Override to handle requests to adjust the volume of the current output. + * + * @param direction The direction to adjust the volume in. + */ + public void onAdjustVolume(int direction) { + } + + /** + * Sets a callback to receive volume changes. + *

+ * Used internally by the support library. + *

+ */ + public void setCallback(Callback callback) { + mCallback = callback; + } + + /** + * Gets the underlying framework {@link android.media.VolumeProvider} object. + *

+ * This method is only supported on API 21+. + *

+ * + * @return An equivalent {@link android.media.VolumeProvider} object, or null if none. + */ + public Object getVolumeProvider() { + if (mVolumeProviderObj != null || Build.VERSION.SDK_INT < 21) { + return mVolumeProviderObj; + } + + mVolumeProviderObj = VolumeProviderCompatApi21.createVolumeProvider( + mControlType, mMaxVolume, mCurrentVolume, new VolumeProviderCompatApi21.Delegate() { + + @Override + public void onSetVolumeTo(int volume) { + VolumeProviderCompat.this.onSetVolumeTo(volume); + } + + @Override + public void onAdjustVolume(int direction) { + VolumeProviderCompat.this.onAdjustVolume(direction); + } + }); + return mVolumeProviderObj; + } + + /** + * Listens for changes to the volume. + */ + public static abstract class Callback { + public abstract void onVolumeChanged(VolumeProviderCompat volumeProvider); + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompatApi21.java new file mode 100644 index 00000000000..c0f21c5f2da --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/VolumeProviderCompatApi21.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media; + +import android.media.VolumeProvider; + +class VolumeProviderCompatApi21 { + public static Object createVolumeProvider(int volumeControl, int maxVolume, int currentVolume, + final Delegate delegate) { + return new VolumeProvider(volumeControl, maxVolume, currentVolume) { + @Override + public void onSetVolumeTo(int volume) { + delegate.onSetVolumeTo(volume); + } + + @Override + public void onAdjustVolume(int direction) { + delegate.onAdjustVolume(direction); + } + }; + } + + public static void setCurrentVolume(Object volumeProviderObj, int currentVolume) { + ((VolumeProvider) volumeProviderObj).setCurrentVolume(currentVolume); + } + + public interface Delegate { + void onSetVolumeTo(int volume); + void onAdjustVolume(int delta); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybean.java b/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybean.java new file mode 100644 index 00000000000..3cf67271e42 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybean.java @@ -0,0 +1,442 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media.routing; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +class MediaRouterJellybean { + private static final String TAG = "MediaRouterJellybean"; + + public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1; + public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2; + public static final int ROUTE_TYPE_USER = 0x00800000; + + public static final int ALL_ROUTE_TYPES = + MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO + | MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO + | MediaRouterJellybean.ROUTE_TYPE_USER; + + public static Object getMediaRouter(Context context) { + return context.getSystemService(Context.MEDIA_ROUTER_SERVICE); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static List getRoutes(Object routerObj) { + final android.media.MediaRouter router = (android.media.MediaRouter)routerObj; + final int count = router.getRouteCount(); + List out = new ArrayList(count); + for (int i = 0; i < count; i++) { + out.add(router.getRouteAt(i)); + } + return out; + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static List getCategories(Object routerObj) { + final android.media.MediaRouter router = (android.media.MediaRouter)routerObj; + final int count = router.getCategoryCount(); + List out = new ArrayList(count); + for (int i = 0; i < count; i++) { + out.add(router.getCategoryAt(i)); + } + return out; + } + + public static Object getSelectedRoute(Object routerObj, int type) { + return ((android.media.MediaRouter)routerObj).getSelectedRoute(type); + } + + public static void selectRoute(Object routerObj, int types, Object routeObj) { + ((android.media.MediaRouter)routerObj).selectRoute(types, + (android.media.MediaRouter.RouteInfo)routeObj); + } + + public static void addCallback(Object routerObj, int types, Object callbackObj) { + ((android.media.MediaRouter)routerObj).addCallback(types, + (android.media.MediaRouter.Callback)callbackObj); + } + + public static void removeCallback(Object routerObj, Object callbackObj) { + ((android.media.MediaRouter)routerObj).removeCallback( + (android.media.MediaRouter.Callback)callbackObj); + } + + public static Object createRouteCategory(Object routerObj, + String name, boolean isGroupable) { + return ((android.media.MediaRouter)routerObj).createRouteCategory(name, isGroupable); + } + + public static Object createUserRoute(Object routerObj, Object categoryObj) { + return ((android.media.MediaRouter)routerObj).createUserRoute( + (android.media.MediaRouter.RouteCategory)categoryObj); + } + + public static void addUserRoute(Object routerObj, Object routeObj) { + ((android.media.MediaRouter)routerObj).addUserRoute( + (android.media.MediaRouter.UserRouteInfo)routeObj); + } + + public static void removeUserRoute(Object routerObj, Object routeObj) { + ((android.media.MediaRouter)routerObj).removeUserRoute( + (android.media.MediaRouter.UserRouteInfo)routeObj); + } + + public static Object createCallback(Callback callback) { + return new CallbackProxy(callback); + } + + public static Object createVolumeCallback(VolumeCallback callback) { + return new VolumeCallbackProxy(callback); + } + + public static final class RouteInfo { + public static CharSequence getName(Object routeObj, Context context) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getName(context); + } + + public static CharSequence getStatus(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getStatus(); + } + + public static int getSupportedTypes(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getSupportedTypes(); + } + + public static Object getCategory(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getCategory(); + } + + public static Drawable getIconDrawable(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getIconDrawable(); + } + + public static int getPlaybackType(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackType(); + } + + public static int getPlaybackStream(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackStream(); + } + + public static int getVolume(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getVolume(); + } + + public static int getVolumeMax(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeMax(); + } + + public static int getVolumeHandling(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeHandling(); + } + + public static Object getTag(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getTag(); + } + + public static void setTag(Object routeObj, Object tag) { + ((android.media.MediaRouter.RouteInfo)routeObj).setTag(tag); + } + + public static void requestSetVolume(Object routeObj, int volume) { + ((android.media.MediaRouter.RouteInfo)routeObj).requestSetVolume(volume); + } + + public static void requestUpdateVolume(Object routeObj, int direction) { + ((android.media.MediaRouter.RouteInfo)routeObj).requestUpdateVolume(direction); + } + + public static Object getGroup(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getGroup(); + } + + public static boolean isGroup(Object routeObj) { + return routeObj instanceof android.media.MediaRouter.RouteGroup; + } + } + + public static final class RouteGroup { + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static List getGroupedRoutes(Object groupObj) { + final android.media.MediaRouter.RouteGroup group = + (android.media.MediaRouter.RouteGroup)groupObj; + final int count = group.getRouteCount(); + List out = new ArrayList(count); + for (int i = 0; i < count; i++) { + out.add(group.getRouteAt(i)); + } + return out; + } + } + + public static final class UserRouteInfo { + public static void setName(Object routeObj, CharSequence name) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setName(name); + } + + public static void setStatus(Object routeObj, CharSequence status) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setStatus(status); + } + + public static void setIconDrawable(Object routeObj, Drawable icon) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setIconDrawable(icon); + } + + public static void setPlaybackType(Object routeObj, int type) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackType(type); + } + + public static void setPlaybackStream(Object routeObj, int stream) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackStream(stream); + } + + public static void setVolume(Object routeObj, int volume) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolume(volume); + } + + public static void setVolumeMax(Object routeObj, int volumeMax) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeMax(volumeMax); + } + + public static void setVolumeHandling(Object routeObj, int volumeHandling) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeHandling(volumeHandling); + } + + public static void setVolumeCallback(Object routeObj, Object volumeCallbackObj) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeCallback( + (android.media.MediaRouter.VolumeCallback)volumeCallbackObj); + } + + public static void setRemoteControlClient(Object routeObj, Object rccObj) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setRemoteControlClient( + (android.media.RemoteControlClient)rccObj); + } + } + + public static final class RouteCategory { + public static CharSequence getName(Object categoryObj, Context context) { + return ((android.media.MediaRouter.RouteCategory)categoryObj).getName(context); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + public static List getRoutes(Object categoryObj) { + ArrayList out = new ArrayList(); + ((android.media.MediaRouter.RouteCategory)categoryObj).getRoutes(out); + return out; + } + + public static int getSupportedTypes(Object categoryObj) { + return ((android.media.MediaRouter.RouteCategory)categoryObj).getSupportedTypes(); + } + + public static boolean isGroupable(Object categoryObj) { + return ((android.media.MediaRouter.RouteCategory)categoryObj).isGroupable(); + } + } + + public static interface Callback { + public void onRouteSelected(int type, Object routeObj); + public void onRouteUnselected(int type, Object routeObj); + public void onRouteAdded(Object routeObj); + public void onRouteRemoved(Object routeObj); + public void onRouteChanged(Object routeObj); + public void onRouteGrouped(Object routeObj, Object groupObj, int index); + public void onRouteUngrouped(Object routeObj, Object groupObj); + public void onRouteVolumeChanged(Object routeObj); + } + + public static interface VolumeCallback { + public void onVolumeSetRequest(Object routeObj, int volume); + public void onVolumeUpdateRequest(Object routeObj, int direction); + } + + /** + * Workaround for limitations of selectRoute() on JB and JB MR1. + * Do not use on JB MR2 and above. + */ + public static final class SelectRouteWorkaround { + private Method mSelectRouteIntMethod; + + public SelectRouteWorkaround() { + if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) { + throw new UnsupportedOperationException(); + } + try { + mSelectRouteIntMethod = android.media.MediaRouter.class.getMethod( + "selectRouteInt", int.class, android.media.MediaRouter.RouteInfo.class); + } catch (NoSuchMethodException ex) { + } + } + + public void selectRoute(Object routerObj, int types, Object routeObj) { + android.media.MediaRouter router = (android.media.MediaRouter)routerObj; + android.media.MediaRouter.RouteInfo route = + (android.media.MediaRouter.RouteInfo)routeObj; + + int routeTypes = route.getSupportedTypes(); + if ((routeTypes & ROUTE_TYPE_USER) == 0) { + // Handle non-user routes. + // On JB and JB MR1, the selectRoute() API only supports programmatically + // selecting user routes. So instead we rely on the hidden selectRouteInt() + // method on these versions of the platform. + // This limitation was removed in JB MR2. + if (mSelectRouteIntMethod != null) { + try { + mSelectRouteIntMethod.invoke(router, types, route); + return; // success! + } catch (IllegalAccessException ex) { + Log.w(TAG, "Cannot programmatically select non-user route. " + + "Media routing may not work.", ex); + } catch (InvocationTargetException ex) { + Log.w(TAG, "Cannot programmatically select non-user route. " + + "Media routing may not work.", ex); + } + } else { + Log.w(TAG, "Cannot programmatically select non-user route " + + "because the platform is missing the selectRouteInt() " + + "method. Media routing may not work."); + } + } + + // Default handling. + router.selectRoute(types, route); + } + } + + /** + * Workaround the fact that the getDefaultRoute() method does not exist in JB and JB MR1. + * Do not use on JB MR2 and above. + */ + public static final class GetDefaultRouteWorkaround { + private Method mGetSystemAudioRouteMethod; + + public GetDefaultRouteWorkaround() { + if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) { + throw new UnsupportedOperationException(); + } + try { + mGetSystemAudioRouteMethod = + android.media.MediaRouter.class.getMethod("getSystemAudioRoute"); + } catch (NoSuchMethodException ex) { + } + } + + public Object getDefaultRoute(Object routerObj) { + android.media.MediaRouter router = (android.media.MediaRouter)routerObj; + + if (mGetSystemAudioRouteMethod != null) { + try { + return mGetSystemAudioRouteMethod.invoke(router); + } catch (IllegalAccessException ex) { + } catch (InvocationTargetException ex) { + } + } + + // Could not find the method or it does not work. + // Return the first route and hope for the best. + return router.getRouteAt(0); + } + } + + static class CallbackProxy + extends android.media.MediaRouter.Callback { + protected final T mCallback; + + public CallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onRouteSelected(android.media.MediaRouter router, + int type, android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteSelected(type, route); + } + + @Override + public void onRouteUnselected(android.media.MediaRouter router, + int type, android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteUnselected(type, route); + } + + @Override + public void onRouteAdded(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteAdded(route); + } + + @Override + public void onRouteRemoved(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteRemoved(route); + } + + @Override + public void onRouteChanged(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteChanged(route); + } + + @Override + public void onRouteGrouped(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route, + android.media.MediaRouter.RouteGroup group, int index) { + mCallback.onRouteGrouped(route, group, index); + } + + @Override + public void onRouteUngrouped(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route, + android.media.MediaRouter.RouteGroup group) { + mCallback.onRouteUngrouped(route, group); + } + + @Override + public void onRouteVolumeChanged(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRouteVolumeChanged(route); + } + } + + static class VolumeCallbackProxy + extends android.media.MediaRouter.VolumeCallback { + protected final T mCallback; + + public VolumeCallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onVolumeSetRequest(android.media.MediaRouter.RouteInfo route, + int volume) { + mCallback.onVolumeSetRequest(route, volume); + } + + @Override + public void onVolumeUpdateRequest(android.media.MediaRouter.RouteInfo route, + int direction) { + mCallback.onVolumeUpdateRequest(route, direction); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr1.java b/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr1.java new file mode 100644 index 00000000000..6e5cfd5d1ab --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr1.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media.routing; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.os.Handler; +import android.util.Log; +import android.view.Display; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class MediaRouterJellybeanMr1 extends MediaRouterJellybean { + private static final String TAG = "MediaRouterJellybeanMr1"; + + public static Object createCallback(Callback callback) { + return new CallbackProxy(callback); + } + + public static final class RouteInfo { + public static boolean isEnabled(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).isEnabled(); + } + + public static Display getPresentationDisplay(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getPresentationDisplay(); + } + } + + public static interface Callback extends MediaRouterJellybean.Callback { + public void onRoutePresentationDisplayChanged(Object routeObj); + } + + /** + * Workaround the fact that the version of MediaRouter.addCallback() that accepts a + * flag to perform an active scan does not exist in JB MR1 so we need to force + * wifi display scans directly through the DisplayManager. + * Do not use on JB MR2 and above. + */ + public static final class ActiveScanWorkaround implements Runnable { + // Time between wifi display scans when actively scanning in milliseconds. + private static final int WIFI_DISPLAY_SCAN_INTERVAL = 15000; + + private final DisplayManager mDisplayManager; + private final Handler mHandler; + private Method mScanWifiDisplaysMethod; + + private boolean mActivelyScanningWifiDisplays; + + public ActiveScanWorkaround(Context context, Handler handler) { + if (Build.VERSION.SDK_INT != 17) { + throw new UnsupportedOperationException(); + } + + mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + mHandler = handler; + try { + mScanWifiDisplaysMethod = DisplayManager.class.getMethod("scanWifiDisplays"); + } catch (NoSuchMethodException ex) { + } + } + + public void setActiveScanRouteTypes(int routeTypes) { + // On JB MR1, there is no API to scan wifi display routes. + // Instead we must make a direct call into the DisplayManager to scan + // wifi displays on this version but only when live video routes are requested. + // See also the JellybeanMr2Impl implementation of this method. + // This was fixed in JB MR2 by adding a new overload of addCallback() to + // enable active scanning on request. + if ((routeTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) { + if (!mActivelyScanningWifiDisplays) { + if (mScanWifiDisplaysMethod != null) { + mActivelyScanningWifiDisplays = true; + mHandler.post(this); + } else { + Log.w(TAG, "Cannot scan for wifi displays because the " + + "DisplayManager.scanWifiDisplays() method is " + + "not available on this device."); + } + } + } else { + if (mActivelyScanningWifiDisplays) { + mActivelyScanningWifiDisplays = false; + mHandler.removeCallbacks(this); + } + } + } + + @Override + public void run() { + if (mActivelyScanningWifiDisplays) { + try { + mScanWifiDisplaysMethod.invoke(mDisplayManager); + } catch (IllegalAccessException ex) { + Log.w(TAG, "Cannot scan for wifi displays.", ex); + } catch (InvocationTargetException ex) { + Log.w(TAG, "Cannot scan for wifi displays.", ex); + } + mHandler.postDelayed(this, WIFI_DISPLAY_SCAN_INTERVAL); + } + } + } + + /** + * Workaround the fact that the isConnecting() method does not exist in JB MR1. + * Do not use on JB MR2 and above. + */ + public static final class IsConnectingWorkaround { + private Method mGetStatusCodeMethod; + private int mStatusConnecting; + + public IsConnectingWorkaround() { + if (Build.VERSION.SDK_INT != 17) { + throw new UnsupportedOperationException(); + } + + try { + Field statusConnectingField = + android.media.MediaRouter.RouteInfo.class.getField("STATUS_CONNECTING"); + mStatusConnecting = statusConnectingField.getInt(null); + mGetStatusCodeMethod = + android.media.MediaRouter.RouteInfo.class.getMethod("getStatusCode"); + } catch (NoSuchFieldException ex) { + } catch (NoSuchMethodException ex) { + } catch (IllegalAccessException ex) { + } + } + + public boolean isConnecting(Object routeObj) { + android.media.MediaRouter.RouteInfo route = + (android.media.MediaRouter.RouteInfo)routeObj; + + if (mGetStatusCodeMethod != null) { + try { + int statusCode = (Integer)mGetStatusCodeMethod.invoke(route); + return statusCode == mStatusConnecting; + } catch (IllegalAccessException ex) { + } catch (InvocationTargetException ex) { + } + } + + // Assume not connecting. + return false; + } + } + + static class CallbackProxy + extends MediaRouterJellybean.CallbackProxy { + public CallbackProxy(T callback) { + super(callback); + } + + @Override + public void onRoutePresentationDisplayChanged(android.media.MediaRouter router, + android.media.MediaRouter.RouteInfo route) { + mCallback.onRoutePresentationDisplayChanged(route); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr2.java b/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr2.java new file mode 100644 index 00000000000..92a1607a01e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/routing/MediaRouterJellybeanMr2.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media.routing; + +class MediaRouterJellybeanMr2 extends MediaRouterJellybeanMr1 { + public static Object getDefaultRoute(Object routerObj) { + return ((android.media.MediaRouter)routerObj).getDefaultRoute(); + } + + public static void addCallback(Object routerObj, int types, Object callbackObj, int flags) { + ((android.media.MediaRouter)routerObj).addCallback(types, + (android.media.MediaRouter.Callback)callbackObj, flags); + } + + public static final class RouteInfo { + public static CharSequence getDescription(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).getDescription(); + } + + public static boolean isConnecting(Object routeObj) { + return ((android.media.MediaRouter.RouteInfo)routeObj).isConnecting(); + } + } + + public static final class UserRouteInfo { + public static void setDescription(Object routeObj, CharSequence description) { + ((android.media.MediaRouter.UserRouteInfo)routeObj).setDescription(description); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaControllerCallback.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaControllerCallback.aidl new file mode 100644 index 00000000000..d905350a3af --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaControllerCallback.aidl @@ -0,0 +1,40 @@ +/* Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media.session; + +import android.os.Bundle; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.ParcelableVolumeInfo; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.v4.media.session.MediaSessionCompat; + +/** + * Callback interface for a MediaSessionCompat to send updates to a + * MediaControllerCompat. This is only used on pre-Lollipop systems. + * @hide + */ +oneway interface IMediaControllerCallback { + void onEvent(String event, in Bundle extras); + void onSessionDestroyed(); + + // These callbacks are for the TransportController + void onPlaybackStateChanged(in PlaybackStateCompat state); + void onMetadataChanged(in MediaMetadataCompat metadata); + void onQueueChanged(in List queue); + void onQueueTitleChanged(CharSequence title); + void onExtrasChanged(in Bundle extras); + void onVolumeInfoChanged(in ParcelableVolumeInfo info); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaSession.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaSession.aidl new file mode 100644 index 00000000000..1ce425dc4ef --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/IMediaSession.aidl @@ -0,0 +1,69 @@ +/* Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media.session; + +import android.app.PendingIntent; +import android.content.Intent; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.session.IMediaControllerCallback; +import android.support.v4.media.session.ParcelableVolumeInfo; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.os.Bundle; +import android.view.KeyEvent; + +import java.util.List; + +/** + * Interface to a MediaSessionCompat. This is only used on pre-Lollipop systems. + * @hide + */ +interface IMediaSession { + void sendCommand(String command, in Bundle args, in MediaSessionCompat.ResultReceiverWrapper cb); + boolean sendMediaButton(in KeyEvent mediaButton); + void registerCallbackListener(in IMediaControllerCallback cb); + void unregisterCallbackListener(in IMediaControllerCallback cb); + boolean isTransportControlEnabled(); + String getPackageName(); + String getTag(); + PendingIntent getLaunchPendingIntent(); + long getFlags(); + ParcelableVolumeInfo getVolumeAttributes(); + void adjustVolume(int direction, int flags, String packageName); + void setVolumeTo(int value, int flags, String packageName); + + // These commands are for the TransportControls + void play(); + void playFromMediaId(String uri, in Bundle extras); + void playFromSearch(String string, in Bundle extras); + void skipToQueueItem(long id); + void pause(); + void stop(); + void next(); + void previous(); + void fastForward(); + void rewind(); + void seekTo(long pos); + void rate(in RatingCompat rating); + void sendCustomAction(String action, in Bundle args); + MediaMetadataCompat getMetadata(); + PlaybackStateCompat getPlaybackState(); + List getQueue(); + CharSequence getQueueTitle(); + Bundle getExtras(); + int getRatingType(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompat.java new file mode 100644 index 00000000000..ef99f43ee25 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompat.java @@ -0,0 +1,1323 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media.session; + +import android.app.PendingIntent; +import android.content.Context; +import android.media.AudioManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.VolumeProviderCompat; +import android.support.v4.media.session.MediaSessionCompat.QueueItem; +import android.support.v4.media.session.PlaybackStateCompat.CustomAction; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; + +import java.util.ArrayList; +import java.util.List; + +/** + * Allows an app to interact with an ongoing media session. Media buttons and + * other commands can be sent to the session. A callback may be registered to + * receive updates from the session, such as metadata and play state changes. + *

+ * A MediaController can be created if you have a {@link MediaSessionCompat.Token} + * from the session owner. + *

+ * MediaController objects are thread-safe. + *

+ * This is a helper for accessing features in {@link android.media.session.MediaSession} + * introduced after API level 4 in a backwards compatible fashion. + */ +public final class MediaControllerCompat { + private static final String TAG = "MediaControllerCompat"; + + private final MediaControllerImpl mImpl; + private final MediaSessionCompat.Token mToken; + + /** + * Creates a media controller from a session. + * + * @param session The session to be controlled. + */ + public MediaControllerCompat(Context context, MediaSessionCompat session) { + if (session == null) { + throw new IllegalArgumentException("session must not be null"); + } + mToken = session.getSessionToken(); + + if (android.os.Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaControllerImplApi21(context, session); + } else { + mImpl = new MediaControllerImplBase(mToken); + } + } + + /** + * Creates a media controller from a session token which may have + * been obtained from another process. + * + * @param sessionToken The token of the session to be controlled. + * @throws RemoteException if the session is not accessible. + */ + public MediaControllerCompat(Context context, MediaSessionCompat.Token sessionToken) + throws RemoteException { + if (sessionToken == null) { + throw new IllegalArgumentException("sessionToken must not be null"); + } + mToken = sessionToken; + + if (android.os.Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaControllerImplApi21(context, sessionToken); + } else { + mImpl = new MediaControllerImplBase(mToken); + } + } + + /** + * Get a {@link TransportControls} instance for this session. + * + * @return A controls instance + */ + public TransportControls getTransportControls() { + return mImpl.getTransportControls(); + } + + /** + * Send the specified media button event to the session. Only media keys can + * be sent by this method, other keys will be ignored. + * + * @param keyEvent The media button event to dispatch. + * @return true if the event was sent to the session, false otherwise. + */ + public boolean dispatchMediaButtonEvent(KeyEvent keyEvent) { + if (keyEvent == null) { + throw new IllegalArgumentException("KeyEvent may not be null"); + } + return mImpl.dispatchMediaButtonEvent(keyEvent); + } + + /** + * Get the current playback state for this session. + * + * @return The current PlaybackState or null + */ + public PlaybackStateCompat getPlaybackState() { + return mImpl.getPlaybackState(); + } + + /** + * Get the current metadata for this session. + * + * @return The current MediaMetadata or null. + */ + public MediaMetadataCompat getMetadata() { + return mImpl.getMetadata(); + } + + /** + * Get the current play queue for this session if one is set. If you only + * care about the current item {@link #getMetadata()} should be used. + * + * @return The current play queue or null. + */ + public List getQueue() { + return mImpl.getQueue(); + } + + /** + * Get the queue title for this session. + */ + public CharSequence getQueueTitle() { + return mImpl.getQueueTitle(); + } + + /** + * Get the extras for this session. + */ + public Bundle getExtras() { + return mImpl.getExtras(); + } + + /** + * Get the rating type supported by the session. One of: + *

    + *
  • {@link RatingCompat#RATING_NONE}
  • + *
  • {@link RatingCompat#RATING_HEART}
  • + *
  • {@link RatingCompat#RATING_THUMB_UP_DOWN}
  • + *
  • {@link RatingCompat#RATING_3_STARS}
  • + *
  • {@link RatingCompat#RATING_4_STARS}
  • + *
  • {@link RatingCompat#RATING_5_STARS}
  • + *
  • {@link RatingCompat#RATING_PERCENTAGE}
  • + *
+ * + * @return The supported rating type + */ + public int getRatingType() { + return mImpl.getRatingType(); + } + + /** + * Get the flags for this session. Flags are defined in + * {@link MediaSessionCompat}. + * + * @return The current set of flags for the session. + */ + public long getFlags() { + return mImpl.getFlags(); + } + + /** + * Get the current playback info for this session. + * + * @return The current playback info or null. + */ + public PlaybackInfo getPlaybackInfo() { + return mImpl.getPlaybackInfo(); + } + + /** + * Get an intent for launching UI associated with this session if one + * exists. + * + * @return A {@link PendingIntent} to launch UI or null. + */ + public PendingIntent getSessionActivity() { + return mImpl.getSessionActivity(); + } + + /** + * Get the token for the session this controller is connected to. + * + * @return The session's token. + */ + public MediaSessionCompat.Token getSessionToken() { + return mToken; + } + + /** + * Set the volume of the output this session is playing on. The command will + * be ignored if it does not support + * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in + * {@link AudioManager} may be used to affect the handling. + * + * @see #getPlaybackInfo() + * @param value The value to set it to, between 0 and the reported max. + * @param flags Flags from {@link AudioManager} to include with the volume + * request. + */ + public void setVolumeTo(int value, int flags) { + mImpl.setVolumeTo(value, flags); + } + + /** + * Adjust the volume of the output this session is playing on. The direction + * must be one of {@link AudioManager#ADJUST_LOWER}, + * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}. + * The command will be ignored if the session does not support + * {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE} or + * {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}. The flags in + * {@link AudioManager} may be used to affect the handling. + * + * @see #getPlaybackInfo() + * @param direction The direction to adjust the volume in. + * @param flags Any flags to pass with the command. + */ + public void adjustVolume(int direction, int flags) { + mImpl.adjustVolume(direction, flags); + } + + /** + * Adds a callback to receive updates from the Session. Updates will be + * posted on the caller's thread. + * + * @param callback The callback object, must not be null. + */ + public void registerCallback(Callback callback) { + registerCallback(callback, null); + } + + /** + * Adds a callback to receive updates from the session. Updates will be + * posted on the specified handler's thread. + * + * @param callback The callback object, must not be null. + * @param handler The handler to post updates on. If null the callers thread + * will be used. + */ + public void registerCallback(Callback callback, Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + if (handler == null) { + handler = new Handler(); + } + mImpl.registerCallback(callback, handler); + } + + /** + * Stop receiving updates on the specified callback. If an update has + * already been posted you may still receive it after calling this method. + * + * @param callback The callback to remove + */ + public void unregisterCallback(Callback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback cannot be null"); + } + mImpl.unregisterCallback(callback); + } + + /** + * Sends a generic command to the session. It is up to the session creator + * to decide what commands and parameters they will support. As such, + * commands should only be sent to sessions that the controller owns. + * + * @param command The command to send + * @param params Any parameters to include with the command + * @param cb The callback to receive the result on + */ + public void sendCommand(String command, Bundle params, ResultReceiver cb) { + if (TextUtils.isEmpty(command)) { + throw new IllegalArgumentException("command cannot be null or empty"); + } + mImpl.sendCommand(command, params, cb); + } + + /** + * Get the session owner's package name. + * + * @return The package name of of the session owner. + */ + public String getPackageName() { + return mImpl.getPackageName(); + } + + /** + * Gets the underlying framework + * {@link android.media.session.MediaController} object. + *

+ * This method is only supported on API 21+. + *

+ * + * @return The underlying {@link android.media.session.MediaController} + * object, or null if none. + */ + public Object getMediaController() { + return mImpl.getMediaController(); + } + + /** + * Callback for receiving updates on from the session. A Callback can be + * registered using {@link #registerCallback} + */ + public static abstract class Callback implements IBinder.DeathRecipient { + private final Object mCallbackObj; + private MessageHandler mHandler; + + private boolean mRegistered = false; + + public Callback() { + if (android.os.Build.VERSION.SDK_INT >= 21) { + mCallbackObj = MediaControllerCompatApi21.createCallback(new StubApi21()); + } else { + mCallbackObj = new StubCompat(); + } + } + + /** + * Override to handle the session being destroyed. The session is no + * longer valid after this call and calls to it will be ignored. + */ + public void onSessionDestroyed() { + } + + /** + * Override to handle custom events sent by the session owner without a + * specified interface. Controllers should only handle these for + * sessions they own. + * + * @param event The event from the session. + * @param extras Optional parameters for the event. + */ + public void onSessionEvent(String event, Bundle extras) { + } + + /** + * Override to handle changes in playback state. + * + * @param state The new playback state of the session + */ + public void onPlaybackStateChanged(PlaybackStateCompat state) { + } + + /** + * Override to handle changes to the current metadata. + * + * @param metadata The current metadata for the session or null if none. + * @see MediaMetadata + */ + public void onMetadataChanged(MediaMetadataCompat metadata) { + } + + /** + * Override to handle changes to items in the queue. + * + * @see MediaSessionCompat.QueueItem + * @param queue A list of items in the current play queue. It should + * include the currently playing item as well as previous and + * upcoming items if applicable. + */ + public void onQueueChanged(List queue) { + } + + /** + * Override to handle changes to the queue title. + * + * @param title The title that should be displayed along with the play + * queue such as "Now Playing". May be null if there is no + * such title. + */ + public void onQueueTitleChanged(CharSequence title) { + } + + /** + * Override to handle chagnes to the {@link MediaSessionCompat} extras. + * + * @param extras The extras that can include other information + * associated with the {@link MediaSessionCompat}. + */ + public void onExtrasChanged(Bundle extras) { + } + + /** + * Override to handle changes to the audio info. + * + * @param info The current audio info for this session. + */ + public void onAudioInfoChanged(PlaybackInfo info) { + } + + @Override + public void binderDied() { + onSessionDestroyed(); + } + + /** + * Set the handler to use for pre 21 callbacks. + */ + private void setHandler(Handler handler) { + mHandler = new MessageHandler(handler.getLooper()); + } + + private class StubApi21 implements MediaControllerCompatApi21.Callback { + @Override + public void onSessionDestroyed() { + Callback.this.onSessionDestroyed(); + } + + @Override + public void onSessionEvent(String event, Bundle extras) { + Callback.this.onSessionEvent(event, extras); + } + + @Override + public void onPlaybackStateChanged(Object stateObj) { + Callback.this.onPlaybackStateChanged( + PlaybackStateCompat.fromPlaybackState(stateObj)); + } + + @Override + public void onMetadataChanged(Object metadataObj) { + Callback.this.onMetadataChanged( + MediaMetadataCompat.fromMediaMetadata(metadataObj)); + } + } + + private class StubCompat extends IMediaControllerCallback.Stub { + + @Override + public void onEvent(String event, Bundle extras) throws RemoteException { + mHandler.post(MessageHandler.MSG_EVENT, event, extras); + } + + @Override + public void onSessionDestroyed() throws RemoteException { + mHandler.post(MessageHandler.MSG_DESTROYED, null, null); + } + + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) throws RemoteException { + mHandler.post(MessageHandler.MSG_UPDATE_PLAYBACK_STATE, state, null); + } + + @Override + public void onMetadataChanged(MediaMetadataCompat metadata) throws RemoteException { + mHandler.post(MessageHandler.MSG_UPDATE_METADATA, metadata, null); + } + + @Override + public void onQueueChanged(List queue) throws RemoteException { + mHandler.post(MessageHandler.MSG_UPDATE_QUEUE, queue, null); + } + + @Override + public void onQueueTitleChanged(CharSequence title) throws RemoteException { + mHandler.post(MessageHandler.MSG_UPDATE_QUEUE_TITLE, title, null); + } + + @Override + public void onExtrasChanged(Bundle extras) throws RemoteException { + mHandler.post(MessageHandler.MSG_UPDATE_EXTRAS, extras, null); + } + + @Override + public void onVolumeInfoChanged(ParcelableVolumeInfo info) throws RemoteException { + PlaybackInfo pi = null; + if (info != null) { + pi = new PlaybackInfo(info.volumeType, info.audioStream, info.controlType, + info.maxVolume, info.currentVolume); + } + mHandler.post(MessageHandler.MSG_UPDATE_VOLUME, pi, null); + } + } + + private class MessageHandler extends Handler { + private static final int MSG_EVENT = 1; + private static final int MSG_UPDATE_PLAYBACK_STATE = 2; + private static final int MSG_UPDATE_METADATA = 3; + private static final int MSG_UPDATE_VOLUME = 4; + private static final int MSG_UPDATE_QUEUE = 5; + private static final int MSG_UPDATE_QUEUE_TITLE = 6; + private static final int MSG_UPDATE_EXTRAS = 7; + private static final int MSG_DESTROYED = 8; + + public MessageHandler(Looper looper) { + super(looper); + } + + @Override + public void handleMessage(Message msg) { + if (!mRegistered) { + return; + } + switch (msg.what) { + case MSG_EVENT: + onSessionEvent((String) msg.obj, msg.getData()); + break; + case MSG_UPDATE_PLAYBACK_STATE: + onPlaybackStateChanged((PlaybackStateCompat) msg.obj); + break; + case MSG_UPDATE_METADATA: + onMetadataChanged((MediaMetadataCompat) msg.obj); + break; + case MSG_UPDATE_QUEUE: + onQueueChanged((List) msg.obj); + break; + case MSG_UPDATE_QUEUE_TITLE: + onQueueTitleChanged((CharSequence) msg.obj); + break; + case MSG_UPDATE_EXTRAS: + onExtrasChanged((Bundle) msg.obj); + break; + case MSG_UPDATE_VOLUME: + onAudioInfoChanged((PlaybackInfo) msg.obj); + break; + case MSG_DESTROYED: + onSessionDestroyed(); + break; + } + } + + public void post(int what, Object obj, Bundle data) { + obtainMessage(what, obj).sendToTarget(); + } + } + } + + /** + * Interface for controlling media playback on a session. This allows an app + * to send media transport commands to the session. + */ + public static abstract class TransportControls { + TransportControls() { + } + + /** + * Request that the player start its playback at its current position. + */ + public abstract void play(); + + /** + * Request that the player start playback for a specific {@link Uri}. + * + * @param mediaId The uri of the requested media. + * @param extras Optional extras that can include extra information + * about the media item to be played. + */ + public abstract void playFromMediaId(String mediaId, Bundle extras); + + /** + * Request that the player start playback for a specific search query. + * An empty or null query should be treated as a request to play any + * music. + * + * @param query The search query. + * @param extras Optional extras that can include extra information + * about the query. + */ + public abstract void playFromSearch(String query, Bundle extras); + + /** + * Play an item with a specific id in the play queue. If you specify an + * id that is not in the play queue, the behavior is undefined. + */ + public abstract void skipToQueueItem(long id); + + /** + * Request that the player pause its playback and stay at its current + * position. + */ + public abstract void pause(); + + /** + * Request that the player stop its playback; it may clear its state in + * whatever way is appropriate. + */ + public abstract void stop(); + + /** + * Move to a new location in the media stream. + * + * @param pos Position to move to, in milliseconds. + */ + public abstract void seekTo(long pos); + + /** + * Start fast forwarding. If playback is already fast forwarding this + * may increase the rate. + */ + public abstract void fastForward(); + + /** + * Skip to the next item. + */ + public abstract void skipToNext(); + + /** + * Start rewinding. If playback is already rewinding this may increase + * the rate. + */ + public abstract void rewind(); + + /** + * Skip to the previous item. + */ + public abstract void skipToPrevious(); + + /** + * Rate the current content. This will cause the rating to be set for + * the current user. The Rating type must match the type returned by + * {@link #getRatingType()}. + * + * @param rating The rating to set for the current content + */ + public abstract void setRating(RatingCompat rating); + + /** + * Send a custom action for the {@link MediaSessionCompat} to perform. + * + * @param customAction The action to perform. + * @param args Optional arguments to supply to the + * {@link MediaSessionCompat} for this custom action. + */ + public abstract void sendCustomAction(PlaybackStateCompat.CustomAction customAction, + Bundle args); + + /** + * Send the id and args from a custom action for the + * {@link MediaSessionCompat} to perform. + * + * @see #sendCustomAction(PlaybackStateCompat.CustomAction action, + * Bundle args) + * @param action The action identifier of the + * {@link PlaybackStateCompat.CustomAction} as specified by + * the {@link MediaSessionCompat}. + * @param args Optional arguments to supply to the + * {@link MediaSessionCompat} for this custom action. + */ + public abstract void sendCustomAction(String action, Bundle args); + } + + /** + * Holds information about the way volume is handled for this session. + */ + public static final class PlaybackInfo { + /** + * The session uses local playback. + */ + public static final int PLAYBACK_TYPE_LOCAL = 1; + /** + * The session uses remote playback. + */ + public static final int PLAYBACK_TYPE_REMOTE = 2; + + private final int mPlaybackType; + // TODO update audio stream with AudioAttributes support version + private final int mAudioStream; + private final int mVolumeControl; + private final int mMaxVolume; + private final int mCurrentVolume; + + PlaybackInfo(int type, int stream, int control, int max, int current) { + mPlaybackType = type; + mAudioStream = stream; + mVolumeControl = control; + mMaxVolume = max; + mCurrentVolume = current; + } + + /** + * Get the type of volume handling, either local or remote. One of: + *
    + *
  • {@link PlaybackInfo#PLAYBACK_TYPE_LOCAL}
  • + *
  • {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE}
  • + *
+ * + * @return The type of volume handling this session is using. + */ + public int getPlaybackType() { + return mPlaybackType; + } + + /** + * Get the stream this is currently controlling volume on. When the volume + * type is {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} this value does not + * have meaning and should be ignored. + * + * @return The stream this session is playing on. + */ + public int getAudioStream() { + // TODO switch to AudioAttributesCompat when it is added. + return mAudioStream; + } + + /** + * Get the type of volume control that can be used. One of: + *
    + *
  • {@link VolumeProviderCompat#VOLUME_CONTROL_ABSOLUTE}
  • + *
  • {@link VolumeProviderCompat#VOLUME_CONTROL_RELATIVE}
  • + *
  • {@link VolumeProviderCompat#VOLUME_CONTROL_FIXED}
  • + *
+ * + * @return The type of volume control that may be used with this + * session. + */ + public int getVolumeControl() { + return mVolumeControl; + } + + /** + * Get the maximum volume that may be set for this session. + * + * @return The maximum allowed volume where this session is playing. + */ + public int getMaxVolume() { + return mMaxVolume; + } + + /** + * Get the current volume for this session. + * + * @return The current volume where this session is playing. + */ + public int getCurrentVolume() { + return mCurrentVolume; + } + } + + interface MediaControllerImpl { + void registerCallback(Callback callback, Handler handler); + + void unregisterCallback(Callback callback); + boolean dispatchMediaButtonEvent(KeyEvent keyEvent); + TransportControls getTransportControls(); + PlaybackStateCompat getPlaybackState(); + MediaMetadataCompat getMetadata(); + + List getQueue(); + CharSequence getQueueTitle(); + Bundle getExtras(); + int getRatingType(); + long getFlags(); + PlaybackInfo getPlaybackInfo(); + PendingIntent getSessionActivity(); + + void setVolumeTo(int value, int flags); + void adjustVolume(int direction, int flags); + void sendCommand(String command, Bundle params, ResultReceiver cb); + + String getPackageName(); + Object getMediaController(); + } + + static class MediaControllerImplBase implements MediaControllerImpl { + private MediaSessionCompat.Token mToken; + private IMediaSession mBinder; + private TransportControls mTransportControls; + + public MediaControllerImplBase(MediaSessionCompat.Token token) { + mToken = token; + mBinder = IMediaSession.Stub.asInterface((IBinder) token.getToken()); + } + + @Override + public void registerCallback(Callback callback, Handler handler) { + if (callback == null) { + throw new IllegalArgumentException("callback may not be null."); + } + try { + mBinder.asBinder().linkToDeath(callback, 0); + mBinder.registerCallbackListener((IMediaControllerCallback) callback.mCallbackObj); + callback.setHandler(handler); + callback.mRegistered = true; + } catch (RemoteException e) { + Log.e(TAG, "Dead object in registerCallback. " + e); + callback.onSessionDestroyed(); + } + } + + @Override + public void unregisterCallback(Callback callback) { + if (callback == null) { + throw new IllegalArgumentException("callback may not be null."); + } + try { + mBinder.unregisterCallbackListener( + (IMediaControllerCallback) callback.mCallbackObj); + mBinder.asBinder().unlinkToDeath(callback, 0); + callback.mRegistered = false; + } catch (RemoteException e) { + Log.e(TAG, "Dead object in unregisterCallback. " + e); + } + } + + @Override + public boolean dispatchMediaButtonEvent(KeyEvent event) { + if (event == null) { + throw new IllegalArgumentException("event may not be null."); + } + try { + mBinder.sendMediaButton(event); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in dispatchMediaButtonEvent. " + e); + } + return false; + } + + @Override + public TransportControls getTransportControls() { + if (mTransportControls == null) { + mTransportControls = new TransportControlsBase(mBinder); + } + + return mTransportControls; + } + + @Override + public PlaybackStateCompat getPlaybackState() { + try { + return mBinder.getPlaybackState(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getPlaybackState. " + e); + } + return null; + } + + @Override + public MediaMetadataCompat getMetadata() { + try { + return mBinder.getMetadata(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getMetadata. " + e); + } + return null; + } + + @Override + public List getQueue() { + try { + return mBinder.getQueue(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getQueue. " + e); + } + return null; + } + + @Override + public CharSequence getQueueTitle() { + try { + return mBinder.getQueueTitle(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getQueueTitle. " + e); + } + return null; + } + + @Override + public Bundle getExtras() { + try { + return mBinder.getExtras(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getExtras. " + e); + } + return null; + } + + @Override + public int getRatingType() { + try { + return mBinder.getRatingType(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getRatingType. " + e); + } + return 0; + } + + @Override + public long getFlags() { + try { + return mBinder.getFlags(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getFlags. " + e); + } + return 0; + } + + @Override + public PlaybackInfo getPlaybackInfo() { + try { + ParcelableVolumeInfo info = mBinder.getVolumeAttributes(); + PlaybackInfo pi = new PlaybackInfo(info.volumeType, info.audioStream, + info.controlType, info.maxVolume, info.currentVolume); + return pi; + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getPlaybackInfo. " + e); + } + return null; + } + + @Override + public PendingIntent getSessionActivity() { + try { + return mBinder.getLaunchPendingIntent(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getSessionActivity. " + e); + } + return null; + } + + @Override + public void setVolumeTo(int value, int flags) { + try { + mBinder.setVolumeTo(value, flags, null); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setVolumeTo. " + e); + } + } + + @Override + public void adjustVolume(int direction, int flags) { + try { + mBinder.adjustVolume(direction, flags, null); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in adjustVolume. " + e); + } + } + + @Override + public void sendCommand(String command, Bundle params, ResultReceiver cb) { + try { + mBinder.sendCommand(command, params, + new MediaSessionCompat.ResultReceiverWrapper(cb)); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in sendCommand. " + e); + } + } + + @Override + public String getPackageName() { + try { + return mBinder.getPackageName(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in getPackageName. " + e); + } + return null; + } + + @Override + public Object getMediaController() { + return null; + } + } + + static class TransportControlsBase extends TransportControls { + private IMediaSession mBinder; + + public TransportControlsBase(IMediaSession binder) { + mBinder = binder; + } + + @Override + public void play() { + try { + mBinder.play(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in play. " + e); + } + } + + @Override + public void playFromMediaId(String mediaId, Bundle extras) { + try { + mBinder.playFromMediaId(mediaId, extras); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in playFromMediaId. " + e); + } + } + + @Override + public void playFromSearch(String query, Bundle extras) { + try { + mBinder.playFromSearch(query, extras); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in playFromSearch. " + e); + } + } + + @Override + public void skipToQueueItem(long id) { + try { + mBinder.skipToQueueItem(id); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in skipToQueueItem. " + e); + } + } + + @Override + public void pause() { + try { + mBinder.pause(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in pause. " + e); + } + } + + @Override + public void stop() { + try { + mBinder.stop(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in stop. " + e); + } + } + + @Override + public void seekTo(long pos) { + try { + mBinder.seekTo(pos); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in seekTo. " + e); + } + } + + @Override + public void fastForward() { + try { + mBinder.fastForward(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in fastForward. " + e); + } + } + + @Override + public void skipToNext() { + try { + mBinder.next(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in skipToNext. " + e); + } + } + + @Override + public void rewind() { + try { + mBinder.rewind(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in rewind. " + e); + } + } + + @Override + public void skipToPrevious() { + try { + mBinder.previous(); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in skipToPrevious. " + e); + } + } + + @Override + public void setRating(RatingCompat rating) { + try { + mBinder.rate(rating); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in setRating. " + e); + } + } + + @Override + public void sendCustomAction(CustomAction customAction, Bundle args) { + sendCustomAction(customAction.getAction(), args); + } + + @Override + public void sendCustomAction(String action, Bundle args) { + try { + mBinder.sendCustomAction(action, args); + } catch (RemoteException e) { + Log.e(TAG, "Dead object in sendCustomAction. " + e); + } + } + } + + static class MediaControllerImplApi21 implements MediaControllerImpl { + private final Object mControllerObj; + + public MediaControllerImplApi21(Context context, MediaSessionCompat session) { + mControllerObj = MediaControllerCompatApi21.fromToken(context, + session.getSessionToken().getToken()); + } + + public MediaControllerImplApi21(Context context, MediaSessionCompat.Token sessionToken) + throws RemoteException { + mControllerObj = MediaControllerCompatApi21.fromToken(context, + sessionToken.getToken()); + if (mControllerObj == null) throw new RemoteException(); + } + + @Override + public void registerCallback(Callback callback, Handler handler) { + MediaControllerCompatApi21.registerCallback(mControllerObj, callback.mCallbackObj, handler); + } + + @Override + public void unregisterCallback(Callback callback) { + MediaControllerCompatApi21.unregisterCallback(mControllerObj, callback.mCallbackObj); + } + + @Override + public boolean dispatchMediaButtonEvent(KeyEvent event) { + return MediaControllerCompatApi21.dispatchMediaButtonEvent(mControllerObj, event); + } + + @Override + public TransportControls getTransportControls() { + Object controlsObj = MediaControllerCompatApi21.getTransportControls(mControllerObj); + return controlsObj != null ? new TransportControlsApi21(controlsObj) : null; + } + + @Override + public PlaybackStateCompat getPlaybackState() { + Object stateObj = MediaControllerCompatApi21.getPlaybackState(mControllerObj); + return stateObj != null ? PlaybackStateCompat.fromPlaybackState(stateObj) : null; + } + + @Override + public MediaMetadataCompat getMetadata() { + Object metadataObj = MediaControllerCompatApi21.getMetadata(mControllerObj); + return metadataObj != null ? MediaMetadataCompat.fromMediaMetadata(metadataObj) : null; + } + + @Override + public List getQueue() { + List queueObjs = MediaControllerCompatApi21.getQueue(mControllerObj); + if (queueObjs == null) { + return null; + } + List queue = + new ArrayList(); + for (Object item : queueObjs) { + queue.add(MediaSessionCompat.QueueItem.obtain(item)); + } + return queue; + } + + @Override + public CharSequence getQueueTitle() { + return MediaControllerCompatApi21.getQueueTitle(mControllerObj); + } + + @Override + public Bundle getExtras() { + return MediaControllerCompatApi21.getExtras(mControllerObj); + } + + @Override + public int getRatingType() { + return MediaControllerCompatApi21.getRatingType(mControllerObj); + } + + @Override + public long getFlags() { + return MediaControllerCompatApi21.getFlags(mControllerObj); + } + + @Override + public PlaybackInfo getPlaybackInfo() { + Object volumeInfoObj = MediaControllerCompatApi21.getPlaybackInfo(mControllerObj); + return volumeInfoObj != null ? new PlaybackInfo( + MediaControllerCompatApi21.PlaybackInfo.getPlaybackType(volumeInfoObj), + MediaControllerCompatApi21.PlaybackInfo.getLegacyAudioStream(volumeInfoObj), + MediaControllerCompatApi21.PlaybackInfo.getVolumeControl(volumeInfoObj), + MediaControllerCompatApi21.PlaybackInfo.getMaxVolume(volumeInfoObj), + MediaControllerCompatApi21.PlaybackInfo.getCurrentVolume(volumeInfoObj)) : null; + } + + @Override + public PendingIntent getSessionActivity() { + return MediaControllerCompatApi21.getSessionActivity(mControllerObj); + } + + @Override + public void setVolumeTo(int value, int flags) { + MediaControllerCompatApi21.setVolumeTo(mControllerObj, value, flags); + } + + @Override + public void adjustVolume(int direction, int flags) { + MediaControllerCompatApi21.adjustVolume(mControllerObj, direction, flags); + } + + @Override + public void sendCommand(String command, Bundle params, ResultReceiver cb) { + MediaControllerCompatApi21.sendCommand(mControllerObj, command, params, cb); + } + + @Override + public String getPackageName() { + return MediaControllerCompatApi21.getPackageName(mControllerObj); + } + + @Override + public Object getMediaController() { + return mControllerObj; + } + } + + static class TransportControlsApi21 extends TransportControls { + private final Object mControlsObj; + + public TransportControlsApi21(Object controlsObj) { + mControlsObj = controlsObj; + } + + @Override + public void play() { + MediaControllerCompatApi21.TransportControls.play(mControlsObj); + } + + @Override + public void pause() { + MediaControllerCompatApi21.TransportControls.pause(mControlsObj); + } + + @Override + public void stop() { + MediaControllerCompatApi21.TransportControls.stop(mControlsObj); + } + + @Override + public void seekTo(long pos) { + MediaControllerCompatApi21.TransportControls.seekTo(mControlsObj, pos); + } + + @Override + public void fastForward() { + MediaControllerCompatApi21.TransportControls.fastForward(mControlsObj); + } + + @Override + public void rewind() { + MediaControllerCompatApi21.TransportControls.rewind(mControlsObj); + } + + @Override + public void skipToNext() { + MediaControllerCompatApi21.TransportControls.skipToNext(mControlsObj); + } + + @Override + public void skipToPrevious() { + MediaControllerCompatApi21.TransportControls.skipToPrevious(mControlsObj); + } + + @Override + public void setRating(RatingCompat rating) { + MediaControllerCompatApi21.TransportControls.setRating(mControlsObj, + rating != null ? rating.getRating() : null); + } + + @Override + public void playFromMediaId(String mediaId, Bundle extras) { + MediaControllerCompatApi21.TransportControls.playFromMediaId(mControlsObj, mediaId, + extras); + } + + @Override + public void playFromSearch(String query, Bundle extras) { + MediaControllerCompatApi21.TransportControls.playFromSearch(mControlsObj, query, + extras); + } + + @Override + public void skipToQueueItem(long id) { + MediaControllerCompatApi21.TransportControls.skipToQueueItem(mControlsObj, id); + } + + @Override + public void sendCustomAction(CustomAction customAction, Bundle args) { + MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj, + customAction.getAction(), args); + } + + @Override + public void sendCustomAction(String action, Bundle args) { + MediaControllerCompatApi21.TransportControls.sendCustomAction(mControlsObj, action, + args); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompatApi21.java new file mode 100644 index 00000000000..6acf4255036 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaControllerCompatApi21.java @@ -0,0 +1,281 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media.session; + +import android.app.PendingIntent; +import android.content.Context; +import android.media.AudioAttributes; +import android.media.AudioManager; +import android.media.MediaMetadata; +import android.media.Rating; +import android.media.session.MediaController; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.view.KeyEvent; + +import java.util.ArrayList; +import java.util.List; + +class MediaControllerCompatApi21 { + public static Object fromToken(Context context, Object sessionToken) { + return new MediaController(context, (MediaSession.Token) sessionToken); + } + + public static Object createCallback(Callback callback) { + return new CallbackProxy(callback); + } + + public static void registerCallback(Object controllerObj, Object callbackObj, Handler handler) { + ((MediaController) controllerObj).registerCallback( + (MediaController.Callback)callbackObj, handler); + } + + public static void unregisterCallback(Object controllerObj, Object callbackObj) { + ((MediaController) controllerObj) + .unregisterCallback((MediaController.Callback) callbackObj); + } + + public static Object getTransportControls(Object controllerObj) { + return ((MediaController)controllerObj).getTransportControls(); + } + + public static Object getPlaybackState(Object controllerObj) { + return ((MediaController)controllerObj).getPlaybackState(); + } + + public static Object getMetadata(Object controllerObj) { + return ((MediaController)controllerObj).getMetadata(); + } + + public static List getQueue(Object controllerObj) { + List queue = ((MediaController) controllerObj).getQueue(); + if (queue == null) { + return null; + } + List queueObjs = new ArrayList(queue); + return queueObjs; + } + + public static CharSequence getQueueTitle(Object controllerObj) { + return ((MediaController) controllerObj).getQueueTitle(); + } + + public static Bundle getExtras(Object controllerObj) { + return ((MediaController) controllerObj).getExtras(); + } + + public static int getRatingType(Object controllerObj) { + return ((MediaController) controllerObj).getRatingType(); + } + + public static long getFlags(Object controllerObj) { + return ((MediaController) controllerObj).getFlags(); + } + + public static Object getPlaybackInfo(Object controllerObj) { + return ((MediaController) controllerObj).getPlaybackInfo(); + } + + public static PendingIntent getSessionActivity(Object controllerObj) { + return ((MediaController) controllerObj).getSessionActivity(); + } + + public static boolean dispatchMediaButtonEvent(Object controllerObj, KeyEvent event) { + return ((MediaController) controllerObj).dispatchMediaButtonEvent(event); + } + + public static void setVolumeTo(Object controllerObj, int value, int flags) { + ((MediaController) controllerObj).setVolumeTo(value, flags); + } + + public static void adjustVolume(Object controllerObj, int direction, int flags) { + ((MediaController) controllerObj).adjustVolume(direction, flags); + } + + public static void sendCommand(Object controllerObj, + String command, Bundle params, ResultReceiver cb) { + ((MediaController) controllerObj).sendCommand(command, params, cb); + } + + public static String getPackageName(Object controllerObj) { + return ((MediaController) controllerObj).getPackageName(); + } + + public static class TransportControls { + public static void play(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).play(); + } + + public static void pause(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).pause(); + } + + public static void stop(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).stop(); + } + + public static void seekTo(Object controlsObj, long pos) { + ((MediaController.TransportControls)controlsObj).seekTo(pos); + } + + public static void fastForward(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).fastForward(); + } + + public static void rewind(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).rewind(); + } + + public static void skipToNext(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).skipToNext(); + } + + public static void skipToPrevious(Object controlsObj) { + ((MediaController.TransportControls)controlsObj).skipToPrevious(); + } + + public static void setRating(Object controlsObj, Object ratingObj) { + ((MediaController.TransportControls)controlsObj).setRating((Rating)ratingObj); + } + + public static void playFromMediaId(Object controlsObj, String mediaId, Bundle extras) { + ((MediaController.TransportControls) controlsObj).playFromMediaId(mediaId, extras); + } + + public static void playFromSearch(Object controlsObj, String query, Bundle extras) { + ((MediaController.TransportControls) controlsObj).playFromSearch(query, extras); + } + + public static void skipToQueueItem(Object controlsObj, long id) { + ((MediaController.TransportControls) controlsObj).skipToQueueItem(id); + } + + public static void sendCustomAction(Object controlsObj, String action, Bundle args) { + ((MediaController.TransportControls) controlsObj).sendCustomAction(action, args); + } + } + + public static class PlaybackInfo { + public static int getPlaybackType(Object volumeInfoObj) { + return ((MediaController.PlaybackInfo)volumeInfoObj).getPlaybackType(); + } + + public static AudioAttributes getAudioAttributes(Object volumeInfoObj) { + return ((MediaController.PlaybackInfo) volumeInfoObj).getAudioAttributes(); + } + + public static int getLegacyAudioStream(Object volumeInfoObj) { + AudioAttributes attrs = getAudioAttributes(volumeInfoObj); + return toLegacyStreamType(attrs); + } + + public static int getVolumeControl(Object volumeInfoObj) { + return ((MediaController.PlaybackInfo)volumeInfoObj).getVolumeControl(); + } + + public static int getMaxVolume(Object volumeInfoObj) { + return ((MediaController.PlaybackInfo)volumeInfoObj).getMaxVolume(); + } + + public static int getCurrentVolume(Object volumeInfoObj) { + return ((MediaController.PlaybackInfo)volumeInfoObj).getCurrentVolume(); + } + + // This is copied from AudioAttributes.toLegacyStreamType. TODO This + // either needs to be kept in sync with that one or toLegacyStreamType + // needs to be made public so it can be used by the support lib. + private static final int FLAG_SCO = 0x1 << 2; + private static final int STREAM_BLUETOOTH_SCO = 6; + private static final int STREAM_SYSTEM_ENFORCED = 7; + private static int toLegacyStreamType(AudioAttributes aa) { + // flags to stream type mapping + if ((aa.getFlags() & AudioAttributes.FLAG_AUDIBILITY_ENFORCED) + == AudioAttributes.FLAG_AUDIBILITY_ENFORCED) { + return STREAM_SYSTEM_ENFORCED; + } + if ((aa.getFlags() & FLAG_SCO) == FLAG_SCO) { + return STREAM_BLUETOOTH_SCO; + } + + // usage to stream type mapping + switch (aa.getUsage()) { + case AudioAttributes.USAGE_MEDIA: + case AudioAttributes.USAGE_GAME: + case AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY: + case AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE: + return AudioManager.STREAM_MUSIC; + case AudioAttributes.USAGE_ASSISTANCE_SONIFICATION: + return AudioManager.STREAM_SYSTEM; + case AudioAttributes.USAGE_VOICE_COMMUNICATION: + return AudioManager.STREAM_VOICE_CALL; + case AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING: + return AudioManager.STREAM_DTMF; + case AudioAttributes.USAGE_ALARM: + return AudioManager.STREAM_ALARM; + case AudioAttributes.USAGE_NOTIFICATION_RINGTONE: + return AudioManager.STREAM_RING; + case AudioAttributes.USAGE_NOTIFICATION: + case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_REQUEST: + case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT: + case AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_DELAYED: + case AudioAttributes.USAGE_NOTIFICATION_EVENT: + return AudioManager.STREAM_NOTIFICATION; + case AudioAttributes.USAGE_UNKNOWN: + default: + return AudioManager.STREAM_MUSIC; + } + } + } + + public static interface Callback { + public void onSessionDestroyed(); + public void onSessionEvent(String event, Bundle extras); + public void onPlaybackStateChanged(Object stateObj); + public void onMetadataChanged(Object metadataObj); + } + + static class CallbackProxy extends MediaController.Callback { + protected final T mCallback; + + public CallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onSessionDestroyed() { + mCallback.onSessionDestroyed(); + } + + @Override + public void onSessionEvent(String event, Bundle extras) { + mCallback.onSessionEvent(event, extras); + } + + @Override + public void onPlaybackStateChanged(PlaybackState state) { + mCallback.onPlaybackStateChanged(state); + } + + @Override + public void onMetadataChanged(MediaMetadata metadata) { + mCallback.onMetadataChanged(metadata); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.aidl new file mode 100644 index 00000000000..d0c2f6fce2c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.aidl @@ -0,0 +1,20 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.support.v4.media.session; + +parcelable MediaSessionCompat.Token; +parcelable MediaSessionCompat.QueueItem; +parcelable MediaSessionCompat.ResultReceiverWrapper; diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.java new file mode 100644 index 00000000000..b8beb78bd55 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompat.java @@ -0,0 +1,1924 @@ + +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media.session; + +import android.app.Activity; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.media.AudioManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.os.SystemClock; +import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.RatingCompat; +import android.support.v4.media.VolumeProviderCompat; +import android.text.TextUtils; +import android.util.Log; +import android.view.KeyEvent; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * Allows interaction with media controllers, volume keys, media buttons, and + * transport controls. + *

+ * A MediaSession should be created when an app wants to publish media playback + * information or handle media keys. In general an app only needs one session + * for all playback, though multiple sessions can be created to provide finer + * grain controls of media. + *

+ * Once a session is created the owner of the session may pass its + * {@link #getSessionToken() session token} to other processes to allow them to + * create a {@link MediaControllerCompat} to interact with the session. + *

+ * To receive commands, media keys, and other events a {@link Callback} must be + * set with {@link #setCallback(Callback)}. + *

+ * When an app is finished performing playback it must call {@link #release()} + * to clean up the session and notify any controllers. + *

+ * MediaSessionCompat objects are not thread safe and all calls should be made + * from the same thread. + *

+ * This is a helper for accessing features in + * {@link android.media.session.MediaSession} introduced after API level 4 in a + * backwards compatible fashion. + */ +public class MediaSessionCompat { + private final MediaSessionImpl mImpl; + private final MediaControllerCompat mController; + private final ArrayList + mActiveListeners = new ArrayList(); + + /** + * Set this flag on the session to indicate that it can handle media button + * events. + */ + public static final int FLAG_HANDLES_MEDIA_BUTTONS = 1 << 0; + + /** + * Set this flag on the session to indicate that it handles transport + * control commands through its {@link Callback}. + */ + public static final int FLAG_HANDLES_TRANSPORT_CONTROLS = 1 << 1; + + /** + * Creates a new session. + * + * @param context The context. + * @param tag A short name for debugging purposes. + * @param mediaButtonEventReceiver The component name for your receiver. + * This must be non-null to support platform versions earlier + * than {@link android.os.Build.VERSION_CODES#LOLLIPOP}. + * @param mbrIntent The PendingIntent for your receiver component that + * handles media button events. This is optional and will be used + * on {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2} and + * later instead of the component name. + */ + public MediaSessionCompat(Context context, String tag, ComponentName mediaButtonEventReceiver, + PendingIntent mbrIntent) { + if (context == null) { + throw new IllegalArgumentException("context must not be null"); + } + if (TextUtils.isEmpty(tag)) { + throw new IllegalArgumentException("tag must not be null or empty"); + } + + if (mediaButtonEventReceiver != null && mbrIntent == null) { + // construct a PendingIntent for the media button + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + // the associated intent will be handled by the component being registered + mediaButtonIntent.setComponent(mediaButtonEventReceiver); + mbrIntent = PendingIntent.getBroadcast(context, + 0/* requestCode, ignored */, mediaButtonIntent, 0/* flags */); + } + if (android.os.Build.VERSION.SDK_INT >= 21) { + mImpl = new MediaSessionImplApi21(context, tag); + mImpl.setMediaButtonReceiver(mbrIntent); + } else { + mImpl = new MediaSessionImplBase(context, tag, mediaButtonEventReceiver, mbrIntent); + } + mController = new MediaControllerCompat(context, this); + } + + private MediaSessionCompat(Context context, MediaSessionImpl impl) { + mImpl = impl; + mController = new MediaControllerCompat(context, this); + } + + /** + * Add a callback to receive updates on for the MediaSession. This includes + * media button and volume events. The caller's thread will be used to post + * events. + * + * @param callback The callback object + */ + public void setCallback(Callback callback) { + setCallback(callback, null); + } + + /** + * Set the callback to receive updates for the MediaSession. This includes + * media button and volume events. Set the callback to null to stop + * receiving events. + * + * @param callback The callback to receive updates on. + * @param handler The handler that events should be posted on. + */ + public void setCallback(Callback callback, Handler handler) { + mImpl.setCallback(callback, handler != null ? handler : new Handler()); + } + + /** + * Set an intent for launching UI for this Session. This can be used as a + * quick link to an ongoing media screen. The intent should be for an + * activity that may be started using + * {@link Activity#startActivity(Intent)}. + * + * @param pi The intent to launch to show UI for this Session. + */ + public void setSessionActivity(PendingIntent pi) { + mImpl.setSessionActivity(pi); + } + + /** + * Set a pending intent for your media button receiver to allow restarting + * playback after the session has been stopped. If your app is started in + * this way an {@link Intent#ACTION_MEDIA_BUTTON} intent will be sent via + * the pending intent. + *

+ * This method will only work on + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. Earlier + * platform versions must include the media button receiver in the + * constructor. + * + * @param mbr The {@link PendingIntent} to send the media button event to. + */ + public void setMediaButtonReceiver(PendingIntent mbr) { + mImpl.setMediaButtonReceiver(mbr); + } + + /** + * Set any flags for the session. + * + * @param flags The flags to set for this session. + */ + public void setFlags(int flags) { + mImpl.setFlags(flags); + } + + /** + * Set the stream this session is playing on. This will affect the system's + * volume handling for this session. If {@link #setPlaybackToRemote} was + * previously called it will stop receiving volume commands and the system + * will begin sending volume changes to the appropriate stream. + *

+ * By default sessions are on {@link AudioManager#STREAM_MUSIC}. + * + * @param stream The {@link AudioManager} stream this session is playing on. + */ + public void setPlaybackToLocal(int stream) { + mImpl.setPlaybackToLocal(stream); + } + + /** + * Configure this session to use remote volume handling. This must be called + * to receive volume button events, otherwise the system will adjust the + * current stream volume for this session. If {@link #setPlaybackToLocal} + * was previously called that stream will stop receiving volume changes for + * this session. + *

+ * On platforms earlier than {@link android.os.Build.VERSION_CODES#LOLLIPOP} + * this will only allow an app to handle volume commands sent directly to + * the session by a {@link MediaControllerCompat}. System routing of volume + * keys will not use the volume provider. + * + * @param volumeProvider The provider that will handle volume changes. May + * not be null. + */ + public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { + if (volumeProvider == null) { + throw new IllegalArgumentException("volumeProvider may not be null!"); + } + mImpl.setPlaybackToRemote(volumeProvider); + } + + /** + * Set if this session is currently active and ready to receive commands. If + * set to false your session's controller may not be discoverable. You must + * set the session to active before it can start receiving media button + * events or transport commands. + *

+ * On platforms earlier than + * {@link android.os.Build.VERSION_CODES#LOLLIPOP}, + * {@link #setMediaButtonReceiver(PendingIntent)} must be called before + * setting this to true. + * + * @param active Whether this session is active or not. + */ + public void setActive(boolean active) { + mImpl.setActive(active); + for (OnActiveChangeListener listener : mActiveListeners) { + listener.onActiveChanged(); + } + } + + /** + * Get the current active state of this session. + * + * @return True if the session is active, false otherwise. + */ + public boolean isActive() { + return mImpl.isActive(); + } + + /** + * Send a proprietary event to all MediaControllers listening to this + * Session. It's up to the Controller/Session owner to determine the meaning + * of any events. + * + * @param event The name of the event to send + * @param extras Any extras included with the event + */ + public void sendSessionEvent(String event, Bundle extras) { + if (TextUtils.isEmpty(event)) { + throw new IllegalArgumentException("event cannot be null or empty"); + } + mImpl.sendSessionEvent(event, extras); + } + + /** + * This must be called when an app has finished performing playback. If + * playback is expected to start again shortly the session can be left open, + * but it must be released if your activity or service is being destroyed. + */ + public void release() { + mImpl.release(); + } + + /** + * Retrieve a token object that can be used by apps to create a + * {@link MediaControllerCompat} for interacting with this session. The + * owner of the session is responsible for deciding how to distribute these + * tokens. + *

+ * On platform versions before + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} this token may only be + * used within your app as there is no way to guarantee other apps are using + * the same version of the support library. + * + * @return A token that can be used to create a media controller for this + * session. + */ + public Token getSessionToken() { + return mImpl.getSessionToken(); + } + + /** + * Get a controller for this session. This is a convenience method to avoid + * having to cache your own controller in process. + * + * @return A controller for this session. + */ + public MediaControllerCompat getController() { + return mController; + } + + /** + * Update the current playback state. + * + * @param state The current state of playback + */ + public void setPlaybackState(PlaybackStateCompat state) { + mImpl.setPlaybackState(state); + } + + /** + * Update the current metadata. New metadata can be created using + * {@link android.media.MediaMetadata.Builder}. + * + * @param metadata The new metadata + */ + public void setMetadata(MediaMetadataCompat metadata) { + mImpl.setMetadata(metadata); + } + + /** + * Update the list of items in the play queue. It is an ordered list and + * should contain the current item, and previous or upcoming items if they + * exist. Specify null if there is no current play queue. + *

+ * The queue should be of reasonable size. If the play queue is unbounded + * within your app, it is better to send a reasonable amount in a sliding + * window instead. + * + * @param queue A list of items in the play queue. + */ + public void setQueue(List queue) { + mImpl.setQueue(queue); + } + + /** + * Set the title of the play queue. The UI should display this title along + * with the play queue itself. e.g. "Play Queue", "Now Playing", or an album + * name. + * + * @param title The title of the play queue. + */ + public void setQueueTitle(CharSequence title) { + mImpl.setQueueTitle(title); + } + + /** + * Set the style of rating used by this session. Apps trying to set the + * rating should use this style. Must be one of the following: + *

    + *
  • {@link RatingCompat#RATING_NONE}
  • + *
  • {@link RatingCompat#RATING_3_STARS}
  • + *
  • {@link RatingCompat#RATING_4_STARS}
  • + *
  • {@link RatingCompat#RATING_5_STARS}
  • + *
  • {@link RatingCompat#RATING_HEART}
  • + *
  • {@link RatingCompat#RATING_PERCENTAGE}
  • + *
  • {@link RatingCompat#RATING_THUMB_UP_DOWN}
  • + *
+ */ + public void setRatingType(int type) { + mImpl.setRatingType(type); + } + + /** + * Set some extras that can be associated with the + * {@link MediaSessionCompat}. No assumptions should be made as to how a + * {@link MediaControllerCompat} will handle these extras. Keys should be + * fully qualified (e.g. com.example.MY_EXTRA) to avoid conflicts. + * + * @param extras The extras associated with the session. + */ + public void setExtras(Bundle extras) { + mImpl.setExtras(extras); + } + + /** + * Gets the underlying framework {@link android.media.session.MediaSession} + * object. + *

+ * This method is only supported on API 21+. + *

+ * + * @return The underlying {@link android.media.session.MediaSession} object, + * or null if none. + */ + public Object getMediaSession() { + return mImpl.getMediaSession(); + } + + /** + * Gets the underlying framework {@link android.media.RemoteControlClient} + * object. + *

+ * This method is only supported on APIs 14-20. On API 21+ + * {@link #getMediaSession()} should be used instead. + * + * @return The underlying {@link android.media.RemoteControlClient} object, + * or null if none. + */ + public Object getRemoteControlClient() { + return mImpl.getRemoteControlClient(); + } + + /** + * Adds a listener to be notified when the active status of this session + * changes. This is primarily used by the support library and should not be + * needed by apps. + * + * @param listener The listener to add. + */ + public void addOnActiveChangeListener(OnActiveChangeListener listener) { + if (listener == null) { + throw new IllegalArgumentException("Listener may not be null"); + } + mActiveListeners.add(listener); + } + + /** + * Stops the listener from being notified when the active status of this + * session changes. + * + * @param listener The listener to remove. + */ + public void removeOnActiveChangeListener(OnActiveChangeListener listener) { + if (listener == null) { + throw new IllegalArgumentException("Listener may not be null"); + } + mActiveListeners.remove(listener); + } + + /** + * Obtain a compat wrapper for an existing MediaSession. + * + * @param mediaSession The {@link android.media.session.MediaSession} to + * wrap. + * @return A compat wrapper for the provided session. + */ + public static MediaSessionCompat obtain(Context context, Object mediaSession) { + return new MediaSessionCompat(context, new MediaSessionImplApi21(mediaSession)); + } + + /** + * Receives transport controls, media buttons, and commands from controllers + * and the system. The callback may be set using {@link #setCallback}. + */ + public abstract static class Callback { + final Object mCallbackObj; + + public Callback() { + if (android.os.Build.VERSION.SDK_INT >= 21) { + mCallbackObj = MediaSessionCompatApi21.createCallback(new StubApi21()); + } else { + mCallbackObj = null; + } + } + + /** + * Called when a controller has sent a custom command to this session. + * The owner of the session may handle custom commands but is not + * required to. + * + * @param command The command name. + * @param extras Optional parameters for the command, may be null. + * @param cb A result receiver to which a result may be sent by the command, may be null. + */ + public void onCommand(String command, Bundle extras, ResultReceiver cb) { + } + + /** + * Override to handle media button events. + * + * @param mediaButtonEvent The media button event intent. + * @return True if the event was handled, false otherwise. + */ + public boolean onMediaButtonEvent(Intent mediaButtonEvent) { + return false; + } + + /** + * Override to handle requests to begin playback. + */ + public void onPlay() { + } + + /** + * Override to handle requests to play a specific mediaId that was + * provided by your app. + */ + public void onPlayFromMediaId(String mediaId, Bundle extras) { + } + + /** + * Override to handle requests to begin playback from a search query. An + * empty query indicates that the app may play any music. The + * implementation should attempt to make a smart choice about what to + * play. + */ + public void onPlayFromSearch(String query, Bundle extras) { + } + + /** + * Override to handle requests to play an item with a given id from the + * play queue. + */ + public void onSkipToQueueItem(long id) { + } + + /** + * Override to handle requests to pause playback. + */ + public void onPause() { + } + + /** + * Override to handle requests to skip to the next media item. + */ + public void onSkipToNext() { + } + + /** + * Override to handle requests to skip to the previous media item. + */ + public void onSkipToPrevious() { + } + + /** + * Override to handle requests to fast forward. + */ + public void onFastForward() { + } + + /** + * Override to handle requests to rewind. + */ + public void onRewind() { + } + + /** + * Override to handle requests to stop playback. + */ + public void onStop() { + } + + /** + * Override to handle requests to seek to a specific position in ms. + * + * @param pos New position to move to, in milliseconds. + */ + public void onSeekTo(long pos) { + } + + /** + * Override to handle the item being rated. + * + * @param rating + */ + public void onSetRating(RatingCompat rating) { + } + + /** + * Called when a {@link MediaControllerCompat} wants a + * {@link PlaybackStateCompat.CustomAction} to be performed. + * + * @param action The action that was originally sent in the + * {@link PlaybackStateCompat.CustomAction}. + * @param extras Optional extras specified by the + * {@link MediaControllerCompat}. + */ + public void onCustomAction(String action, Bundle extras) { + } + + private class StubApi21 implements MediaSessionCompatApi21.Callback { + + @Override + public void onCommand(String command, Bundle extras, ResultReceiver cb) { + Callback.this.onCommand(command, extras, cb); + } + + @Override + public boolean onMediaButtonEvent(Intent mediaButtonIntent) { + return Callback.this.onMediaButtonEvent(mediaButtonIntent); + } + + @Override + public void onPlay() { + Callback.this.onPlay(); + } + + @Override + public void onPlayFromMediaId(String mediaId, Bundle extras) { + Callback.this.onPlayFromMediaId(mediaId, extras); + } + + @Override + public void onPlayFromSearch(String search, Bundle extras) { + Callback.this.onPlayFromSearch(search, extras); + } + + @Override + public void onSkipToQueueItem(long id) { + Callback.this.onSkipToQueueItem(id); + } + + @Override + public void onPause() { + Callback.this.onPause(); + } + + @Override + public void onSkipToNext() { + Callback.this.onSkipToNext(); + } + + @Override + public void onSkipToPrevious() { + Callback.this.onSkipToPrevious(); + } + + @Override + public void onFastForward() { + Callback.this.onFastForward(); + } + + @Override + public void onRewind() { + Callback.this.onRewind(); + } + + @Override + public void onStop() { + Callback.this.onStop(); + } + + @Override + public void onSeekTo(long pos) { + Callback.this.onSeekTo(pos); + } + + @Override + public void onSetRating(Object ratingObj) { + Callback.this.onSetRating(RatingCompat.fromRating(ratingObj)); + } + + @Override + public void onCustomAction(String action, Bundle extras) { + Callback.this.onCustomAction(action, extras); + } + } + } + + /** + * Represents an ongoing session. This may be passed to apps by the session + * owner to allow them to create a {@link MediaControllerCompat} to communicate with + * the session. + */ + public static final class Token implements Parcelable { + private final Object mInner; + + Token(Object inner) { + mInner = inner; + } + + /** + * Creates a compat Token from a framework + * {@link android.media.session.MediaSession.Token} object. + *

+ * This method is only supported on + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and later. + *

+ * + * @param token The framework token object. + * @return A compat Token for use with {@link MediaControllerCompat}. + */ + public static Token fromToken(Object token) { + if (token == null || android.os.Build.VERSION.SDK_INT < 21) { + return null; + } + return new Token(MediaSessionCompatApi21.verifyToken(token)); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (android.os.Build.VERSION.SDK_INT >= 21) { + dest.writeParcelable((Parcelable) mInner, flags); + } else { + dest.writeStrongBinder((IBinder) mInner); + } + } + + /** + * Gets the underlying framework {@link android.media.session.MediaSession.Token} object. + *

+ * This method is only supported on API 21+. + *

+ * + * @return The underlying {@link android.media.session.MediaSession.Token} object, + * or null if none. + */ + public Object getToken() { + return mInner; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public Token createFromParcel(Parcel in) { + Object inner; + if (android.os.Build.VERSION.SDK_INT >= 21) { + inner = in.readParcelable(null); + } else { + inner = in.readStrongBinder(); + } + return new Token(inner); + } + + @Override + public Token[] newArray(int size) { + return new Token[size]; + } + }; + } + + /** + * A single item that is part of the play queue. It contains a description + * of the item and its id in the queue. + */ + public static final class QueueItem implements Parcelable { + /** + * This id is reserved. No items can be explicitly asigned this id. + */ + public static final int UNKNOWN_ID = -1; + + private final MediaDescriptionCompat mDescription; + private final long mId; + + private Object mItem; + + /** + * Create a new {@link MediaSessionCompat.QueueItem}. + * + * @param description The {@link MediaDescriptionCompat} for this item. + * @param id An identifier for this item. It must be unique within the + * play queue and cannot be {@link #UNKNOWN_ID}. + */ + public QueueItem(MediaDescriptionCompat description, long id) { + this(null, description, id); + } + + private QueueItem(Object queueItem, MediaDescriptionCompat description, long id) { + if (description == null) { + throw new IllegalArgumentException("Description cannot be null."); + } + if (id == UNKNOWN_ID) { + throw new IllegalArgumentException("Id cannot be QueueItem.UNKNOWN_ID"); + } + mDescription = description; + mId = id; + mItem = queueItem; + } + + private QueueItem(Parcel in) { + mDescription = MediaDescriptionCompat.CREATOR.createFromParcel(in); + mId = in.readLong(); + } + + /** + * Get the description for this item. + */ + public MediaDescriptionCompat getDescription() { + return mDescription; + } + + /** + * Get the queue id for this item. + */ + public long getQueueId() { + return mId; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + mDescription.writeToParcel(dest, flags); + dest.writeLong(mId); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Get the underlying + * {@link android.media.session.MediaSession.QueueItem}. + *

+ * On builds before {@link android.os.Build.VERSION_CODES#LOLLIPOP} null + * is returned. + * + * @return The underlying + * {@link android.media.session.MediaSession.QueueItem} or null. + */ + public Object getQueueItem() { + if (mItem != null || android.os.Build.VERSION.SDK_INT < 21) { + return mItem; + } + mItem = MediaSessionCompatApi21.QueueItem.createItem(mDescription.getMediaDescription(), + mId); + return mItem; + } + + /** + * Obtain a compat wrapper for an existing QueueItem. + * + * @param queueItem The {@link android.media.session.MediaSession.QueueItem} to + * wrap. + * @return A compat wrapper for the provided item. + */ + public static QueueItem obtain(Object queueItem) { + Object descriptionObj = MediaSessionCompatApi21.QueueItem.getDescription(queueItem); + MediaDescriptionCompat description = MediaDescriptionCompat.fromMediaDescription( + descriptionObj); + long id = MediaSessionCompatApi21.QueueItem.getQueueId(queueItem); + return new QueueItem(queueItem, description, id); + } + + public static final Creator + CREATOR = new Creator() { + + @Override + public MediaSessionCompat.QueueItem createFromParcel(Parcel p) { + return new MediaSessionCompat.QueueItem(p); + } + + @Override + public MediaSessionCompat.QueueItem[] newArray(int size) { + return new MediaSessionCompat.QueueItem[size]; + } + }; + + @Override + public String toString() { + return "MediaSession.QueueItem {" + + "Description=" + mDescription + + ", Id=" + mId + " }"; + } + } + + /** + * This is a wrapper for {@link ResultReceiver} for sending over aidl + * interfaces. The framework version was not exposed to aidls until + * {@link android.os.Build.VERSION_CODES#LOLLIPOP}. + */ + static final class ResultReceiverWrapper implements Parcelable { + private ResultReceiver mResultReceiver; + + public ResultReceiverWrapper(ResultReceiver resultReceiver) { + mResultReceiver = resultReceiver; + } + + ResultReceiverWrapper(Parcel in) { + mResultReceiver = ResultReceiver.CREATOR.createFromParcel(in); + } + + public static final Creator + CREATOR = new Creator() { + @Override + public ResultReceiverWrapper createFromParcel(Parcel p) { + return new ResultReceiverWrapper(p); + } + + @Override + public ResultReceiverWrapper[] newArray(int size) { + return new ResultReceiverWrapper[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + mResultReceiver.writeToParcel(dest, flags); + } + } + + public interface OnActiveChangeListener { + void onActiveChanged(); + } + + interface MediaSessionImpl { + void setCallback(Callback callback, Handler handler); + void setFlags(int flags); + void setPlaybackToLocal(int stream); + void setPlaybackToRemote(VolumeProviderCompat volumeProvider); + void setActive(boolean active); + boolean isActive(); + void sendSessionEvent(String event, Bundle extras); + void release(); + Token getSessionToken(); + void setPlaybackState(PlaybackStateCompat state); + void setMetadata(MediaMetadataCompat metadata); + + void setSessionActivity(PendingIntent pi); + + void setMediaButtonReceiver(PendingIntent mbr); + void setQueue(List queue); + void setQueueTitle(CharSequence title); + + void setRatingType(int type); + void setExtras(Bundle extras); + + Object getMediaSession(); + + Object getRemoteControlClient(); + } + + static class MediaSessionImplBase implements MediaSessionImpl { + private final Context mContext; + private final ComponentName mComponentName; + private final PendingIntent mMediaButtonEventReceiver; + private final Object mRccObj; + private final MediaSessionStub mStub; + private final Token mToken; + private final MessageHandler mHandler; + private final String mPackageName; + private final String mTag; + private final AudioManager mAudioManager; + + private final Object mLock = new Object(); + private final RemoteCallbackList mControllerCallbacks + = new RemoteCallbackList(); + + private boolean mDestroyed = false; + private boolean mIsActive = false; + private boolean mIsRccRegistered = false; + private boolean mIsMbrRegistered = false; + private Callback mCallback; + + private int mFlags; + + private MediaMetadataCompat mMetadata; + private PlaybackStateCompat mState; + private PendingIntent mSessionActivity; + private List mQueue; + private CharSequence mQueueTitle; + private int mRatingType; + private Bundle mExtras; + + private int mVolumeType; + private int mLocalStream; + private VolumeProviderCompat mVolumeProvider; + + private VolumeProviderCompat.Callback mVolumeCallback + = new VolumeProviderCompat.Callback() { + @Override + public void onVolumeChanged(VolumeProviderCompat volumeProvider) { + if (mVolumeProvider != volumeProvider) { + return; + } + ParcelableVolumeInfo info = new ParcelableVolumeInfo(mVolumeType, mLocalStream, + volumeProvider.getVolumeControl(), volumeProvider.getMaxVolume(), + volumeProvider.getCurrentVolume()); + sendVolumeInfoChanged(info); + } + }; + + public MediaSessionImplBase(Context context, String tag, ComponentName mbrComponent, + PendingIntent mbr) { + if (mbrComponent == null) { + throw new IllegalArgumentException( + "MediaButtonReceiver component may not be null."); + } + mContext = context; + mPackageName = context.getPackageName(); + mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + mTag = tag; + mComponentName = mbrComponent; + mMediaButtonEventReceiver = mbr; + mStub = new MediaSessionStub(); + mToken = new Token(mStub); + mHandler = new MessageHandler(Looper.myLooper()); + + mRatingType = RatingCompat.RATING_NONE; + mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL; + mLocalStream = AudioManager.STREAM_MUSIC; + if (android.os.Build.VERSION.SDK_INT >= 14) { + mRccObj = MediaSessionCompatApi14.createRemoteControlClient(mbr); + } else { + mRccObj = null; + } + } + + @Override + public void setCallback(final Callback callback, Handler handler) { + if (callback == mCallback) { + return; + } + if (callback == null || android.os.Build.VERSION.SDK_INT < 18) { + // There's nothing to register on API < 18 since media buttons + // all go through the media button receiver + if (android.os.Build.VERSION.SDK_INT >= 18) { + MediaSessionCompatApi18.setOnPlaybackPositionUpdateListener(mRccObj, null); + } + if (android.os.Build.VERSION.SDK_INT >= 19) { + MediaSessionCompatApi19.setOnMetadataUpdateListener(mRccObj, null); + } + } else { + if (handler == null) { + handler = new Handler(); + } + MediaSessionCompatApi14.Callback cb14 = new MediaSessionCompatApi14.Callback() { + @Override + public void onStop() { + callback.onStop(); + } + + @Override + public void onSkipToPrevious() { + callback.onSkipToPrevious(); + } + + @Override + public void onSkipToNext() { + callback.onSkipToNext(); + } + + @Override + public void onSetRating(Object ratingObj) { + callback.onSetRating(RatingCompat.fromRating(ratingObj)); + } + + @Override + public void onSeekTo(long pos) { + callback.onSeekTo(pos); + } + + @Override + public void onRewind() { + callback.onRewind(); + } + + @Override + public void onPlay() { + callback.onPlay(); + } + + @Override + public void onPause() { + callback.onPause(); + } + + @Override + public boolean onMediaButtonEvent(Intent mediaButtonIntent) { + return callback.onMediaButtonEvent(mediaButtonIntent); + } + + @Override + public void onFastForward() { + callback.onFastForward(); + } + + @Override + public void onCommand(String command, Bundle extras, ResultReceiver cb) { + callback.onCommand(command, extras, cb); + } + }; + if (android.os.Build.VERSION.SDK_INT >= 18) { + Object onPositionUpdateObj = MediaSessionCompatApi18 + .createPlaybackPositionUpdateListener(cb14); + MediaSessionCompatApi18.setOnPlaybackPositionUpdateListener(mRccObj, + onPositionUpdateObj); + } + if (android.os.Build.VERSION.SDK_INT >= 19) { + Object onMetadataUpdateObj = MediaSessionCompatApi19 + .createMetadataUpdateListener(cb14); + MediaSessionCompatApi19.setOnMetadataUpdateListener(mRccObj, + onMetadataUpdateObj); + } + } + mCallback = callback; + } + + @Override + public void setFlags(int flags) { + synchronized (mLock) { + mFlags = flags; + } + update(); + } + + @Override + public void setPlaybackToLocal(int stream) { + if (mVolumeProvider != null) { + mVolumeProvider.setCallback(null); + } + mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_LOCAL; + ParcelableVolumeInfo info = new ParcelableVolumeInfo(mVolumeType, mLocalStream, + VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE, + mAudioManager.getStreamMaxVolume(mLocalStream), + mAudioManager.getStreamVolume(mLocalStream)); + sendVolumeInfoChanged(info); + } + + @Override + public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { + if (volumeProvider == null) { + throw new IllegalArgumentException("volumeProvider may not be null"); + } + if (mVolumeProvider != null) { + mVolumeProvider.setCallback(null); + } + mVolumeType = MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE; + mVolumeProvider = volumeProvider; + ParcelableVolumeInfo info = new ParcelableVolumeInfo(mVolumeType, mLocalStream, + mVolumeProvider.getVolumeControl(), mVolumeProvider.getMaxVolume(), + mVolumeProvider.getCurrentVolume()); + sendVolumeInfoChanged(info); + + volumeProvider.setCallback(mVolumeCallback); + } + + @Override + public void setActive(boolean active) { + if (active == mIsActive) { + return; + } + mIsActive = active; + if (update()) { + setMetadata(mMetadata); + setPlaybackState(mState); + } + } + + @Override + public boolean isActive() { + return mIsActive; + } + + @Override + public void sendSessionEvent(String event, Bundle extras) { + sendEvent(event, extras); + } + + @Override + public void release() { + mIsActive = false; + mDestroyed = true; + update(); + sendSessionDestroyed(); + } + + @Override + public Token getSessionToken() { + return mToken; + } + + @Override + public void setPlaybackState(PlaybackStateCompat state) { + synchronized (mLock) { + mState = state; + } + sendState(state); + if (!mIsActive) { + // Don't set the state until after the RCC is registered + return; + } + if (state == null) { + if (android.os.Build.VERSION.SDK_INT >= 14) { + MediaSessionCompatApi14.setState(mRccObj, PlaybackStateCompat.STATE_NONE); + MediaSessionCompatApi14.setTransportControlFlags(mRccObj, 0); + } + } else { + // Set state + if (android.os.Build.VERSION.SDK_INT >= 18) { + MediaSessionCompatApi18.setState(mRccObj, state.getState(), state.getPosition(), + state.getPlaybackSpeed(), state.getLastPositionUpdateTime()); + } else if (android.os.Build.VERSION.SDK_INT >= 14) { + MediaSessionCompatApi14.setState(mRccObj, state.getState()); + } + + // Set transport control flags + if (android.os.Build.VERSION.SDK_INT >= 19) { + MediaSessionCompatApi19.setTransportControlFlags(mRccObj, state.getActions()); + } else if (android.os.Build.VERSION.SDK_INT >= 18) { + MediaSessionCompatApi18.setTransportControlFlags(mRccObj, state.getActions()); + } else if (android.os.Build.VERSION.SDK_INT >= 14) { + MediaSessionCompatApi14.setTransportControlFlags(mRccObj, state.getActions()); + } + } + } + + @Override + public void setMetadata(MediaMetadataCompat metadata) { + synchronized (mLock) { + mMetadata = metadata; + } + sendMetadata(metadata); + if (!mIsActive) { + // Don't set metadata until after the rcc has been registered + return; + } + if (android.os.Build.VERSION.SDK_INT >= 19) { + MediaSessionCompatApi19.setMetadata(mRccObj, + metadata == null ? null : metadata.getBundle(), + mState == null ? 0 : mState.getActions()); + } else if (android.os.Build.VERSION.SDK_INT >= 14) { + MediaSessionCompatApi14.setMetadata(mRccObj, + metadata == null ? null : metadata.getBundle()); + } + } + + @Override + public void setSessionActivity(PendingIntent pi) { + synchronized (mLock) { + mSessionActivity = pi; + } + } + + @Override + public void setMediaButtonReceiver(PendingIntent mbr) { + // Do nothing, changing this is not supported before API 21. + } + + @Override + public void setQueue(List queue) { + mQueue = queue; + sendQueue(queue); + } + + @Override + public void setQueueTitle(CharSequence title) { + mQueueTitle = title; + sendQueueTitle(title); + } + + @Override + public Object getMediaSession() { + return null; + } + + @Override + public Object getRemoteControlClient() { + return mRccObj; + } + + @Override + public void setRatingType(int type) { + mRatingType = type; + } + + @Override + public void setExtras(Bundle extras) { + mExtras = extras; + } + + // Registers/unregisters the RCC and MediaButtonEventReceiver as needed. + private boolean update() { + boolean registeredRcc = false; + if (mIsActive) { + // On API 8+ register a MBR if it's supported, unregister it + // if support was removed. + if (android.os.Build.VERSION.SDK_INT >= 8) { + if (!mIsMbrRegistered && (mFlags & FLAG_HANDLES_MEDIA_BUTTONS) != 0) { + if (android.os.Build.VERSION.SDK_INT >= 18) { + MediaSessionCompatApi18.registerMediaButtonEventReceiver(mContext, + mMediaButtonEventReceiver); + } else { + MediaSessionCompatApi8.registerMediaButtonEventReceiver(mContext, + mComponentName); + } + mIsMbrRegistered = true; + } else if (mIsMbrRegistered && (mFlags & FLAG_HANDLES_MEDIA_BUTTONS) == 0) { + if (android.os.Build.VERSION.SDK_INT >= 18) { + MediaSessionCompatApi18.unregisterMediaButtonEventReceiver(mContext, + mMediaButtonEventReceiver); + } else { + MediaSessionCompatApi8.unregisterMediaButtonEventReceiver(mContext, + mComponentName); + } + mIsMbrRegistered = false; + } + } + // On API 14+ register a RCC if it's supported, unregister it if + // not. + if (android.os.Build.VERSION.SDK_INT >= 14) { + if (!mIsRccRegistered && (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) != 0) { + MediaSessionCompatApi14.registerRemoteControlClient(mContext, mRccObj); + mIsRccRegistered = true; + registeredRcc = true; + } else if (mIsRccRegistered + && (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) == 0) { + MediaSessionCompatApi14.unregisterRemoteControlClient(mContext, mRccObj); + mIsRccRegistered = false; + } + } + } else { + // When inactive remove any registered components. + if (mIsMbrRegistered) { + if (android.os.Build.VERSION.SDK_INT >= 18) { + MediaSessionCompatApi18.unregisterMediaButtonEventReceiver(mContext, + mMediaButtonEventReceiver); + } else { + MediaSessionCompatApi8.unregisterMediaButtonEventReceiver(mContext, + mComponentName); + } + mIsMbrRegistered = false; + } + if (mIsRccRegistered) { + MediaSessionCompatApi14.unregisterRemoteControlClient(mContext, mRccObj); + mIsRccRegistered = false; + } + } + return registeredRcc; + } + + private void adjustVolume(int direction, int flags) { + if (mVolumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { + if (mVolumeProvider != null) { + mVolumeProvider.onAdjustVolume(direction); + } + } else { + mAudioManager.adjustStreamVolume(direction, mLocalStream, flags); + } + } + + private void setVolumeTo(int value, int flags) { + if (mVolumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { + if (mVolumeProvider != null) { + mVolumeProvider.onSetVolumeTo(value); + } + } else { + mAudioManager.setStreamVolume(mLocalStream, value, flags); + } + } + + private PlaybackStateCompat getStateWithUpdatedPosition() { + PlaybackStateCompat state; + long duration = -1; + synchronized (mLock) { + state = mState; + if (mMetadata != null + && mMetadata.containsKey(MediaMetadataCompat.METADATA_KEY_DURATION)) { + duration = mMetadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION); + } + } + + PlaybackStateCompat result = null; + if (state != null) { + if (state.getState() == PlaybackStateCompat.STATE_PLAYING + || state.getState() == PlaybackStateCompat.STATE_FAST_FORWARDING + || state.getState() == PlaybackStateCompat.STATE_REWINDING) { + long updateTime = state.getLastPositionUpdateTime(); + long currentTime = SystemClock.elapsedRealtime(); + if (updateTime > 0) { + long position = (long) (state.getPlaybackSpeed() + * (currentTime - updateTime)) + state.getPosition(); + if (duration >= 0 && position > duration) { + position = duration; + } else if (position < 0) { + position = 0; + } + PlaybackStateCompat.Builder builder = new PlaybackStateCompat.Builder( + state); + builder.setState(state.getState(), position, state.getPlaybackSpeed(), + currentTime); + result = builder.build(); + } + } + } + return result == null ? state : result; + } + + private void sendVolumeInfoChanged(ParcelableVolumeInfo info) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onVolumeInfoChanged(info); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + + private void sendSessionDestroyed() { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onSessionDestroyed();; + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + mControllerCallbacks.kill(); + } + + private void sendEvent(String event, Bundle extras) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onEvent(event, extras); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + + private void sendState(PlaybackStateCompat state) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onPlaybackStateChanged(state); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + + private void sendMetadata(MediaMetadataCompat metadata) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onMetadataChanged(metadata); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + + private void sendQueue(List queue) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onQueueChanged(queue); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + + private void sendQueueTitle(CharSequence queueTitle) { + int size = mControllerCallbacks.beginBroadcast(); + for (int i = size - 1; i >= 0; i--) { + IMediaControllerCallback cb = mControllerCallbacks.getBroadcastItem(i); + try { + cb.onQueueTitleChanged(queueTitle); + } catch (RemoteException e) { + } + } + mControllerCallbacks.finishBroadcast(); + } + + class MediaSessionStub extends IMediaSession.Stub { + @Override + public void sendCommand(String command, Bundle args, ResultReceiverWrapper cb) { + mHandler.post(MessageHandler.MSG_COMMAND, + new Command(command, args, cb.mResultReceiver)); + } + + @Override + public boolean sendMediaButton(KeyEvent mediaButton) { + boolean handlesMediaButtons = + (mFlags & MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS) != 0; + if (handlesMediaButtons) { + mHandler.post(MessageHandler.MSG_MEDIA_BUTTON, mediaButton); + } + return handlesMediaButtons; + } + + @Override + public void registerCallbackListener(IMediaControllerCallback cb) { + // If this session is already destroyed tell the caller and + // don't add them. + if (mDestroyed) { + try { + cb.onSessionDestroyed(); + } catch (Exception e) { + // ignored + } + return; + } + mControllerCallbacks.register(cb); + } + + @Override + public void unregisterCallbackListener(IMediaControllerCallback cb) { + mControllerCallbacks.unregister(cb); + } + + @Override + public String getPackageName() { + // mPackageName is final so doesn't need synchronize block + return mPackageName; + } + + @Override + public String getTag() { + // mTag is final so doesn't need synchronize block + return mTag; + } + + @Override + public PendingIntent getLaunchPendingIntent() { + synchronized (mLock) { + return mSessionActivity; + } + } + + @Override + public long getFlags() { + synchronized (mLock) { + return mFlags; + } + } + + @Override + public ParcelableVolumeInfo getVolumeAttributes() { + int controlType; + int max; + int current; + int stream; + int volumeType; + synchronized (mLock) { + volumeType = mVolumeType; + stream = mLocalStream; + VolumeProviderCompat vp = mVolumeProvider; + if (volumeType == MediaControllerCompat.PlaybackInfo.PLAYBACK_TYPE_REMOTE) { + controlType = vp.getVolumeControl(); + max = vp.getMaxVolume(); + current = vp.getCurrentVolume(); + } else { + controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE; + max = mAudioManager.getStreamMaxVolume(stream); + current = mAudioManager.getStreamVolume(stream); + } + } + return new ParcelableVolumeInfo(volumeType, stream, controlType, max, current); + } + + @Override + public void adjustVolume(int direction, int flags, String packageName) { + MediaSessionImplBase.this.adjustVolume(direction, flags); + } + + @Override + public void setVolumeTo(int value, int flags, String packageName) { + MediaSessionImplBase.this.setVolumeTo(value, flags); + } + + @Override + public void play() throws RemoteException { + mHandler.post(MessageHandler.MSG_PLAY); + } + + @Override + public void playFromMediaId(String mediaId, Bundle extras) throws RemoteException { + mHandler.post(MessageHandler.MSG_PLAY_MEDIA_ID, mediaId, extras); + } + + @Override + public void playFromSearch(String query, Bundle extras) throws RemoteException { + mHandler.post(MessageHandler.MSG_PLAY_SEARCH, query, extras); + } + + @Override + public void skipToQueueItem(long id) { + mHandler.post(MessageHandler.MSG_SKIP_TO_ITEM, id); + } + + @Override + public void pause() throws RemoteException { + mHandler.post(MessageHandler.MSG_PAUSE); + } + + @Override + public void stop() throws RemoteException { + mHandler.post(MessageHandler.MSG_STOP); + } + + @Override + public void next() throws RemoteException { + mHandler.post(MessageHandler.MSG_NEXT); + } + + @Override + public void previous() throws RemoteException { + mHandler.post(MessageHandler.MSG_PREVIOUS); + } + + @Override + public void fastForward() throws RemoteException { + mHandler.post(MessageHandler.MSG_FAST_FORWARD); + } + + @Override + public void rewind() throws RemoteException { + mHandler.post(MessageHandler.MSG_REWIND); + } + + @Override + public void seekTo(long pos) throws RemoteException { + mHandler.post(MessageHandler.MSG_SEEK_TO, pos); + } + + @Override + public void rate(RatingCompat rating) throws RemoteException { + mHandler.post(MessageHandler.MSG_RATE, rating); + } + + @Override + public void sendCustomAction(String action, Bundle args) + throws RemoteException { + mHandler.post(MessageHandler.MSG_CUSTOM_ACTION, action, args); + } + + @Override + public MediaMetadataCompat getMetadata() { + return mMetadata; + } + + @Override + public PlaybackStateCompat getPlaybackState() { + return getStateWithUpdatedPosition(); + } + + @Override + public List getQueue() { + synchronized (mLock) { + return mQueue; + } + } + + @Override + public CharSequence getQueueTitle() { + return mQueueTitle; + } + + @Override + public Bundle getExtras() { + synchronized (mLock) { + return mExtras; + } + } + + @Override + public int getRatingType() { + return mRatingType; + } + + @Override + public boolean isTransportControlEnabled() { + return (mFlags & FLAG_HANDLES_TRANSPORT_CONTROLS) != 0; + } + } + + private static final class Command { + public final String command; + public final Bundle extras; + public final ResultReceiver stub; + + public Command(String command, Bundle extras, ResultReceiver stub) { + this.command = command; + this.extras = extras; + this.stub = stub; + } + } + + private class MessageHandler extends Handler { + + private static final int MSG_PLAY = 1; + private static final int MSG_PLAY_MEDIA_ID = 2; + private static final int MSG_PLAY_SEARCH = 3; + private static final int MSG_SKIP_TO_ITEM = 4; + private static final int MSG_PAUSE = 5; + private static final int MSG_STOP = 6; + private static final int MSG_NEXT = 7; + private static final int MSG_PREVIOUS = 8; + private static final int MSG_FAST_FORWARD = 9; + private static final int MSG_REWIND = 10; + private static final int MSG_SEEK_TO = 11; + private static final int MSG_RATE = 12; + private static final int MSG_CUSTOM_ACTION = 13; + private static final int MSG_MEDIA_BUTTON = 14; + private static final int MSG_COMMAND = 15; + private static final int MSG_ADJUST_VOLUME = 16; + private static final int MSG_SET_VOLUME = 17; + + public MessageHandler(Looper looper) { + super(looper); + } + + public void post(int what, Object obj, Bundle bundle) { + Message msg = obtainMessage(what, obj); + msg.setData(bundle); + msg.sendToTarget(); + } + + public void post(int what, Object obj) { + obtainMessage(what, obj).sendToTarget(); + } + + public void post(int what) { + post(what, null); + } + + public void post(int what, Object obj, int arg1) { + obtainMessage(what, arg1, 0, obj).sendToTarget(); + } + + @Override + public void handleMessage(Message msg) { + if (mCallback == null) { + return; + } + switch (msg.what) { + case MSG_PLAY: + mCallback.onPlay(); + break; + case MSG_PLAY_MEDIA_ID: + mCallback.onPlayFromMediaId((String) msg.obj, msg.getData()); + break; + case MSG_PLAY_SEARCH: + mCallback.onPlayFromSearch((String) msg.obj, msg.getData()); + break; + case MSG_SKIP_TO_ITEM: + mCallback.onSkipToQueueItem((Long) msg.obj); + break; + case MSG_PAUSE: + mCallback.onPause(); + break; + case MSG_STOP: + mCallback.onStop(); + break; + case MSG_NEXT: + mCallback.onSkipToNext(); + break; + case MSG_PREVIOUS: + mCallback.onSkipToPrevious(); + break; + case MSG_FAST_FORWARD: + mCallback.onFastForward(); + break; + case MSG_REWIND: + mCallback.onRewind(); + break; + case MSG_SEEK_TO: + mCallback.onSeekTo((Long) msg.obj); + break; + case MSG_RATE: + mCallback.onSetRating((RatingCompat) msg.obj); + break; + case MSG_CUSTOM_ACTION: + mCallback.onCustomAction((String) msg.obj, msg.getData()); + break; + case MSG_MEDIA_BUTTON: + KeyEvent keyEvent = (KeyEvent) msg.obj; + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.putExtra(Intent.EXTRA_KEY_EVENT, keyEvent); + mCallback.onMediaButtonEvent(intent); + break; + case MSG_COMMAND: + Command cmd = (Command) msg.obj; + mCallback.onCommand(cmd.command, cmd.extras, cmd.stub); + break; + case MSG_ADJUST_VOLUME: + adjustVolume((Integer) msg.obj, 0); + break; + case MSG_SET_VOLUME: + setVolumeTo((Integer) msg.obj, 0); + break; + } + } + } + } + + static class MediaSessionImplApi21 implements MediaSessionImpl { + private final Object mSessionObj; + private final Token mToken; + + private PendingIntent mMediaButtonIntent; + + public MediaSessionImplApi21(Context context, String tag) { + mSessionObj = MediaSessionCompatApi21.createSession(context, tag); + mToken = new Token(MediaSessionCompatApi21.getSessionToken(mSessionObj)); + } + + public MediaSessionImplApi21(Object mediaSession) { + mSessionObj = MediaSessionCompatApi21.verifySession(mediaSession); + mToken = new Token(MediaSessionCompatApi21.getSessionToken(mSessionObj)); + } + + @Override + public void setCallback(Callback callback, Handler handler) { + MediaSessionCompatApi21.setCallback(mSessionObj, callback.mCallbackObj, handler); + } + + @Override + public void setFlags(int flags) { + MediaSessionCompatApi21.setFlags(mSessionObj, flags); + } + + @Override + public void setPlaybackToLocal(int stream) { + MediaSessionCompatApi21.setPlaybackToLocal(mSessionObj, stream); + } + + @Override + public void setPlaybackToRemote(VolumeProviderCompat volumeProvider) { + MediaSessionCompatApi21.setPlaybackToRemote(mSessionObj, + volumeProvider.getVolumeProvider()); + } + + @Override + public void setActive(boolean active) { + MediaSessionCompatApi21.setActive(mSessionObj, active); + } + + @Override + public boolean isActive() { + return MediaSessionCompatApi21.isActive(mSessionObj); + } + + @Override + public void sendSessionEvent(String event, Bundle extras) { + MediaSessionCompatApi21.sendSessionEvent(mSessionObj, event, extras); + } + + @Override + public void release() { + MediaSessionCompatApi21.release(mSessionObj); + } + + @Override + public Token getSessionToken() { + return mToken; + } + + @Override + public void setPlaybackState(PlaybackStateCompat state) { + MediaSessionCompatApi21.setPlaybackState(mSessionObj, state.getPlaybackState()); + } + + @Override + public void setMetadata(MediaMetadataCompat metadata) { + MediaSessionCompatApi21.setMetadata(mSessionObj, metadata.getMediaMetadata()); + } + + @Override + public void setSessionActivity(PendingIntent pi) { + MediaSessionCompatApi21.setSessionActivity(mSessionObj, pi); + } + + @Override + public void setMediaButtonReceiver(PendingIntent mbr) { + mMediaButtonIntent = mbr; + MediaSessionCompatApi21.setMediaButtonReceiver(mSessionObj, mbr); + } + + @Override + public void setQueue(List queue) { + List queueObjs = null; + if (queue != null) { + queueObjs = new ArrayList(); + for (QueueItem item : queue) { + queueObjs.add(item.getQueueItem()); + } + } + MediaSessionCompatApi21.setQueue(mSessionObj, queueObjs); + } + + @Override + public void setQueueTitle(CharSequence title) { + MediaSessionCompatApi21.setQueueTitle(mSessionObj, title); + } + + @Override + public void setRatingType(int type) { + if (android.os.Build.VERSION.SDK_INT < 22) { + // TODO figure out 21 implementation + } else { + MediaSessionCompatApi22.setRatingType(mSessionObj, type); + } + } + + @Override + public void setExtras(Bundle extras) { + MediaSessionCompatApi21.setExtras(mSessionObj, extras); + } + + @Override + public Object getMediaSession() { + return mSessionObj; + } + + @Override + public Object getRemoteControlClient() { + return null; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi14.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi14.java new file mode 100644 index 00000000000..de644e695d1 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi14.java @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.media.session; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.RemoteControlClient; +import android.os.Bundle; +import android.os.ResultReceiver; + +public class MediaSessionCompatApi14 { + /***** RemoteControlClient States, we only need none as the others were public *******/ + final static int RCC_PLAYSTATE_NONE = 0; + + /***** MediaSession States *******/ + final static int STATE_NONE = 0; + final static int STATE_STOPPED = 1; + final static int STATE_PAUSED = 2; + final static int STATE_PLAYING = 3; + final static int STATE_FAST_FORWARDING = 4; + final static int STATE_REWINDING = 5; + final static int STATE_BUFFERING = 6; + final static int STATE_ERROR = 7; + final static int STATE_CONNECTING = 8; + final static int STATE_SKIPPING_TO_PREVIOUS = 9; + final static int STATE_SKIPPING_TO_NEXT = 10; + final static int STATE_SKIPPING_TO_QUEUE_ITEM = 11; + + /***** PlaybackState actions *****/ + private static final long ACTION_STOP = 1 << 0; + private static final long ACTION_PAUSE = 1 << 1; + private static final long ACTION_PLAY = 1 << 2; + private static final long ACTION_REWIND = 1 << 3; + private static final long ACTION_SKIP_TO_PREVIOUS = 1 << 4; + private static final long ACTION_SKIP_TO_NEXT = 1 << 5; + private static final long ACTION_FAST_FORWARD = 1 << 6; + private static final long ACTION_PLAY_PAUSE = 1 << 9; + + /***** MediaMetadata keys ********/ + private static final String METADATA_KEY_ART = "android.media.metadata.ART"; + private static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART"; + private static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE"; + private static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST"; + private static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION"; + private static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM"; + private static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR"; + private static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER"; + private static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER"; + private static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION"; + private static final String METADATA_KEY_DATE = "android.media.metadata.DATE"; + private static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE"; + private static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER"; + private static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER"; + private static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST"; + + public static Object createRemoteControlClient(PendingIntent mbIntent) { + return new RemoteControlClient(mbIntent); + } + + public static void setState(Object rccObj, int state) { + ((RemoteControlClient) rccObj).setPlaybackState(getRccStateFromState(state)); + } + + public static void setTransportControlFlags(Object rccObj, long actions) { + ((RemoteControlClient) rccObj).setTransportControlFlags( + getRccTransportControlFlagsFromActions(actions)); + } + + public static void setMetadata(Object rccObj, Bundle metadata) { + RemoteControlClient.MetadataEditor editor = ((RemoteControlClient) rccObj).editMetadata( + true); + buildOldMetadata(metadata, editor); + editor.apply(); + } + + public static void registerRemoteControlClient(Context context, Object rccObj) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + am.registerRemoteControlClient((RemoteControlClient) rccObj); + } + + public static void unregisterRemoteControlClient(Context context, Object rccObj) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + am.unregisterRemoteControlClient((RemoteControlClient) rccObj); + } + + static int getRccStateFromState(int state) { + switch (state) { + case STATE_CONNECTING: + case STATE_BUFFERING: + return RemoteControlClient.PLAYSTATE_BUFFERING; + case STATE_ERROR: + return RemoteControlClient.PLAYSTATE_ERROR; + case STATE_FAST_FORWARDING: + return RemoteControlClient.PLAYSTATE_FAST_FORWARDING; + case STATE_NONE: + return RCC_PLAYSTATE_NONE; + case STATE_PAUSED: + return RemoteControlClient.PLAYSTATE_PAUSED; + case STATE_PLAYING: + return RemoteControlClient.PLAYSTATE_PLAYING; + case STATE_REWINDING: + return RemoteControlClient.PLAYSTATE_REWINDING; + case STATE_SKIPPING_TO_PREVIOUS: + return RemoteControlClient.PLAYSTATE_SKIPPING_BACKWARDS; + case STATE_SKIPPING_TO_NEXT: + case STATE_SKIPPING_TO_QUEUE_ITEM: + return RemoteControlClient.PLAYSTATE_SKIPPING_FORWARDS; + case STATE_STOPPED: + return RemoteControlClient.PLAYSTATE_STOPPED; + default: + return -1; + } + } + + static int getRccTransportControlFlagsFromActions(long actions) { + int transportControlFlags = 0; + if ((actions & ACTION_STOP) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_STOP; + } + if ((actions & ACTION_PAUSE) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PAUSE; + } + if ((actions & ACTION_PLAY) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PLAY; + } + if ((actions & ACTION_REWIND) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_REWIND; + } + if ((actions & ACTION_SKIP_TO_PREVIOUS) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PREVIOUS; + } + if ((actions & ACTION_SKIP_TO_NEXT) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_NEXT; + } + if ((actions & ACTION_FAST_FORWARD) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_FAST_FORWARD; + } + if ((actions & ACTION_PLAY_PAUSE) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; + } + return transportControlFlags; + } + + static void buildOldMetadata(Bundle metadata, RemoteControlClient.MetadataEditor editor) { + if (metadata == null) { + return; + } + if (metadata.containsKey(METADATA_KEY_ART)) { + Bitmap art = metadata.getParcelable(METADATA_KEY_ART); + editor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, art); + } else if (metadata.containsKey(METADATA_KEY_ALBUM_ART)) { + // Fall back to album art if the track art wasn't available + Bitmap art = metadata.getParcelable(METADATA_KEY_ALBUM_ART); + editor.putBitmap(RemoteControlClient.MetadataEditor.BITMAP_KEY_ARTWORK, art); + } + if (metadata.containsKey(METADATA_KEY_ALBUM)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, + metadata.getString(METADATA_KEY_ALBUM)); + } + if (metadata.containsKey(METADATA_KEY_ALBUM_ARTIST)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, + metadata.getString(METADATA_KEY_ALBUM_ARTIST)); + } + if (metadata.containsKey(METADATA_KEY_ARTIST)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_ARTIST, + metadata.getString(METADATA_KEY_ARTIST)); + } + if (metadata.containsKey(METADATA_KEY_AUTHOR)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_AUTHOR, + metadata.getString(METADATA_KEY_AUTHOR)); + } + if (metadata.containsKey(METADATA_KEY_COMPILATION)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_COMPILATION, + metadata.getString(METADATA_KEY_COMPILATION)); + } + if (metadata.containsKey(METADATA_KEY_COMPOSER)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_COMPOSER, + metadata.getString(METADATA_KEY_COMPOSER)); + } + if (metadata.containsKey(METADATA_KEY_DATE)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_DATE, + metadata.getString(METADATA_KEY_DATE)); + } + if (metadata.containsKey(METADATA_KEY_DISC_NUMBER)) { + editor.putLong(MediaMetadataRetriever.METADATA_KEY_DISC_NUMBER, + metadata.getLong(METADATA_KEY_DISC_NUMBER)); + } + if (metadata.containsKey(METADATA_KEY_DURATION)) { + editor.putLong(MediaMetadataRetriever.METADATA_KEY_DURATION, + metadata.getLong(METADATA_KEY_DURATION)); + } + if (metadata.containsKey(METADATA_KEY_GENRE)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_GENRE, + metadata.getString(METADATA_KEY_GENRE)); + } + if (metadata.containsKey(METADATA_KEY_TITLE)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, + metadata.getString(METADATA_KEY_TITLE)); + } + if (metadata.containsKey(METADATA_KEY_TRACK_NUMBER)) { + editor.putLong(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER, + metadata.getLong(METADATA_KEY_TRACK_NUMBER)); + } + if (metadata.containsKey(METADATA_KEY_WRITER)) { + editor.putString(MediaMetadataRetriever.METADATA_KEY_WRITER, + metadata.getString(METADATA_KEY_WRITER)); + } + } + + public static interface Callback { + public void onCommand(String command, Bundle extras, ResultReceiver cb); + + public boolean onMediaButtonEvent(Intent mediaButtonIntent); + + public void onPlay(); + + public void onPause(); + + public void onSkipToNext(); + + public void onSkipToPrevious(); + + public void onFastForward(); + + public void onRewind(); + + public void onStop(); + + public void onSeekTo(long pos); + + public void onSetRating(Object ratingObj); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi18.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi18.java new file mode 100644 index 00000000000..1c5dca78ed3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi18.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.media.session; + +import android.app.PendingIntent; +import android.content.Context; +import android.media.AudioManager; +import android.media.RemoteControlClient; +import android.os.SystemClock; + +public class MediaSessionCompatApi18 { + /***** PlaybackState actions *****/ + private static final long ACTION_SEEK_TO = 1 << 8; + + public static Object createPlaybackPositionUpdateListener( + MediaSessionCompatApi14.Callback callback) { + return new OnPlaybackPositionUpdateListener(callback); + } + + public static void registerMediaButtonEventReceiver(Context context, PendingIntent pi) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + am.registerMediaButtonEventReceiver(pi); + } + + public static void unregisterMediaButtonEventReceiver(Context context, PendingIntent pi) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + am.unregisterMediaButtonEventReceiver(pi); + } + + public static void setState(Object rccObj, int state, long position, float speed, + long updateTime) { + long currTime = SystemClock.elapsedRealtime(); + if (state == MediaSessionCompatApi14.STATE_PLAYING && position > 0) { + long diff = 0; + if (updateTime > 0) { + diff = currTime - updateTime; + if (speed > 0 && speed != 1f) { + diff *= speed; + } + } + position += diff; + } + state = MediaSessionCompatApi14.getRccStateFromState(state); + ((RemoteControlClient) rccObj).setPlaybackState(state, position, speed); + } + + public static void setTransportControlFlags(Object rccObj, long actions) { + ((RemoteControlClient) rccObj).setTransportControlFlags( + getRccTransportControlFlagsFromActions(actions)); + } + + public static void setOnPlaybackPositionUpdateListener(Object rccObj, + Object onPositionUpdateObj) { + ((RemoteControlClient) rccObj).setPlaybackPositionUpdateListener( + (RemoteControlClient.OnPlaybackPositionUpdateListener) onPositionUpdateObj); + } + + static int getRccTransportControlFlagsFromActions(long actions) { + int transportControlFlags = + MediaSessionCompatApi14.getRccTransportControlFlagsFromActions(actions); + if ((actions & ACTION_SEEK_TO) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_POSITION_UPDATE; + } + return transportControlFlags; + } + + static class OnPlaybackPositionUpdateListener + implements RemoteControlClient.OnPlaybackPositionUpdateListener { + protected final T mCallback; + + public OnPlaybackPositionUpdateListener(T callback) { + mCallback = callback; + } + + @Override + public void onPlaybackPositionUpdate(long newPositionMs) { + mCallback.onSeekTo(newPositionMs); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi19.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi19.java new file mode 100644 index 00000000000..61039d37fcc --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi19.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.media.session; + +import android.graphics.Bitmap; +import android.media.MediaMetadataEditor; +import android.media.MediaMetadataRetriever; +import android.media.Rating; +import android.media.RemoteControlClient; +import android.os.Bundle; + +public class MediaSessionCompatApi19 { + /***** PlaybackState actions *****/ + private static final long ACTION_SET_RATING = 1 << 7; + + /***** MediaMetadata keys ********/ + private static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING"; + private static final String METADATA_KEY_RATING = "android.media.metadata.RATING"; + private static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR"; + + public static void setTransportControlFlags(Object rccObj, long actions) { + ((RemoteControlClient) rccObj).setTransportControlFlags( + getRccTransportControlFlagsFromActions(actions)); + } + + public static Object createMetadataUpdateListener(MediaSessionCompatApi14.Callback callback) { + return new OnMetadataUpdateListener(callback); + } + + public static void setMetadata(Object rccObj, Bundle metadata, long actions) { + RemoteControlClient.MetadataEditor editor = ((RemoteControlClient) rccObj).editMetadata( + true); + MediaSessionCompatApi14.buildOldMetadata(metadata, editor); + addNewMetadata(metadata, editor); + if ((actions & ACTION_SET_RATING) != 0) { + editor.addEditableKey(RemoteControlClient.MetadataEditor.RATING_KEY_BY_USER); + } + editor.apply(); + } + + public static void setOnMetadataUpdateListener(Object rccObj, Object onMetadataUpdateObj) { + ((RemoteControlClient) rccObj).setMetadataUpdateListener( + (RemoteControlClient.OnMetadataUpdateListener) onMetadataUpdateObj); + } + + static int getRccTransportControlFlagsFromActions(long actions) { + int transportControlFlags = + MediaSessionCompatApi18.getRccTransportControlFlagsFromActions(actions); + if ((actions & ACTION_SET_RATING) != 0) { + transportControlFlags |= RemoteControlClient.FLAG_KEY_MEDIA_RATING; + } + return transportControlFlags; + } + + static void addNewMetadata(Bundle metadata, RemoteControlClient.MetadataEditor editor) { + if (metadata == null) { + return; + } + if (metadata.containsKey(METADATA_KEY_YEAR)) { + editor.putLong(MediaMetadataRetriever.METADATA_KEY_YEAR, + metadata.getLong(METADATA_KEY_YEAR)); + } + if (metadata.containsKey(METADATA_KEY_RATING)) { + editor.putObject(MediaMetadataEditor.RATING_KEY_BY_OTHERS, + metadata.getParcelable(METADATA_KEY_RATING)); + } + if (metadata.containsKey(METADATA_KEY_USER_RATING)) { + editor.putObject(MediaMetadataEditor.RATING_KEY_BY_USER, + metadata.getParcelable(METADATA_KEY_USER_RATING)); + } + } + + static class OnMetadataUpdateListener implements + RemoteControlClient.OnMetadataUpdateListener { + protected final T mCallback; + + public OnMetadataUpdateListener(T callback) { + mCallback = callback; + } + + @Override + public void onMetadataUpdate(int key, Object newValue) { + if (key == MediaMetadataEditor.RATING_KEY_BY_USER && newValue instanceof Rating) { + mCallback.onSetRating(newValue); + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi21.java new file mode 100644 index 00000000000..809439137a2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi21.java @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media.session; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.media.AudioAttributes; +import android.media.MediaDescription; +import android.media.MediaMetadata; +import android.media.Rating; +import android.media.VolumeProvider; +import android.media.session.MediaSession; +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.os.ResultReceiver; + +import java.util.ArrayList; +import java.util.List; + +class MediaSessionCompatApi21 { + public static Object createSession(Context context, String tag) { + return new MediaSession(context, tag); + } + + public static Object verifySession(Object mediaSession) { + if (mediaSession instanceof MediaSession) { + return mediaSession; + } + throw new IllegalArgumentException("mediaSession is not a valid MediaSession object"); + } + + public static Object verifyToken(Object token) { + if (token instanceof MediaSession.Token) { + return token; + } + throw new IllegalArgumentException("token is not a valid MediaSession.Token object"); + } + + public static Object createCallback(Callback callback) { + return new CallbackProxy(callback); + } + + public static void setCallback(Object sessionObj, Object callbackObj, Handler handler) { + ((MediaSession) sessionObj).setCallback((MediaSession.Callback) callbackObj, handler); + } + + public static void setFlags(Object sessionObj, int flags) { + ((MediaSession)sessionObj).setFlags(flags); + } + + public static void setPlaybackToLocal(Object sessionObj, int stream) { + // TODO update APIs to use support version of AudioAttributes + AudioAttributes.Builder bob = new AudioAttributes.Builder(); + bob.setLegacyStreamType(stream); + ((MediaSession) sessionObj).setPlaybackToLocal(bob.build()); + } + + public static void setPlaybackToRemote(Object sessionObj, Object volumeProviderObj) { + ((MediaSession)sessionObj).setPlaybackToRemote((VolumeProvider)volumeProviderObj); + } + + public static void setActive(Object sessionObj, boolean active) { + ((MediaSession)sessionObj).setActive(active); + } + + public static boolean isActive(Object sessionObj) { + return ((MediaSession)sessionObj).isActive(); + } + + public static void sendSessionEvent(Object sessionObj, String event, Bundle extras) { + ((MediaSession)sessionObj).sendSessionEvent(event, extras); + } + + public static void release(Object sessionObj) { + ((MediaSession)sessionObj).release(); + } + + public static Parcelable getSessionToken(Object sessionObj) { + return ((MediaSession)sessionObj).getSessionToken(); + } + + public static void setPlaybackState(Object sessionObj, Object stateObj) { + ((MediaSession)sessionObj).setPlaybackState((PlaybackState)stateObj); + } + + public static void setMetadata(Object sessionObj, Object metadataObj) { + ((MediaSession)sessionObj).setMetadata((MediaMetadata)metadataObj); + } + + public static void setSessionActivity(Object sessionObj, PendingIntent pi) { + ((MediaSession) sessionObj).setSessionActivity(pi); + } + + public static void setMediaButtonReceiver(Object sessionObj, PendingIntent pi) { + ((MediaSession) sessionObj).setMediaButtonReceiver(pi); + } + + public static void setQueue(Object sessionObj, List queueObjs) { + if (queueObjs == null) { + ((MediaSession) sessionObj).setQueue(null); + return; + } + ArrayList queue = new ArrayList(); + for (Object itemObj : queueObjs) { + queue.add((MediaSession.QueueItem) itemObj); + } + ((MediaSession) sessionObj).setQueue(queue); + } + + public static void setQueueTitle(Object sessionObj, CharSequence title) { + ((MediaSession) sessionObj).setQueueTitle(title); + } + + public static void setExtras(Object sessionObj, Bundle extras) { + ((MediaSession) sessionObj).setExtras(extras); + } + + public static interface Callback { + public void onCommand(String command, Bundle extras, ResultReceiver cb); + public boolean onMediaButtonEvent(Intent mediaButtonIntent); + public void onPlay(); + public void onPlayFromMediaId(String mediaId, Bundle extras); + public void onPlayFromSearch(String search, Bundle extras); + public void onSkipToQueueItem(long id); + public void onPause(); + public void onSkipToNext(); + public void onSkipToPrevious(); + public void onFastForward(); + public void onRewind(); + public void onStop(); + public void onSeekTo(long pos); + public void onSetRating(Object ratingObj); + public void onCustomAction(String action, Bundle extras); + } + + static class CallbackProxy extends MediaSession.Callback { + protected final T mCallback; + + public CallbackProxy(T callback) { + mCallback = callback; + } + + @Override + public void onCommand(String command, Bundle args, ResultReceiver cb) { + mCallback.onCommand(command, args, cb); + } + + @Override + public boolean onMediaButtonEvent(Intent mediaButtonIntent) { + return mCallback.onMediaButtonEvent(mediaButtonIntent) + || super.onMediaButtonEvent(mediaButtonIntent); + } + + @Override + public void onPlay() { + mCallback.onPlay(); + } + + @Override + public void onPlayFromMediaId(String mediaId, Bundle extras) { + mCallback.onPlayFromMediaId(mediaId, extras); + } + + @Override + public void onPlayFromSearch(String search, Bundle extras) { + mCallback.onPlayFromSearch(search, extras); + } + + @Override + public void onSkipToQueueItem(long id) { + mCallback.onSkipToQueueItem(id); + } + + @Override + public void onPause() { + mCallback.onPause(); + } + + @Override + public void onSkipToNext() { + mCallback.onSkipToNext(); + } + + @Override + public void onSkipToPrevious() { + mCallback.onSkipToPrevious(); + } + + @Override + public void onFastForward() { + mCallback.onFastForward(); + } + + @Override + public void onRewind() { + mCallback.onRewind(); + } + + @Override + public void onStop() { + mCallback.onStop(); + } + + @Override + public void onSeekTo(long pos) { + mCallback.onSeekTo(pos); + } + + @Override + public void onSetRating(Rating rating) { + mCallback.onSetRating(rating); + } + + @Override + public void onCustomAction(String action, Bundle extras) { + mCallback.onCustomAction(action, extras); + } + } + + static class QueueItem { + + public static Object createItem(Object mediaDescription, long id) { + return new MediaSession.QueueItem((MediaDescription) mediaDescription, id); + } + + public static Object getDescription(Object queueItem) { + return ((MediaSession.QueueItem) queueItem).getDescription(); + } + + public static long getQueueId(Object queueItem) { + return ((MediaSession.QueueItem) queueItem).getQueueId(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi22.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi22.java new file mode 100644 index 00000000000..b847778dfff --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi22.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.media.session; + +import android.media.session.MediaSession; + +class MediaSessionCompatApi22 { + + public static void setRatingType(Object sessionObj, int type) { + ((MediaSession) sessionObj).setRatingType(type); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi8.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi8.java new file mode 100644 index 00000000000..f49eb2b7cb2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/MediaSessionCompatApi8.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.media.session; + +import android.content.ComponentName; +import android.content.Context; +import android.media.AudioManager; + +public class MediaSessionCompatApi8 { + public static void registerMediaButtonEventReceiver(Context context, ComponentName mbr) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + am.registerMediaButtonEventReceiver(mbr); + } + + public static void unregisterMediaButtonEventReceiver(Context context, ComponentName mbr) { + AudioManager am = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + am.unregisterMediaButtonEventReceiver(mbr); + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.aidl new file mode 100644 index 00000000000..2e77c4fbde0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.support.v4.media.session; + +parcelable ParcelableVolumeInfo; diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.java new file mode 100644 index 00000000000..678b33eeb66 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/ParcelableVolumeInfo.java @@ -0,0 +1,77 @@ +/* Copyright 2014, The Android Open Source Project + ** + ** Licensed under the Apache License, Version 2.0 (the "License"); + ** you may not use this file except in compliance with the License. + ** You may obtain a copy of the License at + ** + ** http://www.apache.org/licenses/LICENSE-2.0 + ** + ** Unless required by applicable law or agreed to in writing, software + ** distributed under the License is distributed on an "AS IS" BASIS, + ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ** See the License for the specific language governing permissions and + ** limitations under the License. + */ + +package android.support.v4.media.session; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Convenience class for passing information about the audio configuration of a + * {@link MediaSessionCompat}. + */ +public class ParcelableVolumeInfo implements Parcelable { + public int volumeType; + public int audioStream; + public int controlType; + public int maxVolume; + public int currentVolume; + + public ParcelableVolumeInfo(int volumeType, int audioStream, int controlType, + int maxVolume, + int currentVolume) { + this.volumeType = volumeType; + this.audioStream = audioStream; + this.controlType = controlType; + this.maxVolume = maxVolume; + this.currentVolume = currentVolume; + } + + public ParcelableVolumeInfo(Parcel from) { + volumeType = from.readInt(); + controlType = from.readInt(); + maxVolume = from.readInt(); + currentVolume = from.readInt(); + audioStream = from.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(volumeType); + dest.writeInt(controlType); + dest.writeInt(maxVolume); + dest.writeInt(currentVolume); + dest.writeInt(audioStream); + } + + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + @Override + public ParcelableVolumeInfo createFromParcel(Parcel in) { + return new ParcelableVolumeInfo(in); + } + + @Override + public ParcelableVolumeInfo[] newArray(int size) { + return new ParcelableVolumeInfo[size]; + } + }; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.aidl b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.aidl new file mode 100644 index 00000000000..3d4ef5956a5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.aidl @@ -0,0 +1,18 @@ +/* Copyright 2014, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +package android.support.v4.media.session; + +parcelable PlaybackStateCompat; diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.java new file mode 100644 index 00000000000..09be98cdcd2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompat.java @@ -0,0 +1,970 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.media.session; + + +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.Nullable; +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Playback state for a {@link MediaSessionCompat}. This includes a state like + * {@link PlaybackStateCompat#STATE_PLAYING}, the current playback position, + * and the current control capabilities. + */ +public final class PlaybackStateCompat implements Parcelable { + + /** + * Indicates this session supports the stop command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_STOP = 1 << 0; + + /** + * Indicates this session supports the pause command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PAUSE = 1 << 1; + + /** + * Indicates this session supports the play command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY = 1 << 2; + + /** + * Indicates this session supports the rewind command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_REWIND = 1 << 3; + + /** + * Indicates this session supports the previous command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SKIP_TO_PREVIOUS = 1 << 4; + + /** + * Indicates this session supports the next command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SKIP_TO_NEXT = 1 << 5; + + /** + * Indicates this session supports the fast forward command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_FAST_FORWARD = 1 << 6; + + /** + * Indicates this session supports the set rating command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SET_RATING = 1 << 7; + + /** + * Indicates this session supports the seek to command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SEEK_TO = 1 << 8; + + /** + * Indicates this session supports the play/pause toggle command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY_PAUSE = 1 << 9; + + /** + * Indicates this session supports the play from media id command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY_FROM_MEDIA_ID = 1 << 10; + + /** + * Indicates this session supports the play from search command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_PLAY_FROM_SEARCH = 1 << 11; + + /** + * Indicates this session supports the skip to queue item command. + * + * @see Builder#setActions(long) + */ + public static final long ACTION_SKIP_TO_QUEUE_ITEM = 1 << 12; + + /** + * This is the default playback state and indicates that no media has been + * added yet, or the performer has been reset and has no content to play. + * + * @see Builder#setState + */ + public final static int STATE_NONE = 0; + + /** + * State indicating this item is currently stopped. + * + * @see Builder#setState + */ + public final static int STATE_STOPPED = 1; + + /** + * State indicating this item is currently paused. + * + * @see Builder#setState + */ + public final static int STATE_PAUSED = 2; + + /** + * State indicating this item is currently playing. + * + * @see Builder#setState + */ + public final static int STATE_PLAYING = 3; + + /** + * State indicating this item is currently fast forwarding. + * + * @see Builder#setState + */ + public final static int STATE_FAST_FORWARDING = 4; + + /** + * State indicating this item is currently rewinding. + * + * @see Builder#setState + */ + public final static int STATE_REWINDING = 5; + + /** + * State indicating this item is currently buffering and will begin playing + * when enough data has buffered. + * + * @see Builder#setState + */ + public final static int STATE_BUFFERING = 6; + + /** + * State indicating this item is currently in an error state. The error + * message should also be set when entering this state. + * + * @see Builder#setState + */ + public final static int STATE_ERROR = 7; + + /** + * State indicating the class doing playback is currently connecting to a + * route. Depending on the implementation you may return to the previous + * state when the connection finishes or enter {@link #STATE_NONE}. If + * the connection failed {@link #STATE_ERROR} should be used. + *

+ * On devices earlier than API 21, this will appear as {@link #STATE_BUFFERING} + *

+ * + * @see Builder#setState + */ + public final static int STATE_CONNECTING = 8; + + /** + * State indicating the player is currently skipping to the previous item. + * + * @see Builder#setState + */ + public final static int STATE_SKIPPING_TO_PREVIOUS = 9; + + /** + * State indicating the player is currently skipping to the next item. + * + * @see Builder#setState + */ + public final static int STATE_SKIPPING_TO_NEXT = 10; + + /** + * State indicating the player is currently skipping to a specific item in + * the queue. + *

+ * On devices earlier than API 21, this will appear as {@link #STATE_SKIPPING_TO_NEXT} + *

+ * + * @see Builder#setState + */ + public final static int STATE_SKIPPING_TO_QUEUE_ITEM = 11; + + /** + * Use this value for the position to indicate the position is not known. + */ + public final static long PLAYBACK_POSITION_UNKNOWN = -1; + + private final int mState; + private final long mPosition; + private final long mBufferedPosition; + private final float mSpeed; + private final long mActions; + private final CharSequence mErrorMessage; + private final long mUpdateTime; + private List mCustomActions; + private final long mActiveItemId; + private final Bundle mExtras; + + private Object mStateObj; + + private PlaybackStateCompat(int state, long position, long bufferedPosition, + float rate, long actions, CharSequence errorMessage, long updateTime, + List customActions, + long activeItemId, Bundle extras) { + mState = state; + mPosition = position; + mBufferedPosition = bufferedPosition; + mSpeed = rate; + mActions = actions; + mErrorMessage = errorMessage; + mUpdateTime = updateTime; + mCustomActions = new ArrayList<>(customActions); + mActiveItemId = activeItemId; + mExtras = extras; + } + + private PlaybackStateCompat(Parcel in) { + mState = in.readInt(); + mPosition = in.readLong(); + mSpeed = in.readFloat(); + mUpdateTime = in.readLong(); + mBufferedPosition = in.readLong(); + mActions = in.readLong(); + mErrorMessage = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mCustomActions = in.createTypedArrayList(CustomAction.CREATOR); + mActiveItemId = in.readLong(); + mExtras = in.readBundle(); + } + + @Override + public String toString() { + StringBuilder bob = new StringBuilder("PlaybackState {"); + bob.append("state=").append(mState); + bob.append(", position=").append(mPosition); + bob.append(", buffered position=").append(mBufferedPosition); + bob.append(", speed=").append(mSpeed); + bob.append(", updated=").append(mUpdateTime); + bob.append(", actions=").append(mActions); + bob.append(", error=").append(mErrorMessage); + bob.append(", custom actions=").append(mCustomActions); + bob.append(", active item id=").append(mActiveItemId); + bob.append("}"); + return bob.toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mState); + dest.writeLong(mPosition); + dest.writeFloat(mSpeed); + dest.writeLong(mUpdateTime); + dest.writeLong(mBufferedPosition); + dest.writeLong(mActions); + TextUtils.writeToParcel(mErrorMessage, dest, flags); + dest.writeTypedList(mCustomActions); + dest.writeLong(mActiveItemId); + dest.writeBundle(mExtras); + } + + /** + * Get the current state of playback. One of the following: + *
    + *
  • {@link PlaybackStateCompat#STATE_NONE}
  • + *
  • {@link PlaybackStateCompat#STATE_STOPPED}
  • + *
  • {@link PlaybackStateCompat#STATE_PLAYING}
  • + *
  • {@link PlaybackStateCompat#STATE_PAUSED}
  • + *
  • {@link PlaybackStateCompat#STATE_FAST_FORWARDING}
  • + *
  • {@link PlaybackStateCompat#STATE_REWINDING}
  • + *
  • {@link PlaybackStateCompat#STATE_BUFFERING}
  • + *
  • {@link PlaybackStateCompat#STATE_ERROR}
  • + *
  • {@link PlaybackStateCompat#STATE_CONNECTING}
  • + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_PREVIOUS}
  • + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_NEXT}
  • + *
  • {@link PlaybackStateCompat#STATE_SKIPPING_TO_QUEUE_ITEM}
  • + */ + public int getState() { + return mState; + } + + /** + * Get the current playback position in ms. + */ + public long getPosition() { + return mPosition; + } + + /** + * Get the current buffered position in ms. This is the farthest playback + * point that can be reached from the current position using only buffered + * content. + */ + public long getBufferedPosition() { + return mBufferedPosition; + } + + /** + * Get the current playback speed as a multiple of normal playback. This + * should be negative when rewinding. A value of 1 means normal playback and + * 0 means paused. + * + * @return The current speed of playback. + */ + public float getPlaybackSpeed() { + return mSpeed; + } + + /** + * Get the current actions available on this session. This should use a + * bitmask of the available actions. + *
      + *
    • {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}
    • + *
    • {@link PlaybackStateCompat#ACTION_REWIND}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}
    • + *
    • {@link PlaybackStateCompat#ACTION_PAUSE}
    • + *
    • {@link PlaybackStateCompat#ACTION_STOP}
    • + *
    • {@link PlaybackStateCompat#ACTION_FAST_FORWARD}
    • + *
    • {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}
    • + *
    • {@link PlaybackStateCompat#ACTION_SEEK_TO}
    • + *
    • {@link PlaybackStateCompat#ACTION_SET_RATING}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH}
    • + *
    • {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}
    • + *
    + */ + public long getActions() { + return mActions; + } + + /** + * Get the list of custom actions. + */ + public List getCustomActions() { + return mCustomActions; + } + + /** + * Get a user readable error message. This should be set when the state is + * {@link PlaybackStateCompat#STATE_ERROR}. + */ + public CharSequence getErrorMessage() { + return mErrorMessage; + } + + /** + * Get the elapsed real time at which position was last updated. If the + * position has never been set this will return 0; + * + * @return The last time the position was updated. + */ + public long getLastPositionUpdateTime() { + return mUpdateTime; + } + + /** + * Get the id of the currently active item in the queue. If there is no + * queue or a queue is not supported by the session this will be + * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID}. + * + * @return The id of the currently active item in the queue or + * {@link MediaSessionCompat.QueueItem#UNKNOWN_ID}. + */ + public long getActiveQueueItemId() { + return mActiveItemId; + } + + /** + * Get any custom extras that were set on this playback state. + * + * @return The extras for this state or null. + */ + public @Nullable Bundle getExtras() { + return mExtras; + } + + /** + * Creates an instance from a framework {@link android.media.session.PlaybackState} object. + *

    + * This method is only supported on API 21+. + *

    + * + * @param stateObj A {@link android.media.session.PlaybackState} object, or null if none. + * @return An equivalent {@link PlaybackStateCompat} object, or null if none. + */ + public static PlaybackStateCompat fromPlaybackState(Object stateObj) { + if (stateObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + List customActionObjs = PlaybackStateCompatApi21.getCustomActions(stateObj); + List customActions = null; + if (customActionObjs != null) { + customActions = new ArrayList<>(customActionObjs.size()); + for (Object customActionObj : customActionObjs) { + customActions.add(CustomAction.fromCustomAction(customActionObj)); + } + } + Bundle extras = Build.VERSION.SDK_INT >= 22 + ? PlaybackStateCompatApi22.getExtras(stateObj) + : null; + PlaybackStateCompat state = new PlaybackStateCompat( + PlaybackStateCompatApi21.getState(stateObj), + PlaybackStateCompatApi21.getPosition(stateObj), + PlaybackStateCompatApi21.getBufferedPosition(stateObj), + PlaybackStateCompatApi21.getPlaybackSpeed(stateObj), + PlaybackStateCompatApi21.getActions(stateObj), + PlaybackStateCompatApi21.getErrorMessage(stateObj), + PlaybackStateCompatApi21.getLastPositionUpdateTime(stateObj), + customActions, + PlaybackStateCompatApi21.getActiveQueueItemId(stateObj), + extras); + state.mStateObj = stateObj; + return state; + } + + /** + * Gets the underlying framework {@link android.media.session.PlaybackState} object. + *

    + * This method is only supported on API 21+. + *

    + * + * @return An equivalent {@link android.media.session.PlaybackState} object, or null if none. + */ + public Object getPlaybackState() { + if (mStateObj != null || Build.VERSION.SDK_INT < 21) { + return mStateObj; + } + + List customActions = null; + if (mCustomActions != null) { + customActions = new ArrayList<>(mCustomActions.size()); + for (PlaybackStateCompat.CustomAction customAction : mCustomActions) { + customActions.add(customAction.getCustomAction()); + } + } + if (Build.VERSION.SDK_INT >= 22) { + mStateObj = PlaybackStateCompatApi22.newInstance(mState, mPosition, mBufferedPosition, + mSpeed, mActions, mErrorMessage, mUpdateTime, + customActions, mActiveItemId, mExtras); + } else { + mStateObj = PlaybackStateCompatApi21.newInstance(mState, mPosition, mBufferedPosition, + mSpeed, mActions, mErrorMessage, mUpdateTime, + customActions, mActiveItemId); + } + return mStateObj; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public PlaybackStateCompat createFromParcel(Parcel in) { + return new PlaybackStateCompat(in); + } + + @Override + public PlaybackStateCompat[] newArray(int size) { + return new PlaybackStateCompat[size]; + } + }; + + /** + * {@link PlaybackStateCompat.CustomAction CustomActions} can be used to + * extend the capabilities of the standard transport controls by exposing + * app specific actions to {@link MediaControllerCompat Controllers}. + */ + public static final class CustomAction implements Parcelable { + private final String mAction; + private final CharSequence mName; + private final int mIcon; + private final Bundle mExtras; + + private Object mCustomActionObj; + + /** + * Use {@link PlaybackStateCompat.CustomAction.Builder#build()}. + */ + private CustomAction(String action, CharSequence name, int icon, Bundle extras) { + mAction = action; + mName = name; + mIcon = icon; + mExtras = extras; + } + + private CustomAction(Parcel in) { + mAction = in.readString(); + mName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + mIcon = in.readInt(); + mExtras = in.readBundle(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mAction); + TextUtils.writeToParcel(mName, dest, flags); + dest.writeInt(mIcon); + dest.writeBundle(mExtras); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Creates an instance from a framework + * {@link android.media.session.PlaybackState.CustomAction} object. + *

    + * This method is only supported on API 21+. + *

    + * + * @param customActionObj A {@link android.media.session.PlaybackState.CustomAction} object, + * or null if none. + * @return An equivalent {@link PlaybackStateCompat.CustomAction} object, or null if none. + */ + public static PlaybackStateCompat.CustomAction fromCustomAction(Object customActionObj) { + if (customActionObj == null || Build.VERSION.SDK_INT < 21) { + return null; + } + + PlaybackStateCompat.CustomAction customAction = new PlaybackStateCompat.CustomAction( + PlaybackStateCompatApi21.CustomAction.getAction(customActionObj), + PlaybackStateCompatApi21.CustomAction.getName(customActionObj), + PlaybackStateCompatApi21.CustomAction.getIcon(customActionObj), + PlaybackStateCompatApi21.CustomAction.getExtras(customActionObj)); + customAction.mCustomActionObj = customActionObj; + return customAction; + } + + /** + * Gets the underlying framework {@link android.media.session.PlaybackState.CustomAction} + * object. + *

    + * This method is only supported on API 21+. + *

    + * + * @return An equivalent {@link android.media.session.PlaybackState.CustomAction} object, + * or null if none. + */ + public Object getCustomAction() { + if (mCustomActionObj != null || Build.VERSION.SDK_INT < 21) { + return mCustomActionObj; + } + + mCustomActionObj = PlaybackStateCompatApi21.CustomAction.newInstance(mAction, + mName, mIcon, mExtras); + return mCustomActionObj; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + + @Override + public PlaybackStateCompat.CustomAction createFromParcel(Parcel p) { + return new PlaybackStateCompat.CustomAction(p); + } + + @Override + public PlaybackStateCompat.CustomAction[] newArray(int size) { + return new PlaybackStateCompat.CustomAction[size]; + } + }; + + /** + * Returns the action of the {@link CustomAction}. + * + * @return The action of the {@link CustomAction}. + */ + public String getAction() { + return mAction; + } + + /** + * Returns the display name of this action. e.g. "Favorite" + * + * @return The display name of this {@link CustomAction}. + */ + public CharSequence getName() { + return mName; + } + + /** + * Returns the resource id of the icon in the {@link MediaSessionCompat + * Session's} package. + * + * @return The resource id of the icon in the {@link MediaSessionCompat + * Session's} package. + */ + public int getIcon() { + return mIcon; + } + + /** + * Returns extras which provide additional application-specific + * information about the action, or null if none. These arguments are + * meant to be consumed by a {@link MediaControllerCompat} if it knows + * how to handle them. + * + * @return Optional arguments for the {@link CustomAction}. + */ + public Bundle getExtras() { + return mExtras; + } + + @Override + public String toString() { + return "Action:" + + "mName='" + mName + + ", mIcon=" + mIcon + + ", mExtras=" + mExtras; + } + + /** + * Builder for {@link CustomAction} objects. + */ + public static final class Builder { + private final String mAction; + private final CharSequence mName; + private final int mIcon; + private Bundle mExtras; + + /** + * Creates a {@link CustomAction} builder with the id, name, and + * icon set. + * + * @param action The action of the {@link CustomAction}. + * @param name The display name of the {@link CustomAction}. This + * name will be displayed along side the action if the UI + * supports it. + * @param icon The icon resource id of the {@link CustomAction}. + * This resource id must be in the same package as the + * {@link MediaSessionCompat}. It will be displayed with + * the custom action if the UI supports it. + */ + public Builder(String action, CharSequence name, int icon) { + if (TextUtils.isEmpty(action)) { + throw new IllegalArgumentException( + "You must specify an action to build a CustomAction."); + } + if (TextUtils.isEmpty(name)) { + throw new IllegalArgumentException( + "You must specify a name to build a CustomAction."); + } + if (icon == 0) { + throw new IllegalArgumentException( + "You must specify an icon resource id to build a CustomAction."); + } + mAction = action; + mName = name; + mIcon = icon; + } + + /** + * Set optional extras for the {@link CustomAction}. These extras + * are meant to be consumed by a {@link MediaControllerCompat} if it + * knows how to handle them. Keys should be fully qualified (e.g. + * "com.example.MY_ARG") to avoid collisions. + * + * @param extras Optional extras for the {@link CustomAction}. + * @return this. + */ + public Builder setExtras(Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Build and return the {@link CustomAction} instance with the + * specified values. + * + * @return A new {@link CustomAction} instance. + */ + public CustomAction build() { + return new CustomAction(mAction, mName, mIcon, mExtras); + } + } + } + + /** + * Builder for {@link PlaybackStateCompat} objects. + */ + public static final class Builder { + private final List mCustomActions = new ArrayList<>(); + + private int mState; + private long mPosition; + private long mBufferedPosition; + private float mRate; + private long mActions; + private CharSequence mErrorMessage; + private long mUpdateTime; + private long mActiveItemId = MediaSessionCompat.QueueItem.UNKNOWN_ID; + private Bundle mExtras; + + /** + * Create an empty Builder. + */ + public Builder() { + } + + /** + * Create a Builder using a {@link PlaybackStateCompat} instance to set the + * initial values. + * + * @param source The playback state to copy. + */ + public Builder(PlaybackStateCompat source) { + mState = source.mState; + mPosition = source.mPosition; + mRate = source.mSpeed; + mUpdateTime = source.mUpdateTime; + mBufferedPosition = source.mBufferedPosition; + mActions = source.mActions; + mErrorMessage = source.mErrorMessage; + if (source.mCustomActions != null) { + mCustomActions.addAll(source.mCustomActions); + } + mActiveItemId = source.mActiveItemId; + mExtras = source.mExtras; + } + + /** + * Set the current state of playback. + *

    + * The position must be in ms and indicates the current playback + * position within the track. If the position is unknown use + * {@link #PLAYBACK_POSITION_UNKNOWN}. + *

    + * The rate is a multiple of normal playback and should be 0 when paused + * and negative when rewinding. Normal playback rate is 1.0. + *

    + * The state must be one of the following: + *

      + *
    • {@link PlaybackStateCompat#STATE_NONE}
    • + *
    • {@link PlaybackStateCompat#STATE_STOPPED}
    • + *
    • {@link PlaybackStateCompat#STATE_PLAYING}
    • + *
    • {@link PlaybackStateCompat#STATE_PAUSED}
    • + *
    • {@link PlaybackStateCompat#STATE_FAST_FORWARDING}
    • + *
    • {@link PlaybackStateCompat#STATE_REWINDING}
    • + *
    • {@link PlaybackStateCompat#STATE_BUFFERING}
    • + *
    • {@link PlaybackStateCompat#STATE_ERROR}
    • + *
    • {@link PlaybackStateCompat#STATE_CONNECTING}
    • + *
    • {@link PlaybackStateCompat#STATE_SKIPPING_TO_PREVIOUS}
    • + *
    • {@link PlaybackStateCompat#STATE_SKIPPING_TO_NEXT}
    • + *
    • {@link PlaybackStateCompat#STATE_SKIPPING_TO_QUEUE_ITEM}
    • + *
    + * + * @param state The current state of playback. + * @param position The position in the current track in ms. + * @param playbackSpeed The current rate of playback as a multiple of + * normal playback. + */ + public Builder setState(int state, long position, float playbackSpeed) { + return setState(state, position, playbackSpeed, SystemClock.elapsedRealtime()); + } + + /** + * Set the current state of playback. + *

    + * The position must be in ms and indicates the current playback + * position within the track. If the position is unknown use + * {@link #PLAYBACK_POSITION_UNKNOWN}. + *

    + * The rate is a multiple of normal playback and should be 0 when paused + * and negative when rewinding. Normal playback rate is 1.0. + *

    + * The state must be one of the following: + *

      + *
    • {@link PlaybackStateCompat#STATE_NONE}
    • + *
    • {@link PlaybackStateCompat#STATE_STOPPED}
    • + *
    • {@link PlaybackStateCompat#STATE_PLAYING}
    • + *
    • {@link PlaybackStateCompat#STATE_PAUSED}
    • + *
    • {@link PlaybackStateCompat#STATE_FAST_FORWARDING}
    • + *
    • {@link PlaybackStateCompat#STATE_REWINDING}
    • + *
    • {@link PlaybackStateCompat#STATE_BUFFERING}
    • + *
    • {@link PlaybackStateCompat#STATE_ERROR}
    • + *
    • {@link PlaybackStateCompat#STATE_CONNECTING}
    • + *
    • {@link PlaybackStateCompat#STATE_SKIPPING_TO_PREVIOUS}
    • + *
    • {@link PlaybackStateCompat#STATE_SKIPPING_TO_NEXT}
    • + *
    • {@link PlaybackStateCompat#STATE_SKIPPING_TO_QUEUE_ITEM}
    • + *
    + * + * @param state The current state of playback. + * @param position The position in the current item in ms. + * @param playbackSpeed The current speed of playback as a multiple of + * normal playback. + * @param updateTime The time in the {@link SystemClock#elapsedRealtime} + * timebase that the position was updated at. + * @return this + */ + public Builder setState(int state, long position, float playbackSpeed, long updateTime) { + mState = state; + mPosition = position; + mUpdateTime = updateTime; + mRate = playbackSpeed; + return this; + } + + /** + * Set the current buffered position in ms. This is the farthest + * playback point that can be reached from the current position using + * only buffered content. + * + * @return this + */ + public Builder setBufferedPosition(long bufferPosition) { + mBufferedPosition = bufferPosition; + return this; + } + + /** + * Set the current capabilities available on this session. This should + * use a bitmask of the available capabilities. + *
      + *
    • {@link PlaybackStateCompat#ACTION_SKIP_TO_PREVIOUS}
    • + *
    • {@link PlaybackStateCompat#ACTION_REWIND}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY_PAUSE}
    • + *
    • {@link PlaybackStateCompat#ACTION_PAUSE}
    • + *
    • {@link PlaybackStateCompat#ACTION_STOP}
    • + *
    • {@link PlaybackStateCompat#ACTION_FAST_FORWARD}
    • + *
    • {@link PlaybackStateCompat#ACTION_SKIP_TO_NEXT}
    • + *
    • {@link PlaybackStateCompat#ACTION_SEEK_TO}
    • + *
    • {@link PlaybackStateCompat#ACTION_SET_RATING}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY_FROM_MEDIA_ID}
    • + *
    • {@link PlaybackStateCompat#ACTION_PLAY_FROM_SEARCH}
    • + *
    • {@link PlaybackStateCompat#ACTION_SKIP_TO_QUEUE_ITEM}
    • + *
    + * + * @return this + */ + public Builder setActions(long capabilities) { + mActions = capabilities; + return this; + } + + /** + * Add a custom action to the playback state. Actions can be used to + * expose additional functionality to {@link MediaControllerCompat + * Controllers} beyond what is offered by the standard transport + * controls. + *

    + * e.g. start a radio station based on the current item or skip ahead by + * 30 seconds. + * + * @param action An identifier for this action. It can be sent back to + * the {@link MediaSessionCompat} through + * {@link MediaControllerCompat.TransportControls#sendCustomAction(String, Bundle)}. + * @param name The display name for the action. If text is shown with + * the action or used for accessibility, this is what should + * be used. + * @param icon The resource action of the icon that should be displayed + * for the action. The resource should be in the package of + * the {@link MediaSessionCompat}. + * @return this + */ + public Builder addCustomAction(String action, String name, int icon) { + return addCustomAction(new PlaybackStateCompat.CustomAction(action, name, icon, null)); + } + + /** + * Add a custom action to the playback state. Actions can be used to expose additional + * functionality to {@link MediaControllerCompat Controllers} beyond what is offered + * by the standard transport controls. + *

    + * An example of an action would be to start a radio station based on the current item + * or to skip ahead by 30 seconds. + * + * @param customAction The custom action to add to the {@link PlaybackStateCompat}. + * @return this + */ + public Builder addCustomAction(PlaybackStateCompat.CustomAction customAction) { + if (customAction == null) { + throw new IllegalArgumentException( + "You may not add a null CustomAction to PlaybackStateCompat."); + } + mCustomActions.add(customAction); + return this; + } + + /** + * Set the active item in the play queue by specifying its id. The + * default value is {@link MediaSessionCompat.QueueItem#UNKNOWN_ID} + * + * @param id The id of the active item. + * @return this + */ + public Builder setActiveQueueItemId(long id) { + mActiveItemId = id; + return this; + } + + /** + * Set a user readable error message. This should be set when the state + * is {@link PlaybackStateCompat#STATE_ERROR}. + * + * @return this + */ + public Builder setErrorMessage(CharSequence errorMessage) { + mErrorMessage = errorMessage; + return this; + } + + /** + * Set any custom extras to be included with the playback state. + * + * @param extras The extras to include. + * @return this + */ + public Builder setExtras(Bundle extras) { + mExtras = extras; + return this; + } + + /** + * Creates the playback state object. + */ + public PlaybackStateCompat build() { + return new PlaybackStateCompat(mState, mPosition, mBufferedPosition, + mRate, mActions, mErrorMessage, mUpdateTime, + mCustomActions, mActiveItemId, mExtras); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi21.java new file mode 100644 index 00000000000..09d3e32407b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi21.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media.session; + +import android.media.session.PlaybackState; +import android.os.Bundle; +import android.os.SystemClock; + +import java.util.ArrayList; +import java.util.List; + +class PlaybackStateCompatApi21 { + public static int getState(Object stateObj) { + return ((PlaybackState)stateObj).getState(); + } + + public static long getPosition(Object stateObj) { + return ((PlaybackState)stateObj).getPosition(); + } + + public static long getBufferedPosition(Object stateObj) { + return ((PlaybackState)stateObj).getBufferedPosition(); + } + + public static float getPlaybackSpeed(Object stateObj) { + return ((PlaybackState)stateObj).getPlaybackSpeed(); + } + + public static long getActions(Object stateObj) { + return ((PlaybackState)stateObj).getActions(); + } + + public static CharSequence getErrorMessage(Object stateObj) { + return ((PlaybackState)stateObj).getErrorMessage(); + } + + public static long getLastPositionUpdateTime(Object stateObj) { + return ((PlaybackState)stateObj).getLastPositionUpdateTime(); + } + + public static List getCustomActions(Object stateObj) { + return (List)((PlaybackState)stateObj).getCustomActions(); + } + + public static long getActiveQueueItemId(Object stateObj) { + return ((PlaybackState)stateObj).getActiveQueueItemId(); + } + + public static Object newInstance(int state, long position, long bufferedPosition, + float speed, long actions, CharSequence errorMessage, long updateTime, + List customActions, + long activeItemId) { + PlaybackState.Builder stateObj = new PlaybackState.Builder(); + stateObj.setState(state, position, speed, updateTime); + stateObj.setBufferedPosition(bufferedPosition); + stateObj.setActions(actions); + stateObj.setErrorMessage(errorMessage); + for (Object customAction : customActions) { + stateObj.addCustomAction((PlaybackState.CustomAction) customAction); + } + stateObj.setActiveQueueItemId(activeItemId); + return stateObj.build(); + } + + static final class CustomAction { + public static String getAction(Object customActionObj) { + return ((PlaybackState.CustomAction)customActionObj).getAction(); + } + + public static CharSequence getName(Object customActionObj) { + return ((PlaybackState.CustomAction)customActionObj).getName(); + } + + public static int getIcon(Object customActionObj) { + return ((PlaybackState.CustomAction)customActionObj).getIcon(); + } + public static Bundle getExtras(Object customActionObj) { + return ((PlaybackState.CustomAction)customActionObj).getExtras(); + } + + public static Object newInstance(String action, CharSequence name, + int icon, Bundle extras) { + PlaybackState.CustomAction.Builder customActionObj = + new PlaybackState.CustomAction.Builder(action, name, icon); + customActionObj.setExtras(extras); + return customActionObj.build(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi22.java b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi22.java new file mode 100644 index 00000000000..55d4b835aa7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/media/session/PlaybackStateCompatApi22.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.media.session; + +import android.media.session.PlaybackState; +import android.os.Bundle; + +import java.util.ArrayList; +import java.util.List; + +class PlaybackStateCompatApi22 { + public static Bundle getExtras(Object stateObj) { + return ((PlaybackState)stateObj).getExtras(); + } + + public static Object newInstance(int state, long position, long bufferedPosition, + float speed, long actions, CharSequence errorMessage, long updateTime, + List customActions, + long activeItemId, Bundle extras) { + PlaybackState.Builder stateObj = new PlaybackState.Builder(); + stateObj.setState(state, position, speed, updateTime); + stateObj.setBufferedPosition(bufferedPosition); + stateObj.setActions(actions); + stateObj.setErrorMessage(errorMessage); + for (Object customAction : customActions) { + stateObj.addCustomAction((PlaybackState.CustomAction) customAction); + } + stateObj.setActiveQueueItemId(activeItemId); + stateObj.setExtras(extras); + return stateObj.build(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompat.java new file mode 100644 index 00000000000..14f150a6db6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompat.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.net; + +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_WIFI; + +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Build; + +/** + * Helper for accessing features in {@link ConnectivityManager} introduced after + * API level 16 in a backwards compatible fashion. + */ +public class ConnectivityManagerCompat { + + interface ConnectivityManagerCompatImpl { + boolean isActiveNetworkMetered(ConnectivityManager cm); + } + + static class BaseConnectivityManagerCompatImpl implements ConnectivityManagerCompatImpl { + @Override + public boolean isActiveNetworkMetered(ConnectivityManager cm) { + final NetworkInfo info = cm.getActiveNetworkInfo(); + if (info == null) { + // err on side of caution + return true; + } + + final int type = info.getType(); + switch (type) { + case TYPE_MOBILE: + return true; + case TYPE_WIFI: + return false; + default: + // err on side of caution + return true; + } + } + } + + static class GingerbreadConnectivityManagerCompatImpl implements ConnectivityManagerCompatImpl { + @Override + public boolean isActiveNetworkMetered(ConnectivityManager cm) { + return ConnectivityManagerCompatGingerbread.isActiveNetworkMetered(cm); + } + } + + static class HoneycombMR2ConnectivityManagerCompatImpl + implements ConnectivityManagerCompatImpl { + @Override + public boolean isActiveNetworkMetered(ConnectivityManager cm) { + return ConnectivityManagerCompatHoneycombMR2.isActiveNetworkMetered(cm); + } + } + + static class JellyBeanConnectivityManagerCompatImpl implements ConnectivityManagerCompatImpl { + @Override + public boolean isActiveNetworkMetered(ConnectivityManager cm) { + return ConnectivityManagerCompatJellyBean.isActiveNetworkMetered(cm); + } + } + + private static final ConnectivityManagerCompatImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 16) { + IMPL = new JellyBeanConnectivityManagerCompatImpl(); + } else if (Build.VERSION.SDK_INT >= 13) { + IMPL = new HoneycombMR2ConnectivityManagerCompatImpl(); + } else if (Build.VERSION.SDK_INT >= 8) { + IMPL = new GingerbreadConnectivityManagerCompatImpl(); + } else { + IMPL = new BaseConnectivityManagerCompatImpl(); + } + } + + /** + * Returns if the currently active data network is metered. A network is + * classified as metered when the user is sensitive to heavy data usage on + * that connection. You should check this before doing large data transfers, + * and warn the user or delay the operation until another network is + * available. + */ + public static boolean isActiveNetworkMetered(ConnectivityManager cm) { + return IMPL.isActiveNetworkMetered(cm); + } + + /** + * Return the {@link NetworkInfo} that caused the given + * {@link ConnectivityManager#CONNECTIVITY_ACTION} broadcast. This obtains + * the current state from {@link ConnectivityManager} instead of using the + * potentially-stale value from + * {@link ConnectivityManager#EXTRA_NETWORK_INFO}. May be {@code null}. + */ + public static NetworkInfo getNetworkInfoFromBroadcast(ConnectivityManager cm, Intent intent) { + final NetworkInfo info = intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO); + if (info != null) { + return cm.getNetworkInfo(info.getType()); + } else { + return null; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatGingerbread.java b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatGingerbread.java new file mode 100644 index 00000000000..281aab26266 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatGingerbread.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.net; + +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_MOBILE_DUN; +import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI; +import static android.net.ConnectivityManager.TYPE_MOBILE_MMS; +import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.ConnectivityManager.TYPE_WIMAX; + +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +/** + * Implementation of ConnectivityManagerCompat that can use Gingerbread APIs. + */ +class ConnectivityManagerCompatGingerbread { + public static boolean isActiveNetworkMetered(ConnectivityManager cm) { + final NetworkInfo info = cm.getActiveNetworkInfo(); + if (info == null) { + // err on side of caution + return true; + } + + final int type = info.getType(); + switch (type) { + case TYPE_MOBILE: + case TYPE_MOBILE_DUN: + case TYPE_MOBILE_HIPRI: + case TYPE_MOBILE_MMS: + case TYPE_MOBILE_SUPL: + case TYPE_WIMAX: + return true; + case TYPE_WIFI: + return false; + default: + // err on side of caution + return true; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatHoneycombMR2.java b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatHoneycombMR2.java new file mode 100644 index 00000000000..619c1afc42e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatHoneycombMR2.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.net; + +import static android.net.ConnectivityManager.TYPE_BLUETOOTH; +import static android.net.ConnectivityManager.TYPE_ETHERNET; +import static android.net.ConnectivityManager.TYPE_MOBILE; +import static android.net.ConnectivityManager.TYPE_MOBILE_DUN; +import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI; +import static android.net.ConnectivityManager.TYPE_MOBILE_MMS; +import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL; +import static android.net.ConnectivityManager.TYPE_WIFI; +import static android.net.ConnectivityManager.TYPE_WIMAX; + +import android.net.ConnectivityManager; +import android.net.NetworkInfo; + +/** + * Implementation of ConnectivityManagerCompat that can use Honeycomb MR2 APIs. + */ +class ConnectivityManagerCompatHoneycombMR2 { + public static boolean isActiveNetworkMetered(ConnectivityManager cm) { + final NetworkInfo info = cm.getActiveNetworkInfo(); + if (info == null) { + // err on side of caution + return true; + } + + final int type = info.getType(); + switch (type) { + case TYPE_MOBILE: + case TYPE_MOBILE_DUN: + case TYPE_MOBILE_HIPRI: + case TYPE_MOBILE_MMS: + case TYPE_MOBILE_SUPL: + case TYPE_WIMAX: + return true; + case TYPE_WIFI: + case TYPE_BLUETOOTH: + case TYPE_ETHERNET: + return false; + default: + // err on side of caution + return true; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatJellyBean.java b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatJellyBean.java new file mode 100644 index 00000000000..0795fdd5e63 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/net/ConnectivityManagerCompatJellyBean.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.net; + +import android.net.ConnectivityManager; + +/** + * Implementation of ConnectivityManagerCompat that can use Jellybean APIs. + */ +class ConnectivityManagerCompatJellyBean { + public static boolean isActiveNetworkMetered(ConnectivityManager cm) { + return cm.isActiveNetworkMetered(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompat.java new file mode 100644 index 00000000000..cfc5c0deac0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompat.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.net; + +import android.os.Build; + +import java.net.Socket; +import java.net.SocketException; + +/** + * Helper for accessing features in TrafficStats introduced after API level 14 + * in a backwards compatible fashion. + */ +public class TrafficStatsCompat { + + interface TrafficStatsCompatImpl { + void clearThreadStatsTag(); + int getThreadStatsTag(); + void incrementOperationCount(int operationCount); + void incrementOperationCount(int tag, int operationCount); + void setThreadStatsTag(int tag); + void tagSocket(Socket socket) throws SocketException; + void untagSocket(Socket socket) throws SocketException; + } + + static class BaseTrafficStatsCompatImpl implements TrafficStatsCompatImpl { + private static class SocketTags { + public int statsTag = -1; + } + + private ThreadLocal mThreadSocketTags = new ThreadLocal() { + @Override + protected SocketTags initialValue() { + return new SocketTags(); + } + }; + + @Override + public void clearThreadStatsTag() { + mThreadSocketTags.get().statsTag = -1; + } + + @Override + public int getThreadStatsTag() { + return mThreadSocketTags.get().statsTag; + } + + @Override + public void incrementOperationCount(int operationCount) { + } + + @Override + public void incrementOperationCount(int tag, int operationCount) { + } + + @Override + public void setThreadStatsTag(int tag) { + mThreadSocketTags.get().statsTag = tag; + } + + @Override + public void tagSocket(Socket socket) { + } + + @Override + public void untagSocket(Socket socket) { + } + } + + static class IcsTrafficStatsCompatImpl implements TrafficStatsCompatImpl { + @Override + public void clearThreadStatsTag() { + TrafficStatsCompatIcs.clearThreadStatsTag(); + } + + @Override + public int getThreadStatsTag() { + return TrafficStatsCompatIcs.getThreadStatsTag(); + } + + @Override + public void incrementOperationCount(int operationCount) { + TrafficStatsCompatIcs.incrementOperationCount(operationCount); + } + + @Override + public void incrementOperationCount(int tag, int operationCount) { + TrafficStatsCompatIcs.incrementOperationCount(tag, operationCount); + } + + @Override + public void setThreadStatsTag(int tag) { + TrafficStatsCompatIcs.setThreadStatsTag(tag); + } + + @Override + public void tagSocket(Socket socket) throws SocketException { + TrafficStatsCompatIcs.tagSocket(socket); + } + + @Override + public void untagSocket(Socket socket) throws SocketException { + TrafficStatsCompatIcs.untagSocket(socket); + } + } + + private static final TrafficStatsCompatImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 14) { + IMPL = new IcsTrafficStatsCompatImpl(); + } else { + IMPL = new BaseTrafficStatsCompatImpl(); + } + } + + /** + * Clear active tag used when accounting {@link Socket} traffic originating + * from the current thread. + */ + public static void clearThreadStatsTag() { + IMPL.clearThreadStatsTag(); + } + + /** + * Get the active tag used when accounting {@link Socket} traffic originating + * from the current thread. Only one active tag per thread is supported. + * {@link #tagSocket(Socket)}. + */ + public static int getThreadStatsTag() { + return IMPL.getThreadStatsTag(); + } + + /** + * Increment count of network operations performed under the accounting tag + * currently active on the calling thread. This can be used to derive + * bytes-per-operation. + * + * @param operationCount Number of operations to increment count by. + */ + public static void incrementOperationCount(int operationCount) { + IMPL.incrementOperationCount(operationCount); + } + + /** + * Increment count of network operations performed under the given + * accounting tag. This can be used to derive bytes-per-operation. + * + * @param tag Accounting tag used in {@link #setThreadStatsTag(int)}. + * @param operationCount Number of operations to increment count by. + */ + public static void incrementOperationCount(int tag, int operationCount) { + IMPL.incrementOperationCount(tag, operationCount); + } + + /** + * Set active tag to use when accounting {@link Socket} traffic originating + * from the current thread. Only one active tag per thread is supported. + *

    + * Changes only take effect during subsequent calls to + * {@link #tagSocket(Socket)}. + *

    + * Tags between {@code 0xFFFFFF00} and {@code 0xFFFFFFFF} are reserved and + * used internally by system services like DownloadManager when performing + * traffic on behalf of an application. + */ + public static void setThreadStatsTag(int tag) { + IMPL.setThreadStatsTag(tag); + } + + /** + * Tag the given {@link Socket} with any statistics parameters active for + * the current thread. Subsequent calls always replace any existing + * parameters. When finished, call {@link #untagSocket(Socket)} to remove + * statistics parameters. + * + * @see #setThreadStatsTag(int) + */ + public static void tagSocket(Socket socket) throws SocketException { + IMPL.tagSocket(socket); + } + + /** + * Remove any statistics parameters from the given {@link Socket}. + */ + public static void untagSocket(Socket socket) throws SocketException { + IMPL.untagSocket(socket); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompatIcs.java new file mode 100644 index 00000000000..0f63fceed09 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/net/TrafficStatsCompatIcs.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.net; + +import android.net.TrafficStats; + +import java.net.Socket; +import java.net.SocketException; + +/** + * Implementation of TrafficStatsCompat that can call ICS APIs. + */ +class TrafficStatsCompatIcs { + public static void clearThreadStatsTag() { + TrafficStats.clearThreadStatsTag(); + } + + public static int getThreadStatsTag() { + return TrafficStats.getThreadStatsTag(); + } + + public static void incrementOperationCount(int operationCount) { + TrafficStats.incrementOperationCount(operationCount); + } + + public static void incrementOperationCount(int tag, int operationCount) { + TrafficStats.incrementOperationCount(tag, operationCount); + } + + public static void setThreadStatsTag(int tag) { + TrafficStats.setThreadStatsTag(tag); + } + + public static void tagSocket(Socket socket) throws SocketException { + TrafficStats.tagSocket(socket); + } + + public static void untagSocket(Socket socket) throws SocketException { + TrafficStats.untagSocket(socket); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompat.java b/mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompat.java new file mode 100644 index 00000000000..2af8905095b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompat.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.os; + +import android.os.AsyncTask; +import android.os.Build; + +/** + * Helper for accessing features in {@link android.os.AsyncTask} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AsyncTaskCompat { + + /** + * Executes the task with the specified parameters, allowing multiple tasks to run in parallel + * on a pool of threads managed by {@link android.os.AsyncTask}. + * + * @param task The {@link android.os.AsyncTask} to execute. + * @param params The parameters of the task. + * @return the instance of AsyncTask. + */ + public static AsyncTask executeParallel( + AsyncTask task, + Params... params) { + if (task == null) { + throw new IllegalArgumentException("task can not be null"); + } + + if (Build.VERSION.SDK_INT >= 11) { + // From API 11 onwards, we need to manually select the THREAD_POOL_EXECUTOR + AsyncTaskCompatHoneycomb.executeParallel(task, params); + } else { + // Before API 11, all tasks were run in parallel + task.execute(params); + } + + return task; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompatHoneycomb.java new file mode 100644 index 00000000000..5b05a0e404d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/AsyncTaskCompatHoneycomb.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.os; + +import android.os.AsyncTask; + +/** + * Implementation of AsyncTask compatibility that can call Honeycomb APIs. + */ +class AsyncTaskCompatHoneycomb { + + static void executeParallel( + AsyncTask task, + Params... params) { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, params); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompat.java b/mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompat.java new file mode 100644 index 00000000000..66a2a8eb94d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompat.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.os; + +import android.os.Build; +import android.os.Environment; +import android.util.Log; + +import java.io.File; +import java.io.IOException; + +/** + * Helper for accessing features in {@link Environment} introduced after API + * level 4 in a backwards compatible fashion. + */ +public class EnvironmentCompat { + private static final String TAG = "EnvironmentCompat"; + + /** + * Unknown storage state, such as when a path isn't backed by known storage + * media. + * + * @see #getStorageState(File) + */ + public static final String MEDIA_UNKNOWN = "unknown"; + + /** + * Returns the current state of the storage device that provides the given + * path. + * + * @return one of {@link #MEDIA_UNKNOWN}, {@link Environment#MEDIA_REMOVED}, + * {@link Environment#MEDIA_UNMOUNTED}, + * {@link Environment#MEDIA_CHECKING}, + * {@link Environment#MEDIA_NOFS}, + * {@link Environment#MEDIA_MOUNTED}, + * {@link Environment#MEDIA_MOUNTED_READ_ONLY}, + * {@link Environment#MEDIA_SHARED}, + * {@link Environment#MEDIA_BAD_REMOVAL}, or + * {@link Environment#MEDIA_UNMOUNTABLE}. + */ + public static String getStorageState(File path) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return EnvironmentCompatKitKat.getStorageState(path); + } + + try { + final String canonicalPath = path.getCanonicalPath(); + final String canonicalExternal = Environment.getExternalStorageDirectory() + .getCanonicalPath(); + + if (canonicalPath.startsWith(canonicalExternal)) { + return Environment.getExternalStorageState(); + } + } catch (IOException e) { + Log.w(TAG, "Failed to resolve canonical path: " + e); + } + + return MEDIA_UNKNOWN; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompatKitKat.java new file mode 100644 index 00000000000..9a803dbce30 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/EnvironmentCompatKitKat.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.os; + +import android.os.Environment; + +import java.io.File; + +class EnvironmentCompatKitKat { + public static String getStorageState(File path) { + return Environment.getStorageState(path); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompat.java b/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompat.java new file mode 100644 index 00000000000..fbb064f2b53 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompat.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.os; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Helper for accessing features in {@link android.os.Parcelable} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ParcelableCompat { + + /** + * Factory method for {@link Parcelable.Creator}. + * + * @param callbacks Creator callbacks implementation. + * @return New creator. + */ + public static Parcelable.Creator newCreator( + ParcelableCompatCreatorCallbacks callbacks) { + if (android.os.Build.VERSION.SDK_INT >= 13) { + return ParcelableCompatCreatorHoneycombMR2Stub.instantiate(callbacks); + } + return new CompatCreator(callbacks); + } + + static class CompatCreator implements Parcelable.Creator { + final ParcelableCompatCreatorCallbacks mCallbacks; + + public CompatCreator(ParcelableCompatCreatorCallbacks callbacks) { + mCallbacks = callbacks; + } + + @Override + public T createFromParcel(Parcel source) { + return mCallbacks.createFromParcel(source, null); + } + + @Override + public T[] newArray(int size) { + return mCallbacks.newArray(size); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatCreatorCallbacks.java b/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatCreatorCallbacks.java new file mode 100644 index 00000000000..a577d6f392f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatCreatorCallbacks.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.os; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Callbacks a {@link Parcelable} creator should implement. + */ +public interface ParcelableCompatCreatorCallbacks { + + /** + * Create a new instance of the Parcelable class, instantiating it + * from the given Parcel whose data had previously been written by + * {@link Parcelable#writeToParcel Parcelable.writeToParcel()} and + * using the given ClassLoader. + * + * @param in The Parcel to read the object's data from. + * @param loader The ClassLoader that this object is being created in. + * @return Returns a new instance of the Parcelable class. + */ + public T createFromParcel(Parcel in, ClassLoader loader); + + /** + * Create a new array of the Parcelable class. + * + * @param size Size of the array. + * @return Returns an array of the Parcelable class, with every entry + * initialized to null. + */ + public T[] newArray(int size); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatHoneycombMR2.java b/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatHoneycombMR2.java new file mode 100644 index 00000000000..08acb550708 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/ParcelableCompatHoneycombMR2.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.os; + +import android.os.Parcel; +import android.os.Parcelable; + +class ParcelableCompatCreatorHoneycombMR2Stub { + static Parcelable.Creator instantiate(ParcelableCompatCreatorCallbacks callbacks) { + return new ParcelableCompatCreatorHoneycombMR2(callbacks); + } +} + +class ParcelableCompatCreatorHoneycombMR2 implements Parcelable.ClassLoaderCreator { + private final ParcelableCompatCreatorCallbacks mCallbacks; + + public ParcelableCompatCreatorHoneycombMR2(ParcelableCompatCreatorCallbacks callbacks) { + mCallbacks = callbacks; + } + + public T createFromParcel(Parcel in) { + return mCallbacks.createFromParcel(in, null); + } + + public T createFromParcel(Parcel in, ClassLoader loader) { + return mCallbacks.createFromParcel(in, loader); + } + + public T[] newArray(int size) { + return mCallbacks.newArray(size); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/TraceCompat.java b/mobile/google-android-support-v4/src/android/support/v4/os/TraceCompat.java new file mode 100644 index 00000000000..82b0fd5ec48 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/TraceCompat.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.os; + +import android.os.Build; + +/** + * Writes trace events to the system trace buffer. These trace events can be + * collected and visualized using the Systrace tool. + * + *

    This tracing mechanism is independent of the method tracing mechanism + * offered by {@link android.os.Debug#startMethodTracing}. In particular, it enables + * tracing of events that occur across multiple processes. + *

    For information about using the Systrace tool, read Analyzing Display and Performance + * with Systrace. + */ +public class TraceCompat { + /** + * Writes a trace message to indicate that a given section of code has begun. This call must + * be followed by a corresponding call to {@link #endSection()} on the same thread. + * + *

    At this time the vertical bar character '|', newline character '\n', and + * null character '\0' are used internally by the tracing mechanism. If sectionName contains + * these characters they will be replaced with a space character in the trace. + * + * @param sectionName The name of the code section to appear in the trace. This may be at + * most 127 Unicode code units long. + */ + + public static void beginSection(String sectionName) { + if (Build.VERSION.SDK_INT >= 18) { + TraceJellybeanMR2.beginSection(sectionName); + } + } + + /** + * Writes a trace message to indicate that a given section of code has ended. This call must + * be preceeded by a corresponding call to {@link #beginSection(String)}. Calling this method + * will mark the end of the most recently begun section of code, so care must be taken to + * ensure that beginSection / endSection pairs are properly nested and called from the same + * thread. + */ + public static void endSection() { + if (Build.VERSION.SDK_INT >= 18) { + TraceJellybeanMR2.endSection(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/TraceJellybeanMR2.java b/mobile/google-android-support-v4/src/android/support/v4/os/TraceJellybeanMR2.java new file mode 100644 index 00000000000..6116947b016 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/TraceJellybeanMR2.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.os; + +import android.os.Trace; + +class TraceJellybeanMR2 { + public static void beginSection(String section) { + Trace.beginSection(section); + } + + public static void endSection() { + Trace.endSection(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/os/package.html b/mobile/google-android-support-v4/src/android/support/v4/os/package.html new file mode 100644 index 00000000000..929c967fdc1 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/os/package.html @@ -0,0 +1,6 @@ + + +Support android.os classes to assist with development of applications for +android API level 4 or later. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/print/PrintHelper.java b/mobile/google-android-support-v4/src/android/support/v4/print/PrintHelper.java new file mode 100644 index 00000000000..23429ecf140 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/print/PrintHelper.java @@ -0,0 +1,353 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.print; + +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.net.Uri; + +import java.io.FileNotFoundException; + +/** + * Helper for printing bitmaps. + */ +public final class PrintHelper { + /** + * image will be scaled but leave white space + */ + public static final int SCALE_MODE_FIT = 1; + + /** + * image will fill the paper and be cropped (default) + */ + public static final int SCALE_MODE_FILL = 2; + + /** + * this is a black and white image + */ + public static final int COLOR_MODE_MONOCHROME = 1; + + /** + * this is a color image (default) + */ + public static final int COLOR_MODE_COLOR = 2; + + /** + * Print the image in landscape orientation (default). + */ + public static final int ORIENTATION_LANDSCAPE = 1; + + /** + * Print the image in portrait orientation. + */ + public static final int ORIENTATION_PORTRAIT = 2; + + /** + * Callback for observing when a print operation is completed. + * When print is finished either the system acquired the + * document to print or printing was cancelled. + */ + public interface OnPrintFinishCallback { + + /** + * Called when a print operation is finished. + */ + public void onFinish(); + } + + PrintHelperVersionImpl mImpl; + + /** + * Gets whether the system supports printing. + * + * @return True if printing is supported. + */ + public static boolean systemSupportsPrint() { + if (Build.VERSION.SDK_INT >= 19) { + // Supported on Android 4.4 or later. + return true; + } + return false; + } + + /** + * Interface implemented by classes that support printing + */ + static interface PrintHelperVersionImpl { + + public void setScaleMode(int scaleMode); + + public int getScaleMode(); + + public void setColorMode(int colorMode); + + public int getColorMode(); + + public void setOrientation(int orientation); + + public int getOrientation(); + + public void printBitmap(String jobName, Bitmap bitmap, OnPrintFinishCallback callback); + + public void printBitmap(String jobName, Uri imageFile, OnPrintFinishCallback callback) + throws FileNotFoundException; + } + + /** + * Implementation used when we do not support printing + */ + private static final class PrintHelperStubImpl implements PrintHelperVersionImpl { + int mScaleMode = SCALE_MODE_FILL; + int mColorMode = COLOR_MODE_COLOR; + int mOrientation = ORIENTATION_LANDSCAPE; + @Override + public void setScaleMode(int scaleMode) { + mScaleMode = scaleMode; + } + + @Override + public int getColorMode() { + return mColorMode; + } + + @Override + public void setColorMode(int colorMode) { + mColorMode = colorMode; + } + + @Override + public void setOrientation(int orientation) { mOrientation = orientation; } + + @Override + public int getOrientation() { return mOrientation; } + + @Override + public int getScaleMode() { + return mScaleMode; + } + + @Override + public void printBitmap(String jobName, Bitmap bitmap, OnPrintFinishCallback callback) { + } + + @Override + public void printBitmap(String jobName, Uri imageFile, OnPrintFinishCallback callback) { + } + } + + /** + * Implementation used on KitKat (and above) + */ + private static final class PrintHelperKitkatImpl implements PrintHelperVersionImpl { + private final PrintHelperKitkat mPrintHelper; + + PrintHelperKitkatImpl(Context context) { + mPrintHelper = new PrintHelperKitkat(context); + } + + @Override + public void setScaleMode(int scaleMode) { + mPrintHelper.setScaleMode(scaleMode); + } + + @Override + public int getScaleMode() { + return mPrintHelper.getScaleMode(); + } + + @Override + public void setColorMode(int colorMode) { + mPrintHelper.setColorMode(colorMode); + } + + @Override + public int getColorMode() { + return mPrintHelper.getColorMode(); + } + + @Override + public void setOrientation(int orientation) { + mPrintHelper.setOrientation(orientation); + } + + @Override + public int getOrientation() { + return mPrintHelper.getOrientation(); + } + + @Override + public void printBitmap(String jobName, Bitmap bitmap, + final OnPrintFinishCallback callback) { + PrintHelperKitkat.OnPrintFinishCallback delegateCallback = null; + if (callback != null) { + delegateCallback = new PrintHelperKitkat.OnPrintFinishCallback() { + @Override + public void onFinish() { + callback.onFinish(); + } + }; + } + mPrintHelper.printBitmap(jobName, bitmap, delegateCallback); + } + + @Override + public void printBitmap(String jobName, Uri imageFile, + final OnPrintFinishCallback callback) throws FileNotFoundException { + PrintHelperKitkat.OnPrintFinishCallback delegateCallback = null; + if (callback != null) { + delegateCallback = new PrintHelperKitkat.OnPrintFinishCallback() { + @Override + public void onFinish() { + callback.onFinish(); + } + }; + } + mPrintHelper.printBitmap(jobName, imageFile, delegateCallback); + } + } + + /** + * Returns the PrintHelper that can be used to print images. + * + * @param context A context for accessing system resources. + * @return the PrintHelper to support printing images. + */ + public PrintHelper(Context context) { + if (systemSupportsPrint()) { + mImpl = new PrintHelperKitkatImpl(context); + } else { + mImpl = new PrintHelperStubImpl(); + } + } + + /** + * Selects whether the image will fill the paper and be cropped + * {@link #SCALE_MODE_FIT} + * or whether the image will be scaled but leave white space + * {@link #SCALE_MODE_FILL}. + * + * @param scaleMode {@link #SCALE_MODE_FIT} or + * {@link #SCALE_MODE_FILL} + */ + public void setScaleMode(int scaleMode) { + mImpl.setScaleMode(scaleMode); + } + + /** + * Returns the scale mode with which the image will fill the paper. + * + * @return The scale Mode: {@link #SCALE_MODE_FIT} or + * {@link #SCALE_MODE_FILL} + */ + public int getScaleMode() { + return mImpl.getScaleMode(); + } + + /** + * Sets whether the image will be printed in color (default) + * {@link #COLOR_MODE_COLOR} or in back and white + * {@link #COLOR_MODE_MONOCHROME}. + * + * @param colorMode The color mode which is one of + * {@link #COLOR_MODE_COLOR} and {@link #COLOR_MODE_MONOCHROME}. + */ + public void setColorMode(int colorMode) { + mImpl.setColorMode(colorMode); + } + + /** + * Gets the color mode with which the image will be printed. + * + * @return The color mode which is one of {@link #COLOR_MODE_COLOR} + * and {@link #COLOR_MODE_MONOCHROME}. + */ + public int getColorMode() { + return mImpl.getColorMode(); + } + + /** + * Sets whether the image will be printed in landscape {@link #ORIENTATION_LANDSCAPE} (default) + * or portrait {@link #ORIENTATION_PORTRAIT}. + * + * @param orientation The page orientation which is one of + * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}. + */ + public void setOrientation(int orientation) { + mImpl.setOrientation(orientation); + } + + /** + * Gets whether the image will be printed in landscape or portrait. + * + * @return The page orientation which is one of + * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}. + */ + public int getOrientation() { + return mImpl.getOrientation(); + } + + + /** + * Prints a bitmap. + * + * @param jobName The print job name. + * @param bitmap The bitmap to print. + */ + public void printBitmap(String jobName, Bitmap bitmap) { + mImpl.printBitmap(jobName, bitmap, null); + } + + /** + * Prints a bitmap. + * + * @param jobName The print job name. + * @param bitmap The bitmap to print. + * @param callback Optional callback to observe when printing is finished. + */ + public void printBitmap(String jobName, Bitmap bitmap, OnPrintFinishCallback callback) { + mImpl.printBitmap(jobName, bitmap, callback); + } + + /** + * Prints an image located at the Uri. Image types supported are those of + * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream) + * android.graphics.BitmapFactory.decodeStream(java.io.InputStream)} + * + * @param jobName The print job name. + * @param imageFile The Uri pointing to an image to print. + * @throws FileNotFoundException if Uri is not pointing to a valid image. + */ + public void printBitmap(String jobName, Uri imageFile) throws FileNotFoundException { + mImpl.printBitmap(jobName, imageFile, null); + } + + /** + * Prints an image located at the Uri. Image types supported are those of + * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream) + * android.graphics.BitmapFactory.decodeStream(java.io.InputStream)} + * + * @param jobName The print job name. + * @param imageFile The Uri pointing to an image to print. + * @throws FileNotFoundException if Uri is not pointing to a valid image. + * @param callback Optional callback to observe when printing is finished. + */ + public void printBitmap(String jobName, Uri imageFile, OnPrintFinishCallback callback) + throws FileNotFoundException { + mImpl.printBitmap(jobName, imageFile, callback); + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/print/PrintHelperKitkat.java b/mobile/google-android-support-v4/src/android/support/v4/print/PrintHelperKitkat.java new file mode 100644 index 00000000000..b827b1f3a91 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/print/PrintHelperKitkat.java @@ -0,0 +1,544 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.print; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.graphics.pdf.PdfDocument.Page; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; +import android.print.PageRange; +import android.print.PrintAttributes; +import android.print.PrintDocumentAdapter; +import android.print.PrintDocumentInfo; +import android.print.PrintManager; +import android.print.pdf.PrintedPdfDocument; +import android.util.Log; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * Kitkat specific PrintManager API implementation. + */ +class PrintHelperKitkat { + private static final String LOG_TAG = "PrintHelperKitkat"; + // will be <= 300 dpi on A4 (8.3×11.7) paper (worst case of 150 dpi) + private final static int MAX_PRINT_SIZE = 3500; + final Context mContext; + BitmapFactory.Options mDecodeOptions = null; + private final Object mLock = new Object(); + /** + * image will be scaled but leave white space + */ + public static final int SCALE_MODE_FIT = 1; + /** + * image will fill the paper and be cropped (default) + */ + public static final int SCALE_MODE_FILL = 2; + + /** + * select landscape (default) + */ + public static final int ORIENTATION_LANDSCAPE = 1; + + /** + * select portrait + */ + public static final int ORIENTATION_PORTRAIT = 2; + + /** + * this is a black and white image + */ + public static final int COLOR_MODE_MONOCHROME = 1; + /** + * this is a color image (default) + */ + public static final int COLOR_MODE_COLOR = 2; + + public interface OnPrintFinishCallback { + public void onFinish(); + } + + int mScaleMode = SCALE_MODE_FILL; + + int mColorMode = COLOR_MODE_COLOR; + + int mOrientation = ORIENTATION_LANDSCAPE; + + PrintHelperKitkat(Context context) { + mContext = context; + } + + /** + * Selects whether the image will fill the paper and be cropped + *

    + * {@link #SCALE_MODE_FIT} + * or whether the image will be scaled but leave white space + * {@link #SCALE_MODE_FILL}. + * + * @param scaleMode {@link #SCALE_MODE_FIT} or + * {@link #SCALE_MODE_FILL} + */ + public void setScaleMode(int scaleMode) { + mScaleMode = scaleMode; + } + + /** + * Returns the scale mode with which the image will fill the paper. + * + * @return The scale Mode: {@link #SCALE_MODE_FIT} or + * {@link #SCALE_MODE_FILL} + */ + public int getScaleMode() { + return mScaleMode; + } + + /** + * Sets whether the image will be printed in color (default) + * {@link #COLOR_MODE_COLOR} or in back and white + * {@link #COLOR_MODE_MONOCHROME}. + * + * @param colorMode The color mode which is one of + * {@link #COLOR_MODE_COLOR} and {@link #COLOR_MODE_MONOCHROME}. + */ + public void setColorMode(int colorMode) { + mColorMode = colorMode; + } + + /** + * Sets whether to select landscape (default), {@link #ORIENTATION_LANDSCAPE} + * or portrait {@link #ORIENTATION_PORTRAIT} + * @param orientation The page orientation which is one of + * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}. + */ + public void setOrientation(int orientation) { + mOrientation = orientation; + } + + /** + * Gets the page orientation with which the image will be printed. + * + * @return The preferred orientation which is one of + * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT} + */ + public int getOrientation() { + return mOrientation; + } + + /** + * Gets the color mode with which the image will be printed. + * + * @return The color mode which is one of {@link #COLOR_MODE_COLOR} + * and {@link #COLOR_MODE_MONOCHROME}. + */ + public int getColorMode() { + return mColorMode; + } + + /** + * Prints a bitmap. + * + * @param jobName The print job name. + * @param bitmap The bitmap to print. + * @param callback Optional callback to observe when printing is finished. + */ + public void printBitmap(final String jobName, final Bitmap bitmap, + final OnPrintFinishCallback callback) { + if (bitmap == null) { + return; + } + final int fittingMode = mScaleMode; // grab the fitting mode at time of call + PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE); + PrintAttributes.MediaSize mediaSize = PrintAttributes.MediaSize.UNKNOWN_PORTRAIT; + if (bitmap.getWidth() > bitmap.getHeight()) { + mediaSize = PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE; + } + PrintAttributes attr = new PrintAttributes.Builder() + .setMediaSize(mediaSize) + .setColorMode(mColorMode) + .build(); + + printManager.print(jobName, + new PrintDocumentAdapter() { + private PrintAttributes mAttributes; + + @Override + public void onLayout(PrintAttributes oldPrintAttributes, + PrintAttributes newPrintAttributes, + CancellationSignal cancellationSignal, + LayoutResultCallback layoutResultCallback, + Bundle bundle) { + + mAttributes = newPrintAttributes; + + PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName) + .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO) + .setPageCount(1) + .build(); + boolean changed = !newPrintAttributes.equals(oldPrintAttributes); + layoutResultCallback.onLayoutFinished(info, changed); + } + + @Override + public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor, + CancellationSignal cancellationSignal, + WriteResultCallback writeResultCallback) { + PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext, + mAttributes); + try { + Page page = pdfDocument.startPage(1); + + RectF content = new RectF(page.getInfo().getContentRect()); + + Matrix matrix = getMatrix(bitmap.getWidth(), bitmap.getHeight(), + content, fittingMode); + + // Draw the bitmap. + page.getCanvas().drawBitmap(bitmap, matrix, null); + + // Finish the page. + pdfDocument.finishPage(page); + + try { + // Write the document. + pdfDocument.writeTo(new FileOutputStream( + fileDescriptor.getFileDescriptor())); + // Done. + writeResultCallback.onWriteFinished( + new PageRange[]{PageRange.ALL_PAGES}); + } catch (IOException ioe) { + // Failed. + Log.e(LOG_TAG, "Error writing printed content", ioe); + writeResultCallback.onWriteFailed(null); + } + } finally { + if (pdfDocument != null) { + pdfDocument.close(); + } + if (fileDescriptor != null) { + try { + fileDescriptor.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + } + } + + @Override + public void onFinish() { + if (callback != null) { + callback.onFinish(); + } + } + }, attr); + } + + /** + * Calculates the transform the print an Image to fill the page + * + * @param imageWidth with of bitmap + * @param imageHeight height of bitmap + * @param content The output page dimensions + * @param fittingMode The mode of fitting {@link #SCALE_MODE_FILL} vs {@link #SCALE_MODE_FIT} + * @return Matrix to be used in canvas.drawBitmap(bitmap, matrix, null) call + */ + private Matrix getMatrix(int imageWidth, int imageHeight, RectF content, int fittingMode) { + Matrix matrix = new Matrix(); + + // Compute and apply scale to fill the page. + float scale = content.width() / imageWidth; + if (fittingMode == SCALE_MODE_FILL) { + scale = Math.max(scale, content.height() / imageHeight); + } else { + scale = Math.min(scale, content.height() / imageHeight); + } + matrix.postScale(scale, scale); + + // Center the content. + final float translateX = (content.width() + - imageWidth * scale) / 2; + final float translateY = (content.height() + - imageHeight * scale) / 2; + matrix.postTranslate(translateX, translateY); + return matrix; + } + + /** + * Prints an image located at the Uri. Image types supported are those of + * BitmapFactory.decodeStream (JPEG, GIF, PNG, BMP, WEBP) + * + * @param jobName The print job name. + * @param imageFile The Uri pointing to an image to print. + * @param callback Optional callback to observe when printing is finished. + * @throws FileNotFoundException if Uri is not pointing to a valid image. + */ + public void printBitmap(final String jobName, final Uri imageFile, + final OnPrintFinishCallback callback) throws FileNotFoundException { + final int fittingMode = mScaleMode; + + PrintDocumentAdapter printDocumentAdapter = new PrintDocumentAdapter() { + private PrintAttributes mAttributes; + AsyncTask mLoadBitmap; + Bitmap mBitmap = null; + + @Override + public void onLayout(final PrintAttributes oldPrintAttributes, + final PrintAttributes newPrintAttributes, + final CancellationSignal cancellationSignal, + final LayoutResultCallback layoutResultCallback, + Bundle bundle) { + + mAttributes = newPrintAttributes; + + if (cancellationSignal.isCanceled()) { + layoutResultCallback.onLayoutCancelled(); + return; + } + // we finished the load + if (mBitmap != null) { + PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName) + .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO) + .setPageCount(1) + .build(); + boolean changed = !newPrintAttributes.equals(oldPrintAttributes); + layoutResultCallback.onLayoutFinished(info, changed); + return; + } + + mLoadBitmap = new AsyncTask() { + + @Override + protected void onPreExecute() { + // First register for cancellation requests. + cancellationSignal.setOnCancelListener( + new CancellationSignal.OnCancelListener() { + @Override + public void onCancel() { // on different thread + cancelLoad(); + cancel(false); + } + }); + } + + @Override + protected Bitmap doInBackground(Uri... uris) { + try { + return loadConstrainedBitmap(imageFile, MAX_PRINT_SIZE); + } catch (FileNotFoundException e) { + /* ignore */ + } + return null; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + super.onPostExecute(bitmap); + mBitmap = bitmap; + if (bitmap != null) { + PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName) + .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO) + .setPageCount(1) + .build(); + boolean changed = !newPrintAttributes.equals(oldPrintAttributes); + + layoutResultCallback.onLayoutFinished(info, changed); + + } else { + layoutResultCallback.onLayoutFailed(null); + } + mLoadBitmap = null; + } + + @Override + protected void onCancelled(Bitmap result) { + // Task was cancelled, report that. + layoutResultCallback.onLayoutCancelled(); + mLoadBitmap = null; + } + }.execute(); + } + + private void cancelLoad() { + synchronized (mLock) { // prevent race with set null below + if (mDecodeOptions != null) { + mDecodeOptions.requestCancelDecode(); + mDecodeOptions = null; + } + } + } + + @Override + public void onFinish() { + super.onFinish(); + cancelLoad(); + if (mLoadBitmap != null) { + mLoadBitmap.cancel(true); + } + if (callback != null) { + callback.onFinish(); + } + } + + @Override + public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor, + CancellationSignal cancellationSignal, + WriteResultCallback writeResultCallback) { + PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext, + mAttributes); + try { + + Page page = pdfDocument.startPage(1); + RectF content = new RectF(page.getInfo().getContentRect()); + + // Compute and apply scale to fill the page. + Matrix matrix = getMatrix(mBitmap.getWidth(), mBitmap.getHeight(), + content, fittingMode); + + // Draw the bitmap. + page.getCanvas().drawBitmap(mBitmap, matrix, null); + + // Finish the page. + pdfDocument.finishPage(page); + + try { + // Write the document. + pdfDocument.writeTo(new FileOutputStream( + fileDescriptor.getFileDescriptor())); + // Done. + writeResultCallback.onWriteFinished( + new PageRange[]{PageRange.ALL_PAGES}); + } catch (IOException ioe) { + // Failed. + Log.e(LOG_TAG, "Error writing printed content", ioe); + writeResultCallback.onWriteFailed(null); + } + } finally { + if (pdfDocument != null) { + pdfDocument.close(); + } + if (fileDescriptor != null) { + try { + fileDescriptor.close(); + } catch (IOException ioe) { + /* ignore */ + } + } + } + } + }; + + PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE); + PrintAttributes.Builder builder = new PrintAttributes.Builder(); + builder.setColorMode(mColorMode); + + if (mOrientation == ORIENTATION_LANDSCAPE) { + builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE); + } else if (mOrientation == ORIENTATION_PORTRAIT) { + builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_PORTRAIT); + } + PrintAttributes attr = builder.build(); + + printManager.print(jobName, printDocumentAdapter, attr); + } + + /** + * Loads a bitmap while limiting its size + * + * @param uri location of a valid image + * @param maxSideLength the maximum length of a size + * @return the Bitmap + * @throws FileNotFoundException if the Uri does not point to an image + */ + private Bitmap loadConstrainedBitmap(Uri uri, int maxSideLength) throws FileNotFoundException { + if (maxSideLength <= 0 || uri == null || mContext == null) { + throw new IllegalArgumentException("bad argument to getScaledBitmap"); + } + // Get width and height of stored bitmap + BitmapFactory.Options opt = new BitmapFactory.Options(); + opt.inJustDecodeBounds = true; + loadBitmap(uri, opt); + + int w = opt.outWidth; + int h = opt.outHeight; + + // If bitmap cannot be decoded, return null + if (w <= 0 || h <= 0) { + return null; + } + + // Find best downsampling size + int imageSide = Math.max(w, h); + + int sampleSize = 1; + while (imageSide > maxSideLength) { + imageSide >>>= 1; + sampleSize <<= 1; + } + + // Make sure sample size is reasonable + if (sampleSize <= 0 || 0 >= (int) (Math.min(w, h) / sampleSize)) { + return null; + } + BitmapFactory.Options decodeOptions = null; + synchronized (mLock) { // prevent race with set null below + mDecodeOptions = new BitmapFactory.Options(); + mDecodeOptions.inMutable = true; + mDecodeOptions.inSampleSize = sampleSize; + decodeOptions = mDecodeOptions; + } + try { + return loadBitmap(uri, decodeOptions); + } finally { + synchronized (mLock) { + mDecodeOptions = null; + } + } + } + + /** + * Returns the bitmap from the given uri loaded using the given options. + * Returns null on failure. + */ + private Bitmap loadBitmap(Uri uri, BitmapFactory.Options o) throws FileNotFoundException { + if (uri == null || mContext == null) { + throw new IllegalArgumentException("bad argument to loadBitmap"); + } + InputStream is = null; + try { + is = mContext.getContentResolver().openInputStream(uri); + return BitmapFactory.decodeStream(is, null, o); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException t) { + Log.w(LOG_TAG, "close fail ", t); + } + } + } + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentFile.java b/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentFile.java new file mode 100644 index 00000000000..0529da8ff27 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentFile.java @@ -0,0 +1,345 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.provider; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; + +import java.io.File; + +/** + * Representation of a document backed by either a + * {@link android.provider.DocumentsProvider} or a raw file on disk. This is a + * utility class designed to emulate the traditional {@link File} interface. It + * offers a simplified view of a tree of documents, but it has substantial + * overhead. For optimal performance and a richer feature set, use the + * {@link android.provider.DocumentsContract} methods and constants directly. + *

    + * There are several differences between documents and traditional files: + *

      + *
    • Documents express their display name and MIME type as separate fields, + * instead of relying on file extensions. Some documents providers may still + * choose to append extensions to their display names, but that's an + * implementation detail. + *
    • A single document may appear as the child of multiple directories, so it + * doesn't inherently know who its parent is. That is, documents don't have a + * strong notion of path. You can easily traverse a tree of documents from + * parent to child, but not from child to parent. + *
    • Each document has a unique identifier within that provider. This + * identifier is an opaque implementation detail of the provider, and + * as such it must not be parsed. + *
    + *

    + * Before using this class, first consider if you really need access to an + * entire subtree of documents. The principle of least privilege dictates that + * you should only ask for access to documents you really need. If you only need + * the user to pick a single file, use {@link Intent#ACTION_OPEN_DOCUMENT} or + * {@link Intent#ACTION_GET_CONTENT}. If you want to let the user pick multiple + * files, add {@link Intent#EXTRA_ALLOW_MULTIPLE}. If you only need the user to + * save a single file, use {@link Intent#ACTION_CREATE_DOCUMENT}. If you use + * these APIs, you can pass the resulting {@link Intent#getData()} into + * {@link #fromSingleUri(Context, Uri)} to work with that document. + *

    + * If you really do need full access to an entire subtree of documents, start by + * launching {@link Intent#ACTION_OPEN_DOCUMENT_TREE} to let the user pick a + * directory. Then pass the resulting {@link Intent#getData()} into + * {@link #fromTreeUri(Context, Uri)} to start working with the user selected + * tree. + *

    + * As you navigate the tree of DocumentFile instances, you can always use + * {@link #getUri()} to obtain the Uri representing the underlying document for + * that object, for use with {@link ContentResolver#openInputStream(Uri)}, etc. + *

    + * To simplify your code on devices running + * {@link android.os.Build.VERSION_CODES#KITKAT} or earlier, you can use + * {@link #fromFile(File)} which emulates the behavior of a + * {@link android.provider.DocumentsProvider}. + * + * @see android.provider.DocumentsProvider + * @see android.provider.DocumentsContract + */ +public abstract class DocumentFile { + static final String TAG = "DocumentFile"; + + private final DocumentFile mParent; + + DocumentFile(DocumentFile parent) { + mParent = parent; + } + + /** + * Create a {@link DocumentFile} representing the filesystem tree rooted at + * the given {@link Uri}. This doesn't give you any additional access to the + * underlying files beyond what your app already has. + *

    + * {@link #getUri()} will return {@code file://} Uris for files explored + * through this tree. + */ + public static DocumentFile fromFile(File file) { + return new RawDocumentFile(null, file); + } + + /** + * Create a {@link DocumentFile} representing the single document at the + * given {@link Uri}. This is only useful on devices running + * {@link android.os.Build.VERSION_CODES#KITKAT} or later, and will return + * {@code null} when called on earlier platform versions. + * + * @param singleUri the {@link Intent#getData()} from a successful + * {@link Intent#ACTION_OPEN_DOCUMENT} or + * {@link Intent#ACTION_CREATE_DOCUMENT} request. + */ + public static DocumentFile fromSingleUri(Context context, Uri singleUri) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return new SingleDocumentFile(null, context, singleUri); + } else { + return null; + } + } + + /** + * Create a {@link DocumentFile} representing the document tree rooted at + * the given {@link Uri}. This is only useful on devices running + * {@link android.os.Build.VERSION_CODES#LOLLIPOP} or later, and will return + * {@code null} when called on earlier platform versions. + * + * @param treeUri the {@link Intent#getData()} from a successful + * {@link Intent#ACTION_OPEN_DOCUMENT_TREE} request. + */ + public static DocumentFile fromTreeUri(Context context, Uri treeUri) { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + return new TreeDocumentFile(null, context, + DocumentsContractApi21.prepareTreeUri(treeUri)); + } else { + return null; + } + } + + /** + * Test if given Uri is backed by a + * {@link android.provider.DocumentsProvider}. + */ + public static boolean isDocumentUri(Context context, Uri uri) { + final int version = Build.VERSION.SDK_INT; + if (version >= 19) { + return DocumentsContractApi19.isDocumentUri(context, uri); + } else { + return false; + } + } + + /** + * Create a new document as a direct child of this directory. + * + * @param mimeType MIME type of new document, such as {@code image/png} or + * {@code audio/flac} + * @param displayName name of new document, without any file extension + * appended; the underlying provider may choose to append the + * extension + * @return file representing newly created document, or null if failed + * @throws UnsupportedOperationException when working with a single document + * created from {@link #fromSingleUri(Context, Uri)}. + * @see android.provider.DocumentsContract#createDocument(ContentResolver, + * Uri, String, String) + */ + public abstract DocumentFile createFile(String mimeType, String displayName); + + /** + * Create a new directory as a direct child of this directory. + * + * @param displayName name of new directory + * @return file representing newly created directory, or null if failed + * @throws UnsupportedOperationException when working with a single document + * created from {@link #fromSingleUri(Context, Uri)}. + * @see android.provider.DocumentsContract#createDocument(ContentResolver, + * Uri, String, String) + */ + public abstract DocumentFile createDirectory(String displayName); + + /** + * Return a Uri for the underlying document represented by this file. This + * can be used with other platform APIs to manipulate or share the + * underlying content. You can use {@link #isDocumentUri(Context, Uri)} to + * test if the returned Uri is backed by a + * {@link android.provider.DocumentsProvider}. + * + * @see Intent#setData(Uri) + * @see Intent#setClipData(android.content.ClipData) + * @see ContentResolver#openInputStream(Uri) + * @see ContentResolver#openOutputStream(Uri) + * @see ContentResolver#openFileDescriptor(Uri, String) + */ + public abstract Uri getUri(); + + /** + * Return the display name of this document. + * + * @see android.provider.DocumentsContract.Document#COLUMN_DISPLAY_NAME + */ + public abstract String getName(); + + /** + * Return the MIME type of this document. + * + * @see android.provider.DocumentsContract.Document#COLUMN_MIME_TYPE + */ + public abstract String getType(); + + /** + * Return the parent file of this document. Only defined inside of the + * user-selected tree; you can never escape above the top of the tree. + *

    + * The underlying {@link android.provider.DocumentsProvider} only defines a + * forward mapping from parent to child, so the reverse mapping of child to + * parent offered here is purely a convenience method, and it may be + * incorrect if the underlying tree structure changes. + */ + public DocumentFile getParentFile() { + return mParent; + } + + /** + * Indicates if this file represents a directory. + * + * @return {@code true} if this file is a directory, {@code false} + * otherwise. + * @see android.provider.DocumentsContract.Document#MIME_TYPE_DIR + */ + public abstract boolean isDirectory(); + + /** + * Indicates if this file represents a file. + * + * @return {@code true} if this file is a file, {@code false} otherwise. + * @see android.provider.DocumentsContract.Document#COLUMN_MIME_TYPE + */ + public abstract boolean isFile(); + + /** + * Returns the time when this file was last modified, measured in + * milliseconds since January 1st, 1970, midnight. Returns 0 if the file + * does not exist, or if the modified time is unknown. + * + * @return the time when this file was last modified. + * @see android.provider.DocumentsContract.Document#COLUMN_LAST_MODIFIED + */ + public abstract long lastModified(); + + /** + * Returns the length of this file in bytes. Returns 0 if the file does not + * exist, or if the length is unknown. The result for a directory is not + * defined. + * + * @return the number of bytes in this file. + * @see android.provider.DocumentsContract.Document#COLUMN_SIZE + */ + public abstract long length(); + + /** + * Indicates whether the current context is allowed to read from this file. + * + * @return {@code true} if this file can be read, {@code false} otherwise. + */ + public abstract boolean canRead(); + + /** + * Indicates whether the current context is allowed to write to this file. + * + * @return {@code true} if this file can be written, {@code false} + * otherwise. + * @see android.provider.DocumentsContract.Document#COLUMN_FLAGS + * @see android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE + * @see android.provider.DocumentsContract.Document#FLAG_SUPPORTS_WRITE + * @see android.provider.DocumentsContract.Document#FLAG_DIR_SUPPORTS_CREATE + */ + public abstract boolean canWrite(); + + /** + * Deletes this file. + *

    + * Note that this method does not throw {@code IOException} on + * failure. Callers must check the return value. + * + * @return {@code true} if this file was deleted, {@code false} otherwise. + * @see android.provider.DocumentsContract#deleteDocument(ContentResolver, + * Uri) + */ + public abstract boolean delete(); + + /** + * Returns a boolean indicating whether this file can be found. + * + * @return {@code true} if this file exists, {@code false} otherwise. + */ + public abstract boolean exists(); + + /** + * Returns an array of files contained in the directory represented by this + * file. + * + * @return an array of files or {@code null}. + * @throws UnsupportedOperationException when working with a single document + * created from {@link #fromSingleUri(Context, Uri)}. + * @see android.provider.DocumentsContract#buildChildDocumentsUriUsingTree(Uri, + * String) + */ + public abstract DocumentFile[] listFiles(); + + /** + * Search through {@link #listFiles()} for the first document matching the + * given display name. Returns {@code null} when no matching document is + * found. + * + * @throws UnsupportedOperationException when working with a single document + * created from {@link #fromSingleUri(Context, Uri)}. + */ + public DocumentFile findFile(String displayName) { + for (DocumentFile doc : listFiles()) { + if (displayName.equals(doc.getName())) { + return doc; + } + } + return null; + } + + /** + * Renames this file to {@code displayName}. + *

    + * Note that this method does not throw {@code IOException} on + * failure. Callers must check the return value. + *

    + * Some providers may need to create a new document to reflect the rename, + * potentially with a different MIME type, so {@link #getUri()} and + * {@link #getType()} may change to reflect the rename. + *

    + * When renaming a directory, children previously enumerated through + * {@link #listFiles()} may no longer be valid. + * + * @param displayName the new display name. + * @return true on success. + * @throws UnsupportedOperationException when working with a single document + * created from {@link #fromSingleUri(Context, Uri)}. + * @see android.provider.DocumentsContract#renameDocument(ContentResolver, + * Uri, String) + */ + public abstract boolean renameTo(String displayName); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi19.java b/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi19.java new file mode 100644 index 00000000000..3905e39ff8d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi19.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.provider; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.database.Cursor; +import android.net.Uri; +import android.provider.DocumentsContract; +import android.text.TextUtils; +import android.util.Log; + +class DocumentsContractApi19 { + private static final String TAG = "DocumentFile"; + + public static boolean isDocumentUri(Context context, Uri self) { + return DocumentsContract.isDocumentUri(context, self); + } + + public static String getName(Context context, Uri self) { + return queryForString(context, self, DocumentsContract.Document.COLUMN_DISPLAY_NAME, null); + } + + private static String getRawType(Context context, Uri self) { + return queryForString(context, self, DocumentsContract.Document.COLUMN_MIME_TYPE, null); + } + + public static String getType(Context context, Uri self) { + final String rawType = getRawType(context, self); + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(rawType)) { + return null; + } else { + return rawType; + } + } + + public static boolean isDirectory(Context context, Uri self) { + return DocumentsContract.Document.MIME_TYPE_DIR.equals(getRawType(context, self)); + } + + public static boolean isFile(Context context, Uri self) { + final String type = getRawType(context, self); + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(type) || TextUtils.isEmpty(type)) { + return false; + } else { + return true; + } + } + + public static long lastModified(Context context, Uri self) { + return queryForLong(context, self, DocumentsContract.Document.COLUMN_LAST_MODIFIED, 0); + } + + public static long length(Context context, Uri self) { + return queryForLong(context, self, DocumentsContract.Document.COLUMN_SIZE, 0); + } + + public static boolean canRead(Context context, Uri self) { + // Ignore if grant doesn't allow read + if (context.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_READ_URI_PERMISSION) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + + // Ignore documents without MIME + if (TextUtils.isEmpty(getRawType(context, self))) { + return false; + } + + return true; + } + + public static boolean canWrite(Context context, Uri self) { + // Ignore if grant doesn't allow write + if (context.checkCallingOrSelfUriPermission(self, Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + != PackageManager.PERMISSION_GRANTED) { + return false; + } + + final String type = getRawType(context, self); + final int flags = queryForInt(context, self, DocumentsContract.Document.COLUMN_FLAGS, 0); + + // Ignore documents without MIME + if (TextUtils.isEmpty(type)) { + return false; + } + + // Deletable documents considered writable + if ((flags & DocumentsContract.Document.FLAG_SUPPORTS_DELETE) != 0) { + return true; + } + + if (DocumentsContract.Document.MIME_TYPE_DIR.equals(type) + && (flags & DocumentsContract.Document.FLAG_DIR_SUPPORTS_CREATE) != 0) { + // Directories that allow create considered writable + return true; + } else if (!TextUtils.isEmpty(type) + && (flags & DocumentsContract.Document.FLAG_SUPPORTS_WRITE) != 0) { + // Writable normal files considered writable + return true; + } + + return false; + } + + public static boolean delete(Context context, Uri self) { + return DocumentsContract.deleteDocument(context.getContentResolver(), self); + } + + public static boolean exists(Context context, Uri self) { + final ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(self, new String[] { + DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + return c.getCount() > 0; + } catch (Exception e) { + Log.w(TAG, "Failed query: " + e); + return false; + } finally { + closeQuietly(c); + } + } + + private static String queryForString(Context context, Uri self, String column, + String defaultValue) { + final ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(self, new String[] { column }, null, null, null); + if (c.moveToFirst() && !c.isNull(0)) { + return c.getString(0); + } else { + return defaultValue; + } + } catch (Exception e) { + Log.w(TAG, "Failed query: " + e); + return defaultValue; + } finally { + closeQuietly(c); + } + } + + private static int queryForInt(Context context, Uri self, String column, + int defaultValue) { + return (int) queryForLong(context, self, column, defaultValue); + } + + private static long queryForLong(Context context, Uri self, String column, + long defaultValue) { + final ContentResolver resolver = context.getContentResolver(); + + Cursor c = null; + try { + c = resolver.query(self, new String[] { column }, null, null, null); + if (c.moveToFirst() && !c.isNull(0)) { + return c.getLong(0); + } else { + return defaultValue; + } + } catch (Exception e) { + Log.w(TAG, "Failed query: " + e); + return defaultValue; + } finally { + closeQuietly(c); + } + } + + private static void closeQuietly(AutoCloseable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi21.java b/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi21.java new file mode 100644 index 00000000000..b583a0d08e3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/provider/DocumentsContractApi21.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.provider; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.DocumentsContract; +import android.util.Log; + +import java.util.ArrayList; + +class DocumentsContractApi21 { + private static final String TAG = "DocumentFile"; + + public static Uri createFile(Context context, Uri self, String mimeType, + String displayName) { + return DocumentsContract.createDocument(context.getContentResolver(), self, mimeType, + displayName); + } + + public static Uri createDirectory(Context context, Uri self, String displayName) { + return createFile(context, self, DocumentsContract.Document.MIME_TYPE_DIR, displayName); + } + + public static Uri prepareTreeUri(Uri treeUri) { + return DocumentsContract.buildDocumentUriUsingTree(treeUri, + DocumentsContract.getTreeDocumentId(treeUri)); + } + + public static Uri[] listFiles(Context context, Uri self) { + final ContentResolver resolver = context.getContentResolver(); + final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(self, + DocumentsContract.getDocumentId(self)); + final ArrayList results = new ArrayList(); + + Cursor c = null; + try { + c = resolver.query(childrenUri, new String[] { + DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null); + while (c.moveToNext()) { + final String documentId = c.getString(0); + final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(self, + documentId); + results.add(documentUri); + } + } catch (Exception e) { + Log.w(TAG, "Failed query: " + e); + } finally { + closeQuietly(c); + } + + return results.toArray(new Uri[results.size()]); + } + + public static Uri renameTo(Context context, Uri self, String displayName) { + return DocumentsContract.renameDocument(context.getContentResolver(), self, displayName); + } + + private static void closeQuietly(AutoCloseable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (RuntimeException rethrown) { + throw rethrown; + } catch (Exception ignored) { + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/provider/RawDocumentFile.java b/mobile/google-android-support-v4/src/android/support/v4/provider/RawDocumentFile.java new file mode 100644 index 00000000000..9dc97c65a02 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/provider/RawDocumentFile.java @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.provider; + +import android.net.Uri; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; + +class RawDocumentFile extends DocumentFile { + private File mFile; + + RawDocumentFile(DocumentFile parent, File file) { + super(parent); + mFile = file; + } + + @Override + public DocumentFile createFile(String mimeType, String displayName) { + // Tack on extension when valid MIME type provided + final String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); + if (extension != null) { + displayName += "." + extension; + } + final File target = new File(mFile, displayName); + try { + target.createNewFile(); + return new RawDocumentFile(this, target); + } catch (IOException e) { + Log.w(TAG, "Failed to createFile: " + e); + return null; + } + } + + @Override + public DocumentFile createDirectory(String displayName) { + final File target = new File(mFile, displayName); + if (target.isDirectory() || target.mkdir()) { + return new RawDocumentFile(this, target); + } else { + return null; + } + } + + @Override + public Uri getUri() { + return Uri.fromFile(mFile); + } + + @Override + public String getName() { + return mFile.getName(); + } + + @Override + public String getType() { + if (mFile.isDirectory()) { + return null; + } else { + return getTypeForName(mFile.getName()); + } + } + + @Override + public boolean isDirectory() { + return mFile.isDirectory(); + } + + @Override + public boolean isFile() { + return mFile.isFile(); + } + + @Override + public long lastModified() { + return mFile.lastModified(); + } + + @Override + public long length() { + return mFile.length(); + } + + @Override + public boolean canRead() { + return mFile.canRead(); + } + + @Override + public boolean canWrite() { + return mFile.canWrite(); + } + + @Override + public boolean delete() { + deleteContents(mFile); + return mFile.delete(); + } + + @Override + public boolean exists() { + return mFile.exists(); + } + + @Override + public DocumentFile[] listFiles() { + final ArrayList results = new ArrayList(); + final File[] files = mFile.listFiles(); + if (files != null) { + for (File file : files) { + results.add(new RawDocumentFile(this, file)); + } + } + return results.toArray(new DocumentFile[results.size()]); + } + + @Override + public boolean renameTo(String displayName) { + final File target = new File(mFile.getParentFile(), displayName); + if (mFile.renameTo(target)) { + mFile = target; + return true; + } else { + return false; + } + } + + private static String getTypeForName(String name) { + final int lastDot = name.lastIndexOf('.'); + if (lastDot >= 0) { + final String extension = name.substring(lastDot + 1).toLowerCase(); + final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); + if (mime != null) { + return mime; + } + } + + return "application/octet-stream"; + } + + private static boolean deleteContents(File dir) { + File[] files = dir.listFiles(); + boolean success = true; + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + success &= deleteContents(file); + } + if (!file.delete()) { + Log.w(TAG, "Failed to delete " + file); + success = false; + } + } + } + return success; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/provider/SingleDocumentFile.java b/mobile/google-android-support-v4/src/android/support/v4/provider/SingleDocumentFile.java new file mode 100644 index 00000000000..673d1dd00e2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/provider/SingleDocumentFile.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.provider; + +import android.content.Context; +import android.net.Uri; +import android.support.v4.provider.DocumentsContractApi19; + +class SingleDocumentFile extends DocumentFile { + private Context mContext; + private Uri mUri; + + SingleDocumentFile(DocumentFile parent, Context context, Uri uri) { + super(parent); + mContext = context; + mUri = uri; + } + + @Override + public DocumentFile createFile(String mimeType, String displayName) { + throw new UnsupportedOperationException(); + } + + @Override + public DocumentFile createDirectory(String displayName) { + throw new UnsupportedOperationException(); + } + + @Override + public Uri getUri() { + return mUri; + } + + @Override + public String getName() { + return DocumentsContractApi19.getName(mContext, mUri); + } + + @Override + public String getType() { + return DocumentsContractApi19.getType(mContext, mUri); + } + + @Override + public boolean isDirectory() { + return DocumentsContractApi19.isDirectory(mContext, mUri); + } + + @Override + public boolean isFile() { + return DocumentsContractApi19.isFile(mContext, mUri); + } + + @Override + public long lastModified() { + return DocumentsContractApi19.lastModified(mContext, mUri); + } + + @Override + public long length() { + return DocumentsContractApi19.length(mContext, mUri); + } + + @Override + public boolean canRead() { + return DocumentsContractApi19.canRead(mContext, mUri); + } + + @Override + public boolean canWrite() { + return DocumentsContractApi19.canWrite(mContext, mUri); + } + + @Override + public boolean delete() { + return DocumentsContractApi19.delete(mContext, mUri); + } + + @Override + public boolean exists() { + return DocumentsContractApi19.exists(mContext, mUri); + } + + @Override + public DocumentFile[] listFiles() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean renameTo(String displayName) { + throw new UnsupportedOperationException(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/provider/TreeDocumentFile.java b/mobile/google-android-support-v4/src/android/support/v4/provider/TreeDocumentFile.java new file mode 100644 index 00000000000..96983c06ba9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/provider/TreeDocumentFile.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.provider; + +import android.content.Context; +import android.net.Uri; + +class TreeDocumentFile extends DocumentFile { + private Context mContext; + private Uri mUri; + + TreeDocumentFile(DocumentFile parent, Context context, Uri uri) { + super(parent); + mContext = context; + mUri = uri; + } + + @Override + public DocumentFile createFile(String mimeType, String displayName) { + final Uri result = DocumentsContractApi21.createFile(mContext, mUri, mimeType, displayName); + return (result != null) ? new TreeDocumentFile(this, mContext, result) : null; + } + + @Override + public DocumentFile createDirectory(String displayName) { + final Uri result = DocumentsContractApi21.createDirectory(mContext, mUri, displayName); + return (result != null) ? new TreeDocumentFile(this, mContext, result) : null; + } + + @Override + public Uri getUri() { + return mUri; + } + + @Override + public String getName() { + return DocumentsContractApi19.getName(mContext, mUri); + } + + @Override + public String getType() { + return DocumentsContractApi19.getType(mContext, mUri); + } + + @Override + public boolean isDirectory() { + return DocumentsContractApi19.isDirectory(mContext, mUri); + } + + @Override + public boolean isFile() { + return DocumentsContractApi19.isFile(mContext, mUri); + } + + @Override + public long lastModified() { + return DocumentsContractApi19.lastModified(mContext, mUri); + } + + @Override + public long length() { + return DocumentsContractApi19.length(mContext, mUri); + } + + @Override + public boolean canRead() { + return DocumentsContractApi19.canRead(mContext, mUri); + } + + @Override + public boolean canWrite() { + return DocumentsContractApi19.canWrite(mContext, mUri); + } + + @Override + public boolean delete() { + return DocumentsContractApi19.delete(mContext, mUri); + } + + @Override + public boolean exists() { + return DocumentsContractApi19.exists(mContext, mUri); + } + + @Override + public DocumentFile[] listFiles() { + final Uri[] result = DocumentsContractApi21.listFiles(mContext, mUri); + final DocumentFile[] resultFiles = new DocumentFile[result.length]; + for (int i = 0; i < result.length; i++) { + resultFiles[i] = new TreeDocumentFile(this, mContext, result[i]); + } + return resultFiles; + } + + @Override + public boolean renameTo(String displayName) { + final Uri result = DocumentsContractApi21.renameTo(mContext, mUri, displayName); + if (result != null) { + mUri = result; + return true; + } else { + return false; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICS.java b/mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICS.java new file mode 100644 index 00000000000..4e0c041c6b4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICS.java @@ -0,0 +1,27 @@ +package android.support.v4.speech.tts; + +import android.content.Context; +import android.os.Build; +import android.speech.tts.TextToSpeech; +import android.speech.tts.TextToSpeech.OnInitListener; +import android.util.Log; + +/** Helper class for TTS functionality introduced in ICS */ +class TextToSpeechICS { + private static final String TAG = "android.support.v4.speech.tts"; + + static TextToSpeech construct(Context context, OnInitListener onInitListener, + String engineName) { + if (Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { + if (engineName == null) { + return new TextToSpeech(context, onInitListener); + } else { + Log.w(TAG, "Can't specify tts engine on this device"); + return new TextToSpeech(context, onInitListener); + } + } else { + return new TextToSpeech(context, onInitListener, engineName); + } + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICSMR1.java b/mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICSMR1.java new file mode 100644 index 00000000000..bbad4c13ff5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/speech/tts/TextToSpeechICSMR1.java @@ -0,0 +1,74 @@ +package android.support.v4.speech.tts; + +import android.speech.tts.TextToSpeech; +import android.speech.tts.UtteranceProgressListener; +import android.speech.tts.TextToSpeech.OnUtteranceCompletedListener; + +import java.util.Locale; +import java.util.Set; + +/** Helper class for TTS functionality introduced in ICS MR1 */ +class TextToSpeechICSMR1 { + /** + * Call {@link TextToSpeech#getFeatures} if available. + * + * @return {@link TextToSpeech#getFeatures} or null on older devices. + */ + static Set getFeatures(TextToSpeech tts, Locale locale) { + if (android.os.Build.VERSION.SDK_INT >= + android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + return tts.getFeatures(locale); + } + return null; + } + + public static final String KEY_FEATURE_EMBEDDED_SYNTHESIS = "embeddedTts"; + public static final String KEY_FEATURE_NETWORK_SYNTHESIS = "networkTts"; + + static interface UtteranceProgressListenerICSMR1 { + void onDone(String utteranceId); + void onError(String utteranceId); + void onStart(String utteranceId); + } + + /** + * Call {@link TextToSpeech#setOnUtteranceProgressListener} if ICS-MR1 or newer. + * + * On pre ICS-MR1 devices,{@link TextToSpeech#setOnUtteranceCompletedListener} is + * used to emulate its behavior - at the end of synthesis we call + * {@link UtteranceProgressListenerICSMR1#onStart(String)} and + * {@link UtteranceProgressListenerICSMR1#onDone(String)} one after the other. + * Errors can't be detected. + */ + static void setUtteranceProgressListener(TextToSpeech tts, + final UtteranceProgressListenerICSMR1 listener) { + if (android.os.Build.VERSION.SDK_INT >= + android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + tts.setOnUtteranceProgressListener(new UtteranceProgressListener() { + @Override + public void onStart(String utteranceId) { + listener.onStart(utteranceId); + } + + @Override + public void onError(String utteranceId) { + listener.onError(utteranceId); + } + + @Override + public void onDone(String utteranceId) { + listener.onDone(utteranceId); + } + }); + } else { + tts.setOnUtteranceCompletedListener(new OnUtteranceCompletedListener() { + @Override + public void onUtteranceCompleted(String utteranceId) { + // Emulate onStart. Clients are expecting it will happen. + listener.onStart(utteranceId); + listener.onDone(utteranceId); + } + }); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/text/BidiFormatter.java b/mobile/google-android-support-v4/src/android/support/v4/text/BidiFormatter.java new file mode 100644 index 00000000000..4b9bc3ae364 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/text/BidiFormatter.java @@ -0,0 +1,880 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.text; + +import android.support.v4.view.ViewCompat; + +import java.util.Locale; + +import static android.support.v4.text.TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR; + +/** + * Utility class for formatting text for display in a potentially opposite-directionality context + * without garbling. The directionality of the context is set at formatter creation and the + * directionality of the text can be either estimated or passed in when known. Provides the + * following functionality: + *

    + * 1. Bidi Wrapping + * When text in one language is mixed into a document in another, opposite-directionality language, + * e.g. when an English business name is embedded in a Hebrew web page, both the inserted string + * and the text surrounding it may be displayed incorrectly unless the inserted string is explicitly + * separated from the surrounding text in a "wrapper" that: + *

    + * - Declares its directionality so that the string is displayed correctly. This can be done in + * Unicode bidi formatting codes by {@link #unicodeWrap} and similar methods. + *

    + * - Isolates the string's directionality, so it does not unduly affect the surrounding content. + * Currently, this can only be done using invisible Unicode characters of the same direction as + * the context (LRM or RLM) in addition to the directionality declaration above, thus "resetting" + * the directionality to that of the context. The "reset" may need to be done at both ends of the + * string. Without "reset" after the string, the string will "stick" to a number or logically + * separate opposite-direction text that happens to follow it in-line (even if separated by + * neutral content like spaces and punctuation). Without "reset" before the string, the same can + * happen there, but only with more opposite-direction text, not a number. One approach is to + * "reset" the direction only after each string, on the theory that if the preceding opposite- + * direction text is itself bidi-wrapped, the "reset" after it will prevent the sticking. (Doing + * the "reset" only before each string definitely does not work because we do not want to require + * bidi-wrapping numbers, and a bidi-wrapped opposite-direction string could be followed by a + * number.) Still, the safest policy is to do the "reset" on both ends of each string, since RTL + * message translations often contain untranslated Latin-script brand names and technical terms, + * and one of these can be followed by a bidi-wrapped inserted value. On the other hand, when one + * has such a message, it is best to do the "reset" manually in the message translation itself, + * since the message's opposite-direction text could be followed by an inserted number, which we + * would not bidi-wrap anyway. Thus, "reset" only after the string is the current default. In an + * alternative to "reset", recent additions to the HTML, CSS, and Unicode standards allow the + * isolation to be part of the directionality declaration. This form of isolation is better than + * "reset" because it takes less space, does not require knowing the context directionality, has a + * gentler effect than "reset", and protects both ends of the string. However, we do not yet allow + * using it because required platforms do not yet support it. + *

    + * Providing these wrapping services is the basic purpose of the bidi formatter. + *

    + * 2. Directionality estimation + * How does one know whether a string about to be inserted into surrounding text has the same + * directionality? Well, in many cases, one knows that this must be the case when writing the code + * doing the insertion, e.g. when a localized message is inserted into a localized page. In such + * cases there is no need to involve the bidi formatter at all. In some other cases, it need not be + * the same as the context, but is either constant (e.g. urls are always LTR) or otherwise known. + * In the remaining cases, e.g. when the string is user-entered or comes from a database, the + * language of the string (and thus its directionality) is not known a priori, and must be + * estimated at run-time. The bidi formatter can do this automatically using the default + * first-strong estimation algorithm. It can also be configured to use a custom directionality + * estimation object. + */ +public final class BidiFormatter { + + /** + * The default text direction heuristic. + */ + private static TextDirectionHeuristicCompat DEFAULT_TEXT_DIRECTION_HEURISTIC = FIRSTSTRONG_LTR; + + /** + * Unicode "Left-To-Right Embedding" (LRE) character. + */ + private static final char LRE = '\u202A'; + + /** + * Unicode "Right-To-Left Embedding" (RLE) character. + */ + private static final char RLE = '\u202B'; + + /** + * Unicode "Pop Directional Formatting" (PDF) character. + */ + private static final char PDF = '\u202C'; + + /** + * Unicode "Left-To-Right Mark" (LRM) character. + */ + private static final char LRM = '\u200E'; + + /* + * Unicode "Right-To-Left Mark" (RLM) character. + */ + private static final char RLM = '\u200F'; + + /* + * String representation of LRM + */ + private static final String LRM_STRING = Character.toString(LRM); + + /* + * String representation of RLM + */ + private static final String RLM_STRING = Character.toString(RLM); + + /** + * Empty string constant. + */ + private static final String EMPTY_STRING = ""; + + /** + * A class for building a BidiFormatter with non-default options. + */ + public static final class Builder { + private boolean mIsRtlContext; + private int mFlags; + private TextDirectionHeuristicCompat mTextDirectionHeuristicCompat; + + /** + * Constructor. + * + */ + public Builder() { + initialize(isRtlLocale(Locale.getDefault())); + } + + /** + * Constructor. + * + * @param rtlContext Whether the context directionality is RTL. + */ + public Builder(boolean rtlContext) { + initialize(rtlContext); + } + + /** + * Constructor. + * + * @param locale The context locale. + */ + public Builder(Locale locale) { + initialize(isRtlLocale(locale)); + } + + /** + * Initializes the builder with the given context directionality and default options. + * + * @param isRtlContext Whether the context is RTL or not. + */ + private void initialize(boolean isRtlContext) { + mIsRtlContext = isRtlContext; + mTextDirectionHeuristicCompat = DEFAULT_TEXT_DIRECTION_HEURISTIC; + mFlags = DEFAULT_FLAGS; + } + + /** + * Specifies whether the BidiFormatter to be built should also "reset" directionality before + * a string being bidi-wrapped, not just after it. The default is false. + */ + public Builder stereoReset(boolean stereoReset) { + if (stereoReset) { + mFlags |= FLAG_STEREO_RESET; + } else { + mFlags &= ~FLAG_STEREO_RESET; + } + return this; + } + + /** + * Specifies the default directionality estimation algorithm to be used by the BidiFormatter. + * By default, uses the first-strong heuristic. + * + * @param heuristic the {@code TextDirectionHeuristic} to use. + * @return the builder itself. + */ + public Builder setTextDirectionHeuristic(TextDirectionHeuristicCompat heuristic) { + mTextDirectionHeuristicCompat = heuristic; + return this; + } + + private static BidiFormatter getDefaultInstanceFromContext(boolean isRtlContext) { + return isRtlContext ? DEFAULT_RTL_INSTANCE : DEFAULT_LTR_INSTANCE; + } + + /** + * @return A BidiFormatter with the specified options. + */ + public BidiFormatter build() { + if (mFlags == DEFAULT_FLAGS && + mTextDirectionHeuristicCompat == DEFAULT_TEXT_DIRECTION_HEURISTIC) { + return getDefaultInstanceFromContext(mIsRtlContext); + } + return new BidiFormatter(mIsRtlContext, mFlags, mTextDirectionHeuristicCompat); + } + } + + // + private static final int FLAG_STEREO_RESET = 2; + private static final int DEFAULT_FLAGS = FLAG_STEREO_RESET; + + private static final BidiFormatter DEFAULT_LTR_INSTANCE = new BidiFormatter( + false /* LTR context */, + DEFAULT_FLAGS, + DEFAULT_TEXT_DIRECTION_HEURISTIC); + + private static final BidiFormatter DEFAULT_RTL_INSTANCE = new BidiFormatter( + true /* RTL context */, + DEFAULT_FLAGS, + DEFAULT_TEXT_DIRECTION_HEURISTIC); + + private final boolean mIsRtlContext; + private final int mFlags; + private final TextDirectionHeuristicCompat mDefaultTextDirectionHeuristicCompat; + + /** + * Factory for creating an instance of BidiFormatter for the default locale directionality. + * + */ + public static BidiFormatter getInstance() { + return new Builder().build(); + } + + /** + * Factory for creating an instance of BidiFormatter given the context directionality. + * + * @param rtlContext Whether the context directionality is RTL. + */ + public static BidiFormatter getInstance(boolean rtlContext) { + return new Builder(rtlContext).build(); + } + + /** + * Factory for creating an instance of BidiFormatter given the context locale. + * + * @param locale The context locale. + */ + public static BidiFormatter getInstance(Locale locale) { + return new Builder(locale).build(); + } + + /** + * @param isRtlContext Whether the context directionality is RTL or not. + * @param flags The option flags. + * @param heuristic The default text direction heuristic. + */ + private BidiFormatter(boolean isRtlContext, int flags, TextDirectionHeuristicCompat heuristic) { + mIsRtlContext = isRtlContext; + mFlags = flags; + mDefaultTextDirectionHeuristicCompat = heuristic; + } + + /** + * @return Whether the context directionality is RTL + */ + public boolean isRtlContext() { + return mIsRtlContext; + } + + /** + * @return Whether directionality "reset" should also be done before a string being + * bidi-wrapped, not just after it. + */ + public boolean getStereoReset() { + return (mFlags & FLAG_STEREO_RESET) != 0; + } + + /** + * Returns a Unicode bidi mark matching the context directionality (LRM or RLM) if either the + * overall or the exit directionality of a given string is opposite to the context directionality. + * Putting this after the string (including its directionality declaration wrapping) prevents it + * from "sticking" to other opposite-directionality text or a number appearing after it inline + * with only neutral content in between. Otherwise returns the empty string. While the exit + * directionality is determined by scanning the end of the string, the overall directionality is + * given explicitly by a heuristic to estimate the {@code str}'s directionality. + * + * @param str String after which the mark may need to appear. + * @param heuristic The text direction heuristic that will be used to estimate the {@code str}'s + * directionality. + * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context; + * else, the empty string. + */ + private String markAfter(String str, TextDirectionHeuristicCompat heuristic) { + final boolean isRtl = heuristic.isRtl(str, 0, str.length()); + // getExitDir() is called only if needed (short-circuit). + if (!mIsRtlContext && (isRtl || getExitDir(str) == DIR_RTL)) { + return LRM_STRING; + } + if (mIsRtlContext && (!isRtl || getExitDir(str) == DIR_LTR)) { + return RLM_STRING; + } + return EMPTY_STRING; + } + + /** + * Returns a Unicode bidi mark matching the context directionality (LRM or RLM) if either the + * overall or the entry directionality of a given string is opposite to the context + * directionality. Putting this before the string (including its directionality declaration + * wrapping) prevents it from "sticking" to other opposite-directionality text appearing before + * it inline with only neutral content in between. Otherwise returns the empty string. While the + * entry directionality is determined by scanning the beginning of the string, the overall + * directionality is given explicitly by a heuristic to estimate the {@code str}'s directionality. + * + * @param str String before which the mark may need to appear. + * @param heuristic The text direction heuristic that will be used to estimate the {@code str}'s + * directionality. + * @return LRM for RTL text in LTR context; RLM for LTR text in RTL context; + * else, the empty string. + */ + private String markBefore(String str, TextDirectionHeuristicCompat heuristic) { + final boolean isRtl = heuristic.isRtl(str, 0, str.length()); + // getEntryDir() is called only if needed (short-circuit). + if (!mIsRtlContext && (isRtl || getEntryDir(str) == DIR_RTL)) { + return LRM_STRING; + } + if (mIsRtlContext && (!isRtl || getEntryDir(str) == DIR_LTR)) { + return RLM_STRING; + } + return EMPTY_STRING; + } + + /** + * Estimates the directionality of a string using the default text direction heuristic. + * + * @param str String whose directionality is to be estimated. + * @return true if {@code str}'s estimated overall directionality is RTL. Otherwise returns + * false. + */ + public boolean isRtl(String str) { + return mDefaultTextDirectionHeuristicCompat.isRtl(str, 0, str.length()); + } + + /** + * Formats a string of given directionality for use in plain-text output of the context + * directionality, so an opposite-directionality string is neither garbled nor garbles its + * surroundings. This makes use of Unicode bidi formatting characters. + *

    + * The algorithm: In case the given directionality doesn't match the context directionality, wraps + * the string with Unicode bidi formatting characters: RLE+{@code str}+PDF for RTL text, or + * LRE+{@code str}+PDF for LTR text. + *

    + * If {@code isolate}, directionally isolates the string so that it does not garble its + * surroundings. Currently, this is done by "resetting" the directionality after the string by + * appending a trailing Unicode bidi mark matching the context directionality (LRM or RLM) when + * either the overall directionality or the exit directionality of the string is opposite to that + * of the context. If the formatter was built using {@link Builder#stereoReset(boolean)} and + * passing "true" as an argument, also prepends a Unicode bidi mark matching the context + * directionality when either the overall directionality or the entry directionality of the + * string is opposite to that of the context. Note that as opposed to the overall + * directionality, the entry and exit directionalities are determined from the string itself. + *

    + * Does *not* do HTML-escaping. + * + * @param str The input string. + * @param heuristic The algorithm to be used to estimate the string's overall direction. + * @param isolate Whether to directionally isolate the string to prevent it from garbling the + * content around it + * @return Input string after applying the above processing. + */ + public String unicodeWrap(String str, TextDirectionHeuristicCompat heuristic, boolean isolate) { + final boolean isRtl = heuristic.isRtl(str, 0, str.length()); + StringBuilder result = new StringBuilder(); + if (getStereoReset() && isolate) { + result.append(markBefore(str, + isRtl ? TextDirectionHeuristicsCompat.RTL : TextDirectionHeuristicsCompat.LTR)); + } + if (isRtl != mIsRtlContext) { + result.append(isRtl ? RLE : LRE); + result.append(str); + result.append(PDF); + } else { + result.append(str); + } + if (isolate) { + result.append(markAfter(str, + isRtl ? TextDirectionHeuristicsCompat.RTL : TextDirectionHeuristicsCompat.LTR)); + } + return result.toString(); + } + + /** + * Operates like {@link #unicodeWrap(String, android.support.v4.text.TextDirectionHeuristicCompat, boolean)}, but assumes + * {@code isolate} is true. + * + * @param str The input string. + * @param heuristic The algorithm to be used to estimate the string's overall direction. + * @return Input string after applying the above processing. + */ + public String unicodeWrap(String str, TextDirectionHeuristicCompat heuristic) { + return unicodeWrap(str, heuristic, true /* isolate */); + } + + /** + * Operates like {@link #unicodeWrap(String, android.support.v4.text.TextDirectionHeuristicCompat, boolean)}, but uses the + * formatter's default direction estimation algorithm. + * + * @param str The input string. + * @param isolate Whether to directionally isolate the string to prevent it from garbling the + * content around it + * @return Input string after applying the above processing. + */ + public String unicodeWrap(String str, boolean isolate) { + return unicodeWrap(str, mDefaultTextDirectionHeuristicCompat, isolate); + } + + /** + * Operates like {@link #unicodeWrap(String, android.support.v4.text.TextDirectionHeuristicCompat, boolean)}, but uses the + * formatter's default direction estimation algorithm and assumes {@code isolate} is true. + * + * @param str The input string. + * @return Input string after applying the above processing. + */ + public String unicodeWrap(String str) { + return unicodeWrap(str, mDefaultTextDirectionHeuristicCompat, true /* isolate */); + } + + /** + * Helper method to return true if the Locale directionality is RTL. + * + * @param locale The Locale whose directionality will be checked to be RTL or LTR + * @return true if the {@code locale} directionality is RTL. False otherwise. + */ + private static boolean isRtlLocale(Locale locale) { + return (TextUtilsCompat.getLayoutDirectionFromLocale(locale) == ViewCompat.LAYOUT_DIRECTION_RTL); + } + + /** + * Enum for directionality type. + */ + private static final int DIR_LTR = -1; + private static final int DIR_UNKNOWN = 0; + private static final int DIR_RTL = +1; + + /** + * Returns the directionality of the last character with strong directionality in the string, or + * DIR_UNKNOWN if none was encountered. For efficiency, actually scans backwards from the end of + * the string. Treats a non-BN character between an LRE/RLE/LRO/RLO and its matching PDF as a + * strong character, LTR after LRE/LRO, and RTL after RLE/RLO. The results are undefined for a + * string containing unbalanced LRE/RLE/LRO/RLO/PDF characters. The intended use is to check + * whether a logically separate item that starts with a number or a character of the string's + * exit directionality and follows this string inline (not counting any neutral characters in + * between) would "stick" to it in an opposite-directionality context, thus being displayed in + * an incorrect position. An LRM or RLM character (the one of the context's directionality) + * between the two will prevent such sticking. + * + * @param str the string to check. + */ + private static int getExitDir(String str) { + return new DirectionalityEstimator(str, false /* isHtml */).getExitDir(); + } + + /** + * Returns the directionality of the first character with strong directionality in the string, + * or DIR_UNKNOWN if none was encountered. Treats a non-BN character between an + * LRE/RLE/LRO/RLO and its matching PDF as a strong character, LTR after LRE/LRO, and RTL after + * RLE/RLO. The results are undefined for a string containing unbalanced LRE/RLE/LRO/RLO/PDF + * characters. The intended use is to check whether a logically separate item that ends with a + * character of the string's entry directionality and precedes the string inline (not counting + * any neutral characters in between) would "stick" to it in an opposite-directionality context, + * thus being displayed in an incorrect position. An LRM or RLM character (the one of the + * context's directionality) between the two will prevent such sticking. + * + * @param str the string to check. + */ + private static int getEntryDir(String str) { + return new DirectionalityEstimator(str, false /* isHtml */).getEntryDir(); + } + + /** + * An object that estimates the directionality of a given string by various methods. + * + */ + private static class DirectionalityEstimator { + + // Internal static variables and constants. + + /** + * Size of the bidi character class cache. The results of the Character.getDirectionality() + * calls on the lowest DIR_TYPE_CACHE_SIZE codepoints are kept in an array for speed. + * The 0x700 value is designed to leave all the European and Near Eastern languages in the + * cache. It can be reduced to 0x180, restricting the cache to the Western European + * languages. + */ + private static final int DIR_TYPE_CACHE_SIZE = 0x700; + + /** + * The bidi character class cache. + */ + private static final byte DIR_TYPE_CACHE[]; + + static { + DIR_TYPE_CACHE = new byte[DIR_TYPE_CACHE_SIZE]; + for (int i = 0; i < DIR_TYPE_CACHE_SIZE; i++) { + DIR_TYPE_CACHE[i] = Character.getDirectionality(i); + } + } + + // Internal instance variables. + + /** + * The text to be scanned. + */ + private final String text; + + /** + * Whether the text to be scanned is to be treated as HTML, i.e. skipping over tags and + * entities when looking for the next / preceding dir type. + */ + private final boolean isHtml; + + /** + * The length of the text in chars. + */ + private final int length; + + /** + * The current position in the text. + */ + private int charIndex; + + /** + * The char encountered by the last dirTypeForward or dirTypeBackward call. If it + * encountered a supplementary codepoint, this contains a char that is not a valid + * codepoint. This is ok, because this member is only used to detect some well-known ASCII + * syntax, e.g. "http://" and the beginning of an HTML tag or entity. + */ + private char lastChar; + + /** + * Constructor. + * + * @param text The string to scan. + * @param isHtml Whether the text to be scanned is to be treated as HTML, i.e. skipping over + * tags and entities. + */ + DirectionalityEstimator(String text, boolean isHtml) { + this.text = text; + this.isHtml = isHtml; + length = text.length(); + } + + /** + * Returns the directionality of the first character with strong directionality in the + * string, or DIR_UNKNOWN if none was encountered. Treats a non-BN character between an + * LRE/RLE/LRO/RLO and its matching PDF as a strong character, LTR after LRE/LRO, and RTL + * after RLE/RLO. The results are undefined for a string containing unbalanced + * LRE/RLE/LRO/RLO/PDF characters. + */ + int getEntryDir() { + // The reason for this method name, as opposed to getFirstStrongDir(), is that + // "first strong" is a commonly used description of Unicode's estimation algorithm, + // but the two must treat formatting characters quite differently. Thus, we are staying + // away from both "first" and "last" in these method names to avoid confusion. + charIndex = 0; + int embeddingLevel = 0; + int embeddingLevelDir = DIR_UNKNOWN; + int firstNonEmptyEmbeddingLevel = 0; + while (charIndex < length && firstNonEmptyEmbeddingLevel == 0) { + switch (dirTypeForward()) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + ++embeddingLevel; + embeddingLevelDir = DIR_LTR; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + ++embeddingLevel; + embeddingLevelDir = DIR_RTL; + break; + case Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT: + --embeddingLevel; + // To restore embeddingLevelDir to its previous value, we would need a + // stack, which we want to avoid. Thus, at this point we do not know the + // current embedding's directionality. + embeddingLevelDir = DIR_UNKNOWN; + break; + case Character.DIRECTIONALITY_BOUNDARY_NEUTRAL: + break; + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + if (embeddingLevel == 0) { + return DIR_LTR; + } + firstNonEmptyEmbeddingLevel = embeddingLevel; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + if (embeddingLevel == 0) { + return DIR_RTL; + } + firstNonEmptyEmbeddingLevel = embeddingLevel; + break; + default: + firstNonEmptyEmbeddingLevel = embeddingLevel; + break; + } + } + + // We have either found a non-empty embedding or scanned the entire string finding + // neither a non-empty embedding nor a strong character outside of an embedding. + if (firstNonEmptyEmbeddingLevel == 0) { + // We have not found a non-empty embedding. Thus, the string contains neither a + // non-empty embedding nor a strong character outside of an embedding. + return DIR_UNKNOWN; + } + + // We have found a non-empty embedding. + if (embeddingLevelDir != DIR_UNKNOWN) { + // We know the directionality of the non-empty embedding. + return embeddingLevelDir; + } + + // We do not remember the directionality of the non-empty embedding we found. So, we go + // backwards to find the start of the non-empty embedding and get its directionality. + while (charIndex > 0) { + switch (dirTypeBackward()) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + if (firstNonEmptyEmbeddingLevel == embeddingLevel) { + return DIR_LTR; + } + --embeddingLevel; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + if (firstNonEmptyEmbeddingLevel == embeddingLevel) { + return DIR_RTL; + } + --embeddingLevel; + break; + case Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT: + ++embeddingLevel; + break; + } + } + // We should never get here. + return DIR_UNKNOWN; + } + + /** + * Returns the directionality of the last character with strong directionality in the + * string, or DIR_UNKNOWN if none was encountered. For efficiency, actually scans backwards + * from the end of the string. Treats a non-BN character between an LRE/RLE/LRO/RLO and its + * matching PDF as a strong character, LTR after LRE/LRO, and RTL after RLE/RLO. The results + * are undefined for a string containing unbalanced LRE/RLE/LRO/RLO/PDF characters. + */ + int getExitDir() { + // The reason for this method name, as opposed to getLastStrongDir(), is that "last + // strong" sounds like the exact opposite of "first strong", which is a commonly used + // description of Unicode's estimation algorithm (getUnicodeDir() above), but the two + // must treat formatting characters quite differently. Thus, we are staying away from + // both "first" and "last" in these method names to avoid confusion. + charIndex = length; + int embeddingLevel = 0; + int lastNonEmptyEmbeddingLevel = 0; + while (charIndex > 0) { + switch (dirTypeBackward()) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + if (embeddingLevel == 0) { + return DIR_LTR; + } + if (lastNonEmptyEmbeddingLevel == 0) { + lastNonEmptyEmbeddingLevel = embeddingLevel; + } + break; + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + if (lastNonEmptyEmbeddingLevel == embeddingLevel) { + return DIR_LTR; + } + --embeddingLevel; + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + if (embeddingLevel == 0) { + return DIR_RTL; + } + if (lastNonEmptyEmbeddingLevel == 0) { + lastNonEmptyEmbeddingLevel = embeddingLevel; + } + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + if (lastNonEmptyEmbeddingLevel == embeddingLevel) { + return DIR_RTL; + } + --embeddingLevel; + break; + case Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT: + ++embeddingLevel; + break; + case Character.DIRECTIONALITY_BOUNDARY_NEUTRAL: + break; + default: + if (lastNonEmptyEmbeddingLevel == 0) { + lastNonEmptyEmbeddingLevel = embeddingLevel; + } + break; + } + } + return DIR_UNKNOWN; + } + + // Internal methods + + /** + * Gets the bidi character class, i.e. Character.getDirectionality(), of a given char, using + * a cache for speed. Not designed for supplementary codepoints, whose results we do not + * cache. + */ + private static byte getCachedDirectionality(char c) { + return c < DIR_TYPE_CACHE_SIZE ? DIR_TYPE_CACHE[c] : Character.getDirectionality(c); + } + + /** + * Returns the Character.DIRECTIONALITY_... value of the next codepoint and advances + * charIndex. If isHtml, and the codepoint is '<' or '&', advances through the tag/entity, + * and returns Character.DIRECTIONALITY_WHITESPACE. For an entity, it would be best to + * figure out the actual character, and return its dirtype, but treating it as whitespace is + * good enough for our purposes. + * + * @throws java.lang.IndexOutOfBoundsException if called when charIndex >= length or < 0. + */ + byte dirTypeForward() { + lastChar = text.charAt(charIndex); + if (Character.isHighSurrogate(lastChar)) { + int codePoint = Character.codePointAt(text, charIndex); + charIndex += Character.charCount(codePoint); + return Character.getDirectionality(codePoint); + } + charIndex++; + byte dirType = getCachedDirectionality(lastChar); + if (isHtml) { + // Process tags and entities. + if (lastChar == '<') { + dirType = skipTagForward(); + } else if (lastChar == '&') { + dirType = skipEntityForward(); + } + } + return dirType; + } + + /** + * Returns the Character.DIRECTIONALITY_... value of the preceding codepoint and advances + * charIndex backwards. If isHtml, and the codepoint is the end of a complete HTML tag or + * entity, advances over the whole tag/entity and returns + * Character.DIRECTIONALITY_WHITESPACE. For an entity, it would be best to figure out the + * actual character, and return its dirtype, but treating it as whitespace is good enough + * for our purposes. + * + * @throws java.lang.IndexOutOfBoundsException if called when charIndex > length or <= 0. + */ + byte dirTypeBackward() { + lastChar = text.charAt(charIndex - 1); + if (Character.isLowSurrogate(lastChar)) { + int codePoint = Character.codePointBefore(text, charIndex); + charIndex -= Character.charCount(codePoint); + return Character.getDirectionality(codePoint); + } + charIndex--; + byte dirType = getCachedDirectionality(lastChar); + if (isHtml) { + // Process tags and entities. + if (lastChar == '>') { + dirType = skipTagBackward(); + } else if (lastChar == ';') { + dirType = skipEntityBackward(); + } + } + return dirType; + } + + /** + * Advances charIndex forward through an HTML tag (after the opening < has already been + * read) and returns Character.DIRECTIONALITY_WHITESPACE. If there is no matching >, + * does not change charIndex and returns Character.DIRECTIONALITY_OTHER_NEUTRALS (for the + * < that hadn't been part of a tag after all). + */ + private byte skipTagForward() { + int initialCharIndex = charIndex; + while (charIndex < length) { + lastChar = text.charAt(charIndex++); + if (lastChar == '>') { + // The end of the tag. + return Character.DIRECTIONALITY_WHITESPACE; + } + if (lastChar == '"' || lastChar == '\'') { + // Skip over a quoted attribute value inside the tag. + char quote = lastChar; + while (charIndex < length && (lastChar = text.charAt(charIndex++)) != quote) {} + } + } + // The original '<' wasn't the start of a tag after all. + charIndex = initialCharIndex; + lastChar = '<'; + return Character.DIRECTIONALITY_OTHER_NEUTRALS; + } + + /** + * Advances charIndex backward through an HTML tag (after the closing > has already been + * read) and returns Character.DIRECTIONALITY_WHITESPACE. If there is no matching <, does + * not change charIndex and returns Character.DIRECTIONALITY_OTHER_NEUTRALS (for the > + * that hadn't been part of a tag after all). Nevertheless, the running time for calling + * skipTagBackward() in a loop remains linear in the size of the text, even for a text like + * ">>>>", because skipTagBackward() also stops looking for a matching < + * when it encounters another >. + */ + private byte skipTagBackward() { + int initialCharIndex = charIndex; + while (charIndex > 0) { + lastChar = text.charAt(--charIndex); + if (lastChar == '<') { + // The start of the tag. + return Character.DIRECTIONALITY_WHITESPACE; + } + if (lastChar == '>') { + break; + } + if (lastChar == '"' || lastChar == '\'') { + // Skip over a quoted attribute value inside the tag. + char quote = lastChar; + while (charIndex > 0 && (lastChar = text.charAt(--charIndex)) != quote) {} + } + } + // The original '>' wasn't the end of a tag after all. + charIndex = initialCharIndex; + lastChar = '>'; + return Character.DIRECTIONALITY_OTHER_NEUTRALS; + } + + /** + * Advances charIndex forward through an HTML character entity tag (after the opening + * & has already been read) and returns Character.DIRECTIONALITY_WHITESPACE. It would be + * best to figure out the actual character and return its dirtype, but this is good enough. + */ + private byte skipEntityForward() { + while (charIndex < length && (lastChar = text.charAt(charIndex++)) != ';') {} + return Character.DIRECTIONALITY_WHITESPACE; + } + + /** + * Advances charIndex backward through an HTML character entity tag (after the closing ; + * has already been read) and returns Character.DIRECTIONALITY_WHITESPACE. It would be best + * to figure out the actual character and return its dirtype, but this is good enough. + * If there is no matching &, does not change charIndex and returns + * Character.DIRECTIONALITY_OTHER_NEUTRALS (for the ';' that did not start an entity after + * all). Nevertheless, the running time for calling skipEntityBackward() in a loop remains + * linear in the size of the text, even for a text like ";;;;;;;", because skipTagBackward() + * also stops looking for a matching & when it encounters another ;. + */ + private byte skipEntityBackward() { + int initialCharIndex = charIndex; + while (charIndex > 0) { + lastChar = text.charAt(--charIndex); + if (lastChar == '&') { + return Character.DIRECTIONALITY_WHITESPACE; + } + if (lastChar == ';') { + break; + } + } + charIndex = initialCharIndex; + lastChar = ';'; + return Character.DIRECTIONALITY_OTHER_NEUTRALS; + } + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/text/ICUCompat.java b/mobile/google-android-support-v4/src/android/support/v4/text/ICUCompat.java new file mode 100644 index 00000000000..6df35ff7bba --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/text/ICUCompat.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.text; + +import android.os.Build; + +public class ICUCompat { + + interface ICUCompatImpl { + public String getScript(String locale); + public String addLikelySubtags(String locale); + } + + static class ICUCompatImplBase implements ICUCompatImpl { + @Override + public String getScript(String locale) { + return null; + } + + @Override + public String addLikelySubtags(String locale) { + return locale; + } + } + + static class ICUCompatImplIcs implements ICUCompatImpl { + @Override + public String getScript(String locale) { + return ICUCompatIcs.getScript(locale); + } + + @Override + public String addLikelySubtags(String locale) { + return ICUCompatIcs.addLikelySubtags(locale); + } + } + + private static final ICUCompatImpl IMPL; + + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 14) { + IMPL = new ICUCompatImplIcs(); + } else { + IMPL = new ICUCompatImplBase(); + } + } + + /** + * Returns the script (language code) of a script. + * + * @param locale The locale. + * @return a String representing the script (language code) of the locale. + */ + public static String getScript(String locale) { + return IMPL.getScript(locale); + } + + /** + * Add the likely subtags for a provided locale ID, per the algorithm described in the following + * CLDR technical report: + * + * http://www.unicode.org/reports/tr35/#Likely_Subtags + * + * If locale is already in the maximal form, or there is no data available for maximization, + * it will be just returned. For example, "und-Zzzz" cannot be maximized, since there is no + * reasonable maximization. + * + * Examples: + * + * "en" maximizes to "en_Latn_US" + * "de" maximizes to "de_Latn_US" + * "sr" maximizes to "sr_Cyrl_RS" + * "sh" maximizes to "sr_Latn_RS" (Note this will not reverse.) + * "zh_Hani" maximizes to "zh_Hans_CN" (Note this will not reverse.) + + * @param locale The locale to maximize + * + * @return the maximized locale + */ + public static String addLikelySubtags(String locale) { + return IMPL.addLikelySubtags(locale); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/text/ICUCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/text/ICUCompatIcs.java new file mode 100644 index 00000000000..7dc5d3c1b75 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/text/ICUCompatIcs.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.text; + +import android.util.Log; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +class ICUCompatIcs { + + private static final String TAG = "ICUCompatIcs"; + + private static Method sGetScriptMethod; + private static Method sAddLikelySubtagsMethod; + + static { + try { + final Class clazz = Class.forName("libcore.icu.ICU"); + if (clazz != null) { + sGetScriptMethod = clazz.getMethod("getScript", + new Class[]{ String.class }); + sAddLikelySubtagsMethod = clazz.getMethod("addLikelySubtags", + new Class[]{ String.class }); + } + } catch (Exception e) { + // Nothing we can do here, we just log the exception + Log.w(TAG, e); + } + } + + public static String getScript(String locale) { + try { + if (sGetScriptMethod != null) { + final Object[] args = new Object[] { locale }; + return (String) sGetScriptMethod.invoke(null, args); + } + } catch (IllegalAccessException e) { + // Nothing we can do here, we just log the exception + Log.w(TAG, e); + } + catch (InvocationTargetException e) { + // Nothing we can do here, we just log the exception + Log.w(TAG, e); + } + return null; + } + + public static String addLikelySubtags(String locale) { + try { + if (sAddLikelySubtagsMethod != null) { + final Object[] args = new Object[] { locale }; + return (String) sAddLikelySubtagsMethod.invoke(null, args); + } + } catch (IllegalAccessException e) { + // Nothing we can do here, we just log the exception + Log.w(TAG, e); + } + catch (InvocationTargetException e) { + // Nothing we can do here, we just log the exception + Log.w(TAG, e); + } + return locale; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicCompat.java b/mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicCompat.java new file mode 100644 index 00000000000..ab9b2f70f1d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicCompat.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.text; + +/** + * Interface for objects that use a heuristic for guessing at the paragraph direction by examining text. + */ +public interface TextDirectionHeuristicCompat { + /** + * Guess if a chars array is in the RTL direction or not. + * + * @param array the char array. + * @param start start index, inclusive. + * @param count the length to check, must not be negative and not greater than + * {@code array.length - start}. + * @return true if all chars in the range are to be considered in a RTL direction, + * false otherwise. + */ + boolean isRtl(char[] array, int start, int count); + + /** + * Guess if a {@code CharSequence} is in the RTL direction or not. + * + * @param cs the CharSequence. + * @param start start index, inclusive. + * @param count the length to check, must not be negative and not greater than + * {@code CharSequence.length() - start}. + * @return true if all chars in the range are to be considered in a RTL direction, + * false otherwise. + */ + boolean isRtl(CharSequence cs, int start, int count); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicsCompat.java new file mode 100644 index 00000000000..1fddfd1ee6e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/text/TextDirectionHeuristicsCompat.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.text; + + +import android.support.v4.view.ViewCompat; + +import java.nio.CharBuffer; + +/** + * Some objects that implement TextDirectionHeuristic. + * + */ +public class TextDirectionHeuristicsCompat { + + /** + * Always decides that the direction is left to right. + */ + public static final android.support.v4.text.TextDirectionHeuristicCompat LTR = + new TextDirectionHeuristicInternal(null /* no algorithm */, false); + + /** + * Always decides that the direction is right to left. + */ + public static final android.support.v4.text.TextDirectionHeuristicCompat RTL = + new TextDirectionHeuristicInternal(null /* no algorithm */, true); + + /** + * Determines the direction based on the first strong directional character, including bidi + * format chars, falling back to left to right if it finds none. This is the default behavior + * of the Unicode Bidirectional Algorithm. + */ + public static final android.support.v4.text.TextDirectionHeuristicCompat FIRSTSTRONG_LTR = + new TextDirectionHeuristicInternal(FirstStrong.INSTANCE, false); + + /** + * Determines the direction based on the first strong directional character, including bidi + * format chars, falling back to right to left if it finds none. This is similar to the default + * behavior of the Unicode Bidirectional Algorithm, just with different fallback behavior. + */ + public static final android.support.v4.text.TextDirectionHeuristicCompat FIRSTSTRONG_RTL = + new TextDirectionHeuristicInternal(FirstStrong.INSTANCE, true); + + /** + * If the text contains any strong right to left non-format character, determines that the + * direction is right to left, falling back to left to right if it finds none. + */ + public static final android.support.v4.text.TextDirectionHeuristicCompat ANYRTL_LTR = + new TextDirectionHeuristicInternal(AnyStrong.INSTANCE_RTL, false); + + /** + * Force the paragraph direction to the Locale direction. Falls back to left to right. + */ + public static final android.support.v4.text.TextDirectionHeuristicCompat LOCALE = + TextDirectionHeuristicLocale.INSTANCE; + + /** + * State constants for taking care about true / false / unknown + */ + private static final int STATE_TRUE = 0; + private static final int STATE_FALSE = 1; + private static final int STATE_UNKNOWN = 2; + + private static int isRtlText(int directionality) { + switch (directionality) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + return STATE_FALSE; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + return STATE_TRUE; + default: + return STATE_UNKNOWN; + } + } + + private static int isRtlTextOrFormat(int directionality) { + switch (directionality) { + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_EMBEDDING: + case Character.DIRECTIONALITY_LEFT_TO_RIGHT_OVERRIDE: + return STATE_FALSE; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + return STATE_TRUE; + default: + return STATE_UNKNOWN; + } + } + + /** + * Computes the text direction based on an algorithm. Subclasses implement + * {@link #defaultIsRtl} to handle cases where the algorithm cannot determine the + * direction from the text alone. + */ + private static abstract class TextDirectionHeuristicImpl implements TextDirectionHeuristicCompat { + private final TextDirectionAlgorithm mAlgorithm; + + public TextDirectionHeuristicImpl(TextDirectionAlgorithm algorithm) { + mAlgorithm = algorithm; + } + + /** + * Return true if the default text direction is rtl. + */ + abstract protected boolean defaultIsRtl(); + + @Override + public boolean isRtl(char[] array, int start, int count) { + return isRtl(CharBuffer.wrap(array), start, count); + } + + @Override + public boolean isRtl(CharSequence cs, int start, int count) { + if (cs == null || start < 0 || count < 0 || cs.length() - count < start) { + throw new IllegalArgumentException(); + } + if (mAlgorithm == null) { + return defaultIsRtl(); + } + return doCheck(cs, start, count); + } + + private boolean doCheck(CharSequence cs, int start, int count) { + switch(mAlgorithm.checkRtl(cs, start, count)) { + case STATE_TRUE: + return true; + case STATE_FALSE: + return false; + default: + return defaultIsRtl(); + } + } + } + + private static class TextDirectionHeuristicInternal extends TextDirectionHeuristicImpl { + private final boolean mDefaultIsRtl; + + private TextDirectionHeuristicInternal(TextDirectionAlgorithm algorithm, + boolean defaultIsRtl) { + super(algorithm); + mDefaultIsRtl = defaultIsRtl; + } + + @Override + protected boolean defaultIsRtl() { + return mDefaultIsRtl; + } + } + + /** + * Interface for an algorithm to guess the direction of a paragraph of text. + */ + private static interface TextDirectionAlgorithm { + /** + * Returns whether the range of text is RTL according to the algorithm. + */ + int checkRtl(CharSequence cs, int start, int count); + } + + /** + * Algorithm that uses the first strong directional character to determine the paragraph + * direction. This is the standard Unicode Bidirectional algorithm. + */ + private static class FirstStrong implements TextDirectionAlgorithm { + @Override + public int checkRtl(CharSequence cs, int start, int count) { + int result = STATE_UNKNOWN; + for (int i = start, e = start + count; i < e && result == STATE_UNKNOWN; ++i) { + result = isRtlTextOrFormat(Character.getDirectionality(cs.charAt(i))); + } + return result; + } + + private FirstStrong() { + } + + public static final FirstStrong INSTANCE = new FirstStrong(); + } + + /** + * Algorithm that uses the presence of any strong directional non-format + * character (e.g. excludes LRE, LRO, RLE, RLO) to determine the + * direction of text. + */ + private static class AnyStrong implements TextDirectionAlgorithm { + private final boolean mLookForRtl; + + @Override + public int checkRtl(CharSequence cs, int start, int count) { + boolean haveUnlookedFor = false; + for (int i = start, e = start + count; i < e; ++i) { + switch (isRtlText(Character.getDirectionality(cs.charAt(i)))) { + case STATE_TRUE: + if (mLookForRtl) { + return STATE_TRUE; + } + haveUnlookedFor = true; + break; + case STATE_FALSE: + if (!mLookForRtl) { + return STATE_FALSE; + } + haveUnlookedFor = true; + break; + default: + break; + } + } + if (haveUnlookedFor) { + return mLookForRtl ? STATE_FALSE : STATE_TRUE; + } + return STATE_UNKNOWN; + } + + private AnyStrong(boolean lookForRtl) { + this.mLookForRtl = lookForRtl; + } + + public static final AnyStrong INSTANCE_RTL = new AnyStrong(true); + public static final AnyStrong INSTANCE_LTR = new AnyStrong(false); + } + + /** + * Algorithm that uses the Locale direction to force the direction of a paragraph. + */ + private static class TextDirectionHeuristicLocale extends TextDirectionHeuristicImpl { + + public TextDirectionHeuristicLocale() { + super(null); + } + + @Override + protected boolean defaultIsRtl() { + final int dir = TextUtilsCompat.getLayoutDirectionFromLocale(java.util.Locale.getDefault()); + return (dir == ViewCompat.LAYOUT_DIRECTION_RTL); + } + + public static final TextDirectionHeuristicLocale INSTANCE = + new TextDirectionHeuristicLocale(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/text/TextUtilsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/text/TextUtilsCompat.java new file mode 100644 index 00000000000..436d72fe0e5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/text/TextUtilsCompat.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.text; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.view.ViewCompat; + +import java.util.Locale; + +public class TextUtilsCompat { + + /** + * Html-encode the string. + * @param s the string to be encoded + * @return the encoded string + */ + @NonNull + public static String htmlEncode(@NonNull String s) { + StringBuilder sb = new StringBuilder(); + char c; + for (int i = 0; i < s.length(); i++) { + c = s.charAt(i); + switch (c) { + case '<': + sb.append("<"); //$NON-NLS-1$ + break; + case '>': + sb.append(">"); //$NON-NLS-1$ + break; + case '&': + sb.append("&"); //$NON-NLS-1$ + break; + case '\'': + //http://www.w3.org/TR/xhtml1 + // The named character reference ' (the apostrophe, U+0027) was introduced in + // XML 1.0 but does not appear in HTML. Authors should therefore use ' instead + // of ' to work as expected in HTML 4 user agents. + sb.append("'"); //$NON-NLS-1$ + break; + case '"': + sb.append("""); //$NON-NLS-1$ + break; + default: + sb.append(c); + } + } + return sb.toString(); + } + + /** + * Return the layout direction for a given Locale + * + * @param locale the Locale for which we want the layout direction. Can be null. + * @return the layout direction. This may be one of: + * {@link ViewCompat#LAYOUT_DIRECTION_LTR} or + * {@link ViewCompat#LAYOUT_DIRECTION_RTL}. + * + * Be careful: this code will need to be updated when vertical scripts will be supported + */ + public static int getLayoutDirectionFromLocale(@Nullable Locale locale) { + if (locale != null && !locale.equals(ROOT)) { + final String scriptSubtag = ICUCompat.getScript( + ICUCompat.addLikelySubtags(locale.toString())); + if (scriptSubtag == null) return getLayoutDirectionFromFirstChar(locale); + + if (scriptSubtag.equalsIgnoreCase(ARAB_SCRIPT_SUBTAG) || + scriptSubtag.equalsIgnoreCase(HEBR_SCRIPT_SUBTAG)) { + return ViewCompat.LAYOUT_DIRECTION_RTL; + } + } + + return ViewCompat.LAYOUT_DIRECTION_LTR; + } + + /** + * Fallback algorithm to detect the locale direction. Rely on the fist char of the + * localized locale name. This will not work if the localized locale name is in English + * (this is the case for ICU 4.4 and "Urdu" script) + * + * @param locale + * @return the layout direction. This may be one of: + * {@link ViewCompat#LAYOUT_DIRECTION_LTR} or + * {@link ViewCompat#LAYOUT_DIRECTION_RTL}. + * + * Be careful: this code will need to be updated when vertical scripts will be supported + */ + private static int getLayoutDirectionFromFirstChar(Locale locale) { + switch(Character.getDirectionality(locale.getDisplayName(locale).charAt(0))) { + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + return ViewCompat.LAYOUT_DIRECTION_RTL; + + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + default: + return ViewCompat.LAYOUT_DIRECTION_LTR; + } + } + + public static final Locale ROOT = new Locale("", ""); + + private static String ARAB_SCRIPT_SUBTAG = "Arab"; + private static String HEBR_SCRIPT_SUBTAG = "Hebr"; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/ArrayMap.java b/mobile/google-android-support-v4/src/android/support/v4/util/ArrayMap.java new file mode 100644 index 00000000000..0a8a0ab5447 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/ArrayMap.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.util; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +/** + * ArrayMap is a generic key->value mapping data structure that is + * designed to be more memory efficient than a traditional {@link java.util.HashMap}, + * this implementation is a version of the platform's + * {@link android.util.ArrayMap} that can be used on older versions of the platform. + * It keeps its mappings in an array data structure -- an integer array of hash + * codes for each item, and an Object array of the key/value pairs. This allows it to + * avoid having to create an extra object for every entry put in to the map, and it + * also tries to control the growth of the size of these arrays more aggressively + * (since growing them only requires copying the entries in the array, not rebuilding + * a hash map). + * + *

    If you don't need the standard Java container APIs provided here (iterators etc), + * consider using {@link SimpleArrayMap} instead.

    + * + *

    Note that this implementation is not intended to be appropriate for data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashMap, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

    + * + *

    Because this container is intended to better balance memory use, unlike most other + * standard Java containers it will shrink its array as items are removed from it. Currently + * you have no control over this shrinking -- if you set a capacity and then remove an + * item, it may reduce the capacity to better match the current size. In the future an + * explicit call to set the capacity should turn off this aggressive shrinking behavior.

    + */ +public class ArrayMap extends SimpleArrayMap implements Map { + MapCollections mCollections; + + public ArrayMap() { + super(); + } + + /** + * Create a new ArrayMap with a given initial capacity. + */ + public ArrayMap(int capacity) { + super(capacity); + } + + /** + * Create a new ArrayMap with the mappings from the given ArrayMap. + */ + public ArrayMap(SimpleArrayMap map) { + super(map); + } + + private MapCollections getCollection() { + if (mCollections == null) { + mCollections = new MapCollections() { + @Override + protected int colGetSize() { + return mSize; + } + + @Override + protected Object colGetEntry(int index, int offset) { + return mArray[(index<<1) + offset]; + } + + @Override + protected int colIndexOfKey(Object key) { + return indexOfKey(key); + } + + @Override + protected int colIndexOfValue(Object value) { + return indexOfValue(value); + } + + @Override + protected Map colGetMap() { + return ArrayMap.this; + } + + @Override + protected void colPut(K key, V value) { + put(key, value); + } + + @Override + protected V colSetValue(int index, V value) { + return setValueAt(index, value); + } + + @Override + protected void colRemoveAt(int index) { + removeAt(index); + } + + @Override + protected void colClear() { + clear(); + } + }; + } + return mCollections; + } + + /** + * Determine if the array map contains all of the keys in the given collection. + * @param collection The collection whose contents are to be checked against. + * @return Returns true if this array map contains a key for every entry + * in collection, else returns false. + */ + public boolean containsAll(Collection collection) { + return MapCollections.containsAllHelper(this, collection); + } + + /** + * Perform a {@link #put(Object, Object)} of all key/value pairs in map + * @param map The map whose contents are to be retrieved. + */ + @Override + public void putAll(Map map) { + ensureCapacity(mSize + map.size()); + for (Map.Entry entry : map.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + /** + * Remove all keys in the array map that exist in the given collection. + * @param collection The collection whose contents are to be used to remove keys. + * @return Returns true if any keys were removed from the array map, else false. + */ + public boolean removeAll(Collection collection) { + return MapCollections.removeAllHelper(this, collection); + } + + /** + * Remove all keys in the array map that do not exist in the given collection. + * @param collection The collection whose contents are to be used to determine which + * keys to keep. + * @return Returns true if any keys were removed from the array map, else false. + */ + public boolean retainAll(Collection collection) { + return MapCollections.retainAllHelper(this, collection); + } + + /** + * Return a {@link java.util.Set} for iterating over and interacting with all mappings + * in the array map. + * + *

    Note: this is a very inefficient way to access the array contents, it + * requires generating a number of temporary objects.

    + * + *

    Note:

    the semantics of this + * Set are subtly different than that of a {@link java.util.HashMap}: most important, + * the {@link java.util.Map.Entry Map.Entry} object returned by its iterator is a single + * object that exists for the entire iterator, so you can not hold on to it + * after calling {@link java.util.Iterator#next() Iterator.next}.

    + */ + @Override + public Set> entrySet() { + return getCollection().getEntrySet(); + } + + /** + * Return a {@link java.util.Set} for iterating over and interacting with all keys + * in the array map. + * + *

    Note: this is a fairly inefficient way to access the array contents, it + * requires generating a number of temporary objects.

    + */ + @Override + public Set keySet() { + return getCollection().getKeySet(); + } + + /** + * Return a {@link java.util.Collection} for iterating over and interacting with all values + * in the array map. + * + *

    Note: this is a fairly inefficient way to access the array contents, it + * requires generating a number of temporary objects.

    + */ + @Override + public Collection values() { + return getCollection().getValues(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/AtomicFile.java b/mobile/google-android-support-v4/src/android/support/v4/util/AtomicFile.java new file mode 100644 index 00000000000..69b255bbd55 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/AtomicFile.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.util; + +import android.util.Log; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Static library support version of the framework's {@link android.util.AtomicFile}, + * a helper class for performing atomic operations on a file by creating a + * backup file until a write has successfully completed. + *

    + * Atomic file guarantees file integrity by ensuring that a file has + * been completely written and sync'd to disk before removing its backup. + * As long as the backup file exists, the original file is considered + * to be invalid (left over from a previous attempt to write the file). + *

    + * Atomic file does not confer any file locking semantics. + * Do not use this class when the file may be accessed or modified concurrently + * by multiple threads or processes. The caller is responsible for ensuring + * appropriate mutual exclusion invariants whenever it accesses the file. + *

    + */ +public class AtomicFile { + private final File mBaseName; + private final File mBackupName; + + /** + * Create a new AtomicFile for a file located at the given File path. + * The secondary backup file will be the same file path with ".bak" appended. + */ + public AtomicFile(File baseName) { + mBaseName = baseName; + mBackupName = new File(baseName.getPath() + ".bak"); + } + + /** + * Return the path to the base file. You should not generally use this, + * as the data at that path may not be valid. + */ + public File getBaseFile() { + return mBaseName; + } + + /** + * Delete the atomic file. This deletes both the base and backup files. + */ + public void delete() { + mBaseName.delete(); + mBackupName.delete(); + } + + /** + * Start a new write operation on the file. This returns a FileOutputStream + * to which you can write the new file data. The existing file is replaced + * with the new data. You must not directly close the given + * FileOutputStream; instead call either {@link #finishWrite(FileOutputStream)} + * or {@link #failWrite(FileOutputStream)}. + * + *

    Note that if another thread is currently performing + * a write, this will simply replace whatever that thread is writing + * with the new file being written by this thread, and when the other + * thread finishes the write the new write operation will no longer be + * safe (or will be lost). You must do your own threading protection for + * access to AtomicFile. + */ + public FileOutputStream startWrite() throws IOException { + // Rename the current file so it may be used as a backup during the next read + if (mBaseName.exists()) { + if (!mBackupName.exists()) { + if (!mBaseName.renameTo(mBackupName)) { + Log.w("AtomicFile", "Couldn't rename file " + mBaseName + + " to backup file " + mBackupName); + } + } else { + mBaseName.delete(); + } + } + FileOutputStream str = null; + try { + str = new FileOutputStream(mBaseName); + } catch (FileNotFoundException e) { + File parent = mBaseName.getParentFile(); + if (!parent.mkdir()) { + throw new IOException("Couldn't create directory " + mBaseName); + } + try { + str = new FileOutputStream(mBaseName); + } catch (FileNotFoundException e2) { + throw new IOException("Couldn't create " + mBaseName); + } + } + return str; + } + + /** + * Call when you have successfully finished writing to the stream + * returned by {@link #startWrite()}. This will close, sync, and + * commit the new data. The next attempt to read the atomic file + * will return the new file stream. + */ + public void finishWrite(FileOutputStream str) { + if (str != null) { + sync(str); + try { + str.close(); + mBackupName.delete(); + } catch (IOException e) { + Log.w("AtomicFile", "finishWrite: Got exception:", e); + } + } + } + + /** + * Call when you have failed for some reason at writing to the stream + * returned by {@link #startWrite()}. This will close the current + * write stream, and roll back to the previous state of the file. + */ + public void failWrite(FileOutputStream str) { + if (str != null) { + sync(str); + try { + str.close(); + mBaseName.delete(); + mBackupName.renameTo(mBaseName); + } catch (IOException e) { + Log.w("AtomicFile", "failWrite: Got exception:", e); + } + } + } + + /** + * Open the atomic file for reading. If there previously was an + * incomplete write, this will roll back to the last good data before + * opening for read. You should call close() on the FileInputStream when + * you are done reading from it. + * + *

    Note that if another thread is currently performing + * a write, this will incorrectly consider it to be in the state of a bad + * write and roll back, causing the new data currently being written to + * be dropped. You must do your own threading protection for access to + * AtomicFile. + */ + public FileInputStream openRead() throws FileNotFoundException { + if (mBackupName.exists()) { + mBaseName.delete(); + mBackupName.renameTo(mBaseName); + } + return new FileInputStream(mBaseName); + } + + /** + * A convenience for {@link #openRead()} that also reads all of the + * file contents into a byte array which is returned. + */ + public byte[] readFully() throws IOException { + FileInputStream stream = openRead(); + try { + int pos = 0; + int avail = stream.available(); + byte[] data = new byte[avail]; + while (true) { + int amt = stream.read(data, pos, data.length-pos); + //Log.i("foo", "Read " + amt + " bytes at " + pos + // + " of avail " + data.length); + if (amt <= 0) { + //Log.i("foo", "**** FINISHED READING: pos=" + pos + // + " len=" + data.length); + return data; + } + pos += amt; + avail = stream.available(); + if (avail > data.length-pos) { + byte[] newData = new byte[pos+avail]; + System.arraycopy(data, 0, newData, 0, pos); + data = newData; + } + } + } finally { + stream.close(); + } + } + + static boolean sync(FileOutputStream stream) { + try { + if (stream != null) { + stream.getFD().sync(); + } + return true; + } catch (IOException e) { + } + return false; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/CircularArray.java b/mobile/google-android-support-v4/src/android/support/v4/util/CircularArray.java new file mode 100644 index 00000000000..760c9860e09 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/CircularArray.java @@ -0,0 +1,252 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package android.support.v4.util; + +/** + * CircularArray is a generic circular array data structure that provides O(1) random read, O(1) + * prepend and O(1) append. The CircularArray automatically grows its capacity when number of added + * items is over its capacity. + */ +public final class CircularArray { + private E[] mElements; + private int mHead; + private int mTail; + private int mCapacityBitmask; + + private void doubleCapacity() { + int n = mElements.length; + int r = n - mHead; + int newCapacity = n << 1; + if (newCapacity < 0) { + throw new RuntimeException("Max array capacity exceeded"); + } + Object[] a = new Object[newCapacity]; + System.arraycopy(mElements, mHead, a, 0, r); + System.arraycopy(mElements, 0, a, r, mHead); + mElements = (E[]) a; + mHead = 0; + mTail = n; + mCapacityBitmask = newCapacity - 1; + } + + /** + * Create a CircularArray with default capacity. + */ + public CircularArray() { + this(8); + } + + /** + * Create a CircularArray with capacity for at least minCapacity elements. + * + * @param minCapacity The minimum capacity required for the CircularArray. + */ + public CircularArray(int minCapacity) { + if (minCapacity <= 0) { + throw new IllegalArgumentException("capacity must be positive"); + } + int arrayCapacity = minCapacity; + // If minCapacity isn't a power of 2, round up to the next highest power + // of 2. + if (Integer.bitCount(minCapacity) != 1) { + arrayCapacity = 1 << (Integer.highestOneBit(minCapacity) + 1); + } + mCapacityBitmask = arrayCapacity - 1; + mElements = (E[]) new Object[arrayCapacity]; + } + + /** + * Add an element in front of the CircularArray. + * @param e Element to add. + */ + public void addFirst(E e) { + mHead = (mHead - 1) & mCapacityBitmask; + mElements[mHead] = e; + if (mHead == mTail) { + doubleCapacity(); + } + } + + /** + * Add an element at end of the CircularArray. + * @param e Element to add. + */ + public void addLast(E e) { + mElements[mTail] = e; + mTail = (mTail + 1) & mCapacityBitmask; + if (mTail == mHead) { + doubleCapacity(); + } + } + + /** + * Remove first element from front of the CircularArray and return it. + * @return The element removed. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularArray is empty. + */ + public E popFirst() { + if (mHead == mTail) { + throw new ArrayIndexOutOfBoundsException(); + } + E result = mElements[mHead]; + mElements[mHead] = null; + mHead = (mHead + 1) & mCapacityBitmask; + return result; + } + + /** + * Remove last element from end of the CircularArray and return it. + * @return The element removed. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularArray is empty. + */ + public E popLast() { + if (mHead == mTail) { + throw new ArrayIndexOutOfBoundsException(); + } + int t = (mTail - 1) & mCapacityBitmask; + E result = mElements[t]; + mElements[t] = null; + mTail = t; + return result; + } + + /** + * Remove all elements from the CircularArray. + */ + public void clear() { + removeFromStart(size()); + } + + /** + * Remove multiple elements from front of the CircularArray, ignore when numOfElements + * is less than or equals to 0. + * @param numOfElements Number of elements to remove. + * @throws {@link ArrayIndexOutOfBoundsException} if numOfElements is larger than + * {@link #size()} + */ + public void removeFromStart(int numOfElements) { + if (numOfElements <= 0) { + return; + } + if (numOfElements > size()) { + throw new ArrayIndexOutOfBoundsException(); + } + int end = mElements.length; + if (numOfElements < end - mHead) { + end = mHead + numOfElements; + } + for (int i = mHead; i < end; i++) { + mElements[i] = null; + } + int removed = (end - mHead); + numOfElements -= removed; + mHead = (mHead + removed) & mCapacityBitmask; + if (numOfElements > 0) { + // mHead wrapped to 0 + for (int i = 0; i < numOfElements; i++) { + mElements[i] = null; + } + mHead = numOfElements; + } + } + + /** + * Remove multiple elements from end of the CircularArray, ignore when numOfElements + * is less than or equals to 0. + * @param numOfElements Number of elements to remove. + * @throws {@link ArrayIndexOutOfBoundsException} if numOfElements is larger than + * {@link #size()} + */ + public void removeFromEnd(int numOfElements) { + if (numOfElements <= 0) { + return; + } + if (numOfElements > size()) { + throw new ArrayIndexOutOfBoundsException(); + } + int start = 0; + if (numOfElements < mTail) { + start = mTail - numOfElements; + } + for (int i = start; i < mTail; i++) { + mElements[i] = null; + } + int removed = (mTail - start); + numOfElements -= removed; + mTail = mTail - removed; + if (numOfElements > 0) { + // mTail wrapped to mElements.length + mTail = mElements.length; + int newTail = mTail - numOfElements; + for (int i = newTail; i < mTail; i++) { + mElements[i] = null; + } + mTail = newTail; + } + } + + /** + * Get first element of the CircularArray. + * @return The first element. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularArray is empty. + */ + public E getFirst() { + if (mHead == mTail) { + throw new ArrayIndexOutOfBoundsException(); + } + return mElements[mHead]; + } + + /** + * Get last element of the CircularArray. + * @return The last element. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularArray is empty. + */ + public E getLast() { + if (mHead == mTail) { + throw new ArrayIndexOutOfBoundsException(); + } + return mElements[(mTail - 1) & mCapacityBitmask]; + } + + /** + * Get nth (0 <= n <= size()-1) element of the CircularArray. + * @param n The zero based element index in the CircularArray. + * @return The nth element. + * @throws {@link ArrayIndexOutOfBoundsException} if n < 0 or n >= size(). + */ + public E get(int n) { + if (n < 0 || n >= size()) { + throw new ArrayIndexOutOfBoundsException(); + } + return mElements[(mHead + n) & mCapacityBitmask]; + } + + /** + * Get number of elements in the CircularArray. + * @return Number of elements in the CircularArray. + */ + public int size() { + return (mTail - mHead) & mCapacityBitmask; + } + + /** + * Return true if size() is 0. + * @return true if size() is 0. + */ + public boolean isEmpty() { + return mHead == mTail; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/CircularIntArray.java b/mobile/google-android-support-v4/src/android/support/v4/util/CircularIntArray.java new file mode 100644 index 00000000000..23894368088 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/CircularIntArray.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package android.support.v4.util; + +/** + * CircularIntArray is a circular integer array data structure that provides O(1) random read, O(1) + * prepend and O(1) append. The CircularIntArray automatically grows its capacity when number of + * added integers is over its capacity. + */ +public final class CircularIntArray +{ + private int[] mElements; + private int mHead; + private int mTail; + private int mCapacityBitmask; + + private void doubleCapacity() { + int n = mElements.length; + int r = n - mHead; + int newCapacity = n << 1; + if (newCapacity < 0) { + throw new RuntimeException("Max array capacity exceeded"); + } + int[] a = new int[newCapacity]; + System.arraycopy(mElements, mHead, a, 0, r); + System.arraycopy(mElements, 0, a, r, mHead); + mElements = a; + mHead = 0; + mTail = n; + mCapacityBitmask = newCapacity - 1; + } + + /** + * Create a CircularIntArray with default capacity. + */ + public CircularIntArray() { + this(8); + } + + /** + * Create a CircularIntArray with capacity for at least minCapacity elements. + * + * @param minCapacity The minimum capacity required for the CircularIntArray. + */ + public CircularIntArray(int minCapacity) { + if (minCapacity <= 0) { + throw new IllegalArgumentException("capacity must be positive"); + } + int arrayCapacity = minCapacity; + // If minCapacity isn't a power of 2, round up to the next highest power + // of 2. + if (Integer.bitCount(minCapacity) != 1) { + arrayCapacity = 1 << (Integer.highestOneBit(minCapacity) + 1); + } + mCapacityBitmask = arrayCapacity - 1; + mElements = new int[arrayCapacity]; + } + + /** + * Add an integer in front of the CircularIntArray. + * @param e Integer to add. + */ + public void addFirst(int e) { + mHead = (mHead - 1) & mCapacityBitmask; + mElements[mHead] = e; + if (mHead == mTail) { + doubleCapacity(); + } + } + + /** + * Add an integer at end of the CircularIntArray. + * @param e Integer to add. + */ + public void addLast(int e) { + mElements[mTail] = e; + mTail = (mTail + 1) & mCapacityBitmask; + if (mTail == mHead) { + doubleCapacity(); + } + } + + /** + * Remove first integer from front of the CircularIntArray and return it. + * @return The integer removed. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularIntArray is empty. + */ + public int popFirst() { + if (mHead == mTail) throw new ArrayIndexOutOfBoundsException(); + int result = mElements[mHead]; + mHead = (mHead + 1) & mCapacityBitmask; + return result; + } + + /** + * Remove last integer from end of the CircularIntArray and return it. + * @return The integer removed. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularIntArray is empty. + */ + public int popLast() { + if (mHead == mTail) throw new ArrayIndexOutOfBoundsException(); + int t = (mTail - 1) & mCapacityBitmask; + int result = mElements[t]; + mTail = t; + return result; + } + + /** + * Remove all integers from the CircularIntArray. + */ + public void clear() { + mTail = mHead; + } + + /** + * Remove multiple integers from front of the CircularIntArray, ignore when numOfElements + * is less than or equals to 0. + * @param numOfElements Number of integers to remove. + * @throws {@link ArrayIndexOutOfBoundsException} if numOfElements is larger than + * {@link #size()} + */ + public void removeFromStart(int numOfElements) { + if (numOfElements <= 0) { + return; + } + if (numOfElements > size()) { + throw new ArrayIndexOutOfBoundsException(); + } + mHead = (mHead + numOfElements) & mCapacityBitmask; + } + + /** + * Remove multiple elements from end of the CircularIntArray, ignore when numOfElements + * is less than or equals to 0. + * @param numOfElements Number of integers to remove. + * @throws {@link ArrayIndexOutOfBoundsException} if numOfElements is larger than + * {@link #size()} + */ + public void removeFromEnd(int numOfElements) { + if (numOfElements <= 0) { + return; + } + if (numOfElements > size()) { + throw new ArrayIndexOutOfBoundsException(); + } + mTail = (mTail - numOfElements) & mCapacityBitmask; + } + + /** + * Get first integer of the CircularIntArray. + * @return The first integer. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularIntArray is empty. + */ + public int getFirst() { + if (mHead == mTail) throw new ArrayIndexOutOfBoundsException(); + return mElements[mHead]; + } + + /** + * Get last integer of the CircularIntArray. + * @return The last integer. + * @throws {@link ArrayIndexOutOfBoundsException} if CircularIntArray is empty. + */ + public int getLast() { + if (mHead == mTail) throw new ArrayIndexOutOfBoundsException(); + return mElements[(mTail - 1) & mCapacityBitmask]; + } + + /** + * Get nth (0 <= n <= size()-1) integer of the CircularIntArray. + * @param n The zero based element index in the CircularIntArray. + * @return The nth integer. + * @throws {@link ArrayIndexOutOfBoundsException} if n < 0 or n >= size(). + */ + public int get(int n) { + if (n < 0 || n >= size()) throw new ArrayIndexOutOfBoundsException(); + return mElements[(mHead + n) & mCapacityBitmask]; + } + + /** + * Get number of integers in the CircularIntArray. + * @return Number of integers in the CircularIntArray. + */ + public int size() { + return (mTail - mHead) & mCapacityBitmask; + } + + /** + * Return true if size() is 0. + * @return true if size() is 0. + */ + public boolean isEmpty() { + return mHead == mTail; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/ContainerHelpers.java b/mobile/google-android-support-v4/src/android/support/v4/util/ContainerHelpers.java new file mode 100644 index 00000000000..74f39d33394 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/ContainerHelpers.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.util; + +class ContainerHelpers { + static final int[] EMPTY_INTS = new int[0]; + static final long[] EMPTY_LONGS = new long[0]; + static final Object[] EMPTY_OBJECTS = new Object[0]; + + public static int idealIntArraySize(int need) { + return idealByteArraySize(need * 4) / 4; + } + + public static int idealLongArraySize(int need) { + return idealByteArraySize(need * 8) / 8; + } + + public static int idealByteArraySize(int need) { + for (int i = 4; i < 32; i++) + if (need <= (1 << i) - 12) + return (1 << i) - 12; + + return need; + } + + public static boolean equal(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + // This is Arrays.binarySearch(), but doesn't do any argument validation. + static int binarySearch(int[] array, int size, int value) { + int lo = 0; + int hi = size - 1; + + while (lo <= hi) { + int mid = (lo + hi) >>> 1; + int midVal = array[mid]; + + if (midVal < value) { + lo = mid + 1; + } else if (midVal > value) { + hi = mid - 1; + } else { + return mid; // value found + } + } + return ~lo; // value not present + } + + static int binarySearch(long[] array, int size, long value) { + int lo = 0; + int hi = size - 1; + + while (lo <= hi) { + final int mid = (lo + hi) >>> 1; + final long midVal = array[mid]; + + if (midVal < value) { + lo = mid + 1; + } else if (midVal > value) { + hi = mid - 1; + } else { + return mid; // value found + } + } + return ~lo; // value not present + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/DebugUtils.java b/mobile/google-android-support-v4/src/android/support/v4/util/DebugUtils.java new file mode 100644 index 00000000000..9d4f14ee71e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/DebugUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.util; + +/** + * Helper for accessing features in {@link android.util.DebugUtils} + * introduced after API level 4 in a backwards compatible fashion. + * + * @hide + */ +public class DebugUtils { + + public static void buildShortClassTag(Object cls, StringBuilder out) { + if (cls == null) { + out.append("null"); + } else { + String simpleName = cls.getClass().getSimpleName(); + if (simpleName == null || simpleName.length() <= 0) { + simpleName = cls.getClass().getName(); + int end = simpleName.lastIndexOf('.'); + if (end > 0) { + simpleName = simpleName.substring(end+1); + } + } + out.append(simpleName); + out.append('{'); + out.append(Integer.toHexString(System.identityHashCode(cls))); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/LogWriter.java b/mobile/google-android-support-v4/src/android/support/v4/util/LogWriter.java new file mode 100644 index 00000000000..53453e32270 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/LogWriter.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.util; + +import android.util.Log; + +import java.io.Writer; + +/** + * Helper for accessing features in {@link android.util.LogWriter} + * introduced after API level 4 in a backwards compatible fashion. + * + * @hide + */ +public class LogWriter extends Writer { + private final String mTag; + private StringBuilder mBuilder = new StringBuilder(128); + + /** + * Create a new Writer that sends to the log with the given priority + * and tag. + * + * @param tag A string tag to associate with each printed log statement. + */ + public LogWriter(String tag) { + mTag = tag; + } + + @Override public void close() { + flushBuilder(); + } + + @Override public void flush() { + flushBuilder(); + } + + @Override public void write(char[] buf, int offset, int count) { + for(int i = 0; i < count; i++) { + char c = buf[offset + i]; + if ( c == '\n') { + flushBuilder(); + } + else { + mBuilder.append(c); + } + } + } + + private void flushBuilder() { + if (mBuilder.length() > 0) { + Log.d(mTag, mBuilder.toString()); + mBuilder.delete(0, mBuilder.length()); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/LongSparseArray.java b/mobile/google-android-support-v4/src/android/support/v4/util/LongSparseArray.java new file mode 100644 index 00000000000..25b6bb971e5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/LongSparseArray.java @@ -0,0 +1,391 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.util; + +/** + * SparseArray mapping longs to Objects, a version of the platform's + * {@link android.util.LongSparseArray} that can be used on older versions of the + * platform. Unlike a normal array of Objects, + * there can be gaps in the indices. It is intended to be more memory efficient + * than using a HashMap to map Longs to Objects, both because it avoids + * auto-boxing keys and its data structure doesn't rely on an extra entry object + * for each mapping. + * + *

    Note that this container keeps its mappings in an array data structure, + * using a binary search to find keys. The implementation is not intended to be appropriate for + * data structures + * that may contain large numbers of items. It is generally slower than a traditional + * HashMap, since lookups require a binary search and adds and removes require inserting + * and deleting entries in the array. For containers holding up to hundreds of items, + * the performance difference is not significant, less than 50%.

    + * + *

    To help with performance, the container includes an optimization when removing + * keys: instead of compacting its array immediately, it leaves the removed entry marked + * as deleted. The entry can then be re-used for the same key, or compacted later in + * a single garbage collection step of all removed entries. This garbage collection will + * need to be performed at any time the array needs to be grown or the the map size or + * entry values are retrieved.

    + */ +public class LongSparseArray implements Cloneable { + private static final Object DELETED = new Object(); + private boolean mGarbage = false; + + private long[] mKeys; + private Object[] mValues; + private int mSize; + + /** + * Creates a new LongSparseArray containing no mappings. + */ + public LongSparseArray() { + this(10); + } + + /** + * Creates a new LongSparseArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public LongSparseArray(int initialCapacity) { + if (initialCapacity == 0) { + mKeys = ContainerHelpers.EMPTY_LONGS; + mValues = ContainerHelpers.EMPTY_OBJECTS; + } else { + initialCapacity = ContainerHelpers.idealLongArraySize(initialCapacity); + mKeys = new long[initialCapacity]; + mValues = new Object[initialCapacity]; + } + mSize = 0; + } + + @Override + @SuppressWarnings("unchecked") + public LongSparseArray clone() { + LongSparseArray clone = null; + try { + clone = (LongSparseArray) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Gets the Object mapped from the specified key, or null + * if no such mapping has been made. + */ + public E get(long key) { + return get(key, null); + } + + /** + * Gets the Object mapped from the specified key, or the specified Object + * if no such mapping has been made. + */ + @SuppressWarnings("unchecked") + public E get(long key, E valueIfKeyNotFound) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i < 0 || mValues[i] == DELETED) { + return valueIfKeyNotFound; + } else { + return (E) mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(long key) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + if (mValues[i] != DELETED) { + mValues[i] = DELETED; + mGarbage = true; + } + } + } + + /** + * Alias for {@link #delete(long)}. + */ + public void remove(long key) { + delete(key); + } + + /** + * Removes the mapping at the specified index. + */ + public void removeAt(int index) { + if (mValues[index] != DELETED) { + mValues[index] = DELETED; + mGarbage = true; + } + } + + private void gc() { + // Log.e("SparseArray", "gc start with " + mSize); + + int n = mSize; + int o = 0; + long[] keys = mKeys; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + Object val = values[i]; + + if (val != DELETED) { + if (i != o) { + keys[o] = keys[i]; + values[o] = val; + values[i] = null; + } + + o++; + } + } + + mGarbage = false; + mSize = o; + + // Log.e("SparseArray", "gc end with " + mSize); + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(long key, E value) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + if (i < mSize && mValues[i] == DELETED) { + mKeys[i] = key; + mValues[i] = value; + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + + // Search again because indices may have changed. + i = ~ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + if (mSize >= mKeys.length) { + int n = ContainerHelpers.idealLongArraySize(mSize + 1); + + long[] nkeys = new long[n]; + Object[] nvalues = new Object[n]; + + // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + if (mSize - i != 0) { + // Log.e("SparseArray", "move " + (mSize - i)); + System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); + System.arraycopy(mValues, i, mValues, i + 1, mSize - i); + } + + mKeys[i] = key; + mValues[i] = value; + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this LongSparseArray + * currently stores. + */ + public int size() { + if (mGarbage) { + gc(); + } + + return mSize; + } + + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * LongSparseArray stores. + */ + public long keyAt(int index) { + if (mGarbage) { + gc(); + } + + return mKeys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * LongSparseArray stores. + */ + @SuppressWarnings("unchecked") + public E valueAt(int index) { + if (mGarbage) { + gc(); + } + + return (E) mValues[index]; + } + + /** + * Given an index in the range 0...size()-1, sets a new + * value for the indexth key-value mapping that this + * LongSparseArray stores. + */ + public void setValueAt(int index, E value) { + if (mGarbage) { + gc(); + } + + mValues[index] = value; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(long key) { + if (mGarbage) { + gc(); + } + + return ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + * Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + */ + public int indexOfValue(E value) { + if (mGarbage) { + gc(); + } + + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this LongSparseArray. + */ + public void clear() { + int n = mSize; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + values[i] = null; + } + + mSize = 0; + mGarbage = false; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(long key, E value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + } + + int pos = mSize; + if (pos >= mKeys.length) { + int n = ContainerHelpers.idealLongArraySize(pos + 1); + + long[] nkeys = new long[n]; + Object[] nvalues = new Object[n]; + + // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + mKeys[pos] = key; + mValues[pos] = value; + mSize = pos + 1; + } + + /** + * {@inheritDoc} + * + *

    This implementation composes a string by iterating over its mappings. If + * this map contains itself as a value, the string "(this Map)" + * will appear in its place. + */ + @Override + public String toString() { + if (size() <= 0) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + long key = keyAt(i); + buffer.append(key); + buffer.append('='); + Object value = valueAt(i); + if (value != this) { + buffer.append(value); + } else { + buffer.append("(this Map)"); + } + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/LruCache.java b/mobile/google-android-support-v4/src/android/support/v4/util/LruCache.java new file mode 100644 index 00000000000..524e985ee7d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/LruCache.java @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.util; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Static library version of {@link android.util.LruCache}. Used to write apps + * that run on API levels prior to 12. When running on API level 12 or above, + * this implementation is still used; it does not try to switch to the + * framework's implementation. See the framework SDK documentation for a class + * overview. + */ +public class LruCache { + private final LinkedHashMap map; + + /** Size of this cache in units. Not necessarily the number of elements. */ + private int size; + private int maxSize; + + private int putCount; + private int createCount; + private int evictionCount; + private int hitCount; + private int missCount; + + /** + * @param maxSize for caches that do not override {@link #sizeOf}, this is + * the maximum number of entries in the cache. For all other caches, + * this is the maximum sum of the sizes of the entries in this cache. + */ + public LruCache(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + this.maxSize = maxSize; + this.map = new LinkedHashMap(0, 0.75f, true); + } + + /** + * Sets the size of the cache. + * + * @param maxSize The new maximum size. + */ + public void resize(int maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + + synchronized (this) { + this.maxSize = maxSize; + } + trimToSize(maxSize); + } + + /** + * Returns the value for {@code key} if it exists in the cache or can be + * created by {@code #create}. If a value was returned, it is moved to the + * head of the queue. This returns null if a value is not cached and cannot + * be created. + */ + public final V get(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V mapValue; + synchronized (this) { + mapValue = map.get(key); + if (mapValue != null) { + hitCount++; + return mapValue; + } + missCount++; + } + + /* + * Attempt to create a value. This may take a long time, and the map + * may be different when create() returns. If a conflicting value was + * added to the map while create() was working, we leave that value in + * the map and release the created value. + */ + + V createdValue = create(key); + if (createdValue == null) { + return null; + } + + synchronized (this) { + createCount++; + mapValue = map.put(key, createdValue); + + if (mapValue != null) { + // There was a conflict so undo that last put + map.put(key, mapValue); + } else { + size += safeSizeOf(key, createdValue); + } + } + + if (mapValue != null) { + entryRemoved(false, key, createdValue, mapValue); + return mapValue; + } else { + trimToSize(maxSize); + return createdValue; + } + } + + /** + * Caches {@code value} for {@code key}. The value is moved to the head of + * the queue. + * + * @return the previous value mapped by {@code key}. + */ + public final V put(K key, V value) { + if (key == null || value == null) { + throw new NullPointerException("key == null || value == null"); + } + + V previous; + synchronized (this) { + putCount++; + size += safeSizeOf(key, value); + previous = map.put(key, value); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, value); + } + + trimToSize(maxSize); + return previous; + } + + /** + * Remove the eldest entries until the total of remaining entries is at or + * below the requested size. + * + * @param maxSize the maximum size of the cache before returning. May be -1 + * to evict even 0-sized elements. + */ + public void trimToSize(int maxSize) { + while (true) { + K key; + V value; + synchronized (this) { + if (size < 0 || (map.isEmpty() && size != 0)) { + throw new IllegalStateException(getClass().getName() + + ".sizeOf() is reporting inconsistent results!"); + } + + if (size <= maxSize || map.isEmpty()) { + break; + } + + Map.Entry toEvict = map.entrySet().iterator().next(); + key = toEvict.getKey(); + value = toEvict.getValue(); + map.remove(key); + size -= safeSizeOf(key, value); + evictionCount++; + } + + entryRemoved(true, key, value, null); + } + } + + /** + * Removes the entry for {@code key} if it exists. + * + * @return the previous value mapped by {@code key}. + */ + public final V remove(K key) { + if (key == null) { + throw new NullPointerException("key == null"); + } + + V previous; + synchronized (this) { + previous = map.remove(key); + if (previous != null) { + size -= safeSizeOf(key, previous); + } + } + + if (previous != null) { + entryRemoved(false, key, previous, null); + } + + return previous; + } + + /** + * Called for entries that have been evicted or removed. This method is + * invoked when a value is evicted to make space, removed by a call to + * {@link #remove}, or replaced by a call to {@link #put}. The default + * implementation does nothing. + * + *

    The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + * @param evicted true if the entry is being removed to make space, false + * if the removal was caused by a {@link #put} or {@link #remove}. + * @param newValue the new value for {@code key}, if it exists. If non-null, + * this removal was caused by a {@link #put}. Otherwise it was caused by + * an eviction or a {@link #remove}. + */ + protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {} + + /** + * Called after a cache miss to compute a value for the corresponding key. + * Returns the computed value or null if no value can be computed. The + * default implementation returns null. + * + *

    The method is called without synchronization: other threads may + * access the cache while this method is executing. + * + *

    If a value for {@code key} exists in the cache when this method + * returns, the created value will be released with {@link #entryRemoved} + * and discarded. This can occur when multiple threads request the same key + * at the same time (causing multiple values to be created), or when one + * thread calls {@link #put} while another is creating a value for the same + * key. + */ + protected V create(K key) { + return null; + } + + private int safeSizeOf(K key, V value) { + int result = sizeOf(key, value); + if (result < 0) { + throw new IllegalStateException("Negative size: " + key + "=" + value); + } + return result; + } + + /** + * Returns the size of the entry for {@code key} and {@code value} in + * user-defined units. The default implementation returns 1 so that size + * is the number of entries and max size is the maximum number of entries. + * + *

    An entry's size must not change while it is in the cache. + */ + protected int sizeOf(K key, V value) { + return 1; + } + + /** + * Clear the cache, calling {@link #entryRemoved} on each removed entry. + */ + public final void evictAll() { + trimToSize(-1); // -1 will evict 0-sized elements + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the number + * of entries in the cache. For all other caches, this returns the sum of + * the sizes of the entries in this cache. + */ + public synchronized final int size() { + return size; + } + + /** + * For caches that do not override {@link #sizeOf}, this returns the maximum + * number of entries in the cache. For all other caches, this returns the + * maximum sum of the sizes of the entries in this cache. + */ + public synchronized final int maxSize() { + return maxSize; + } + + /** + * Returns the number of times {@link #get} returned a value that was + * already present in the cache. + */ + public synchronized final int hitCount() { + return hitCount; + } + + /** + * Returns the number of times {@link #get} returned null or required a new + * value to be created. + */ + public synchronized final int missCount() { + return missCount; + } + + /** + * Returns the number of times {@link #create(Object)} returned a value. + */ + public synchronized final int createCount() { + return createCount; + } + + /** + * Returns the number of times {@link #put} was called. + */ + public synchronized final int putCount() { + return putCount; + } + + /** + * Returns the number of values that have been evicted. + */ + public synchronized final int evictionCount() { + return evictionCount; + } + + /** + * Returns a copy of the current contents of the cache, ordered from least + * recently accessed to most recently accessed. + */ + public synchronized final Map snapshot() { + return new LinkedHashMap(map); + } + + @Override public synchronized final String toString() { + int accesses = hitCount + missCount; + int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0; + return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]", + maxSize, hitCount, missCount, hitPercent); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/MapCollections.java b/mobile/google-android-support-v4/src/android/support/v4/util/MapCollections.java new file mode 100644 index 00000000000..5ef54103736 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/MapCollections.java @@ -0,0 +1,558 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.util; + +import java.lang.reflect.Array; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +/** + * Helper for writing standard Java collection interfaces to a data + * structure like {@link ArrayMap}. + * @hide + */ +abstract class MapCollections { + EntrySet mEntrySet; + KeySet mKeySet; + ValuesCollection mValues; + + final class ArrayIterator implements Iterator { + final int mOffset; + int mSize; + int mIndex; + boolean mCanRemove = false; + + ArrayIterator(int offset) { + mOffset = offset; + mSize = colGetSize(); + } + + @Override + public boolean hasNext() { + return mIndex < mSize; + } + + @Override + public T next() { + Object res = colGetEntry(mIndex, mOffset); + mIndex++; + mCanRemove = true; + return (T)res; + } + + @Override + public void remove() { + if (!mCanRemove) { + throw new IllegalStateException(); + } + mIndex--; + mSize--; + mCanRemove = false; + colRemoveAt(mIndex); + } + } + + final class MapIterator implements Iterator>, Map.Entry { + int mEnd; + int mIndex; + boolean mEntryValid = false; + + MapIterator() { + mEnd = colGetSize() - 1; + mIndex = -1; + } + + @Override + public boolean hasNext() { + return mIndex < mEnd; + } + + @Override + public Map.Entry next() { + mIndex++; + mEntryValid = true; + return this; + } + + @Override + public void remove() { + if (!mEntryValid) { + throw new IllegalStateException(); + } + colRemoveAt(mIndex); + mIndex--; + mEnd--; + mEntryValid = false; + } + + @Override + public K getKey() { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + return (K)colGetEntry(mIndex, 0); + } + + @Override + public V getValue() { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + return (V)colGetEntry(mIndex, 1); + } + + @Override + public V setValue(V object) { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + return colSetValue(mIndex, object); + } + + @Override + public final boolean equals(Object o) { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + if (!(o instanceof Map.Entry)) { + return false; + } + Map.Entry e = (Map.Entry) o; + return ContainerHelpers.equal(e.getKey(), colGetEntry(mIndex, 0)) + && ContainerHelpers.equal(e.getValue(), colGetEntry(mIndex, 1)); + } + + @Override + public final int hashCode() { + if (!mEntryValid) { + throw new IllegalStateException( + "This container does not support retaining Map.Entry objects"); + } + final Object key = colGetEntry(mIndex, 0); + final Object value = colGetEntry(mIndex, 1); + return (key == null ? 0 : key.hashCode()) ^ + (value == null ? 0 : value.hashCode()); + } + + @Override + public final String toString() { + return getKey() + "=" + getValue(); + } + } + + final class EntrySet implements Set> { + @Override + public boolean add(Map.Entry object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection> collection) { + int oldSize = colGetSize(); + for (Map.Entry entry : collection) { + colPut(entry.getKey(), entry.getValue()); + } + return oldSize != colGetSize(); + } + + @Override + public void clear() { + colClear(); + } + + @Override + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry e = (Map.Entry) o; + int index = colIndexOfKey(e.getKey()); + if (index < 0) { + return false; + } + Object foundVal = colGetEntry(index, 1); + return ContainerHelpers.equal(foundVal, e.getValue()); + } + + @Override + public boolean containsAll(Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!contains(it.next())) { + return false; + } + } + return true; + } + + @Override + public boolean isEmpty() { + return colGetSize() == 0; + } + + @Override + public Iterator> iterator() { + return new MapIterator(); + } + + @Override + public boolean remove(Object object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean removeAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean retainAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public int size() { + return colGetSize(); + } + + @Override + public Object[] toArray() { + throw new UnsupportedOperationException(); + } + + @Override + public T[] toArray(T[] array) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean equals(Object object) { + return equalsSetHelper(this, object); + } + + @Override + public int hashCode() { + int result = 0; + for (int i=colGetSize()-1; i>=0; i--) { + final Object key = colGetEntry(i, 0); + final Object value = colGetEntry(i, 1); + result += ( (key == null ? 0 : key.hashCode()) ^ + (value == null ? 0 : value.hashCode()) ); + } + return result; + } + }; + + final class KeySet implements Set { + + @Override + public boolean add(K object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + colClear(); + } + + @Override + public boolean contains(Object object) { + return colIndexOfKey(object) >= 0; + } + + @Override + public boolean containsAll(Collection collection) { + return containsAllHelper(colGetMap(), collection); + } + + @Override + public boolean isEmpty() { + return colGetSize() == 0; + } + + @Override + public Iterator iterator() { + return new ArrayIterator(0); + } + + @Override + public boolean remove(Object object) { + int index = colIndexOfKey(object); + if (index >= 0) { + colRemoveAt(index); + return true; + } + return false; + } + + @Override + public boolean removeAll(Collection collection) { + return removeAllHelper(colGetMap(), collection); + } + + @Override + public boolean retainAll(Collection collection) { + return retainAllHelper(colGetMap(), collection); + } + + @Override + public int size() { + return colGetSize(); + } + + @Override + public Object[] toArray() { + return toArrayHelper(0); + } + + @Override + public T[] toArray(T[] array) { + return toArrayHelper(array, 0); + } + + @Override + public boolean equals(Object object) { + return equalsSetHelper(this, object); + } + + @Override + public int hashCode() { + int result = 0; + for (int i=colGetSize()-1; i>=0; i--) { + Object obj = colGetEntry(i, 0); + result += obj == null ? 0 : obj.hashCode(); + } + return result; + } + }; + + final class ValuesCollection implements Collection { + + @Override + public boolean add(V object) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean addAll(Collection collection) { + throw new UnsupportedOperationException(); + } + + @Override + public void clear() { + colClear(); + } + + @Override + public boolean contains(Object object) { + return colIndexOfValue(object) >= 0; + } + + @Override + public boolean containsAll(Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!contains(it.next())) { + return false; + } + } + return true; + } + + @Override + public boolean isEmpty() { + return colGetSize() == 0; + } + + @Override + public Iterator iterator() { + return new ArrayIterator(1); + } + + @Override + public boolean remove(Object object) { + int index = colIndexOfValue(object); + if (index >= 0) { + colRemoveAt(index); + return true; + } + return false; + } + + @Override + public boolean removeAll(Collection collection) { + int N = colGetSize(); + boolean changed = false; + for (int i=0; i collection) { + int N = colGetSize(); + boolean changed = false; + for (int i=0; i T[] toArray(T[] array) { + return toArrayHelper(array, 1); + } + }; + + public static boolean containsAllHelper(Map map, Collection collection) { + Iterator it = collection.iterator(); + while (it.hasNext()) { + if (!map.containsKey(it.next())) { + return false; + } + } + return true; + } + + public static boolean removeAllHelper(Map map, Collection collection) { + int oldSize = map.size(); + Iterator it = collection.iterator(); + while (it.hasNext()) { + map.remove(it.next()); + } + return oldSize != map.size(); + } + + public static boolean retainAllHelper(Map map, Collection collection) { + int oldSize = map.size(); + Iterator it = map.keySet().iterator(); + while (it.hasNext()) { + if (!collection.contains(it.next())) { + it.remove(); + } + } + return oldSize != map.size(); + } + + + public Object[] toArrayHelper(int offset) { + final int N = colGetSize(); + Object[] result = new Object[N]; + for (int i=0; i T[] toArrayHelper(T[] array, int offset) { + final int N = colGetSize(); + if (array.length < N) { + @SuppressWarnings("unchecked") T[] newArray + = (T[]) Array.newInstance(array.getClass().getComponentType(), N); + array = newArray; + } + for (int i=0; i N) { + array[N] = null; + } + return array; + } + + public static boolean equalsSetHelper(Set set, Object object) { + if (set == object) { + return true; + } + if (object instanceof Set) { + Set s = (Set) object; + + try { + return set.size() == s.size() && set.containsAll(s); + } catch (NullPointerException ignored) { + return false; + } catch (ClassCastException ignored) { + return false; + } + } + return false; + } + + public Set> getEntrySet() { + if (mEntrySet == null) { + mEntrySet = new EntrySet(); + } + return mEntrySet; + } + + public Set getKeySet() { + if (mKeySet == null) { + mKeySet = new KeySet(); + } + return mKeySet; + } + + public Collection getValues() { + if (mValues == null) { + mValues = new ValuesCollection(); + } + return mValues; + } + + protected abstract int colGetSize(); + protected abstract Object colGetEntry(int index, int offset); + protected abstract int colIndexOfKey(Object key); + protected abstract int colIndexOfValue(Object key); + protected abstract Map colGetMap(); + protected abstract void colPut(K key, V value); + protected abstract V colSetValue(int index, V value); + protected abstract void colRemoveAt(int index); + protected abstract void colClear(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/Pair.java b/mobile/google-android-support-v4/src/android/support/v4/util/Pair.java new file mode 100644 index 00000000000..7f9e61e92d0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/Pair.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.util; + +/** + * Container to ease passing around a tuple of two objects. This object provides a sensible + * implementation of equals(), returning true if equals() is true on each of the contained + * objects. + */ +public class Pair { + public final F first; + public final S second; + + /** + * Constructor for a Pair. + * + * @param first the first object in the Pair + * @param second the second object in the pair + */ + public Pair(F first, S second) { + this.first = first; + this.second = second; + } + + /** + * Checks the two objects for equality by delegating to their respective + * {@link Object#equals(Object)} methods. + * + * @param o the {@link Pair} to which this one is to be checked for equality + * @return true if the underlying objects of the Pair are both considered + * equal + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof Pair)) { + return false; + } + Pair p = (Pair) o; + return objectsEqual(p.first, first) && objectsEqual(p.second, second); + } + + private static boolean objectsEqual(Object a, Object b) { + return a == b || (a != null && a.equals(b)); + } + + /** + * Compute a hash code using the hash codes of the underlying objects + * + * @return a hashcode of the Pair + */ + @Override + public int hashCode() { + return (first == null ? 0 : first.hashCode()) ^ (second == null ? 0 : second.hashCode()); + } + + /** + * Convenience method for creating an appropriately typed pair. + * @param a the first object in the Pair + * @param b the second object in the pair + * @return a Pair that is templatized with the types of a and b + */ + public static Pair create(A a, B b) { + return new Pair(a, b); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/Pools.java b/mobile/google-android-support-v4/src/android/support/v4/util/Pools.java new file mode 100644 index 00000000000..e3907a1390c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/Pools.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.util; + + +/** + * Helper class for crating pools of objects. An example use looks like this: + *

    + * public class MyPooledClass {
    + *
    + *     private static final SynchronizedPool sPool =
    + *             new SynchronizedPool(10);
    + *
    + *     public static MyPooledClass obtain() {
    + *         MyPooledClass instance = sPool.acquire();
    + *         return (instance != null) ? instance : new MyPooledClass();
    + *     }
    + *
    + *     public void recycle() {
    + *          // Clear state if needed.
    + *          sPool.release(this);
    + *     }
    + *
    + *     . . .
    + * }
    + * 
    + * + */ +public final class Pools { + + /** + * Interface for managing a pool of objects. + * + * @param The pooled type. + */ + public static interface Pool { + + /** + * @return An instance from the pool if such, null otherwise. + */ + public T acquire(); + + /** + * Release an instance to the pool. + * + * @param instance The instance to release. + * @return Whether the instance was put in the pool. + * + * @throws IllegalStateException If the instance is already in the pool. + */ + public boolean release(T instance); + } + + private Pools() { + /* do nothing - hiding constructor */ + } + + /** + * Simple (non-synchronized) pool of objects. + * + * @param The pooled type. + */ + public static class SimplePool implements Pool { + private final Object[] mPool; + + private int mPoolSize; + + /** + * Creates a new instance. + * + * @param maxPoolSize The max pool size. + * + * @throws IllegalArgumentException If the max pool size is less than zero. + */ + public SimplePool(int maxPoolSize) { + if (maxPoolSize <= 0) { + throw new IllegalArgumentException("The max pool size must be > 0"); + } + mPool = new Object[maxPoolSize]; + } + + @Override + @SuppressWarnings("unchecked") + public T acquire() { + if (mPoolSize > 0) { + final int lastPooledIndex = mPoolSize - 1; + T instance = (T) mPool[lastPooledIndex]; + mPool[lastPooledIndex] = null; + mPoolSize--; + return instance; + } + return null; + } + + @Override + public boolean release(T instance) { + if (isInPool(instance)) { + throw new IllegalStateException("Already in the pool!"); + } + if (mPoolSize < mPool.length) { + mPool[mPoolSize] = instance; + mPoolSize++; + return true; + } + return false; + } + + private boolean isInPool(T instance) { + for (int i = 0; i < mPoolSize; i++) { + if (mPool[i] == instance) { + return true; + } + } + return false; + } + } + + /** + * Synchronized) pool of objects. + * + * @param The pooled type. + */ + public static class SynchronizedPool extends SimplePool { + private final Object mLock = new Object(); + + /** + * Creates a new instance. + * + * @param maxPoolSize The max pool size. + * + * @throws IllegalArgumentException If the max pool size is less than zero. + */ + public SynchronizedPool(int maxPoolSize) { + super(maxPoolSize); + } + + @Override + public T acquire() { + synchronized (mLock) { + return super.acquire(); + } + } + + @Override + public boolean release(T element) { + synchronized (mLock) { + return super.release(element); + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/SimpleArrayMap.java b/mobile/google-android-support-v4/src/android/support/v4/util/SimpleArrayMap.java new file mode 100644 index 00000000000..1b1577a6a08 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/SimpleArrayMap.java @@ -0,0 +1,615 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.util; + +import android.util.Log; + +import java.util.Map; + +/** + * Base implementation of {@link ArrayMap} that doesn't include any standard Java + * container API interoperability. These features are generally heavier-weight ways + * to interact with the container, so discouraged, but they can be useful to make it + * easier to use as a drop-in replacement for HashMap. If you don't need them, this + * class can be preferrable since it doesn't bring in any of the implementation of those + * APIs, allowing that code to be stripped by ProGuard. + */ +public class SimpleArrayMap { + private static final boolean DEBUG = false; + private static final String TAG = "ArrayMap"; + + /** + * The minimum amount by which the capacity of a ArrayMap will increase. + * This is tuned to be relatively space-efficient. + */ + private static final int BASE_SIZE = 4; + + /** + * Maximum number of entries to have in array caches. + */ + private static final int CACHE_SIZE = 10; + + /** + * Caches of small array objects to avoid spamming garbage. The cache + * Object[] variable is a pointer to a linked list of array objects. + * The first entry in the array is a pointer to the next array in the + * list; the second entry is a pointer to the int[] hash code array for it. + */ + static Object[] mBaseCache; + static int mBaseCacheSize; + static Object[] mTwiceBaseCache; + static int mTwiceBaseCacheSize; + + int[] mHashes; + Object[] mArray; + int mSize; + + int indexOf(Object key, int hash) { + final int N = mSize; + + // Important fast case: if nothing is in here, nothing to look for. + if (N == 0) { + return ~0; + } + + int index = ContainerHelpers.binarySearch(mHashes, N, hash); + + // If the hash code wasn't found, then we have no entry for this key. + if (index < 0) { + return index; + } + + // If the key at the returned index matches, that's what we want. + if (key.equals(mArray[index<<1])) { + return index; + } + + // Search for a matching key after the index. + int end; + for (end = index + 1; end < N && mHashes[end] == hash; end++) { + if (key.equals(mArray[end << 1])) return end; + } + + // Search for a matching key before the index. + for (int i = index - 1; i >= 0 && mHashes[i] == hash; i--) { + if (key.equals(mArray[i << 1])) return i; + } + + // Key not found -- return negative value indicating where a + // new entry for this key should go. We use the end of the + // hash chain to reduce the number of array entries that will + // need to be copied when inserting. + return ~end; + } + + int indexOfNull() { + final int N = mSize; + + // Important fast case: if nothing is in here, nothing to look for. + if (N == 0) { + return ~0; + } + + int index = ContainerHelpers.binarySearch(mHashes, N, 0); + + // If the hash code wasn't found, then we have no entry for this key. + if (index < 0) { + return index; + } + + // If the key at the returned index matches, that's what we want. + if (null == mArray[index<<1]) { + return index; + } + + // Search for a matching key after the index. + int end; + for (end = index + 1; end < N && mHashes[end] == 0; end++) { + if (null == mArray[end << 1]) return end; + } + + // Search for a matching key before the index. + for (int i = index - 1; i >= 0 && mHashes[i] == 0; i--) { + if (null == mArray[i << 1]) return i; + } + + // Key not found -- return negative value indicating where a + // new entry for this key should go. We use the end of the + // hash chain to reduce the number of array entries that will + // need to be copied when inserting. + return ~end; + } + + private void allocArrays(final int size) { + if (size == (BASE_SIZE*2)) { + synchronized (ArrayMap.class) { + if (mTwiceBaseCache != null) { + final Object[] array = mTwiceBaseCache; + mArray = array; + mTwiceBaseCache = (Object[])array[0]; + mHashes = (int[])array[1]; + array[0] = array[1] = null; + mTwiceBaseCacheSize--; + if (DEBUG) Log.d(TAG, "Retrieving 2x cache " + mHashes + + " now have " + mTwiceBaseCacheSize + " entries"); + return; + } + } + } else if (size == BASE_SIZE) { + synchronized (ArrayMap.class) { + if (mBaseCache != null) { + final Object[] array = mBaseCache; + mArray = array; + mBaseCache = (Object[])array[0]; + mHashes = (int[])array[1]; + array[0] = array[1] = null; + mBaseCacheSize--; + if (DEBUG) Log.d(TAG, "Retrieving 1x cache " + mHashes + + " now have " + mBaseCacheSize + " entries"); + return; + } + } + } + + mHashes = new int[size]; + mArray = new Object[size<<1]; + } + + private static void freeArrays(final int[] hashes, final Object[] array, final int size) { + if (hashes.length == (BASE_SIZE*2)) { + synchronized (ArrayMap.class) { + if (mTwiceBaseCacheSize < CACHE_SIZE) { + array[0] = mTwiceBaseCache; + array[1] = hashes; + for (int i=(size<<1)-1; i>=2; i--) { + array[i] = null; + } + mTwiceBaseCache = array; + mTwiceBaseCacheSize++; + if (DEBUG) Log.d(TAG, "Storing 2x cache " + array + + " now have " + mTwiceBaseCacheSize + " entries"); + } + } + } else if (hashes.length == BASE_SIZE) { + synchronized (ArrayMap.class) { + if (mBaseCacheSize < CACHE_SIZE) { + array[0] = mBaseCache; + array[1] = hashes; + for (int i=(size<<1)-1; i>=2; i--) { + array[i] = null; + } + mBaseCache = array; + mBaseCacheSize++; + if (DEBUG) Log.d(TAG, "Storing 1x cache " + array + + " now have " + mBaseCacheSize + " entries"); + } + } + } + } + + /** + * Create a new empty ArrayMap. The default capacity of an array map is 0, and + * will grow once items are added to it. + */ + public SimpleArrayMap() { + mHashes = ContainerHelpers.EMPTY_INTS; + mArray = ContainerHelpers.EMPTY_OBJECTS; + mSize = 0; + } + + /** + * Create a new ArrayMap with a given initial capacity. + */ + public SimpleArrayMap(int capacity) { + if (capacity == 0) { + mHashes = ContainerHelpers.EMPTY_INTS; + mArray = ContainerHelpers.EMPTY_OBJECTS; + } else { + allocArrays(capacity); + } + mSize = 0; + } + + /** + * Create a new ArrayMap with the mappings from the given ArrayMap. + */ + public SimpleArrayMap(SimpleArrayMap map) { + this(); + if (map != null) { + putAll(map); + } + } + + /** + * Make the array map empty. All storage is released. + */ + public void clear() { + if (mSize != 0) { + freeArrays(mHashes, mArray, mSize); + mHashes = ContainerHelpers.EMPTY_INTS; + mArray = ContainerHelpers.EMPTY_OBJECTS; + mSize = 0; + } + } + + /** + * Ensure the array map can hold at least minimumCapacity + * items. + */ + public void ensureCapacity(int minimumCapacity) { + if (mHashes.length < minimumCapacity) { + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(minimumCapacity); + if (mSize > 0) { + System.arraycopy(ohashes, 0, mHashes, 0, mSize); + System.arraycopy(oarray, 0, mArray, 0, mSize<<1); + } + freeArrays(ohashes, oarray, mSize); + } + } + + /** + * Check whether a key exists in the array. + * + * @param key The key to search for. + * @return Returns true if the key exists, else false. + */ + public boolean containsKey(Object key) { + return indexOfKey(key) >= 0; + } + + /** + * Returns the index of a key in the set. + * + * @param key The key to search for. + * @return Returns the index of the key if it exists, else a negative integer. + */ + public int indexOfKey(Object key) { + return key == null ? indexOfNull() : indexOf(key, key.hashCode()); + } + + int indexOfValue(Object value) { + final int N = mSize*2; + final Object[] array = mArray; + if (value == null) { + for (int i=1; i>1; + } + } + } else { + for (int i=1; i>1; + } + } + } + return -1; + } + + /** + * Check whether a value exists in the array. This requires a linear search + * through the entire array. + * + * @param value The value to search for. + * @return Returns true if the value exists, else false. + */ + public boolean containsValue(Object value) { + return indexOfValue(value) >= 0; + } + + /** + * Retrieve a value from the array. + * @param key The key of the value to retrieve. + * @return Returns the value associated with the given key, + * or null if there is no such key. + */ + public V get(Object key) { + final int index = indexOfKey(key); + return index >= 0 ? (V)mArray[(index<<1)+1] : null; + } + + /** + * Return the key at the given index in the array. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the key stored at the given index. + */ + public K keyAt(int index) { + return (K)mArray[index << 1]; + } + + /** + * Return the value at the given index in the array. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value stored at the given index. + */ + public V valueAt(int index) { + return (V)mArray[(index << 1) + 1]; + } + + /** + * Set the value at a given index in the array. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @param value The new value to store at this index. + * @return Returns the previous value at the given index. + */ + public V setValueAt(int index, V value) { + index = (index << 1) + 1; + V old = (V)mArray[index]; + mArray[index] = value; + return old; + } + + /** + * Return true if the array map contains no items. + */ + public boolean isEmpty() { + return mSize <= 0; + } + + /** + * Add a new value to the array map. + * @param key The key under which to store the value. Must not be null. If + * this key already exists in the array, its value will be replaced. + * @param value The value to store for the given key. + * @return Returns the old value that was stored for the given key, or null if there + * was no such key. + */ + public V put(K key, V value) { + final int hash; + int index; + if (key == null) { + hash = 0; + index = indexOfNull(); + } else { + hash = key.hashCode(); + index = indexOf(key, hash); + } + if (index >= 0) { + index = (index<<1) + 1; + final V old = (V)mArray[index]; + mArray[index] = value; + return old; + } + + index = ~index; + if (mSize >= mHashes.length) { + final int n = mSize >= (BASE_SIZE*2) ? (mSize+(mSize>>1)) + : (mSize >= BASE_SIZE ? (BASE_SIZE*2) : BASE_SIZE); + + if (DEBUG) Log.d(TAG, "put: grow from " + mHashes.length + " to " + n); + + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + + if (mHashes.length > 0) { + if (DEBUG) Log.d(TAG, "put: copy 0-" + mSize + " to 0"); + System.arraycopy(ohashes, 0, mHashes, 0, ohashes.length); + System.arraycopy(oarray, 0, mArray, 0, oarray.length); + } + + freeArrays(ohashes, oarray, mSize); + } + + if (index < mSize) { + if (DEBUG) Log.d(TAG, "put: move " + index + "-" + (mSize-index) + + " to " + (index+1)); + System.arraycopy(mHashes, index, mHashes, index + 1, mSize - index); + System.arraycopy(mArray, index << 1, mArray, (index + 1) << 1, (mSize - index) << 1); + } + + mHashes[index] = hash; + mArray[index<<1] = key; + mArray[(index<<1)+1] = value; + mSize++; + return null; + } + + /** + * Perform a {@link #put(Object, Object)} of all key/value pairs in array + * @param array The array whose contents are to be retrieved. + */ + public void putAll(SimpleArrayMap array) { + final int N = array.mSize; + ensureCapacity(mSize + N); + if (mSize == 0) { + if (N > 0) { + System.arraycopy(array.mHashes, 0, mHashes, 0, N); + System.arraycopy(array.mArray, 0, mArray, 0, N<<1); + mSize = N; + } + } else { + for (int i=0; i= 0) { + return removeAt(index); + } + + return null; + } + + /** + * Remove the key/value mapping at the given index. + * @param index The desired index, must be between 0 and {@link #size()}-1. + * @return Returns the value that was stored at this index. + */ + public V removeAt(int index) { + final Object old = mArray[(index << 1) + 1]; + if (mSize <= 1) { + // Now empty. + if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to 0"); + freeArrays(mHashes, mArray, mSize); + mHashes = ContainerHelpers.EMPTY_INTS; + mArray = ContainerHelpers.EMPTY_OBJECTS; + mSize = 0; + } else { + if (mHashes.length > (BASE_SIZE*2) && mSize < mHashes.length/3) { + // Shrunk enough to reduce size of arrays. We don't allow it to + // shrink smaller than (BASE_SIZE*2) to avoid flapping between + // that and BASE_SIZE. + final int n = mSize > (BASE_SIZE*2) ? (mSize + (mSize>>1)) : (BASE_SIZE*2); + + if (DEBUG) Log.d(TAG, "remove: shrink from " + mHashes.length + " to " + n); + + final int[] ohashes = mHashes; + final Object[] oarray = mArray; + allocArrays(n); + + mSize--; + if (index > 0) { + if (DEBUG) Log.d(TAG, "remove: copy from 0-" + index + " to 0"); + System.arraycopy(ohashes, 0, mHashes, 0, index); + System.arraycopy(oarray, 0, mArray, 0, index << 1); + } + if (index < mSize) { + if (DEBUG) Log.d(TAG, "remove: copy from " + (index+1) + "-" + mSize + + " to " + index); + System.arraycopy(ohashes, index + 1, mHashes, index, mSize - index); + System.arraycopy(oarray, (index + 1) << 1, mArray, index << 1, + (mSize - index) << 1); + } + } else { + mSize--; + if (index < mSize) { + if (DEBUG) Log.d(TAG, "remove: move " + (index+1) + "-" + mSize + + " to " + index); + System.arraycopy(mHashes, index + 1, mHashes, index, mSize - index); + System.arraycopy(mArray, (index + 1) << 1, mArray, index << 1, + (mSize - index) << 1); + } + mArray[mSize << 1] = null; + mArray[(mSize << 1) + 1] = null; + } + } + return (V)old; + } + + /** + * Return the number of items in this array map. + */ + public int size() { + return mSize; + } + + /** + * {@inheritDoc} + * + *

    This implementation returns false if the object is not a map, or + * if the maps have different sizes. Otherwise, for each key in this map, + * values of both maps are compared. If the values for any key are not + * equal, the method returns false, otherwise it returns true. + */ + @Override + public boolean equals(Object object) { + if (this == object) { + return true; + } + if (object instanceof Map) { + Map map = (Map) object; + if (size() != map.size()) { + return false; + } + + try { + for (int i=0; iThis implementation composes a string by iterating over its mappings. If + * this map contains itself as a key or a value, the string "(this Map)" + * will appear in its place. + */ + @Override + public String toString() { + if (isEmpty()) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + Object key = keyAt(i); + if (key != this) { + buffer.append(key); + } else { + buffer.append("(this Map)"); + } + buffer.append('='); + Object value = valueAt(i); + if (value != this) { + buffer.append(value); + } else { + buffer.append("(this Map)"); + } + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/SparseArrayCompat.java b/mobile/google-android-support-v4/src/android/support/v4/util/SparseArrayCompat.java new file mode 100644 index 00000000000..aedc4ad08d8 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/SparseArrayCompat.java @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.util; + +/** + * A copy of the current platform (currently {@link android.os.Build.VERSION_CODES#KITKAT} + * version of {@link android.util.SparseArray}; provides a removeAt() method and other things. + */ +public class SparseArrayCompat implements Cloneable { + private static final Object DELETED = new Object(); + private boolean mGarbage = false; + + private int[] mKeys; + private Object[] mValues; + private int mSize; + + /** + * Creates a new SparseArray containing no mappings. + */ + public SparseArrayCompat() { + this(10); + } + + /** + * Creates a new SparseArray containing no mappings that will not + * require any additional memory allocation to store the specified + * number of mappings. If you supply an initial capacity of 0, the + * sparse array will be initialized with a light-weight representation + * not requiring any additional array allocations. + */ + public SparseArrayCompat(int initialCapacity) { + if (initialCapacity == 0) { + mKeys = ContainerHelpers.EMPTY_INTS; + mValues = ContainerHelpers.EMPTY_OBJECTS; + } else { + initialCapacity = ContainerHelpers.idealIntArraySize(initialCapacity); + mKeys = new int[initialCapacity]; + mValues = new Object[initialCapacity]; + } + mSize = 0; + } + + @Override + @SuppressWarnings("unchecked") + public SparseArrayCompat clone() { + SparseArrayCompat clone = null; + try { + clone = (SparseArrayCompat) super.clone(); + clone.mKeys = mKeys.clone(); + clone.mValues = mValues.clone(); + } catch (CloneNotSupportedException cnse) { + /* ignore */ + } + return clone; + } + + /** + * Gets the Object mapped from the specified key, or null + * if no such mapping has been made. + */ + public E get(int key) { + return get(key, null); + } + + /** + * Gets the Object mapped from the specified key, or the specified Object + * if no such mapping has been made. + */ + @SuppressWarnings("unchecked") + public E get(int key, E valueIfKeyNotFound) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i < 0 || mValues[i] == DELETED) { + return valueIfKeyNotFound; + } else { + return (E) mValues[i]; + } + } + + /** + * Removes the mapping from the specified key, if there was any. + */ + public void delete(int key) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + if (mValues[i] != DELETED) { + mValues[i] = DELETED; + mGarbage = true; + } + } + } + + /** + * Alias for {@link #delete(int)}. + */ + public void remove(int key) { + delete(key); + } + + /** + * Removes the mapping at the specified index. + */ + public void removeAt(int index) { + if (mValues[index] != DELETED) { + mValues[index] = DELETED; + mGarbage = true; + } + } + + /** + * Remove a range of mappings as a batch. + * + * @param index Index to begin at + * @param size Number of mappings to remove + */ + public void removeAtRange(int index, int size) { + final int end = Math.min(mSize, index + size); + for (int i = index; i < end; i++) { + removeAt(i); + } + } + + private void gc() { + // Log.e("SparseArray", "gc start with " + mSize); + + int n = mSize; + int o = 0; + int[] keys = mKeys; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + Object val = values[i]; + + if (val != DELETED) { + if (i != o) { + keys[o] = keys[i]; + values[o] = val; + values[i] = null; + } + + o++; + } + } + + mGarbage = false; + mSize = o; + + // Log.e("SparseArray", "gc end with " + mSize); + } + + /** + * Adds a mapping from the specified key to the specified value, + * replacing the previous mapping from the specified key if there + * was one. + */ + public void put(int key, E value) { + int i = ContainerHelpers.binarySearch(mKeys, mSize, key); + + if (i >= 0) { + mValues[i] = value; + } else { + i = ~i; + + if (i < mSize && mValues[i] == DELETED) { + mKeys[i] = key; + mValues[i] = value; + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + + // Search again because indices may have changed. + i = ~ ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + if (mSize >= mKeys.length) { + int n = ContainerHelpers.idealIntArraySize(mSize + 1); + + int[] nkeys = new int[n]; + Object[] nvalues = new Object[n]; + + // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + if (mSize - i != 0) { + // Log.e("SparseArray", "move " + (mSize - i)); + System.arraycopy(mKeys, i, mKeys, i + 1, mSize - i); + System.arraycopy(mValues, i, mValues, i + 1, mSize - i); + } + + mKeys[i] = key; + mValues[i] = value; + mSize++; + } + } + + /** + * Returns the number of key-value mappings that this SparseArray + * currently stores. + */ + public int size() { + if (mGarbage) { + gc(); + } + + return mSize; + } + + /** + * Given an index in the range 0...size()-1, returns + * the key from the indexth key-value mapping that this + * SparseArray stores. + */ + public int keyAt(int index) { + if (mGarbage) { + gc(); + } + + return mKeys[index]; + } + + /** + * Given an index in the range 0...size()-1, returns + * the value from the indexth key-value mapping that this + * SparseArray stores. + */ + @SuppressWarnings("unchecked") + public E valueAt(int index) { + if (mGarbage) { + gc(); + } + + return (E) mValues[index]; + } + + /** + * Given an index in the range 0...size()-1, sets a new + * value for the indexth key-value mapping that this + * SparseArray stores. + */ + public void setValueAt(int index, E value) { + if (mGarbage) { + gc(); + } + + mValues[index] = value; + } + + /** + * Returns the index for which {@link #keyAt} would return the + * specified key, or a negative number if the specified + * key is not mapped. + */ + public int indexOfKey(int key) { + if (mGarbage) { + gc(); + } + + return ContainerHelpers.binarySearch(mKeys, mSize, key); + } + + /** + * Returns an index for which {@link #valueAt} would return the + * specified key, or a negative number if no keys map to the + * specified value. + *

    Beware that this is a linear search, unlike lookups by key, + * and that multiple keys can map to the same value and this will + * find only one of them. + *

    Note also that unlike most collections' {@code indexOf} methods, + * this method compares values using {@code ==} rather than {@code equals}. + */ + public int indexOfValue(E value) { + if (mGarbage) { + gc(); + } + + for (int i = 0; i < mSize; i++) + if (mValues[i] == value) + return i; + + return -1; + } + + /** + * Removes all key-value mappings from this SparseArray. + */ + public void clear() { + int n = mSize; + Object[] values = mValues; + + for (int i = 0; i < n; i++) { + values[i] = null; + } + + mSize = 0; + mGarbage = false; + } + + /** + * Puts a key/value pair into the array, optimizing for the case where + * the key is greater than all existing keys in the array. + */ + public void append(int key, E value) { + if (mSize != 0 && key <= mKeys[mSize - 1]) { + put(key, value); + return; + } + + if (mGarbage && mSize >= mKeys.length) { + gc(); + } + + int pos = mSize; + if (pos >= mKeys.length) { + int n = ContainerHelpers.idealIntArraySize(pos + 1); + + int[] nkeys = new int[n]; + Object[] nvalues = new Object[n]; + + // Log.e("SparseArray", "grow " + mKeys.length + " to " + n); + System.arraycopy(mKeys, 0, nkeys, 0, mKeys.length); + System.arraycopy(mValues, 0, nvalues, 0, mValues.length); + + mKeys = nkeys; + mValues = nvalues; + } + + mKeys[pos] = key; + mValues[pos] = value; + mSize = pos + 1; + } + + /** + * {@inheritDoc} + * + *

    This implementation composes a string by iterating over its mappings. If + * this map contains itself as a value, the string "(this Map)" + * will appear in its place. + */ + @Override + public String toString() { + if (size() <= 0) { + return "{}"; + } + + StringBuilder buffer = new StringBuilder(mSize * 28); + buffer.append('{'); + for (int i=0; i 0) { + buffer.append(", "); + } + int key = keyAt(i); + buffer.append(key); + buffer.append('='); + Object value = valueAt(i); + if (value != this) { + buffer.append(value); + } else { + buffer.append("(this Map)"); + } + } + buffer.append('}'); + return buffer.toString(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/TimeUtils.java b/mobile/google-android-support-v4/src/android/support/v4/util/TimeUtils.java new file mode 100644 index 00000000000..a57c94c9e21 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/TimeUtils.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.util; + +import java.io.PrintWriter; + +/** + * Helper for accessing features in {@link android.util.TimeUtils} + * introduced after API level 4 in a backwards compatible fashion. + * + * @hide + */ +public class TimeUtils { + /** @hide Field length that can hold 999 days of time */ + public static final int HUNDRED_DAY_FIELD_LEN = 19; + + private static final int SECONDS_PER_MINUTE = 60; + private static final int SECONDS_PER_HOUR = 60 * 60; + private static final int SECONDS_PER_DAY = 24 * 60 * 60; + + private static final Object sFormatSync = new Object(); + private static char[] sFormatStr = new char[HUNDRED_DAY_FIELD_LEN+5]; + + static private int accumField(int amt, int suffix, boolean always, int zeropad) { + if (amt > 99 || (always && zeropad >= 3)) { + return 3+suffix; + } + if (amt > 9 || (always && zeropad >= 2)) { + return 2+suffix; + } + if (always || amt > 0) { + return 1+suffix; + } + return 0; + } + + static private int printField(char[] formatStr, int amt, char suffix, int pos, + boolean always, int zeropad) { + if (always || amt > 0) { + final int startPos = pos; + if ((always && zeropad >= 3) || amt > 99) { + int dig = amt/100; + formatStr[pos] = (char)(dig + '0'); + pos++; + amt -= (dig*100); + } + if ((always && zeropad >= 2) || amt > 9 || startPos != pos) { + int dig = amt/10; + formatStr[pos] = (char)(dig + '0'); + pos++; + amt -= (dig*10); + } + formatStr[pos] = (char)(amt + '0'); + pos++; + formatStr[pos] = suffix; + pos++; + } + return pos; + } + + private static int formatDurationLocked(long duration, int fieldLen) { + if (sFormatStr.length < fieldLen) { + sFormatStr = new char[fieldLen]; + } + + char[] formatStr = sFormatStr; + + if (duration == 0) { + int pos = 0; + fieldLen -= 1; + while (pos < fieldLen) { + formatStr[pos] = ' '; + } + formatStr[pos] = '0'; + return pos+1; + } + + char prefix; + if (duration > 0) { + prefix = '+'; + } else { + prefix = '-'; + duration = -duration; + } + + int millis = (int)(duration%1000); + int seconds = (int) Math.floor(duration / 1000); + int days = 0, hours = 0, minutes = 0; + + if (seconds > SECONDS_PER_DAY) { + days = seconds / SECONDS_PER_DAY; + seconds -= days * SECONDS_PER_DAY; + } + if (seconds > SECONDS_PER_HOUR) { + hours = seconds / SECONDS_PER_HOUR; + seconds -= hours * SECONDS_PER_HOUR; + } + if (seconds > SECONDS_PER_MINUTE) { + minutes = seconds / SECONDS_PER_MINUTE; + seconds -= minutes * SECONDS_PER_MINUTE; + } + + int pos = 0; + + if (fieldLen != 0) { + int myLen = accumField(days, 1, false, 0); + myLen += accumField(hours, 1, myLen > 0, 2); + myLen += accumField(minutes, 1, myLen > 0, 2); + myLen += accumField(seconds, 1, myLen > 0, 2); + myLen += accumField(millis, 2, true, myLen > 0 ? 3 : 0) + 1; + while (myLen < fieldLen) { + formatStr[pos] = ' '; + pos++; + myLen++; + } + } + + formatStr[pos] = prefix; + pos++; + + int start = pos; + boolean zeropad = fieldLen != 0; + pos = printField(formatStr, days, 'd', pos, false, 0); + pos = printField(formatStr, hours, 'h', pos, pos != start, zeropad ? 2 : 0); + pos = printField(formatStr, minutes, 'm', pos, pos != start, zeropad ? 2 : 0); + pos = printField(formatStr, seconds, 's', pos, pos != start, zeropad ? 2 : 0); + pos = printField(formatStr, millis, 'm', pos, true, (zeropad && pos != start) ? 3 : 0); + formatStr[pos] = 's'; + return pos + 1; + } + + /** @hide Just for debugging; not internationalized. */ + public static void formatDuration(long duration, StringBuilder builder) { + synchronized (sFormatSync) { + int len = formatDurationLocked(duration, 0); + builder.append(sFormatStr, 0, len); + } + } + + /** @hide Just for debugging; not internationalized. */ + public static void formatDuration(long duration, PrintWriter pw, int fieldLen) { + synchronized (sFormatSync) { + int len = formatDurationLocked(duration, fieldLen); + pw.print(new String(sFormatStr, 0, len)); + } + } + + /** @hide Just for debugging; not internationalized. */ + public static void formatDuration(long duration, PrintWriter pw) { + formatDuration(duration, pw, 0); + } + + /** @hide Just for debugging; not internationalized. */ + public static void formatDuration(long time, long now, PrintWriter pw) { + if (time == 0) { + pw.print("--"); + return; + } + formatDuration(time-now, pw, 0); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/util/package.html b/mobile/google-android-support-v4/src/android/support/v4/util/package.html new file mode 100644 index 00000000000..afde9b750d7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/util/package.html @@ -0,0 +1,6 @@ + + +Support android.util classes to assist with development of applications for +android API level 4 or later. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompat.java new file mode 100644 index 00000000000..0ea755ee672 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompat.java @@ -0,0 +1,502 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.os.Build; +import android.os.Bundle; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +/** + * Helper for accessing {@link View.AccessibilityDelegate} introduced after + * API level 4 in a backwards compatible fashion. + */ +public class AccessibilityDelegateCompat { + + static interface AccessibilityDelegateImpl { + public Object newAccessiblityDelegateDefaultImpl(); + public Object newAccessiblityDelegateBridge(AccessibilityDelegateCompat listener); + public boolean dispatchPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event); + public void onInitializeAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event); + public void onInitializeAccessibilityNodeInfo(Object delegate, View host, + AccessibilityNodeInfoCompat info); + public void onPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event); + public boolean onRequestSendAccessibilityEvent(Object delegate, ViewGroup host, View child, + AccessibilityEvent event); + public void sendAccessibilityEvent(Object delegate, View host, int eventType); + public void sendAccessibilityEventUnchecked(Object delegate, View host, + AccessibilityEvent event); + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(Object delegate, + View host); + public boolean performAccessibilityAction(Object delegate, View host, int action, + Bundle args); + } + + static class AccessibilityDelegateStubImpl implements AccessibilityDelegateImpl { + public Object newAccessiblityDelegateDefaultImpl() { + return null; + } + + @Override + public Object newAccessiblityDelegateBridge(AccessibilityDelegateCompat listener) { + return null; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + return false; + } + + @Override + public void onInitializeAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + + } + + @Override + public void onInitializeAccessibilityNodeInfo(Object delegate, View host, + AccessibilityNodeInfoCompat info) { + + } + + @Override + public void onPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + + } + + @Override + public boolean onRequestSendAccessibilityEvent(Object delegate, ViewGroup host, View child, + AccessibilityEvent event) { + return true; + } + + @Override + public void sendAccessibilityEvent(Object delegate, View host, int eventType) { + + } + + @Override + public void sendAccessibilityEventUnchecked(Object delegate, View host, + AccessibilityEvent event) { + + } + + @Override + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(Object delegate, + View host) { + return null; + } + + @Override + public boolean performAccessibilityAction(Object delegate, View host, int action, + Bundle args) { + return false; + } + } + + static class AccessibilityDelegateIcsImpl extends AccessibilityDelegateStubImpl { + @Override + public Object newAccessiblityDelegateDefaultImpl() { + return AccessibilityDelegateCompatIcs.newAccessibilityDelegateDefaultImpl(); + } + + @Override + public Object newAccessiblityDelegateBridge(final AccessibilityDelegateCompat compat) { + return AccessibilityDelegateCompatIcs.newAccessibilityDelegateBridge( + new AccessibilityDelegateCompatIcs.AccessibilityDelegateBridge() { + @Override + public boolean dispatchPopulateAccessibilityEvent(View host, + AccessibilityEvent event) { + return compat.dispatchPopulateAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + compat.onInitializeAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, Object info) { + compat.onInitializeAccessibilityNodeInfo(host, + new AccessibilityNodeInfoCompat(info)); + } + + @Override + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + compat.onPopulateAccessibilityEvent(host, event); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + return compat.onRequestSendAccessibilityEvent(host, child, event); + } + + @Override + public void sendAccessibilityEvent(View host, int eventType) { + compat.sendAccessibilityEvent(host, eventType); + } + + @Override + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { + compat.sendAccessibilityEventUnchecked(host, event); + } + }); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + return AccessibilityDelegateCompatIcs.dispatchPopulateAccessibilityEvent(delegate, + host, event); + } + + @Override + public void onInitializeAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + AccessibilityDelegateCompatIcs.onInitializeAccessibilityEvent(delegate, host, event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(Object delegate, View host, + AccessibilityNodeInfoCompat info) { + AccessibilityDelegateCompatIcs.onInitializeAccessibilityNodeInfo(delegate, host, + info.getInfo()); + } + + @Override + public void onPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + AccessibilityDelegateCompatIcs.onPopulateAccessibilityEvent(delegate, host, event); + } + + @Override + public boolean onRequestSendAccessibilityEvent(Object delegate, ViewGroup host, View child, + AccessibilityEvent event) { + return AccessibilityDelegateCompatIcs.onRequestSendAccessibilityEvent(delegate, host, + child, event); + } + + @Override + public void sendAccessibilityEvent(Object delegate, View host, int eventType) { + AccessibilityDelegateCompatIcs.sendAccessibilityEvent(delegate, host, eventType); + } + + @Override + public void sendAccessibilityEventUnchecked(Object delegate, View host, + AccessibilityEvent event) { + AccessibilityDelegateCompatIcs.sendAccessibilityEventUnchecked(delegate, host, event); + } + } + + static class AccessibilityDelegateJellyBeanImpl extends AccessibilityDelegateIcsImpl { + @Override + public Object newAccessiblityDelegateBridge(final AccessibilityDelegateCompat compat) { + return AccessibilityDelegateCompatJellyBean.newAccessibilityDelegateBridge( + new AccessibilityDelegateCompatJellyBean + .AccessibilityDelegateBridgeJellyBean() { + @Override + public boolean dispatchPopulateAccessibilityEvent(View host, + AccessibilityEvent event) { + return compat.dispatchPopulateAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + compat.onInitializeAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, Object info) { + compat.onInitializeAccessibilityNodeInfo(host, + new AccessibilityNodeInfoCompat(info)); + } + + @Override + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + compat.onPopulateAccessibilityEvent(host, event); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + return compat.onRequestSendAccessibilityEvent(host, child, event); + } + + @Override + public void sendAccessibilityEvent(View host, int eventType) { + compat.sendAccessibilityEvent(host, eventType); + } + + @Override + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { + compat.sendAccessibilityEventUnchecked(host, event); + } + + @Override + public Object getAccessibilityNodeProvider(View host) { + AccessibilityNodeProviderCompat provider = + compat.getAccessibilityNodeProvider(host); + return (provider != null) ? provider.getProvider() : null; + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + return compat.performAccessibilityAction(host, action, args); + } + }); + } + + @Override + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(Object delegate, + View host) { + Object provider = AccessibilityDelegateCompatJellyBean.getAccessibilityNodeProvider( + delegate, host); + if (provider != null) { + return new AccessibilityNodeProviderCompat(provider); + } + return null; + } + + @Override + public boolean performAccessibilityAction(Object delegate, View host, int action, + Bundle args) { + return AccessibilityDelegateCompatJellyBean.performAccessibilityAction(delegate, + host, action, args); + } + } + + private static final AccessibilityDelegateImpl IMPL; + private static final Object DEFAULT_DELEGATE; + + static { + if (Build.VERSION.SDK_INT >= 16) { // JellyBean + IMPL = new AccessibilityDelegateJellyBeanImpl(); + } else if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new AccessibilityDelegateIcsImpl(); + } else { + IMPL = new AccessibilityDelegateStubImpl(); + } + DEFAULT_DELEGATE = IMPL.newAccessiblityDelegateDefaultImpl(); + } + + final Object mBridge; + + /** + * Creates a new instance. + */ + public AccessibilityDelegateCompat() { + mBridge = IMPL.newAccessiblityDelegateBridge(this); + } + + /** + * @return The wrapped bridge implementation. + */ + Object getBridge() { + return mBridge; + } + + /** + * Sends an accessibility event of the given type. If accessibility is not + * enabled this method has no effect. + *

    + * The default implementation behaves as {@link View#sendAccessibilityEvent(int) + * View#sendAccessibilityEvent(int)} for the case of no accessibility delegate + * been set. + *

    + * + * @param host The View hosting the delegate. + * @param eventType The type of the event to send. + * + * @see View#sendAccessibilityEvent(int) View#sendAccessibilityEvent(int) + */ + public void sendAccessibilityEvent(View host, int eventType) { + IMPL.sendAccessibilityEvent(DEFAULT_DELEGATE, host, eventType); + } + + /** + * Sends an accessibility event. This method behaves exactly as + * {@link #sendAccessibilityEvent(View, int)} but takes as an argument an + * empty {@link AccessibilityEvent} and does not perform a check whether + * accessibility is enabled. + *

    + * The default implementation behaves as + * {@link View#sendAccessibilityEventUnchecked(AccessibilityEvent) + * View#sendAccessibilityEventUnchecked(AccessibilityEvent)} for + * the case of no accessibility delegate been set. + *

    + * + * @param host The View hosting the delegate. + * @param event The event to send. + * + * @see View#sendAccessibilityEventUnchecked(AccessibilityEvent) + * View#sendAccessibilityEventUnchecked(AccessibilityEvent) + */ + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { + IMPL.sendAccessibilityEventUnchecked(DEFAULT_DELEGATE, host, event); + } + + /** + * Dispatches an {@link AccessibilityEvent} to the host {@link View} first and then + * to its children for adding their text content to the event. + *

    + * The default implementation behaves as + * {@link View#dispatchPopulateAccessibilityEvent(AccessibilityEvent) + * View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)} for + * the case of no accessibility delegate been set. + *

    + * + * @param host The View hosting the delegate. + * @param event The event. + * @return True if the event population was completed. + * + * @see View#dispatchPopulateAccessibilityEvent(AccessibilityEvent) + * View#dispatchPopulateAccessibilityEvent(AccessibilityEvent) + */ + public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + return IMPL.dispatchPopulateAccessibilityEvent(DEFAULT_DELEGATE, host, event); + } + + /** + * Gives a chance to the host View to populate the accessibility event with its + * text content. + *

    + * The default implementation behaves as + * {@link ViewCompat#onPopulateAccessibilityEvent(View, AccessibilityEvent) + * ViewCompat#onPopulateAccessibilityEvent(AccessibilityEvent)} for + * the case of no accessibility delegate been set. + *

    + * + * @param host The View hosting the delegate. + * @param event The accessibility event which to populate. + * + * @see ViewCompat#onPopulateAccessibilityEvent(View ,AccessibilityEvent) + * ViewCompat#onPopulateAccessibilityEvent(View, AccessibilityEvent) + */ + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + IMPL.onPopulateAccessibilityEvent(DEFAULT_DELEGATE, host, event); + } + + /** + * Initializes an {@link AccessibilityEvent} with information about the + * the host View which is the event source. + *

    + * The default implementation behaves as + * {@link ViewCompat#onInitializeAccessibilityEvent(View v, AccessibilityEvent event) + * ViewCompat#onInitalizeAccessibilityEvent(View v, AccessibilityEvent event)} for + * the case of no accessibility delegate been set. + *

    + * + * @param host The View hosting the delegate. + * @param event The event to initialize. + * + * @see ViewCompat#onInitializeAccessibilityEvent(View, AccessibilityEvent) + * ViewCompat#onInitializeAccessibilityEvent(View, AccessibilityEvent) + */ + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + IMPL.onInitializeAccessibilityEvent(DEFAULT_DELEGATE, host, event); + } + + /** + * Initializes an {@link AccessibilityNodeInfoCompat} with information about the host view. + *

    + * The default implementation behaves as + * {@link ViewCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat) + * ViewCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat)} for + * the case of no accessibility delegate been set. + *

    + * + * @param host The View hosting the delegate. + * @param info The instance to initialize. + * + * @see ViewCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat) + * ViewCompat#onInitializeAccessibilityNodeInfo(View, AccessibilityNodeInfoCompat) + */ + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + IMPL.onInitializeAccessibilityNodeInfo(DEFAULT_DELEGATE, host, info); + } + + /** + * Called when a child of the host View has requested sending an + * {@link AccessibilityEvent} and gives an opportunity to the parent (the host) + * to augment the event. + *

    + * The default implementation behaves as + * {@link ViewGroupCompat#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent) + * ViewGroupCompat#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent)} for + * the case of no accessibility delegate been set. + *

    + * + * @param host The View hosting the delegate. + * @param child The child which requests sending the event. + * @param event The event to be sent. + * @return True if the event should be sent + * + * @see ViewGroupCompat#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent) + * ViewGroupCompat#onRequestSendAccessibilityEvent(ViewGroup, View, AccessibilityEvent) + */ + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + return IMPL.onRequestSendAccessibilityEvent(DEFAULT_DELEGATE, host, child, event); + } + + /** + * Gets the provider for managing a virtual view hierarchy rooted at this View + * and reported to {@link android.accessibilityservice.AccessibilityService}s + * that explore the window content. + *

    + * The default implementation behaves as + * {@link ViewCompat#getAccessibilityNodeProvider(View) ViewCompat#getAccessibilityNodeProvider(View)} + * for the case of no accessibility delegate been set. + *

    + * + * @return The provider. + * + * @see AccessibilityNodeProviderCompat + */ + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) { + return IMPL.getAccessibilityNodeProvider(DEFAULT_DELEGATE, host); + } + + /** + * Performs the specified accessibility action on the view. For + * possible accessibility actions look at {@link AccessibilityNodeInfoCompat}. + *

    + * The default implementation behaves as + * {@link View#performAccessibilityAction(int, Bundle) + * View#performAccessibilityAction(int, Bundle)} for the case of + * no accessibility delegate been set. + *

    + * + * @param action The action to perform. + * @return Whether the action was performed. + * + * @see View#performAccessibilityAction(int, Bundle) + * View#performAccessibilityAction(int, Bundle) + */ + public boolean performAccessibilityAction(View host, int action, Bundle args) { + return IMPL.performAccessibilityAction(DEFAULT_DELEGATE, host, action, args); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatIcs.java new file mode 100644 index 00000000000..4adeb0cd0d4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatIcs.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * ICS specific AccessibilityDelegate API implementation. + */ +class AccessibilityDelegateCompatIcs { + + public interface AccessibilityDelegateBridge { + public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event); + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event); + public void onInitializeAccessibilityNodeInfo(View host, Object info); + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event); + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event); + public void sendAccessibilityEvent(View host, int eventType); + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event); + } + + public static Object newAccessibilityDelegateDefaultImpl() { + return new AccessibilityDelegate(); + } + + public static Object newAccessibilityDelegateBridge(final AccessibilityDelegateBridge bridge) { + return new AccessibilityDelegate() { + @Override + public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + return bridge.dispatchPopulateAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + bridge.onInitializeAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + bridge.onInitializeAccessibilityNodeInfo(host, info); + } + + @Override + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + bridge.onPopulateAccessibilityEvent(host, event); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + return bridge.onRequestSendAccessibilityEvent(host, child, event); + } + + @Override + public void sendAccessibilityEvent(View host, int eventType) { + bridge.sendAccessibilityEvent(host, eventType); + } + + @Override + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { + bridge.sendAccessibilityEventUnchecked(host, event); + } + }; + } + + public static boolean dispatchPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + return ((AccessibilityDelegate) delegate).dispatchPopulateAccessibilityEvent(host, event); + } + + public static void onInitializeAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + ((AccessibilityDelegate) delegate).onInitializeAccessibilityEvent(host, event); + } + + public static void onInitializeAccessibilityNodeInfo(Object delegate, View host, Object info) { + ((AccessibilityDelegate) delegate).onInitializeAccessibilityNodeInfo(host, + (AccessibilityNodeInfo) info); + } + + public static void onPopulateAccessibilityEvent(Object delegate, View host, + AccessibilityEvent event) { + ((AccessibilityDelegate) delegate).onPopulateAccessibilityEvent(host, event); + } + + public static boolean onRequestSendAccessibilityEvent(Object delegate, ViewGroup host, + View child, AccessibilityEvent event) { + return ((AccessibilityDelegate) delegate).onRequestSendAccessibilityEvent(host, child, + event); + } + + public static void sendAccessibilityEvent(Object delegate, View host, int eventType) { + ((AccessibilityDelegate) delegate).sendAccessibilityEvent(host, eventType); + } + + public static void sendAccessibilityEventUnchecked(Object delegate, View host, + AccessibilityEvent event) { + ((AccessibilityDelegate) delegate).sendAccessibilityEventUnchecked(host, event); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatJellyBean.java b/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatJellyBean.java new file mode 100644 index 00000000000..6651b624d48 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/AccessibilityDelegateCompatJellyBean.java @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.os.Bundle; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +/** + * JellyBean specific AccessibilityDelegate API implementation. + */ +class AccessibilityDelegateCompatJellyBean { + + public interface AccessibilityDelegateBridgeJellyBean { + public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event); + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event); + public void onInitializeAccessibilityNodeInfo(View host, Object info); + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event); + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event); + public void sendAccessibilityEvent(View host, int eventType); + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event); + public Object getAccessibilityNodeProvider(View host); + public boolean performAccessibilityAction(View host, int action, Bundle args); + } + + public static Object newAccessibilityDelegateBridge( + final AccessibilityDelegateBridgeJellyBean bridge) { + return new AccessibilityDelegate() { + + @Override + public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + return bridge.dispatchPopulateAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + bridge.onInitializeAccessibilityEvent(host, event); + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + bridge.onInitializeAccessibilityNodeInfo(host, info); + } + + @Override + public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + bridge.onPopulateAccessibilityEvent(host, event); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + return bridge.onRequestSendAccessibilityEvent(host, child, event); + } + + @Override + public void sendAccessibilityEvent(View host, int eventType) { + bridge.sendAccessibilityEvent(host, eventType); + } + + @Override + public void sendAccessibilityEventUnchecked(View host, AccessibilityEvent event) { + bridge.sendAccessibilityEventUnchecked(host, event); + } + + @Override + public AccessibilityNodeProvider getAccessibilityNodeProvider(View host) { + return (AccessibilityNodeProvider) bridge.getAccessibilityNodeProvider(host); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + return bridge.performAccessibilityAction(host, action, args); + } + }; + } + + public static Object getAccessibilityNodeProvider(Object delegate, + View host) { + return ((AccessibilityDelegate) delegate).getAccessibilityNodeProvider(host); + } + + public static boolean performAccessibilityAction(Object delegate, View host, int action, + Bundle args) { + return ((AccessibilityDelegate) delegate).performAccessibilityAction(host, action, args); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ActionProvider.java b/mobile/google-android-support-v4/src/android/support/v4/view/ActionProvider.java new file mode 100644 index 00000000000..f11876a89cd --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ActionProvider.java @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.util.Log; +import android.view.MenuItem; +import android.view.SubMenu; +import android.view.View; + +/** + * This class is a mediator for accomplishing a given task, for example sharing a file. It is + * responsible for creating a view that performs an action that accomplishes the task. This class + * also implements other functions such a performing a default action. + * + *

    Note: This class is included in the support library for compatibility + * with API level 4 and higher. If you're developing your app for API level 14 and higher + * only, you should instead use the framework {@link android.view.ActionProvider} + * class.

    + * + *

    An ActionProvider can be + * optionally specified for a {@link android.view.MenuItem} and in such a case it will be + * responsible for + * creating the action view that appears in the {@link android.app.ActionBar} as a substitute for + * the menu item when the item is displayed as an action item. Also the provider is responsible for + * performing a default action if a menu item placed on the overflow menu of the ActionBar is + * selected and none of the menu item callbacks has handled the selection. For this case the + * provider can also optionally provide a sub-menu for accomplishing the task at hand. + * + *

    There are two ways for using an action provider for creating and handling of action views: + * + *

    • Setting the action provider on a {@link android.view.MenuItem} directly by + * calling {@link + * android.support.v4.view.MenuItemCompat#setActionProvider(android.view.MenuItem, ActionProvider)}. + *
    • + * + *
    • Declaring the action provider in the menu XML resource. For example: + * + *
      
      + *   <item android:id="@+id/my_menu_item"
      + *     android:title="@string/my_menu_item_title"
      + *     android:icon="@drawable/my_menu_item_icon"
      + *     android:showAsAction="ifRoom"
      + *     android:actionProviderClass="foo.bar.SomeActionProvider" />
      + * 
      + *

    + * + * @see android.support.v4.view.MenuItemCompat#setActionProvider(android.view.MenuItem, ActionProvider) + * @see android.support.v4.view.MenuItemCompat#getActionProvider(android.view.MenuItem) + */ +public abstract class ActionProvider { + private static final String TAG = "ActionProvider(support)"; + private final Context mContext; + + private SubUiVisibilityListener mSubUiVisibilityListener; + private VisibilityListener mVisibilityListener; + + /** + * Creates a new instance. + * + * @param context Context for accessing resources. + */ + public ActionProvider(Context context) { + mContext = context; + } + + /** + * Gets the context associated with this action provider. + */ + public Context getContext() { + return mContext; + } + + /** + * Factory method for creating new action views. + * + * @return A new action view. + */ + public abstract View onCreateActionView(); + + /** + * Factory method called by the Android framework to create new action views. + * This method returns a new action view for the given MenuItem. + * + *

    If your ActionProvider implementation overrides the deprecated no-argument overload + * {@link #onCreateActionView()}, overriding this method for devices running API 16 or later + * is recommended but optional. The default implementation calls {@link #onCreateActionView()} + * for compatibility with applications written for older platform versions.

    + * + * @param forItem MenuItem to create the action view for + * @return the new action view + */ + public View onCreateActionView(MenuItem forItem) { + return onCreateActionView(); + } + + /** + * The result of this method determines whether or not {@link #isVisible()} will be used + * by the {@link MenuItem} this ActionProvider is bound to help determine its visibility. + * + * @return true if this ActionProvider overrides the visibility of the MenuItem + * it is bound to, false otherwise. The default implementation returns false. + * @see #isVisible() + */ + public boolean overridesItemVisibility() { + return false; + } + + /** + * If {@link #overridesItemVisibility()} returns true, the return value of this method + * will help determine the visibility of the {@link MenuItem} this ActionProvider is bound to. + * + *

    If the MenuItem's visibility is explicitly set to false by the application, + * the MenuItem will not be shown, even if this method returns true.

    + * + * @return true if the MenuItem this ActionProvider is bound to is visible, false if + * it is invisible. The default implementation returns true. + */ + public boolean isVisible() { + return true; + } + + /** + * If this ActionProvider is associated with an item in a menu, + * refresh the visibility of the item based on {@link #overridesItemVisibility()} and + * {@link #isVisible()}. If {@link #overridesItemVisibility()} returns false, this call + * will have no effect. + */ + public void refreshVisibility() { + if (mVisibilityListener != null && overridesItemVisibility()) { + mVisibilityListener.onActionProviderVisibilityChanged(isVisible()); + } + } + + /** + * Performs an optional default action. + * + *

    For the case of an action provider placed in a menu + * item not shown as an action this method is invoked if previous callbacks for processing menu + * selection has handled the event. + * + *

    A menu item selection is processed in the following order: + * + *

    • Receiving a call to + * {@link android.view.MenuItem.OnMenuItemClickListener#onMenuItemClick + * MenuItem.OnMenuItemClickListener.onMenuItemClick}.
    • + * + *
    • Receiving a call to + * {@link android.app.Activity#onOptionsItemSelected(android.view.MenuItem)} + * FragmentActivity.onOptionsItemSelected(MenuItem)} + *
    • + * + *
    • Receiving a call to + * {@link android.support.v4.app.Fragment#onOptionsItemSelected(android.view.MenuItem)} + * Fragment.onOptionsItemSelected(MenuItem)}
    • + * + *
    • Launching the {@link android.content.Intent} set via + * {@link android.view.MenuItem#setIntent(android.content.Intent) + * MenuItem.setIntent(android.content.Intent)} + *
    • + * + *
    • Invoking this method.
    + * + *

    The default implementation does not perform any action and returns false. + */ + public boolean onPerformDefaultAction() { + return false; + } + + /** + * Determines if this ActionProvider has a submenu associated with it. + * + *

    Associated submenus will be shown when an action view is not. This provider instance will + * receive a call to {@link #onPrepareSubMenu(SubMenu)} after the call to {@link + * #onPerformDefaultAction()} and before a submenu is displayed to the user. + * + * @return true if the item backed by this provider should have an associated submenu + */ + public boolean hasSubMenu() { + return false; + } + + /** + * Called to prepare an associated submenu for the menu item backed by this ActionProvider. + * + *

    if {@link #hasSubMenu()} returns true, this method will be called when the menu item is + * selected to prepare the submenu for presentation to the user. Apps may use this to create or + * alter submenu content right before display. + * + * @param subMenu Submenu that will be displayed + */ + public void onPrepareSubMenu(SubMenu subMenu) { + } + + /** + * Notify the system that the visibility of an action view's sub-UI such as an anchored popup + * has changed. This will affect how other system visibility notifications occur. + * + * @hide Pending future API approval + */ + public void subUiVisibilityChanged(boolean isVisible) { + if (mSubUiVisibilityListener != null) { + mSubUiVisibilityListener.onSubUiVisibilityChanged(isVisible); + } + } + + /** + * @hide Internal use only + */ + public void setSubUiVisibilityListener(SubUiVisibilityListener listener) { + mSubUiVisibilityListener = listener; + } + + /** + * Set a listener to be notified when this ActionProvider's overridden visibility changes. + * This should only be used by MenuItem implementations. + * + * @param listener listener to set + */ + public void setVisibilityListener(VisibilityListener listener) { + if (mVisibilityListener != null && listener != null) { + Log.w(TAG, "setVisibilityListener: Setting a new ActionProvider.VisibilityListener " + + "when one is already set. Are you reusing this " + getClass().getSimpleName() + + " instance while it is still in use somewhere else?"); + } + mVisibilityListener = listener; + } + + /** + * @hide Internal use only + */ + public interface SubUiVisibilityListener { + + public void onSubUiVisibilityChanged(boolean isVisible); + } + + /** + * Listens to changes in visibility as reported by {@link ActionProvider#refreshVisibility()}. + * + * @see ActionProvider#overridesItemVisibility() + * @see ActionProvider#isVisible() + */ + public interface VisibilityListener { + public void onActionProviderVisibilityChanged(boolean isVisible); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/GestureDetectorCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/GestureDetectorCompat.java new file mode 100644 index 00000000000..bcd1a0ac7da --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/GestureDetectorCompat.java @@ -0,0 +1,564 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.view.GestureDetector; +import android.view.GestureDetector.OnDoubleTapListener; +import android.view.GestureDetector.OnGestureListener; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; + +/** + * Detects various gestures and events using the supplied {@link MotionEvent}s. + * The {@link OnGestureListener} callback will notify users when a particular + * motion event has occurred. This class should only be used with {@link MotionEvent}s + * reported via touch (don't use for trackball events). + * + *

    This compatibility implementation of the framework's GestureDetector guarantees + * the newer focal point scrolling behavior from Jellybean MR1 on all platform versions.

    + * + * To use this class: + *
      + *
    • Create an instance of the {@code GestureDetectorCompat} for your {@link View} + *
    • In the {@link View#onTouchEvent(MotionEvent)} method ensure you call + * {@link #onTouchEvent(MotionEvent)}. The methods defined in your callback + * will be executed when the events occur. + *
    + */ +public class GestureDetectorCompat { + interface GestureDetectorCompatImpl { + boolean isLongpressEnabled(); + boolean onTouchEvent(MotionEvent ev); + void setIsLongpressEnabled(boolean enabled); + void setOnDoubleTapListener(OnDoubleTapListener listener); + } + + static class GestureDetectorCompatImplBase implements GestureDetectorCompatImpl { + private int mTouchSlopSquare; + private int mDoubleTapSlopSquare; + private int mMinimumFlingVelocity; + private int mMaximumFlingVelocity; + + private static final int LONGPRESS_TIMEOUT = ViewConfiguration.getLongPressTimeout(); + private static final int TAP_TIMEOUT = ViewConfiguration.getTapTimeout(); + private static final int DOUBLE_TAP_TIMEOUT = ViewConfiguration.getDoubleTapTimeout(); + + // constants for Message.what used by GestureHandler below + private static final int SHOW_PRESS = 1; + private static final int LONG_PRESS = 2; + private static final int TAP = 3; + + private final Handler mHandler; + private final OnGestureListener mListener; + private OnDoubleTapListener mDoubleTapListener; + + private boolean mStillDown; + private boolean mDeferConfirmSingleTap; + private boolean mInLongPress; + private boolean mAlwaysInTapRegion; + private boolean mAlwaysInBiggerTapRegion; + + private MotionEvent mCurrentDownEvent; + private MotionEvent mPreviousUpEvent; + + /** + * True when the user is still touching for the second tap (down, move, and + * up events). Can only be true if there is a double tap listener attached. + */ + private boolean mIsDoubleTapping; + + private float mLastFocusX; + private float mLastFocusY; + private float mDownFocusX; + private float mDownFocusY; + + private boolean mIsLongpressEnabled; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + private class GestureHandler extends Handler { + GestureHandler() { + super(); + } + + GestureHandler(Handler handler) { + super(handler.getLooper()); + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case SHOW_PRESS: + mListener.onShowPress(mCurrentDownEvent); + break; + + case LONG_PRESS: + dispatchLongPress(); + break; + + case TAP: + // If the user's finger is still down, do not count it as a tap + if (mDoubleTapListener != null) { + if (!mStillDown) { + mDoubleTapListener.onSingleTapConfirmed(mCurrentDownEvent); + } else { + mDeferConfirmSingleTap = true; + } + } + break; + + default: + throw new RuntimeException("Unknown message " + msg); //never + } + } + } + + /** + * Creates a GestureDetector with the supplied listener. + * You may only use this constructor from a UI thread (this is the usual situation). + * @see android.os.Handler#Handler() + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * @param handler the handler to use + * + * @throws NullPointerException if {@code listener} is null. + */ + public GestureDetectorCompatImplBase(Context context, OnGestureListener listener, + Handler handler) { + if (handler != null) { + mHandler = new GestureHandler(handler); + } else { + mHandler = new GestureHandler(); + } + mListener = listener; + if (listener instanceof OnDoubleTapListener) { + setOnDoubleTapListener((OnDoubleTapListener) listener); + } + init(context); + } + + private void init(Context context) { + if (context == null) { + throw new IllegalArgumentException("Context must not be null"); + } + if (mListener == null) { + throw new IllegalArgumentException("OnGestureListener must not be null"); + } + mIsLongpressEnabled = true; + + final ViewConfiguration configuration = ViewConfiguration.get(context); + final int touchSlop = configuration.getScaledTouchSlop(); + final int doubleTapSlop = configuration.getScaledDoubleTapSlop(); + mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity(); + + mTouchSlopSquare = touchSlop * touchSlop; + mDoubleTapSlopSquare = doubleTapSlop * doubleTapSlop; + } + + /** + * Sets the listener which will be called for double-tap and related + * gestures. + * + * @param onDoubleTapListener the listener invoked for all the callbacks, or + * null to stop listening for double-tap gestures. + */ + public void setOnDoubleTapListener(OnDoubleTapListener onDoubleTapListener) { + mDoubleTapListener = onDoubleTapListener; + } + + /** + * Set whether longpress is enabled, if this is enabled when a user + * presses and holds down you get a longpress event and nothing further. + * If it's disabled the user can press and hold down and then later + * moved their finger and you will get scroll events. By default + * longpress is enabled. + * + * @param isLongpressEnabled whether longpress should be enabled. + */ + public void setIsLongpressEnabled(boolean isLongpressEnabled) { + mIsLongpressEnabled = isLongpressEnabled; + } + + /** + * @return true if longpress is enabled, else false. + */ + public boolean isLongpressEnabled() { + return mIsLongpressEnabled; + } + + /** + * Analyzes the given motion event and if applicable triggers the + * appropriate callbacks on the {@link OnGestureListener} supplied. + * + * @param ev The current motion event. + * @return true if the {@link OnGestureListener} consumed the event, + * else false. + */ + public boolean onTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final boolean pointerUp = + (action & MotionEventCompat.ACTION_MASK) == MotionEventCompat.ACTION_POINTER_UP; + final int skipIndex = pointerUp ? MotionEventCompat.getActionIndex(ev) : -1; + + // Determine focal point + float sumX = 0, sumY = 0; + final int count = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < count; i++) { + if (skipIndex == i) continue; + sumX += MotionEventCompat.getX(ev, i); + sumY += MotionEventCompat.getY(ev, i); + } + final int div = pointerUp ? count - 1 : count; + final float focusX = sumX / div; + final float focusY = sumY / div; + + boolean handled = false; + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEventCompat.ACTION_POINTER_DOWN: + mDownFocusX = mLastFocusX = focusX; + mDownFocusY = mLastFocusY = focusY; + // Cancel long press and taps + cancelTaps(); + break; + + case MotionEventCompat.ACTION_POINTER_UP: + mDownFocusX = mLastFocusX = focusX; + mDownFocusY = mLastFocusY = focusY; + + // Check the dot product of current velocities. + // If the pointer that left was opposing another velocity vector, clear. + mVelocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); + final int upIndex = MotionEventCompat.getActionIndex(ev); + final int id1 = MotionEventCompat.getPointerId(ev, upIndex); + final float x1 = VelocityTrackerCompat.getXVelocity(mVelocityTracker, id1); + final float y1 = VelocityTrackerCompat.getYVelocity(mVelocityTracker, id1); + for (int i = 0; i < count; i++) { + if (i == upIndex) continue; + + final int id2 = MotionEventCompat.getPointerId(ev, i); + final float x = x1 * VelocityTrackerCompat.getXVelocity(mVelocityTracker, id2); + final float y = y1 * VelocityTrackerCompat.getYVelocity(mVelocityTracker, id2); + + final float dot = x + y; + if (dot < 0) { + mVelocityTracker.clear(); + break; + } + } + break; + + case MotionEvent.ACTION_DOWN: + if (mDoubleTapListener != null) { + boolean hadTapMessage = mHandler.hasMessages(TAP); + if (hadTapMessage) mHandler.removeMessages(TAP); + if ((mCurrentDownEvent != null) && (mPreviousUpEvent != null) && hadTapMessage && + isConsideredDoubleTap(mCurrentDownEvent, mPreviousUpEvent, ev)) { + // This is a second tap + mIsDoubleTapping = true; + // Give a callback with the first tap of the double-tap + handled |= mDoubleTapListener.onDoubleTap(mCurrentDownEvent); + // Give a callback with down event of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); + } else { + // This is a first tap + mHandler.sendEmptyMessageDelayed(TAP, DOUBLE_TAP_TIMEOUT); + } + } + + mDownFocusX = mLastFocusX = focusX; + mDownFocusY = mLastFocusY = focusY; + if (mCurrentDownEvent != null) { + mCurrentDownEvent.recycle(); + } + mCurrentDownEvent = MotionEvent.obtain(ev); + mAlwaysInTapRegion = true; + mAlwaysInBiggerTapRegion = true; + mStillDown = true; + mInLongPress = false; + mDeferConfirmSingleTap = false; + + if (mIsLongpressEnabled) { + mHandler.removeMessages(LONG_PRESS); + mHandler.sendEmptyMessageAtTime(LONG_PRESS, mCurrentDownEvent.getDownTime() + + TAP_TIMEOUT + LONGPRESS_TIMEOUT); + } + mHandler.sendEmptyMessageAtTime(SHOW_PRESS, mCurrentDownEvent.getDownTime() + TAP_TIMEOUT); + handled |= mListener.onDown(ev); + break; + + case MotionEvent.ACTION_MOVE: + if (mInLongPress) { + break; + } + final float scrollX = mLastFocusX - focusX; + final float scrollY = mLastFocusY - focusY; + if (mIsDoubleTapping) { + // Give the move events of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); + } else if (mAlwaysInTapRegion) { + final int deltaX = (int) (focusX - mDownFocusX); + final int deltaY = (int) (focusY - mDownFocusY); + int distance = (deltaX * deltaX) + (deltaY * deltaY); + if (distance > mTouchSlopSquare) { + handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); + mLastFocusX = focusX; + mLastFocusY = focusY; + mAlwaysInTapRegion = false; + mHandler.removeMessages(TAP); + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + } + if (distance > mTouchSlopSquare) { + mAlwaysInBiggerTapRegion = false; + } + } else if ((Math.abs(scrollX) >= 1) || (Math.abs(scrollY) >= 1)) { + handled = mListener.onScroll(mCurrentDownEvent, ev, scrollX, scrollY); + mLastFocusX = focusX; + mLastFocusY = focusY; + } + break; + + case MotionEvent.ACTION_UP: + mStillDown = false; + MotionEvent currentUpEvent = MotionEvent.obtain(ev); + if (mIsDoubleTapping) { + // Finally, give the up event of the double-tap + handled |= mDoubleTapListener.onDoubleTapEvent(ev); + } else if (mInLongPress) { + mHandler.removeMessages(TAP); + mInLongPress = false; + } else if (mAlwaysInTapRegion) { + handled = mListener.onSingleTapUp(ev); + if (mDeferConfirmSingleTap && mDoubleTapListener != null) { + mDoubleTapListener.onSingleTapConfirmed(ev); + } + } else { + // A fling must travel the minimum tap distance + final VelocityTracker velocityTracker = mVelocityTracker; + final int pointerId = MotionEventCompat.getPointerId(ev, 0); + velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity); + final float velocityY = VelocityTrackerCompat.getYVelocity( + velocityTracker, pointerId); + final float velocityX = VelocityTrackerCompat.getXVelocity( + velocityTracker, pointerId); + + if ((Math.abs(velocityY) > mMinimumFlingVelocity) + || (Math.abs(velocityX) > mMinimumFlingVelocity)){ + handled = mListener.onFling(mCurrentDownEvent, ev, velocityX, velocityY); + } + } + if (mPreviousUpEvent != null) { + mPreviousUpEvent.recycle(); + } + // Hold the event we obtained above - listeners may have changed the original. + mPreviousUpEvent = currentUpEvent; + if (mVelocityTracker != null) { + // This may have been cleared when we called out to the + // application above. + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + mIsDoubleTapping = false; + mDeferConfirmSingleTap = false; + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + break; + + case MotionEvent.ACTION_CANCEL: + cancel(); + break; + } + + return handled; + } + + private void cancel() { + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + mHandler.removeMessages(TAP); + mVelocityTracker.recycle(); + mVelocityTracker = null; + mIsDoubleTapping = false; + mStillDown = false; + mAlwaysInTapRegion = false; + mAlwaysInBiggerTapRegion = false; + mDeferConfirmSingleTap = false; + if (mInLongPress) { + mInLongPress = false; + } + } + + private void cancelTaps() { + mHandler.removeMessages(SHOW_PRESS); + mHandler.removeMessages(LONG_PRESS); + mHandler.removeMessages(TAP); + mIsDoubleTapping = false; + mAlwaysInTapRegion = false; + mAlwaysInBiggerTapRegion = false; + mDeferConfirmSingleTap = false; + if (mInLongPress) { + mInLongPress = false; + } + } + + private boolean isConsideredDoubleTap(MotionEvent firstDown, MotionEvent firstUp, + MotionEvent secondDown) { + if (!mAlwaysInBiggerTapRegion) { + return false; + } + + if (secondDown.getEventTime() - firstUp.getEventTime() > DOUBLE_TAP_TIMEOUT) { + return false; + } + + int deltaX = (int) firstDown.getX() - (int) secondDown.getX(); + int deltaY = (int) firstDown.getY() - (int) secondDown.getY(); + return (deltaX * deltaX + deltaY * deltaY < mDoubleTapSlopSquare); + } + + private void dispatchLongPress() { + mHandler.removeMessages(TAP); + mDeferConfirmSingleTap = false; + mInLongPress = true; + mListener.onLongPress(mCurrentDownEvent); + } + } + + static class GestureDetectorCompatImplJellybeanMr2 implements GestureDetectorCompatImpl { + private final GestureDetector mDetector; + + public GestureDetectorCompatImplJellybeanMr2(Context context, OnGestureListener listener, + Handler handler) { + mDetector = new GestureDetector(context, listener, handler); + } + + @Override + public boolean isLongpressEnabled() { + return mDetector.isLongpressEnabled(); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + return mDetector.onTouchEvent(ev); + } + + @Override + public void setIsLongpressEnabled(boolean enabled) { + mDetector.setIsLongpressEnabled(enabled); + } + + @Override + public void setOnDoubleTapListener(OnDoubleTapListener listener) { + mDetector.setOnDoubleTapListener(listener); + } + } + + private final GestureDetectorCompatImpl mImpl; + + /** + * Creates a GestureDetectorCompat with the supplied listener. + * As usual, you may only use this constructor from a UI thread. + * @see android.os.Handler#Handler() + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + */ + public GestureDetectorCompat(Context context, OnGestureListener listener) { + this(context, listener, null); + } + + /** + * Creates a GestureDetectorCompat with the supplied listener. + * As usual, you may only use this constructor from a UI thread. + * @see android.os.Handler#Handler() + * + * @param context the application's context + * @param listener the listener invoked for all the callbacks, this must + * not be null. + * @param handler the handler that will be used for posting deferred messages + */ + public GestureDetectorCompat(Context context, OnGestureListener listener, Handler handler) { + if (Build.VERSION.SDK_INT > 17) { + mImpl = new GestureDetectorCompatImplJellybeanMr2(context, listener, handler); + } else { + mImpl = new GestureDetectorCompatImplBase(context, listener, handler); + } + } + + /** + * @return true if longpress is enabled, else false. + */ + public boolean isLongpressEnabled() { + return mImpl.isLongpressEnabled(); + } + + /** + * Analyzes the given motion event and if applicable triggers the + * appropriate callbacks on the {@link OnGestureListener} supplied. + * + * @param event The current motion event. + * @return true if the {@link OnGestureListener} consumed the event, + * else false. + */ + public boolean onTouchEvent(MotionEvent event) { + return mImpl.onTouchEvent(event); + } + + /** + * Set whether longpress is enabled, if this is enabled when a user + * presses and holds down you get a longpress event and nothing further. + * If it's disabled the user can press and hold down and then later + * moved their finger and you will get scroll events. By default + * longpress is enabled. + * + * @param enabled whether longpress should be enabled. + */ + public void setIsLongpressEnabled(boolean enabled) { + mImpl.setIsLongpressEnabled(enabled); + } + + /** + * Sets the listener which will be called for double-tap and related + * gestures. + * + * @param listener the listener invoked for all the callbacks, or + * null to stop listening for double-tap gestures. + */ + public void setOnDoubleTapListener(OnDoubleTapListener listener) { + mImpl.setOnDoubleTapListener(listener); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/GravityCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/GravityCompat.java new file mode 100644 index 00000000000..0e8558eb663 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/GravityCompat.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.graphics.Rect; +import android.os.Build; +import android.view.Gravity; + +/** + * Compatibility shim for accessing newer functionality from {@link android.view.Gravity}. + */ +public class GravityCompat { + interface GravityCompatImpl { + int getAbsoluteGravity(int gravity, int layoutDirection); + void apply(int gravity, int w, int h, Rect container, Rect outRect, int layoutDirection); + void apply(int gravity, int w, int h, Rect container, int xAdj, int yAdj, + Rect outRect, int layoutDirection); + void applyDisplay(int gravity, Rect display, Rect inoutObj, int layoutDirection); + } + + static class GravityCompatImplBase implements GravityCompatImpl { + @Override + public int getAbsoluteGravity(int gravity, int layoutDirection) { + // Just strip off the relative bit to get LEFT/RIGHT. + return gravity & ~RELATIVE_LAYOUT_DIRECTION; + } + + @Override + public void apply(int gravity, int w, int h, Rect container, Rect outRect, + int layoutDirection) { + Gravity.apply(gravity, w, h, container, outRect); + } + + @Override + public void apply(int gravity, int w, int h, Rect container, int xAdj, int yAdj, + Rect outRect, int layoutDirection) { + Gravity.apply(gravity, w, h, container, xAdj, yAdj, outRect); + } + + @Override + public void applyDisplay(int gravity, Rect display, Rect inoutObj, + int layoutDirection) { + Gravity.applyDisplay(gravity, display, inoutObj); + } + } + + static class GravityCompatImplJellybeanMr1 implements GravityCompatImpl { + @Override + public int getAbsoluteGravity(int gravity, int layoutDirection) { + return GravityCompatJellybeanMr1.getAbsoluteGravity(gravity, layoutDirection); + } + + @Override + public void apply(int gravity, int w, int h, Rect container, Rect outRect, + int layoutDirection) { + GravityCompatJellybeanMr1.apply(gravity, w, h, container, outRect, layoutDirection); + } + + @Override + public void apply(int gravity, int w, int h, Rect container, int xAdj, int yAdj, + Rect outRect, int layoutDirection) { + GravityCompatJellybeanMr1.apply(gravity, w, h, container, xAdj, yAdj, outRect, + layoutDirection); + } + + @Override + public void applyDisplay(int gravity, Rect display, Rect inoutObj, int layoutDirection) { + GravityCompatJellybeanMr1.applyDisplay(gravity, display, inoutObj, layoutDirection); + } + } + + static final GravityCompatImpl IMPL; + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 17) { + IMPL = new GravityCompatImplJellybeanMr1(); + } else { + IMPL = new GravityCompatImplBase(); + } + } + + /** Raw bit controlling whether the layout direction is relative or not (START/END instead of + * absolute LEFT/RIGHT). + */ + public static final int RELATIVE_LAYOUT_DIRECTION = 0x00800000; + + /** Push object to x-axis position at the start of its container, not changing its size. */ + public static final int START = RELATIVE_LAYOUT_DIRECTION | Gravity.LEFT; + + /** Push object to x-axis position at the end of its container, not changing its size. */ + public static final int END = RELATIVE_LAYOUT_DIRECTION | Gravity.RIGHT; + + /** + * Binary mask for the horizontal gravity and script specific direction bit. + */ + public static final int RELATIVE_HORIZONTAL_GRAVITY_MASK = START | END; + + /** + * Apply a gravity constant to an object and take care if layout direction is RTL or not. + * + * @param gravity The desired placement of the object, as defined by the + * constants in this class. + * @param w The horizontal size of the object. + * @param h The vertical size of the object. + * @param container The frame of the containing space, in which the object + * will be placed. Should be large enough to contain the + * width and height of the object. + * @param outRect Receives the computed frame of the object in its + * container. + * @param layoutDirection The layout direction. + * + * @see ViewCompat#LAYOUT_DIRECTION_LTR + * @see ViewCompat#LAYOUT_DIRECTION_RTL + */ + public static void apply(int gravity, int w, int h, Rect container, + Rect outRect, int layoutDirection) { + IMPL.apply(gravity, w, h, container, outRect, layoutDirection); + } + + /** + * Apply a gravity constant to an object. + * + * @param gravity The desired placement of the object, as defined by the + * constants in this class. + * @param w The horizontal size of the object. + * @param h The vertical size of the object. + * @param container The frame of the containing space, in which the object + * will be placed. Should be large enough to contain the + * width and height of the object. + * @param xAdj Offset to apply to the X axis. If gravity is LEFT this + * pushes it to the right; if gravity is RIGHT it pushes it to + * the left; if gravity is CENTER_HORIZONTAL it pushes it to the + * right or left; otherwise it is ignored. + * @param yAdj Offset to apply to the Y axis. If gravity is TOP this pushes + * it down; if gravity is BOTTOM it pushes it up; if gravity is + * CENTER_VERTICAL it pushes it down or up; otherwise it is + * ignored. + * @param outRect Receives the computed frame of the object in its + * container. + * @param layoutDirection The layout direction. + * + * @see ViewCompat#LAYOUT_DIRECTION_LTR + * @see ViewCompat#LAYOUT_DIRECTION_RTL + */ + public static void apply(int gravity, int w, int h, Rect container, + int xAdj, int yAdj, Rect outRect, int layoutDirection) { + IMPL.apply(gravity, w, h, container, xAdj, yAdj, outRect, layoutDirection); + } + + /** + * Apply additional gravity behavior based on the overall "display" that an + * object exists in. This can be used after + * {@link android.view.Gravity#apply(int, int, int, Rect, int, int, Rect)} to place the object + * within a visible display. By default this moves or clips the object + * to be visible in the display; the gravity flags + * {@link android.view.Gravity#DISPLAY_CLIP_HORIZONTAL} and + * {@link android.view.Gravity#DISPLAY_CLIP_VERTICAL} can be used to change this behavior. + * + * @param gravity Gravity constants to modify the placement within the + * display. + * @param display The rectangle of the display in which the object is + * being placed. + * @param inoutObj Supplies the current object position; returns with it + * modified if needed to fit in the display. + * @param layoutDirection The layout direction. + * + * @see ViewCompat#LAYOUT_DIRECTION_LTR + * @see ViewCompat#LAYOUT_DIRECTION_RTL + */ + public static void applyDisplay(int gravity, Rect display, Rect inoutObj, int layoutDirection) { + IMPL.applyDisplay(gravity, display, inoutObj, layoutDirection); + } + + /** + *

    Convert script specific gravity to absolute horizontal value.

    + * + * if horizontal direction is LTR, then START will set LEFT and END will set RIGHT. + * if horizontal direction is RTL, then START will set RIGHT and END will set LEFT. + * + * + * @param gravity The gravity to convert to absolute (horizontal) values. + * @param layoutDirection The layout direction. + * @return gravity converted to absolute (horizontal) values. + */ + public static int getAbsoluteGravity(int gravity, int layoutDirection) { + return IMPL.getAbsoluteGravity(gravity, layoutDirection); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/GravityCompatJellybeanMr1.java b/mobile/google-android-support-v4/src/android/support/v4/view/GravityCompatJellybeanMr1.java new file mode 100644 index 00000000000..1e6077d5027 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/GravityCompatJellybeanMr1.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.graphics.Rect; +import android.view.Gravity; + +class GravityCompatJellybeanMr1 { + + public static int getAbsoluteGravity(int gravity, int layoutDirection) { + return Gravity.getAbsoluteGravity(gravity, layoutDirection); + } + + public static void apply(int gravity, int w, int h, Rect container, Rect outRect, + int layoutDirection) { + Gravity.apply(gravity, w, h, container, outRect, layoutDirection); + } + + public static void apply(int gravity, int w, int h, Rect container, int xAdj, int yAdj, + Rect outRect, int layoutDirection) { + Gravity.apply(gravity, w, h, container, xAdj, yAdj, outRect, layoutDirection); + } + + public static void applyDisplay(int gravity, Rect display, Rect inoutObj, int layoutDirection) { + Gravity.applyDisplay(gravity, display, inoutObj, layoutDirection); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/InputDeviceCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/InputDeviceCompat.java new file mode 100644 index 00000000000..7a202cdd829 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/InputDeviceCompat.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +/** + * Helper class for accessing values in {@link android.view.InputDevice}. + */ +public class InputDeviceCompat { + + /** + * A mask for input source classes. + * + * Each distinct input source constant has one or more input source class bits set to + * specify the desired interpretation for its input events. + */ + public static final int SOURCE_CLASS_MASK = 0x000000ff; + + /** + * The input source has no class. + * + * It is up to the application to determine how to handle the device based on the device type. + */ + public static final int SOURCE_CLASS_NONE = 0x00000000; + + /** + * The input source has buttons or keys. + * Examples: {@link #SOURCE_KEYBOARD}, {@link #SOURCE_DPAD}. + * + * A {@link android.view.KeyEvent} should be interpreted as a button or key press. + */ + public static final int SOURCE_CLASS_BUTTON = 0x00000001; + + /** + * The input source is a pointing device associated with a display. + * Examples: {@link #SOURCE_TOUCHSCREEN}, {@link #SOURCE_MOUSE}. + * + * A {@link android.view.MotionEvent} should be interpreted as absolute coordinates in + * display units according to the {@link android.view.View} hierarchy. Pointer down/up + * indicated when + * the finger touches the display or when the selection button is pressed/released. + * + * Use {@link android.view.InputDevice#getMotionRange} to query the range of the pointing + * device. Some devices permit + * touches outside the display area so the effective range may be somewhat smaller or larger + * than the actual display size. + */ + public static final int SOURCE_CLASS_POINTER = 0x00000002; + + /** + * The input source is a trackball navigation device. + * Examples: {@link #SOURCE_TRACKBALL}. + * + * A {@link android.view.MotionEvent} should be interpreted as relative movements in + * device-specific + * units used for navigation purposes. Pointer down/up indicates when the selection button + * is pressed/released. + * + * Use {@link android.view.InputDevice#getMotionRange} to query the range of motion. + */ + public static final int SOURCE_CLASS_TRACKBALL = 0x00000004; + + /** + * The input source is an absolute positioning device not associated with a display + * (unlike {@link #SOURCE_CLASS_POINTER}). + * + * A {@link android.view.MotionEvent} should be interpreted as absolute coordinates in + * device-specific surface units. + * + * Use {@link android.view.InputDevice#getMotionRange} to query the range of positions. + */ + public static final int SOURCE_CLASS_POSITION = 0x00000008; + + /** + * The input source is a joystick. + * + * A {@link android.view.MotionEvent} should be interpreted as absolute joystick movements. + * + * Use {@link android.view.InputDevice#getMotionRange} to query the range of positions. + */ + public static final int SOURCE_CLASS_JOYSTICK = 0x00000010; + + /** + * The input source is unknown. + */ + public static final int SOURCE_UNKNOWN = 0x00000000; + + /** + * The input source is a keyboard. + * + * This source indicates pretty much anything that has buttons. Use + * {@link android.view.InputDevice#getKeyboardType()} to determine whether the keyboard has + * alphabetic keys + * and can be used to enter text. + * + * @see #SOURCE_CLASS_BUTTON + */ + public static final int SOURCE_KEYBOARD = 0x00000100 | SOURCE_CLASS_BUTTON; + + /** + * The input source is a DPad. + * + * @see #SOURCE_CLASS_BUTTON + */ + public static final int SOURCE_DPAD = 0x00000200 | SOURCE_CLASS_BUTTON; + + /** + * The input source is a game pad. + * (It may also be a {@link #SOURCE_JOYSTICK}). + * + * @see #SOURCE_CLASS_BUTTON + */ + public static final int SOURCE_GAMEPAD = 0x00000400 | SOURCE_CLASS_BUTTON; + + /** + * The input source is a touch screen pointing device. + * + * @see #SOURCE_CLASS_POINTER + */ + public static final int SOURCE_TOUCHSCREEN = 0x00001000 | SOURCE_CLASS_POINTER; + + /** + * The input source is a mouse pointing device. + * This code is also used for other mouse-like pointing devices such as trackpads + * and trackpoints. + * + * @see #SOURCE_CLASS_POINTER + */ + public static final int SOURCE_MOUSE = 0x00002000 | SOURCE_CLASS_POINTER; + + /** + * The input source is a stylus pointing device. + *

    + * Note that this bit merely indicates that an input device is capable of obtaining + * input from a stylus. To determine whether a given touch event was produced + * by a stylus, examine the tool type returned by {@link android.view.MotionEvent#getToolType(int)} + * for each individual pointer. + *

    + * A single touch event may multiple pointers with different tool types, + * such as an event that has one pointer with tool type + * {@link android.view.MotionEvent#TOOL_TYPE_FINGER} and another pointer with tool type + * {@link android.view.MotionEvent#TOOL_TYPE_STYLUS}. So it is important to examine + * the tool type of each pointer, regardless of the source reported + * by {@link android.view.MotionEvent#getSource()}. + *

    + * + * @see #SOURCE_CLASS_POINTER + */ + public static final int SOURCE_STYLUS = 0x00004000 | SOURCE_CLASS_POINTER; + + /** + * The input source is a trackball. + * + * @see #SOURCE_CLASS_TRACKBALL + */ + public static final int SOURCE_TRACKBALL = 0x00010000 | SOURCE_CLASS_TRACKBALL; + + /** + * The input source is a touch pad or digitizer tablet that is not + * associated with a display (unlike {@link #SOURCE_TOUCHSCREEN}). + * + * @see #SOURCE_CLASS_POSITION + */ + public static final int SOURCE_TOUCHPAD = 0x00100000 | SOURCE_CLASS_POSITION; + + /** + * The input source is a touch device whose motions should be interpreted as navigation events. + * + * For example, an upward swipe should be as an upward focus traversal in the same manner as + * pressing up on a D-Pad would be. Swipes to the left, right and down should be treated in a + * similar manner. + * + * @see #SOURCE_CLASS_NONE + */ + public static final int SOURCE_TOUCH_NAVIGATION = 0x00200000 | SOURCE_CLASS_NONE; + + /** + * The input source is a joystick. + * (It may also be a {@link #SOURCE_GAMEPAD}). + * + * @see #SOURCE_CLASS_JOYSTICK + */ + public static final int SOURCE_JOYSTICK = 0x01000000 | SOURCE_CLASS_JOYSTICK; + + /** + * The input source is a device connected through HDMI-based bus. + * + * The key comes in through HDMI-CEC or MHL signal line, and is treated as if it were + * generated by a locally connected DPAD or keyboard. + */ + public static final int SOURCE_HDMI = 0x02000000 | SOURCE_CLASS_BUTTON; + + /** + * A special input source constant that is used when filtering input devices + * to match devices that provide any type of input source. + */ + public static final int SOURCE_ANY = 0xffffff00; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompat.java new file mode 100644 index 00000000000..fc0ee2e7a7b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompat.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.KeyEvent; +import android.view.View; + +/** + * Helper for accessing features in {@link KeyEvent} introduced after + * API level 4 in a backwards compatible fashion. + */ +public class KeyEventCompat { + /** + * Interface for the full API. + */ + interface KeyEventVersionImpl { + public int normalizeMetaState(int metaState); + public boolean metaStateHasModifiers(int metaState, int modifiers); + public boolean metaStateHasNoModifiers(int metaState); + public void startTracking(KeyEvent event); + public boolean isTracking(KeyEvent event); + public Object getKeyDispatcherState(View view); + public boolean dispatch(KeyEvent event, KeyEvent.Callback receiver, Object state, + Object target); + } + + /** + * Interface implementation that doesn't use anything about v4 APIs. + */ + static class BaseKeyEventVersionImpl implements KeyEventVersionImpl { + private static final int META_MODIFIER_MASK = + KeyEvent.META_SHIFT_ON | KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_RIGHT_ON + | KeyEvent.META_ALT_ON | KeyEvent.META_ALT_LEFT_ON | KeyEvent.META_ALT_RIGHT_ON + | KeyEvent.META_SYM_ON; + + // Mask of all lock key meta states. + private static final int META_ALL_MASK = META_MODIFIER_MASK; + + private static int metaStateFilterDirectionalModifiers(int metaState, + int modifiers, int basic, int left, int right) { + final boolean wantBasic = (modifiers & basic) != 0; + final int directional = left | right; + final boolean wantLeftOrRight = (modifiers & directional) != 0; + + if (wantBasic) { + if (wantLeftOrRight) { + throw new IllegalArgumentException("bad arguments"); + } + return metaState & ~directional; + } else if (wantLeftOrRight) { + return metaState & ~basic; + } else { + return metaState; + } + } + + @Override + public int normalizeMetaState(int metaState) { + if ((metaState & (KeyEvent.META_SHIFT_LEFT_ON | KeyEvent.META_SHIFT_RIGHT_ON)) != 0) { + metaState |= KeyEvent.META_SHIFT_ON; + } + if ((metaState & (KeyEvent.META_ALT_LEFT_ON | KeyEvent.META_ALT_RIGHT_ON)) != 0) { + metaState |= KeyEvent.META_ALT_ON; + } + return metaState & META_ALL_MASK; + } + + @Override + public boolean metaStateHasModifiers(int metaState, int modifiers) { + metaState = normalizeMetaState(metaState) & META_MODIFIER_MASK; + metaState = metaStateFilterDirectionalModifiers(metaState, modifiers, + KeyEvent.META_SHIFT_ON, KeyEvent.META_SHIFT_LEFT_ON, KeyEvent.META_SHIFT_RIGHT_ON); + metaState = metaStateFilterDirectionalModifiers(metaState, modifiers, + KeyEvent.META_ALT_ON, KeyEvent.META_ALT_LEFT_ON, KeyEvent.META_ALT_RIGHT_ON); + return metaState == modifiers; + } + + @Override + public boolean metaStateHasNoModifiers(int metaState) { + return (normalizeMetaState(metaState) & META_MODIFIER_MASK) == 0; + } + + @Override + public void startTracking(KeyEvent event) { + } + + @Override + public boolean isTracking(KeyEvent event) { + return false; + } + + @Override + public Object getKeyDispatcherState(View view) { + return null; + } + + @Override + public boolean dispatch(KeyEvent event, KeyEvent.Callback receiver, Object state, + Object target) { + return event.dispatch(receiver); + } + } + + static class EclairKeyEventVersionImpl extends BaseKeyEventVersionImpl { + @Override + public void startTracking(KeyEvent event) { + KeyEventCompatEclair.startTracking(event); + } + + @Override + public boolean isTracking(KeyEvent event) { + return KeyEventCompatEclair.isTracking(event); + } + + @Override + public Object getKeyDispatcherState(View view) { + return KeyEventCompatEclair.getKeyDispatcherState(view); + } + + @Override + public boolean dispatch(KeyEvent event, KeyEvent.Callback receiver, Object state, + Object target) { + return KeyEventCompatEclair.dispatch(event, receiver, state, target); + } + } + + /** + * Interface implementation for devices with at least v11 APIs. + */ + static class HoneycombKeyEventVersionImpl extends EclairKeyEventVersionImpl { + @Override + public int normalizeMetaState(int metaState) { + return KeyEventCompatHoneycomb.normalizeMetaState(metaState); + } + + @Override + public boolean metaStateHasModifiers(int metaState, int modifiers) { + return KeyEventCompatHoneycomb.metaStateHasModifiers(metaState, modifiers); + } + + @Override + public boolean metaStateHasNoModifiers(int metaState) { + return KeyEventCompatHoneycomb.metaStateHasNoModifiers(metaState); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final KeyEventVersionImpl IMPL; + static { + if (android.os.Build.VERSION.SDK_INT >= 11) { + IMPL = new HoneycombKeyEventVersionImpl(); + } else { + IMPL = new BaseKeyEventVersionImpl(); + } + } + + // ------------------------------------------------------------------- + + public static int normalizeMetaState(int metaState) { + return IMPL.normalizeMetaState(metaState); + } + + public static boolean metaStateHasModifiers(int metaState, int modifiers) { + return IMPL.metaStateHasModifiers(metaState, modifiers); + } + + public static boolean metaStateHasNoModifiers(int metaState) { + return IMPL.metaStateHasNoModifiers(metaState); + } + + public static boolean hasModifiers(KeyEvent event, int modifiers) { + return IMPL.metaStateHasModifiers(event.getMetaState(), modifiers); + } + + public static boolean hasNoModifiers(KeyEvent event) { + return IMPL.metaStateHasNoModifiers(event.getMetaState()); + } + + public static void startTracking(KeyEvent event) { + IMPL.startTracking(event); + } + + public static boolean isTracking(KeyEvent event) { + return IMPL.isTracking(event); + } + + public static Object getKeyDispatcherState(View view) { + return IMPL.getKeyDispatcherState(view); + } + + public static boolean dispatch(KeyEvent event, KeyEvent.Callback receiver, Object state, + Object target) { + return IMPL.dispatch(event, receiver, state, target); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatEclair.java b/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatEclair.java new file mode 100644 index 00000000000..b3b0d11afc3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatEclair.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.view.KeyEvent; +import android.view.View; + +class KeyEventCompatEclair { + public static Object getKeyDispatcherState(View view) { + return view.getKeyDispatcherState(); + } + + public static boolean dispatch(KeyEvent event, KeyEvent.Callback receiver, Object state, + Object target) { + return event.dispatch(receiver, (KeyEvent.DispatcherState)state, target); + } + + public static void startTracking(KeyEvent event) { + event.startTracking(); + } + + public static boolean isTracking(KeyEvent event) { + return event.isTracking(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatHoneycomb.java new file mode 100644 index 00000000000..f1bd1179d65 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/KeyEventCompatHoneycomb.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.KeyEvent; + +/** + * Implementation of key event compatibility that can call Honeycomb APIs. + */ +class KeyEventCompatHoneycomb { + public static int normalizeMetaState(int metaState) { + return KeyEvent.normalizeMetaState(metaState); + } + + public static boolean metaStateHasModifiers(int metaState, int modifiers) { + return KeyEvent.metaStateHasModifiers(metaState, modifiers); + } + + public static boolean metaStateHasNoModifiers(int metaState) { + return KeyEvent.metaStateHasNoModifiers(metaState); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompat.java new file mode 100644 index 00000000000..d67a739bde1 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompat.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.os.Build; +import android.view.LayoutInflater; + +/** + * Helper for accessing features in {@link android.view.LayoutInflater} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class LayoutInflaterCompat { + + interface LayoutInflaterCompatImpl { + public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory); + } + + static class LayoutInflaterCompatImplBase implements LayoutInflaterCompatImpl { + @Override + public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory) { + LayoutInflaterCompatBase.setFactory(layoutInflater, factory); + } + } + + static class LayoutInflaterCompatImplV11 extends LayoutInflaterCompatImplBase { + @Override + public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory) { + LayoutInflaterCompatHC.setFactory(layoutInflater, factory); + } + } + + static class LayoutInflaterCompatImplV21 extends LayoutInflaterCompatImplV11 { + @Override + public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory) { + LayoutInflaterCompatLollipop.setFactory(layoutInflater, factory); + } + } + + static final LayoutInflaterCompatImpl IMPL; + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + IMPL = new LayoutInflaterCompatImplV21(); + } else if (version >= 11) { + IMPL = new LayoutInflaterCompatImplV11(); + } else { + IMPL = new LayoutInflaterCompatImplBase(); + } + } + + /* + * Hide the constructor. + */ + private LayoutInflaterCompat() { + } + + /** + * Attach a custom Factory interface for creating views while using + * this LayoutInflater. This must not be null, and can only be set once; + * after setting, you can not change the factory. + * + * @see LayoutInflater#setFactory(android.view.LayoutInflater.Factory) + */ + public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) { + IMPL.setFactory(inflater, factory); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatBase.java b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatBase.java new file mode 100644 index 00000000000..ea19333b2aa --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatBase.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; + +class LayoutInflaterCompatBase { + + static class FactoryWrapper implements LayoutInflater.Factory { + + final LayoutInflaterFactory mDelegateFactory; + + FactoryWrapper(LayoutInflaterFactory delegateFactory) { + mDelegateFactory = delegateFactory; + } + + @Override + public View onCreateView(String name, Context context, AttributeSet attrs) { + return mDelegateFactory.onCreateView(null, name, context, attrs); + } + + public String toString() { + return getClass().getName() + "{" + mDelegateFactory + "}"; + } + } + + static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) { + inflater.setFactory(factory != null ? new FactoryWrapper(factory) : null); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatHC.java b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatHC.java new file mode 100644 index 00000000000..06e72f49ce0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatHC.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; + +import java.lang.reflect.Field; +import java.util.ArrayList; + +class LayoutInflaterCompatHC { + private static final String TAG = "LayoutInflaterCompatHC"; + + private static Field sLayoutInflaterFactory2Field; + private static boolean sCheckedField; + + static class FactoryWrapperHC extends LayoutInflaterCompatBase.FactoryWrapper + implements LayoutInflater.Factory2 { + + FactoryWrapperHC(LayoutInflaterFactory delegateFactory) { + super(delegateFactory); + } + + @Override + public View onCreateView(View parent, String name, Context context, + AttributeSet attributeSet) { + return mDelegateFactory.onCreateView(parent, name, context, attributeSet); + } + } + + static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) { + final LayoutInflater.Factory2 factory2 = factory != null + ? new FactoryWrapperHC(factory) : null; + inflater.setFactory2(factory2); + + final LayoutInflater.Factory f = inflater.getFactory(); + if (f instanceof LayoutInflater.Factory2) { + // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21). + // We will now try and force set the merged factory to mFactory2 + forceSetFactory2(inflater, (LayoutInflater.Factory2) f); + } else { + // Else, we will force set the original wrapped Factory2 + forceSetFactory2(inflater, factory2); + } + } + + /** + * For APIs >= 11 && < 21, there was a framework bug that prevented a LayoutInflater's + * Factory2 from being merged properly if set after a cloneInContext from a LayoutInflater + * that already had a Factory2 registered. We work around that bug here. If we can't we + * log an error. + */ + static void forceSetFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) { + if (!sCheckedField) { + try { + sLayoutInflaterFactory2Field = LayoutInflater.class.getDeclaredField("mFactory2"); + sLayoutInflaterFactory2Field.setAccessible(true); + } catch (NoSuchFieldException e) { + Log.e(TAG, "forceSetFactory2 Could not find field 'mFactory2' on class " + + LayoutInflater.class.getName() + + "; inflation may have unexpected results.", e); + } + sCheckedField = true; + } + if (sLayoutInflaterFactory2Field != null) { + try { + sLayoutInflaterFactory2Field.set(inflater, factory); + } catch (IllegalAccessException e) { + Log.e(TAG, "forceSetFactory2 could not set the Factory2 on LayoutInflater " + + inflater + "; inflation may have unexpected results.", e); + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatLollipop.java new file mode 100644 index 00000000000..b62c74e2b43 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterCompatLollipop.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.view.LayoutInflater; + +class LayoutInflaterCompatLollipop { + static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) { + inflater.setFactory2(factory != null + ? new LayoutInflaterCompatHC.FactoryWrapperHC(factory) : null); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterFactory.java b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterFactory.java new file mode 100644 index 00000000000..02b3d6360f7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/LayoutInflaterFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +/** + * Used with {@code LayoutInflaterCompat.setFactory()}. Offers the same API as + * {@code LayoutInflater.Factory2}. + */ +public interface LayoutInflaterFactory { + + /** + * Hook you can supply that is called when inflating from a LayoutInflater. + * You can use this to customize the tag names available in your XML + * layout files. + * + * @param parent The parent that the created view will be placed + * in; note that this may be null. + * @param name Tag name to be inflated. + * @param context The context the view is being created in. + * @param attrs Inflation attributes as specified in XML file. + * + * @return View Newly created view. Return null for the default + * behavior. + */ + public View onCreateView(View parent, String name, Context context, AttributeSet attrs); + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompat.java new file mode 100644 index 00000000000..86072c04446 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompat.java @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.os.Build; +import android.view.ViewGroup; + +/** + * Helper for accessing API features in + * {@link android.view.ViewGroup.MarginLayoutParams MarginLayoutParams} added after API 4. + */ +public class MarginLayoutParamsCompat { + interface MarginLayoutParamsCompatImpl { + int getMarginStart(ViewGroup.MarginLayoutParams lp); + int getMarginEnd(ViewGroup.MarginLayoutParams lp); + void setMarginStart(ViewGroup.MarginLayoutParams lp, int marginStart); + void setMarginEnd(ViewGroup.MarginLayoutParams lp, int marginEnd); + boolean isMarginRelative(ViewGroup.MarginLayoutParams lp); + int getLayoutDirection(ViewGroup.MarginLayoutParams lp); + void setLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection); + void resolveLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection); + } + + static class MarginLayoutParamsCompatImplBase implements MarginLayoutParamsCompatImpl { + + @Override + public int getMarginStart(ViewGroup.MarginLayoutParams lp) { + return lp.leftMargin; + } + + @Override + public int getMarginEnd(ViewGroup.MarginLayoutParams lp) { + return lp.rightMargin; + } + + @Override + public void setMarginStart(ViewGroup.MarginLayoutParams lp, int marginStart) { + lp.leftMargin = marginStart; + } + + @Override + public void setMarginEnd(ViewGroup.MarginLayoutParams lp, int marginEnd) { + lp.rightMargin = marginEnd; + } + + @Override + public boolean isMarginRelative(ViewGroup.MarginLayoutParams lp) { + return false; + } + + @Override + public int getLayoutDirection(ViewGroup.MarginLayoutParams lp) { + return ViewCompat.LAYOUT_DIRECTION_LTR; + } + + @Override + public void setLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection) { + // No-op + } + + @Override + public void resolveLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection) { + // No-op + } + } + + static class MarginLayoutParamsCompatImplJbMr1 implements MarginLayoutParamsCompatImpl { + + @Override + public int getMarginStart(ViewGroup.MarginLayoutParams lp) { + return MarginLayoutParamsCompatJellybeanMr1.getMarginStart(lp); + } + + @Override + public int getMarginEnd(ViewGroup.MarginLayoutParams lp) { + return MarginLayoutParamsCompatJellybeanMr1.getMarginEnd(lp); + } + + @Override + public void setMarginStart(ViewGroup.MarginLayoutParams lp, int marginStart) { + MarginLayoutParamsCompatJellybeanMr1.setMarginStart(lp, marginStart); + } + + @Override + public void setMarginEnd(ViewGroup.MarginLayoutParams lp, int marginEnd) { + MarginLayoutParamsCompatJellybeanMr1.setMarginEnd(lp, marginEnd); + } + + @Override + public boolean isMarginRelative(ViewGroup.MarginLayoutParams lp) { + return MarginLayoutParamsCompatJellybeanMr1.isMarginRelative(lp); + } + + @Override + public int getLayoutDirection(ViewGroup.MarginLayoutParams lp) { + return MarginLayoutParamsCompatJellybeanMr1.getLayoutDirection(lp); + } + + @Override + public void setLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection) { + MarginLayoutParamsCompatJellybeanMr1.setLayoutDirection(lp, layoutDirection); + } + + @Override + public void resolveLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection) { + MarginLayoutParamsCompatJellybeanMr1.resolveLayoutDirection(lp, layoutDirection); + } + } + + static final MarginLayoutParamsCompatImpl IMPL; + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 17) { // jb-mr1 + IMPL = new MarginLayoutParamsCompatImplJbMr1(); + } else { + IMPL = new MarginLayoutParamsCompatImplBase(); + } + } + + /** + * Get the relative starting margin that was set. + * + *

    On platform versions supporting bidirectional text and layouts + * this value will be resolved into the LayoutParams object's left or right + * margin as appropriate when the associated View is attached to a window + * or when the layout direction of that view changes.

    + * + * @param lp LayoutParams to query + * @return the margin along the starting edge in pixels + */ + public static int getMarginStart(ViewGroup.MarginLayoutParams lp) { + return IMPL.getMarginStart(lp); + } + + /** + * Get the relative ending margin that was set. + * + *

    On platform versions supporting bidirectional text and layouts + * this value will be resolved into the LayoutParams object's left or right + * margin as appropriate when the associated View is attached to a window + * or when the layout direction of that view changes.

    + * + * @param lp LayoutParams to query + * @return the margin along the ending edge in pixels + */ + public static int getMarginEnd(ViewGroup.MarginLayoutParams lp) { + return IMPL.getMarginEnd(lp); + } + + /** + * Set the relative start margin. + * + *

    On platform versions supporting bidirectional text and layouts + * this value will be resolved into the LayoutParams object's left or right + * margin as appropriate when the associated View is attached to a window + * or when the layout direction of that view changes.

    + * + * @param lp LayoutParams to query + * @param marginStart the desired start margin in pixels + */ + public static void setMarginStart(ViewGroup.MarginLayoutParams lp, int marginStart) { + IMPL.setMarginStart(lp, marginStart); + } + + /** + * Set the relative end margin. + * + *

    On platform versions supporting bidirectional text and layouts + * this value will be resolved into the LayoutParams object's left or right + * margin as appropriate when the associated View is attached to a window + * or when the layout direction of that view changes.

    + * + * @param lp LayoutParams to query + * @param marginEnd the desired end margin in pixels + */ + public static void setMarginEnd(ViewGroup.MarginLayoutParams lp, int marginEnd) { + IMPL.setMarginEnd(lp, marginEnd); + } + + /** + * Check if margins are relative. + * + * @return true if either marginStart or marginEnd has been set. + */ + public static boolean isMarginRelative(ViewGroup.MarginLayoutParams lp) { + return IMPL.isMarginRelative(lp); + } + + /** + * Retuns the layout direction. Can be either {@link ViewCompat#LAYOUT_DIRECTION_LTR} or + * {@link ViewCompat#LAYOUT_DIRECTION_RTL}. + * + * @return the layout direction. + */ + public static int getLayoutDirection(ViewGroup.MarginLayoutParams lp) { + return IMPL.getLayoutDirection(lp); + } + + /** + * Set the layout direction. + * + * @param layoutDirection the layout direction. + * Should be either {@link ViewCompat#LAYOUT_DIRECTION_LTR} + * or {@link ViewCompat#LAYOUT_DIRECTION_RTL}. + */ + public static void setLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection) { + IMPL.setLayoutDirection(lp, layoutDirection); + } + + /** + * This will be called by {@link android.view.View#requestLayout()}. Left and Right margins + * may be overridden depending on layout direction. + */ + public static void resolveLayoutDirection(ViewGroup.MarginLayoutParams lp, + int layoutDirection) { + IMPL.resolveLayoutDirection(lp, layoutDirection); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompatJellybeanMr1.java b/mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompatJellybeanMr1.java new file mode 100644 index 00000000000..2fe9bc8d68e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MarginLayoutParamsCompatJellybeanMr1.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.view.ViewGroup; + +class MarginLayoutParamsCompatJellybeanMr1 { + public static int getMarginStart(ViewGroup.MarginLayoutParams lp) { + return lp.getMarginStart(); + } + + public static int getMarginEnd(ViewGroup.MarginLayoutParams lp) { + return lp.getMarginEnd(); + } + + public static void setMarginStart(ViewGroup.MarginLayoutParams lp, int marginStart) { + lp.setMarginStart(marginStart); + } + + public static void setMarginEnd(ViewGroup.MarginLayoutParams lp, int marginEnd) { + lp.setMarginEnd(marginEnd); + } + + public static boolean isMarginRelative(ViewGroup.MarginLayoutParams lp) { + return lp.isMarginRelative(); + } + + public static int getLayoutDirection(ViewGroup.MarginLayoutParams lp) { + return lp.getLayoutDirection(); + } + + public static void setLayoutDirection(ViewGroup.MarginLayoutParams lp, int layoutDirection) { + lp.setLayoutDirection(layoutDirection); + } + + public static void resolveLayoutDirection(ViewGroup.MarginLayoutParams lp, + int layoutDirection) { + lp.resolveLayoutDirection(layoutDirection); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MenuCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/MenuCompat.java new file mode 100644 index 00000000000..73c26ee204e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MenuCompat.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.MenuItem; + +/** + * Helper for accessing features in {@link android.view.Menu} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class MenuCompat { + /** + * Call {@link MenuItem#setShowAsAction(int) MenuItem.setShowAsAction()}. + * + * @deprecated Use {@link MenuItemCompat#setShowAsAction(MenuItem, int) + * MenuItemCompat.setShowAsAction(MenuItem, int)} + */ + @Deprecated + public static void setShowAsAction(MenuItem item, int actionEnum) { + MenuItemCompat.setShowAsAction(item, actionEnum); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompat.java new file mode 100644 index 00000000000..8a52e9ec6eb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompat.java @@ -0,0 +1,440 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.support.v4.internal.view.SupportMenuItem; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; + +/** + * Helper for accessing features in {@link android.view.MenuItem} + * introduced after API level 4 in a backwards compatible fashion. + *

    Note: You cannot get an instance of this class. Instead, + * it provides static methods that correspond to the methods in {@link + * android.view.MenuItem}, but take a {@link android.view.MenuItem} object as an additional + * argument.

    + */ +public class MenuItemCompat { + private static final String TAG = "MenuItemCompat"; + + /** + * Never show this item as a button in an Action Bar. + */ + public static final int SHOW_AS_ACTION_NEVER = 0; + + /** + * Show this item as a button in an Action Bar if the system + * decides there is room for it. + */ + public static final int SHOW_AS_ACTION_IF_ROOM = 1; + + /** + * Always show this item as a button in an Action Bar. Use sparingly! + * If too many items are set to always show in the Action Bar it can + * crowd the Action Bar and degrade the user experience on devices with + * smaller screens. A good rule of thumb is to have no more than 2 + * items set to always show at a time. + */ + public static final int SHOW_AS_ACTION_ALWAYS = 2; + + /** + * When this item is in the action bar, always show it with a + * text label even if it also has an icon specified. + */ + public static final int SHOW_AS_ACTION_WITH_TEXT = 4; + + /** + * This item's action view collapses to a normal menu item. + * When expanded, the action view temporarily takes over + * a larger segment of its container. + */ + public static final int SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW = 8; + + /** + * Interface for the full API. + */ + interface MenuVersionImpl { + void setShowAsAction(MenuItem item, int actionEnum); + MenuItem setActionView(MenuItem item, View view); + MenuItem setActionView(MenuItem item, int resId); + View getActionView(MenuItem item); + boolean expandActionView(MenuItem item); + boolean collapseActionView(MenuItem item); + boolean isActionViewExpanded(MenuItem item); + MenuItem setOnActionExpandListener(MenuItem item, OnActionExpandListener listener); + } + + /** + * Interface definition for a callback to be invoked when a menu item marked with {@link + * #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW} is expanded or collapsed. + * + * @see #expandActionView(android.view.MenuItem) + * @see #collapseActionView(android.view.MenuItem) + * @see #setShowAsAction(android.view.MenuItem, int) + */ + public interface OnActionExpandListener { + + /** + * Called when a menu item with {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW} + * is expanded. + * + * @param item Item that was expanded + * @return true if the item should expand, false if expansion should be suppressed. + */ + public boolean onMenuItemActionExpand(MenuItem item); + + /** + * Called when a menu item with {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW} + * is collapsed. + * + * @param item Item that was collapsed + * @return true if the item should collapse, false if collapsing should be suppressed. + */ + public boolean onMenuItemActionCollapse(MenuItem item); + } + + /** + * Interface implementation that doesn't use anything about v4 APIs. + */ + static class BaseMenuVersionImpl implements MenuVersionImpl { + @Override + public void setShowAsAction(MenuItem item, int actionEnum) { + } + + @Override + public MenuItem setActionView(MenuItem item, View view) { + return item; + } + + @Override + public MenuItem setActionView(MenuItem item, int resId) { + return item; + } + + @Override + public View getActionView(MenuItem item) { + return null; + } + + @Override + public boolean expandActionView(MenuItem item) { + return false; + } + + @Override + public boolean collapseActionView(MenuItem item) { + return false; + } + + @Override + public boolean isActionViewExpanded(MenuItem item) { + return false; + } + + @Override + public MenuItem setOnActionExpandListener(MenuItem item, OnActionExpandListener listener) { + return item; + } + } + + /** + * Interface implementation for devices with at least v11 APIs. + */ + static class HoneycombMenuVersionImpl implements MenuVersionImpl { + @Override + public void setShowAsAction(MenuItem item, int actionEnum) { + MenuItemCompatHoneycomb.setShowAsAction(item, actionEnum); + } + + @Override + public MenuItem setActionView(MenuItem item, View view) { + return MenuItemCompatHoneycomb.setActionView(item, view); + } + + @Override + public MenuItem setActionView(MenuItem item, int resId) { + return MenuItemCompatHoneycomb.setActionView(item, resId); + } + + @Override + public View getActionView(MenuItem item) { + return MenuItemCompatHoneycomb.getActionView(item); + } + + @Override + public boolean expandActionView(MenuItem item) { + return false; + } + + @Override + public boolean collapseActionView(MenuItem item) { + return false; + } + + @Override + public boolean isActionViewExpanded(MenuItem item) { + return false; + } + + @Override + public MenuItem setOnActionExpandListener(MenuItem item, OnActionExpandListener listener) { + return item; + } + } + + static class IcsMenuVersionImpl extends HoneycombMenuVersionImpl { + @Override + public boolean expandActionView(MenuItem item) { + return MenuItemCompatIcs.expandActionView(item); + } + + @Override + public boolean collapseActionView(MenuItem item) { + return MenuItemCompatIcs.collapseActionView(item); + } + + @Override + public boolean isActionViewExpanded(MenuItem item) { + return MenuItemCompatIcs.isActionViewExpanded(item); + } + + @Override + public MenuItem setOnActionExpandListener(MenuItem item, + final OnActionExpandListener listener) { + if (listener == null) { + return MenuItemCompatIcs.setOnActionExpandListener(item, null); + } + /* + * MenuItemCompatIcs is a dependency of this segment of the support lib + * but not the other way around, so we need to take an extra step here to proxy + * to the right types. + */ + return MenuItemCompatIcs.setOnActionExpandListener(item, + new MenuItemCompatIcs.SupportActionExpandProxy() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return listener.onMenuItemActionExpand(item); + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + return listener.onMenuItemActionCollapse(item); + } + }); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final MenuVersionImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 14) { + IMPL = new IcsMenuVersionImpl(); + } else if (version >= 11) { + IMPL = new HoneycombMenuVersionImpl(); + } else { + IMPL = new BaseMenuVersionImpl(); + } + } + + // ------------------------------------------------------------------- + + /** + * Sets how this item should display in the presence of a compatible Action Bar. If the given + * item is compatible, this will call the item's supported implementation of + * {@link MenuItem#setShowAsAction(int)}. + * + * @param item - the item to change + * @param actionEnum - How the item should display. + */ + public static void setShowAsAction(MenuItem item, int actionEnum) { + if (item instanceof SupportMenuItem) { + ((SupportMenuItem) item).setShowAsAction(actionEnum); + } else { + IMPL.setShowAsAction(item, actionEnum); + } + } + + /** + * Set an action view for this menu item. An action view will be displayed in place + * of an automatically generated menu item element in the UI when this item is shown + * as an action within a parent. + * + * @param item the item to change + * @param view View to use for presenting this item to the user. + * @return This Item so additional setters can be called. + * + * @see #setShowAsAction(MenuItem, int) + */ + public static MenuItem setActionView(MenuItem item, View view) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).setActionView(view); + } + return IMPL.setActionView(item, view); + } + + /** + * Set an action view for this menu item. An action view will be displayed in place + * of an automatically generated menu item element in the UI when this item is shown + * as an action within a parent. + *

    + * Note: Setting an action view overrides the action provider + * set via {@link #setActionProvider(MenuItem, ActionProvider)}. + *

    + * + * @param item the item to change + * @param resId Layout resource to use for presenting this item to the user. + * @return This Item so additional setters can be called. + * + * @see #setShowAsAction(MenuItem, int) + */ + public static MenuItem setActionView(MenuItem item, int resId) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).setActionView(resId); + } + return IMPL.setActionView(item, resId); + } + + /** + * Returns the currently set action view for this menu item. + * + * @param item the item to query + * @return This item's action view + */ + public static View getActionView(MenuItem item) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).getActionView(); + } + return IMPL.getActionView(item); + } + + /** + * Sets the {@link ActionProvider} responsible for creating an action view if + * the item is placed on the action bar. The provider also provides a default + * action invoked if the item is placed in the overflow menu. + *

    + * Note: Setting an action provider overrides the action view + * set via {@link #setActionView(MenuItem, View)}. + *

    + * + * @param item item to change + * @param provider The action provider. + * @return This Item so additional setters can be called. + * + * @see ActionProvider + */ + public static MenuItem setActionProvider(MenuItem item, ActionProvider provider) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).setSupportActionProvider(provider); + } + // TODO Wrap the support ActionProvider and assign it + Log.w(TAG, "setActionProvider: item does not implement SupportMenuItem; ignoring"); + return item; + } + + /** + * Gets the {@link ActionProvider}. + * + * @return The action provider. + * + * @see ActionProvider + * @see #setActionProvider(MenuItem, ActionProvider) + */ + public static ActionProvider getActionProvider(MenuItem item) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).getSupportActionProvider(); + } + + // TODO Wrap the framework ActionProvider and return it + Log.w(TAG, "getActionProvider: item does not implement SupportMenuItem; returning null"); + return null; + } + + /** + * Expand the action view associated with this menu item. + * The menu item must have an action view set, as well as + * the showAsAction flag {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. + * If a listener has been set using + * {@link #setOnActionExpandListener(MenuItem, OnActionExpandListener)} + * it will have its {@link OnActionExpandListener#onMenuItemActionExpand(MenuItem)} + * method invoked. The listener may return false from this method to prevent expanding + * the action view. + * + * @return true if the action view was expanded, false otherwise. + */ + public static boolean expandActionView(MenuItem item) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).expandActionView(); + } + return IMPL.expandActionView(item); + } + + /** + * Collapse the action view associated with this menu item. The menu item must have an action + * view set, as well as the showAsAction flag {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. If a + * listener has been set using {@link #setOnActionExpandListener(MenuItem, + * android.support.v4.view.MenuItemCompat.OnActionExpandListener)} + * it will have its {@link + * android.support.v4.view.MenuItemCompat.OnActionExpandListener#onMenuItemActionCollapse(MenuItem)} + * method invoked. The listener may return false from this method to prevent collapsing + * the action view. + * + * @return true if the action view was collapsed, false otherwise. + */ + public static boolean collapseActionView(MenuItem item) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).collapseActionView(); + } + return IMPL.collapseActionView(item); + } + + /** + * Returns true if this menu item's action view has been expanded. + * + * @return true if the item's action view is expanded, false otherwise. + * @see #expandActionView(MenuItem) + * @see #collapseActionView(MenuItem) + * @see #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW + * @see android.support.v4.view.MenuItemCompat.OnActionExpandListener + */ + public static boolean isActionViewExpanded(MenuItem item) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).isActionViewExpanded(); + } + return IMPL.isActionViewExpanded(item); + } + + /** + * Set an {@link OnActionExpandListener} on this menu + * item to be notified when the associated action view is expanded or collapsed. + * The menu item must be configured to expand or collapse its action view using the flag + * {@link #SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW}. + * + * @param listener Listener that will respond to expand/collapse events + * @return This menu item instance for call chaining + */ + public static MenuItem setOnActionExpandListener(MenuItem item, + OnActionExpandListener listener) { + if (item instanceof SupportMenuItem) { + return ((SupportMenuItem) item).setSupportOnActionExpandListener(listener); + } + return IMPL.setOnActionExpandListener(item, listener); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatHoneycomb.java new file mode 100644 index 00000000000..1a0e513ee79 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatHoneycomb.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.MenuItem; +import android.view.View; + +/** + * Implementation of menu compatibility that can call Honeycomb APIs. + */ +class MenuItemCompatHoneycomb { + public static void setShowAsAction(MenuItem item, int actionEnum) { + item.setShowAsAction(actionEnum); + } + + public static MenuItem setActionView(MenuItem item, View view) { + return item.setActionView(view); + } + + public static MenuItem setActionView(MenuItem item, int resId) { + return item.setActionView(resId); + } + + public static View getActionView(MenuItem item) { + return item.getActionView(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatIcs.java new file mode 100644 index 00000000000..8e65079d012 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MenuItemCompatIcs.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.view.MenuItem; + +class MenuItemCompatIcs { + public static boolean expandActionView(MenuItem item) { + return item.expandActionView(); + } + + public static boolean collapseActionView(MenuItem item) { + return item.collapseActionView(); + } + + public static boolean isActionViewExpanded(MenuItem item) { + return item.isActionViewExpanded(); + } + + public static MenuItem setOnActionExpandListener(MenuItem item, + SupportActionExpandProxy listener) { + return item.setOnActionExpandListener(new OnActionExpandListenerWrapper(listener)); + } + + /** + * Work around the support lib's build dependency chain. The actual API-lib + * depends on -ics, but -ics doesn't depend on the API-lib so it doesn't know + * that MenuItemCompat.OnActionExpandListener exists. + */ + interface SupportActionExpandProxy { + boolean onMenuItemActionExpand(MenuItem item); + boolean onMenuItemActionCollapse(MenuItem item); + } + + // support => framework + static class OnActionExpandListenerWrapper implements MenuItem.OnActionExpandListener { + private SupportActionExpandProxy mWrapped; + + public OnActionExpandListenerWrapper(SupportActionExpandProxy wrapped) { + mWrapped = wrapped; + } + + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return mWrapped.onMenuItemActionExpand(item); + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + return mWrapped.onMenuItemActionCollapse(item); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompat.java new file mode 100644 index 00000000000..8c7d07dc6e7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompat.java @@ -0,0 +1,519 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.os.Build; +import android.view.MotionEvent; + +/** + * Helper for accessing features in {@link MotionEvent} introduced + * after API level 4 in a backwards compatible fashion. + */ +public class MotionEventCompat { + /** + * Interface for the full API. + */ + interface MotionEventVersionImpl { + public int findPointerIndex(MotionEvent event, int pointerId); + public int getPointerId(MotionEvent event, int pointerIndex); + public float getX(MotionEvent event, int pointerIndex); + public float getY(MotionEvent event, int pointerIndex); + public int getPointerCount(MotionEvent event); + public int getSource(MotionEvent event); + float getAxisValue(MotionEvent event, int axis); + float getAxisValue(MotionEvent event, int axis, int pointerIndex); + } + + /** + * Interface implementation that doesn't use anything about v4 APIs. + */ + static class BaseMotionEventVersionImpl implements MotionEventVersionImpl { + @Override + public int findPointerIndex(MotionEvent event, int pointerId) { + if (pointerId == 0) { + // id 0 == index 0 and vice versa. + return 0; + } + return -1; + } + @Override + public int getPointerId(MotionEvent event, int pointerIndex) { + if (pointerIndex == 0) { + // index 0 == id 0 and vice versa. + return 0; + } + throw new IndexOutOfBoundsException("Pre-Eclair does not support multiple pointers"); + } + @Override + public float getX(MotionEvent event, int pointerIndex) { + if (pointerIndex == 0) { + return event.getX(); + } + throw new IndexOutOfBoundsException("Pre-Eclair does not support multiple pointers"); + } + @Override + public float getY(MotionEvent event, int pointerIndex) { + if (pointerIndex == 0) { + return event.getY(); + } + throw new IndexOutOfBoundsException("Pre-Eclair does not support multiple pointers"); + } + @Override + public int getPointerCount(MotionEvent event) { + return 1; + } + + @Override + public int getSource(MotionEvent event) { + return InputDeviceCompat.SOURCE_UNKNOWN; + } + + @Override + public float getAxisValue(MotionEvent event, int axis) { + return 0; + } + + @Override + public float getAxisValue(MotionEvent event, int axis, int pointerIndex) { + return 0; + } + } + + /** + * Interface implementation for devices with at least v5 APIs. + */ + static class EclairMotionEventVersionImpl extends BaseMotionEventVersionImpl { + @Override + public int findPointerIndex(MotionEvent event, int pointerId) { + return MotionEventCompatEclair.findPointerIndex(event, pointerId); + } + @Override + public int getPointerId(MotionEvent event, int pointerIndex) { + return MotionEventCompatEclair.getPointerId(event, pointerIndex); + } + @Override + public float getX(MotionEvent event, int pointerIndex) { + return MotionEventCompatEclair.getX(event, pointerIndex); + } + @Override + public float getY(MotionEvent event, int pointerIndex) { + return MotionEventCompatEclair.getY(event, pointerIndex); + } + @Override + public int getPointerCount(MotionEvent event) { + return MotionEventCompatEclair.getPointerCount(event); + } + } + + /** + * Interface implementation for devices with at least v8 APIs. + */ + static class GingerbreadMotionEventVersionImpl extends EclairMotionEventVersionImpl { + @Override + public int getSource(MotionEvent event) { + return MotionEventCompatGingerbread.getSource(event); + } + } + + /** + * Interface implementation for devices with at least v12 APIs. + */ + static class HoneycombMr1MotionEventVersionImpl extends GingerbreadMotionEventVersionImpl { + + @Override + public float getAxisValue(MotionEvent event, int axis) { + return MotionEventCompatHoneycombMr1.getAxisValue(event, axis); + } + + @Override + public float getAxisValue(MotionEvent event, int axis, int pointerIndex) { + return MotionEventCompatHoneycombMr1.getAxisValue(event, axis, pointerIndex); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final MotionEventVersionImpl IMPL; + static { + if (Build.VERSION.SDK_INT >= 12) { + IMPL = new HoneycombMr1MotionEventVersionImpl(); + } else if (Build.VERSION.SDK_INT >= 9) { + IMPL = new GingerbreadMotionEventVersionImpl(); + } else if (Build.VERSION.SDK_INT >= 5) { + IMPL = new EclairMotionEventVersionImpl(); + } else { + IMPL = new BaseMotionEventVersionImpl(); + } + } + + // ------------------------------------------------------------------- + + /** + * Synonym for {@link MotionEvent#ACTION_MASK}. + */ + public static final int ACTION_MASK = 0xff; + + /** + * Synonym for {@link MotionEvent#ACTION_POINTER_DOWN}. + */ + public static final int ACTION_POINTER_DOWN = 5; + + /** + * Synonym for {@link MotionEvent#ACTION_POINTER_UP}. + */ + public static final int ACTION_POINTER_UP = 6; + + /** + * Synonym for {@link MotionEvent#ACTION_HOVER_MOVE}. + */ + public static final int ACTION_HOVER_MOVE = 7; + + /** + * Synonym for {@link MotionEvent#ACTION_SCROLL}. + */ + public static final int ACTION_SCROLL = 8; + + /** + * Synonym for {@link MotionEvent#ACTION_POINTER_INDEX_MASK}. + */ + public static final int ACTION_POINTER_INDEX_MASK = 0xff00; + + /** + * Synonym for {@link MotionEvent#ACTION_POINTER_INDEX_SHIFT}. + */ + public static final int ACTION_POINTER_INDEX_SHIFT = 8; + + /** + * Synonym for {@link MotionEvent#ACTION_HOVER_ENTER}. + */ + public static final int ACTION_HOVER_ENTER = 9; + + /** + * Synonym for {@link MotionEvent#ACTION_HOVER_EXIT}. + */ + public static final int ACTION_HOVER_EXIT = 10; + + /** + * Synonym for {@link MotionEvent#AXIS_X}. + */ + public static final int AXIS_X = 0; + + /** + * Synonym for {@link MotionEvent#AXIS_Y}. + */ + public static final int AXIS_Y = 1; + + /** + * Synonym for {@link MotionEvent#AXIS_PRESSURE}. + */ + public static final int AXIS_PRESSURE = 2; + + /** + * Synonym for {@link MotionEvent#AXIS_SIZE}. + */ + public static final int AXIS_SIZE = 3; + + /** + * Synonym for {@link MotionEvent#AXIS_TOUCH_MAJOR}. + */ + public static final int AXIS_TOUCH_MAJOR = 4; + + /** + * Synonym for {@link MotionEvent#AXIS_TOUCH_MINOR}. + */ + public static final int AXIS_TOUCH_MINOR = 5; + + /** + * Synonym for {@link MotionEvent#AXIS_TOOL_MAJOR}. + */ + public static final int AXIS_TOOL_MAJOR = 6; + + /** + * Synonym for {@link MotionEvent#AXIS_TOOL_MINOR}. + */ + public static final int AXIS_TOOL_MINOR = 7; + + /** + * Synonym for {@link MotionEvent#AXIS_ORIENTATION}. + */ + public static final int AXIS_ORIENTATION = 8; + + /** + * Synonym for {@link MotionEvent#AXIS_VSCROLL}. + */ + public static final int AXIS_VSCROLL = 9; + + /** + * Synonym for {@link MotionEvent#AXIS_HSCROLL}. + */ + public static final int AXIS_HSCROLL = 10; + + /** + * Synonym for {@link MotionEvent#AXIS_Z}. + */ + public static final int AXIS_Z = 11; + + /** + * Synonym for {@link MotionEvent#AXIS_RX}. + */ + public static final int AXIS_RX = 12; + + /** + * Synonym for {@link MotionEvent#AXIS_RY}. + */ + public static final int AXIS_RY = 13; + + /** + * Synonym for {@link MotionEvent#AXIS_RZ}. + */ + public static final int AXIS_RZ = 14; + + /** + * Synonym for {@link MotionEvent#AXIS_HAT_X}. + */ + public static final int AXIS_HAT_X = 15; + + /** + * Synonym for {@link MotionEvent#AXIS_HAT_Y}. + */ + public static final int AXIS_HAT_Y = 16; + + /** + * Synonym for {@link MotionEvent#AXIS_LTRIGGER}. + */ + public static final int AXIS_LTRIGGER = 17; + + /** + * Synonym for {@link MotionEvent#AXIS_RTRIGGER}. + */ + public static final int AXIS_RTRIGGER = 18; + + /** + * Synonym for {@link MotionEvent#AXIS_THROTTLE}. + */ + public static final int AXIS_THROTTLE = 19; + + /** + * Synonym for {@link MotionEvent#AXIS_RUDDER}. + */ + public static final int AXIS_RUDDER = 20; + + /** + * Synonym for {@link MotionEvent#AXIS_WHEEL}. + */ + public static final int AXIS_WHEEL = 21; + + /** + * Synonym for {@link MotionEvent#AXIS_GAS}. + */ + public static final int AXIS_GAS = 22; + + /** + * Synonym for {@link MotionEvent#AXIS_BRAKE}. + */ + public static final int AXIS_BRAKE = 23; + + /** + * Synonym for {@link MotionEvent#AXIS_DISTANCE}. + */ + public static final int AXIS_DISTANCE = 24; + + /** + * Synonym for {@link MotionEvent#AXIS_TILT}. + */ + public static final int AXIS_TILT = 25; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_1}. + */ + public static final int AXIS_GENERIC_1 = 32; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_2}. + */ + public static final int AXIS_GENERIC_2 = 33; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_3}. + */ + public static final int AXIS_GENERIC_3 = 34; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_4}. + */ + public static final int AXIS_GENERIC_4 = 35; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_5}. + */ + public static final int AXIS_GENERIC_5 = 36; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_6}. + */ + public static final int AXIS_GENERIC_6 = 37; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_7}. + */ + public static final int AXIS_GENERIC_7 = 38; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_8}. + */ + public static final int AXIS_GENERIC_8 = 39; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_9}. + */ + public static final int AXIS_GENERIC_9 = 40; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_10}. + */ + public static final int AXIS_GENERIC_10 = 41; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_11}. + */ + public static final int AXIS_GENERIC_11 = 42; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_12}. + */ + public static final int AXIS_GENERIC_12 = 43; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_13}. + */ + public static final int AXIS_GENERIC_13 = 44; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_14}. + */ + public static final int AXIS_GENERIC_14 = 45; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_15}. + */ + public static final int AXIS_GENERIC_15 = 46; + + /** + * Synonym for {@link MotionEvent#AXIS_GENERIC_16}. + */ + public static final int AXIS_GENERIC_16 = 47; + + /** + * Call {@link MotionEvent#getAction}, returning only the {@link #ACTION_MASK} + * portion. + */ + public static int getActionMasked(MotionEvent event) { + return event.getAction() & ACTION_MASK; + } + + /** + * Call {@link MotionEvent#getAction}, returning only the pointer index + * portion + */ + public static int getActionIndex(MotionEvent event) { + return (event.getAction() & ACTION_POINTER_INDEX_MASK) + >> ACTION_POINTER_INDEX_SHIFT; + } + + /** + * Call {@link MotionEvent#findPointerIndex(int)}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#ECLAIR} device, + * does nothing and returns -1. + */ + public static int findPointerIndex(MotionEvent event, int pointerId) { + return IMPL.findPointerIndex(event, pointerId); + } + + /** + * Call {@link MotionEvent#getPointerId(int)}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#ECLAIR} device, + * {@link IndexOutOfBoundsException} is thrown. + */ + public static int getPointerId(MotionEvent event, int pointerIndex) { + return IMPL.getPointerId(event, pointerIndex); + } + + /** + * Call {@link MotionEvent#getX(int)}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#ECLAIR} device, + * {@link IndexOutOfBoundsException} is thrown. + */ + public static float getX(MotionEvent event, int pointerIndex) { + return IMPL.getX(event, pointerIndex); + } + + /** + * Call {@link MotionEvent#getY(int)}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#ECLAIR} device, + * {@link IndexOutOfBoundsException} is thrown. + */ + public static float getY(MotionEvent event, int pointerIndex) { + return IMPL.getY(event, pointerIndex); + } + + /** + * The number of pointers of data contained in this event. Always + * >= 1. + */ + public static int getPointerCount(MotionEvent event) { + return IMPL.getPointerCount(event); + } + + /** + * Gets the source of the event. + * + * @return The event source or {@link InputDeviceCompat#SOURCE_UNKNOWN} if unknown. + */ + public static int getSource(MotionEvent event) { + return IMPL.getSource(event); + } + + /** + * Get axis value for the first pointer index (may be an + * arbitrary pointer identifier). + * + * @param axis The axis identifier for the axis value to retrieve. + * + * @see #AXIS_X + * @see #AXIS_Y + */ + public static float getAxisValue(MotionEvent event, int axis) { + return IMPL.getAxisValue(event, axis); + } + + /** + * Returns the value of the requested axis for the given pointer index + * (use {@link #getPointerId(MotionEvent, int)} to find the pointer identifier for this index). + * + * @param axis The axis identifier for the axis value to retrieve. + * @param pointerIndex Raw index of pointer to retrieve. Value may be from 0 + * (the first pointer that is down) to {@link #getPointerCount(MotionEvent)}-1. + * @return The value of the axis, or 0 if the axis is not available. + * + * @see #AXIS_X + * @see #AXIS_Y + */ + public static float getAxisValue(MotionEvent event, int axis, int pointerIndex) { + return IMPL.getAxisValue(event, axis, pointerIndex); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatEclair.java b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatEclair.java new file mode 100644 index 00000000000..7ecfb5cf84f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatEclair.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.MotionEvent; + +/** + * Implementation of motion event compatibility that can call Eclair APIs. + */ +class MotionEventCompatEclair { + public static int findPointerIndex(MotionEvent event, int pointerId) { + return event.findPointerIndex(pointerId); + } + public static int getPointerId(MotionEvent event, int pointerIndex) { + return event.getPointerId(pointerIndex); + } + public static float getX(MotionEvent event, int pointerIndex) { + return event.getX(pointerIndex); + } + public static float getY(MotionEvent event, int pointerIndex) { + return event.getY(pointerIndex); + } + public static int getPointerCount(MotionEvent event) { + return event.getPointerCount(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatGingerbread.java b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatGingerbread.java new file mode 100644 index 00000000000..f43dc0a0747 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatGingerbread.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.MotionEvent; + +/** + * Motion event compatibility class for API 8+. + */ +class MotionEventCompatGingerbread { + public static int getSource(MotionEvent event) { + return event.getSource(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatHoneycombMr1.java b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatHoneycombMr1.java new file mode 100644 index 00000000000..406fcf32ed9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/MotionEventCompatHoneycombMr1.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.MotionEvent; + +/** + * Motion event compatibility class for API 12+. + */ +class MotionEventCompatHoneycombMr1 { + static float getAxisValue(MotionEvent event, int axis) { + return event.getAxisValue(axis); + } + + static float getAxisValue(MotionEvent event, int axis, int pointerIndex) { + return event.getAxisValue(axis, pointerIndex); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChild.java b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChild.java new file mode 100644 index 00000000000..40e0a09fdf3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChild.java @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; + +/** + * This interface should be implemented by {@link android.view.View View} subclasses that wish + * to support dispatching nested scrolling operations to a cooperating parent + * {@link android.view.ViewGroup ViewGroup}. + * + *

    Classes implementing this interface should create a final instance of a + * {@link NestedScrollingChildHelper} as a field and delegate any View methods to the + * NestedScrollingChildHelper methods of the same signature.

    + * + *

    Views invoking nested scrolling functionality should always do so from the relevant + * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility + * shim static methods. This ensures interoperability with nested scrolling views on Android + * 5.0 Lollipop and newer.

    + */ +public interface NestedScrollingChild { + /** + * Enable or disable nested scrolling for this view. + * + *

    If this property is set to true the view will be permitted to initiate nested + * scrolling operations with a compatible parent view in the current hierarchy. If this + * view does not implement nested scrolling this will have no effect. Disabling nested scrolling + * while a nested scroll is in progress has the effect of {@link #stopNestedScroll() stopping} + * the nested scroll.

    + * + * @param enabled true to enable nested scrolling, false to disable + * + * @see #isNestedScrollingEnabled() + */ + public void setNestedScrollingEnabled(boolean enabled); + + /** + * Returns true if nested scrolling is enabled for this view. + * + *

    If nested scrolling is enabled and this View class implementation supports it, + * this view will act as a nested scrolling child view when applicable, forwarding data + * about the scroll operation in progress to a compatible and cooperating nested scrolling + * parent.

    + * + * @return true if nested scrolling is enabled + * + * @see #setNestedScrollingEnabled(boolean) + */ + public boolean isNestedScrollingEnabled(); + + /** + * Begin a nestable scroll operation along the given axes. + * + *

    A view starting a nested scroll promises to abide by the following contract:

    + * + *

    The view will call startNestedScroll upon initiating a scroll operation. In the case + * of a touch scroll this corresponds to the initial {@link MotionEvent#ACTION_DOWN}. + * In the case of touch scrolling the nested scroll will be terminated automatically in + * the same manner as {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}. + * In the event of programmatic scrolling the caller must explicitly call + * {@link #stopNestedScroll()} to indicate the end of the nested scroll.

    + * + *

    If startNestedScroll returns true, a cooperative parent was found. + * If it returns false the caller may ignore the rest of this contract until the next scroll. + * Calling startNestedScroll while a nested scroll is already in progress will return true.

    + * + *

    At each incremental step of the scroll the caller should invoke + * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} + * once it has calculated the requested scrolling delta. If it returns true the nested scrolling + * parent at least partially consumed the scroll and the caller should adjust the amount it + * scrolls by.

    + * + *

    After applying the remainder of the scroll delta the caller should invoke + * {@link #dispatchNestedScroll(int, int, int, int, int[]) dispatchNestedScroll}, passing + * both the delta consumed and the delta unconsumed. A nested scrolling parent may treat + * these values differently. See + * {@link NestedScrollingParent#onNestedScroll(View, int, int, int, int)}. + *

    + * + * @param axes Flags consisting of a combination of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL} + * and/or {@link ViewCompat#SCROLL_AXIS_VERTICAL}. + * @return true if a cooperative parent was found and nested scrolling has been enabled for + * the current gesture. + * + * @see #stopNestedScroll() + * @see #dispatchNestedPreScroll(int, int, int[], int[]) + * @see #dispatchNestedScroll(int, int, int, int, int[]) + */ + public boolean startNestedScroll(int axes); + + /** + * Stop a nested scroll in progress. + * + *

    Calling this method when a nested scroll is not currently in progress is harmless.

    + * + * @see #startNestedScroll(int) + */ + public void stopNestedScroll(); + + /** + * Returns true if this view has a nested scrolling parent. + * + *

    The presence of a nested scrolling parent indicates that this view has initiated + * a nested scroll and it was accepted by an ancestor view further up the view hierarchy.

    + * + * @return whether this view has a nested scrolling parent + */ + public boolean hasNestedScrollingParent(); + + /** + * Dispatch one step of a nested scroll in progress. + * + *

    Implementations of views that support nested scrolling should call this to report + * info about a scroll in progress to the current nested scrolling parent. If a nested scroll + * is not currently in progress or nested scrolling is not + * {@link #isNestedScrollingEnabled() enabled} for this view this method does nothing.

    + * + *

    Compatible View implementations should also call + * {@link #dispatchNestedPreScroll(int, int, int[], int[]) dispatchNestedPreScroll} before + * consuming a component of the scroll event themselves.

    + * + * @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step + * @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step + * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the event was dispatched, false if it could not be dispatched. + * @see #dispatchNestedPreScroll(int, int, int[], int[]) + */ + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow); + + /** + * Dispatch one step of a nested scroll in progress before this view consumes any portion of it. + * + *

    Nested pre-scroll events are to nested scroll events what touch intercept is to touch. + * dispatchNestedPreScroll offers an opportunity for the parent view in a nested + * scrolling operation to consume some or all of the scroll operation before the child view + * consumes it.

    + * + * @param dx Horizontal scroll distance in pixels + * @param dy Vertical scroll distance in pixels + * @param consumed Output. If not null, consumed[0] will contain the consumed component of dx + * and consumed[1] the consumed dy. + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the parent consumed some or all of the scroll delta + * @see #dispatchNestedScroll(int, int, int, int, int[]) + */ + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow); + + /** + * Dispatch a fling to a nested scrolling parent. + * + *

    This method should be used to indicate that a nested scrolling child has detected + * suitable conditions for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

    + * + *

    If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.

    + * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @param consumed true if the child consumed the fling, false otherwise + * @return true if the nested scrolling parent consumed or otherwise reacted to the fling + */ + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed); + + /** + * Dispatch a fling to a nested scrolling parent before it is processed by this view. + * + *

    Nested pre-fling events are to nested fling events what touch intercept is to touch + * and what nested pre-scroll is to nested scroll. dispatchNestedPreFling + * offsets an opportunity for the parent view in a nested fling to fully consume the fling + * before the child view consumes it. If this method returns true, a nested + * parent view consumed the fling and this view should not scroll as a result.

    + * + *

    For a better user experience, only one view in a nested scrolling chain should consume + * the fling at a time. If a parent view consumed the fling this method will return false. + * Custom view implementations should account for this in two ways:

    + * + *
      + *
    • If a custom view is paged and needs to settle to a fixed page-point, do not + * call dispatchNestedPreFling; consume the fling and settle to a valid + * position regardless.
    • + *
    • If a nested parent does consume the fling, this view should not scroll at all, + * even to settle back to a valid idle position.
    • + *
    + * + *

    Views should also not offer fling velocities to nested parent views along an axis + * where scrolling is not currently supported; a {@link android.widget.ScrollView ScrollView} + * should not offer a horizontal fling velocity to its parents since scrolling along that + * axis is not permitted and carrying velocity along that motion does not make sense.

    + * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @return true if a nested scrolling parent consumed the fling + */ + public boolean dispatchNestedPreFling(float velocityX, float velocityY); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChildHelper.java b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChildHelper.java new file mode 100644 index 00000000000..9e25667756a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingChildHelper.java @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.view.View; +import android.view.ViewParent; + +/** + * Helper class for implementing nested scrolling child views compatible with Android platform + * versions earlier than Android 5.0 Lollipop (API 21). + * + *

    {@link android.view.View View} subclasses should instantiate a final instance of this + * class as a field at construction. For each View method that has a matching + * method signature in this class, delegate the operation to the helper instance in an overriden + * method implementation. This implements the standard framework policy for nested scrolling.

    + * + *

    Views invoking nested scrolling functionality should always do so from the relevant + * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility + * shim static methods. This ensures interoperability with nested scrolling views on Android + * 5.0 Lollipop and newer.

    + */ +public class NestedScrollingChildHelper { + private final View mView; + private ViewParent mNestedScrollingParent; + private boolean mIsNestedScrollingEnabled; + private int[] mTempNestedScrollConsumed; + + /** + * Construct a new helper for a given view. + */ + public NestedScrollingChildHelper(View view) { + mView = view; + } + + /** + * Enable nested scrolling. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @param enabled true to enable nested scrolling dispatch from this view, false otherwise + */ + public void setNestedScrollingEnabled(boolean enabled) { + if (mIsNestedScrollingEnabled) { + ViewCompat.stopNestedScroll(mView); + } + mIsNestedScrollingEnabled = enabled; + } + + /** + * Check if nested scrolling is enabled for this view. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @return true if nested scrolling is enabled for this view + */ + public boolean isNestedScrollingEnabled() { + return mIsNestedScrollingEnabled; + } + + /** + * Check if this view has a nested scrolling parent view currently receiving events for + * a nested scroll in progress. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @return true if this view has a nested scrolling parent, false otherwise + */ + public boolean hasNestedScrollingParent() { + return mNestedScrollingParent != null; + } + + /** + * Start a new nested scroll for this view. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @param axes Supported nested scroll axes. + * See {@link NestedScrollingChild#startNestedScroll(int)}. + * @return true if a cooperating parent view was found and nested scrolling started successfully + */ + public boolean startNestedScroll(int axes) { + if (hasNestedScrollingParent()) { + // Already in progress + return true; + } + if (isNestedScrollingEnabled()) { + ViewParent p = mView.getParent(); + View child = mView; + while (p != null) { + if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) { + mNestedScrollingParent = p; + ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes); + return true; + } + if (p instanceof View) { + child = (View) p; + } + p = p.getParent(); + } + } + return false; + } + + /** + * Stop a nested scroll in progress. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + */ + public void stopNestedScroll() { + if (mNestedScrollingParent != null) { + ViewParentCompat.onStopNestedScroll(mNestedScrollingParent, mView); + mNestedScrollingParent = null; + } + } + + /** + * Dispatch one step of a nested scrolling operation to the current nested scrolling parent. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @return true if the parent consumed any of the nested scroll + */ + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + if (dxConsumed != 0 || dyConsumed != 0 || dxUnconsumed != 0 || dyUnconsumed != 0) { + int startX = 0; + int startY = 0; + if (offsetInWindow != null) { + mView.getLocationInWindow(offsetInWindow); + startX = offsetInWindow[0]; + startY = offsetInWindow[1]; + } + + ViewParentCompat.onNestedScroll(mNestedScrollingParent, mView, dxConsumed, + dyConsumed, dxUnconsumed, dyUnconsumed); + + if (offsetInWindow != null) { + mView.getLocationInWindow(offsetInWindow); + offsetInWindow[0] -= startX; + offsetInWindow[1] -= startY; + } + return true; + } else if (offsetInWindow != null) { + // No motion, no dispatch. Keep offsetInWindow up to date. + offsetInWindow[0] = 0; + offsetInWindow[1] = 0; + } + } + return false; + } + + /** + * Dispatch one step of a nested pre-scrolling operation to the current nested scrolling parent. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @return true if the parent consumed any of the nested scroll + */ + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + if (dx != 0 || dy != 0) { + int startX = 0; + int startY = 0; + if (offsetInWindow != null) { + mView.getLocationInWindow(offsetInWindow); + startX = offsetInWindow[0]; + startY = offsetInWindow[1]; + } + + if (consumed == null) { + if (mTempNestedScrollConsumed == null) { + mTempNestedScrollConsumed = new int[2]; + } + consumed = mTempNestedScrollConsumed; + } + consumed[0] = 0; + consumed[1] = 0; + ViewParentCompat.onNestedPreScroll(mNestedScrollingParent, mView, dx, dy, consumed); + + if (offsetInWindow != null) { + mView.getLocationInWindow(offsetInWindow); + offsetInWindow[0] -= startX; + offsetInWindow[1] -= startY; + } + return consumed[0] != 0 || consumed[1] != 0; + } else if (offsetInWindow != null) { + offsetInWindow[0] = 0; + offsetInWindow[1] = 0; + } + } + return false; + } + + /** + * Dispatch a nested fling operation to the current nested scrolling parent. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @return true if the parent consumed the nested fling + */ + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + return ViewParentCompat.onNestedFling(mNestedScrollingParent, mView, velocityX, + velocityY, consumed); + } + return false; + } + + /** + * Dispatch a nested pre-fling operation to the current nested scrolling parent. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @return true if the parent consumed the nested fling + */ + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + if (isNestedScrollingEnabled() && mNestedScrollingParent != null) { + return ViewParentCompat.onNestedPreFling(mNestedScrollingParent, mView, velocityX, + velocityY); + } + return false; + } + + /** + * View subclasses should always call this method on their + * NestedScrollingChildHelper when detached from a window. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + */ + public void onDetachedFromWindow() { + ViewCompat.stopNestedScroll(mView); + } + + /** + * Called when a nested scrolling child stops its current nested scroll operation. + * + *

    This is a delegate method. Call it from your {@link android.view.View View} subclass + * method/{@link NestedScrollingChild} interface method with the same signature to implement + * the standard policy.

    + * + * @param child Child view stopping its nested scroll. This may not be a direct child view. + */ + public void onStopNestedScroll(View child) { + ViewCompat.stopNestedScroll(mView); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParent.java b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParent.java new file mode 100644 index 00000000000..bc056b6d1fb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParent.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; + +/** + * This interface should be implemented by {@link android.view.ViewGroup ViewGroup} subclasses + * that wish to support scrolling operations delegated by a nested child view. + * + *

    Classes implementing this interface should create a final instance of a + * {@link NestedScrollingParentHelper} as a field and delegate any View or ViewGroup methods + * to the NestedScrollingParentHelper methods of the same signature.

    + * + *

    Views invoking nested scrolling functionality should always do so from the relevant + * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility + * shim static methods. This ensures interoperability with nested scrolling views on Android + * 5.0 Lollipop and newer.

    + */ +public interface NestedScrollingParent { + /** + * React to a descendant view initiating a nestable scroll operation, claiming the + * nested scroll operation if appropriate. + * + *

    This method will be called in response to a descendant view invoking + * {@link ViewCompat#startNestedScroll(View, int)}. Each parent up the view hierarchy will be + * given an opportunity to respond and claim the nested scrolling operation by returning + * true.

    + * + *

    This method may be overridden by ViewParent implementations to indicate when the view + * is willing to support a nested scrolling operation that is about to begin. If it returns + * true, this ViewParent will become the target view's nested scrolling parent for the duration + * of the scroll operation in progress. When the nested scroll is finished this ViewParent + * will receive a call to {@link #onStopNestedScroll(View)}. + *

    + * + * @param child Direct child of this ViewParent containing target + * @param target View that initiated the nested scroll + * @param nestedScrollAxes Flags consisting of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, + * {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both + * @return true if this ViewParent accepts the nested scroll operation + */ + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes); + + /** + * React to the successful claiming of a nested scroll operation. + * + *

    This method will be called after + * {@link #onStartNestedScroll(View, View, int) onStartNestedScroll} returns true. It offers + * an opportunity for the view and its superclasses to perform initial configuration + * for the nested scroll. Implementations of this method should always call their superclass's + * implementation of this method if one is present.

    + * + * @param child Direct child of this ViewParent containing target + * @param target View that initiated the nested scroll + * @param nestedScrollAxes Flags consisting of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, + * {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both + * @see #onStartNestedScroll(View, View, int) + * @see #onStopNestedScroll(View) + */ + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes); + + /** + * React to a nested scroll operation ending. + * + *

    Perform cleanup after a nested scrolling operation. + * This method will be called when a nested scroll stops, for example when a nested touch + * scroll ends with a {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL} event. + * Implementations of this method should always call their superclass's implementation of this + * method if one is present.

    + * + * @param target View that initiated the nested scroll + */ + public void onStopNestedScroll(View target); + + /** + * React to a nested scroll in progress. + * + *

    This method will be called when the ViewParent's current nested scrolling child view + * dispatches a nested scroll event. To receive calls to this method the ViewParent must have + * previously returned true for a call to + * {@link #onStartNestedScroll(View, View, int)}.

    + * + *

    Both the consumed and unconsumed portions of the scroll distance are reported to the + * ViewParent. An implementation may choose to use the consumed portion to match or chase scroll + * position of multiple child elements, for example. The unconsumed portion may be used to + * allow continuous dragging of multiple scrolling or draggable elements, such as scrolling + * a list within a vertical drawer where the drawer begins dragging once the edge of inner + * scrolling content is reached.

    + * + * @param target The descendent view controlling the nested scroll + * @param dxConsumed Horizontal scroll distance in pixels already consumed by target + * @param dyConsumed Vertical scroll distance in pixels already consumed by target + * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by target + * @param dyUnconsumed Vertical scroll distance in pixels not consumed by target + */ + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed); + + /** + * React to a nested scroll in progress before the target view consumes a portion of the scroll. + * + *

    When working with nested scrolling often the parent view may want an opportunity + * to consume the scroll before the nested scrolling child does. An example of this is a + * drawer that contains a scrollable list. The user will want to be able to scroll the list + * fully into view before the list itself begins scrolling.

    + * + *

    onNestedPreScroll is called when a nested scrolling child invokes + * {@link View#dispatchNestedPreScroll(int, int, int[], int[])}. The implementation should + * report how any pixels of the scroll reported by dx, dy were consumed in the + * consumed array. Index 0 corresponds to dx and index 1 corresponds to dy. + * This parameter will never be null. Initial values for consumed[0] and consumed[1] + * will always be 0.

    + * + * @param target View that initiated the nested scroll + * @param dx Horizontal scroll distance in pixels + * @param dy Vertical scroll distance in pixels + * @param consumed Output. The horizontal and vertical scroll distance consumed by this parent + */ + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed); + + /** + * Request a fling from a nested scroll. + * + *

    This method signifies that a nested scrolling child has detected suitable conditions + * for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

    + * + *

    If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.

    + * + * @param target View that initiated the nested scroll + * @param velocityX Horizontal velocity in pixels per second + * @param velocityY Vertical velocity in pixels per second + * @param consumed true if the child consumed the fling, false otherwise + * @return true if this parent consumed or otherwise reacted to the fling + */ + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed); + + /** + * React to a nested fling before the target view consumes it. + * + *

    This method siginfies that a nested scrolling child has detected a fling with the given + * velocity along each axis. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

    + * + *

    If a nested scrolling parent is consuming motion as part of a + * {@link #onNestedPreScroll(View, int, int, int[]) pre-scroll}, it may be appropriate for + * it to also consume the pre-fling to complete that same motion. By returning + * true from this method, the parent indicates that the child should not + * fling its own internal content as well.

    + * + * @param target View that initiated the nested scroll + * @param velocityX Horizontal velocity in pixels per second + * @param velocityY Vertical velocity in pixels per second + * @return true if this parent consumed the fling ahead of the target view + */ + public boolean onNestedPreFling(View target, float velocityX, float velocityY); + + /** + * Return the current axes of nested scrolling for this NestedScrollingParent. + * + *

    A NestedScrollingParent returning something other than {@link ViewCompat#SCROLL_AXIS_NONE} + * is currently acting as a nested scrolling parent for one or more descendant views in + * the hierarchy.

    + * + * @return Flags indicating the current axes of nested scrolling + * @see ViewCompat#SCROLL_AXIS_HORIZONTAL + * @see ViewCompat#SCROLL_AXIS_VERTICAL + * @see ViewCompat#SCROLL_AXIS_NONE + */ + public int getNestedScrollAxes(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParentHelper.java b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParentHelper.java new file mode 100644 index 00000000000..91522f1ca85 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/NestedScrollingParentHelper.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.view.View; +import android.view.ViewGroup; + +/** + * Helper class for implementing nested scrolling parent views compatible with Android platform + * versions earlier than Android 5.0 Lollipop (API 21). + * + *

    {@link android.view.ViewGroup ViewGroup} subclasses should instantiate a final instance + * of this class as a field at construction. For each ViewGroup method that has + * a matching method signature in this class, delegate the operation to the helper instance + * in an overriden method implementation. This implements the standard framework policy + * for nested scrolling.

    + * + *

    Views invoking nested scrolling functionality should always do so from the relevant + * {@link ViewCompat}, {@link ViewGroupCompat} or {@link ViewParentCompat} compatibility + * shim static methods. This ensures interoperability with nested scrolling views on Android + * 5.0 Lollipop and newer.

    + */ +public class NestedScrollingParentHelper { + private final ViewGroup mViewGroup; + private int mNestedScrollAxes; + + /** + * Construct a new helper for a given ViewGroup + */ + public NestedScrollingParentHelper(ViewGroup viewGroup) { + mViewGroup = viewGroup; + } + + /** + * Called when a nested scrolling operation initiated by a descendant view is accepted + * by this ViewGroup. + * + *

    This is a delegate method. Call it from your {@link android.view.ViewGroup ViewGroup} + * subclass method/{@link NestedScrollingParent} interface method with the same signature + * to implement the standard policy.

    + */ + public void onNestedScrollAccepted(View child, View target, int axes) { + mNestedScrollAxes = axes; + } + + /** + * Return the current axes of nested scrolling for this ViewGroup. + * + *

    This is a delegate method. Call it from your {@link android.view.ViewGroup ViewGroup} + * subclass method/{@link NestedScrollingParent} interface method with the same signature + * to implement the standard policy.

    + */ + public int getNestedScrollAxes() { + return mNestedScrollAxes; + } + + /** + * React to a nested scroll operation ending. + * + *

    This is a delegate method. Call it from your {@link android.view.ViewGroup ViewGroup} + * subclass method/{@link NestedScrollingParent} interface method with the same signature + * to implement the standard policy.

    + * + * @param target View that initiated the nested scroll + */ + public void onStopNestedScroll(View target) { + mNestedScrollAxes = 0; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/OnApplyWindowInsetsListener.java b/mobile/google-android-support-v4/src/android/support/v4/view/OnApplyWindowInsetsListener.java new file mode 100644 index 00000000000..2c2b95f890e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/OnApplyWindowInsetsListener.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.View; + +/** + * Listener for applying window insets on a view in a custom way. + * + *

    Apps may choose to implement this interface if they want to apply custom policy + * to the way that window insets are treated for a view. If an OnApplyWindowInsetsListener + * is set, it's + * {@link #onApplyWindowInsets(android.view.View, WindowInsetsCompat) onApplyWindowInsets} + * method will be called instead of the View's own {@code onApplyWindowInsets} method. + * The listener may optionally call the parameter View's onApplyWindowInsets + * method to apply the View's normal behavior as part of its own.

    + */ +public interface OnApplyWindowInsetsListener { + /** + * When {@link ViewCompat#setOnApplyWindowInsetsListener(View, OnApplyWindowInsetsListener) set} + * on a View, this listener method will be called instead of the view's own + * {@code onApplyWindowInsets} method. + * + * @param v The view applying window insets + * @param insets The insets to apply + * @return The insets supplied, minus any insets that were consumed + */ + public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets); +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/PagerAdapter.java b/mobile/google-android-support-v4/src/android/support/v4/view/PagerAdapter.java new file mode 100644 index 00000000000..ef524047d06 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/PagerAdapter.java @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.database.DataSetObservable; +import android.database.DataSetObserver; +import android.os.Parcelable; +import android.view.View; +import android.view.ViewGroup; + +/** + * Base class providing the adapter to populate pages inside of + * a {@link ViewPager}. You will most likely want to use a more + * specific implementation of this, such as + * {@link android.support.v4.app.FragmentPagerAdapter} or + * {@link android.support.v4.app.FragmentStatePagerAdapter}. + * + *

    When you implement a PagerAdapter, you must override the following methods + * at minimum:

    + *
      + *
    • {@link #instantiateItem(ViewGroup, int)}
    • + *
    • {@link #destroyItem(ViewGroup, int, Object)}
    • + *
    • {@link #getCount()}
    • + *
    • {@link #isViewFromObject(View, Object)}
    • + *
    + * + *

    PagerAdapter is more general than the adapters used for + * {@link android.widget.AdapterView AdapterViews}. Instead of providing a + * View recycling mechanism directly ViewPager uses callbacks to indicate the + * steps taken during an update. A PagerAdapter may implement a form of View + * recycling if desired or use a more sophisticated method of managing page + * Views such as Fragment transactions where each page is represented by its + * own Fragment.

    + * + *

    ViewPager associates each page with a key Object instead of working with + * Views directly. This key is used to track and uniquely identify a given page + * independent of its position in the adapter. A call to the PagerAdapter method + * {@link #startUpdate(ViewGroup)} indicates that the contents of the ViewPager + * are about to change. One or more calls to {@link #instantiateItem(ViewGroup, int)} + * and/or {@link #destroyItem(ViewGroup, int, Object)} will follow, and the end + * of an update will be signaled by a call to {@link #finishUpdate(ViewGroup)}. + * By the time {@link #finishUpdate(ViewGroup) finishUpdate} returns the views + * associated with the key objects returned by + * {@link #instantiateItem(ViewGroup, int) instantiateItem} should be added to + * the parent ViewGroup passed to these methods and the views associated with + * the keys passed to {@link #destroyItem(ViewGroup, int, Object) destroyItem} + * should be removed. The method {@link #isViewFromObject(View, Object)} identifies + * whether a page View is associated with a given key object.

    + * + *

    A very simple PagerAdapter may choose to use the page Views themselves + * as key objects, returning them from {@link #instantiateItem(ViewGroup, int)} + * after creation and adding them to the parent ViewGroup. A matching + * {@link #destroyItem(ViewGroup, int, Object)} implementation would remove the + * View from the parent ViewGroup and {@link #isViewFromObject(View, Object)} + * could be implemented as return view == object;.

    + * + *

    PagerAdapter supports data set changes. Data set changes must occur on the + * main thread and must end with a call to {@link #notifyDataSetChanged()} similar + * to AdapterView adapters derived from {@link android.widget.BaseAdapter}. A data + * set change may involve pages being added, removed, or changing position. The + * ViewPager will keep the current page active provided the adapter implements + * the method {@link #getItemPosition(Object)}.

    + */ +public abstract class PagerAdapter { + private DataSetObservable mObservable = new DataSetObservable(); + + public static final int POSITION_UNCHANGED = -1; + public static final int POSITION_NONE = -2; + + /** + * Return the number of views available. + */ + public abstract int getCount(); + + /** + * Called when a change in the shown pages is going to start being made. + * @param container The containing View which is displaying this adapter's + * page views. + */ + public void startUpdate(ViewGroup container) { + startUpdate((View) container); + } + + /** + * Create the page for the given position. The adapter is responsible + * for adding the view to the container given here, although it only + * must ensure this is done by the time it returns from + * {@link #finishUpdate(ViewGroup)}. + * + * @param container The containing View in which the page will be shown. + * @param position The page position to be instantiated. + * @return Returns an Object representing the new page. This does not + * need to be a View, but can be some other container of the page. + */ + public Object instantiateItem(ViewGroup container, int position) { + return instantiateItem((View) container, position); + } + + /** + * Remove a page for the given position. The adapter is responsible + * for removing the view from its container, although it only must ensure + * this is done by the time it returns from {@link #finishUpdate(ViewGroup)}. + * + * @param container The containing View from which the page will be removed. + * @param position The page position to be removed. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + */ + public void destroyItem(ViewGroup container, int position, Object object) { + destroyItem((View) container, position, object); + } + + /** + * Called to inform the adapter of which item is currently considered to + * be the "primary", that is the one show to the user as the current page. + * + * @param container The containing View from which the page will be removed. + * @param position The page position that is now the primary. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + */ + public void setPrimaryItem(ViewGroup container, int position, Object object) { + setPrimaryItem((View) container, position, object); + } + + /** + * Called when the a change in the shown pages has been completed. At this + * point you must ensure that all of the pages have actually been added or + * removed from the container as appropriate. + * @param container The containing View which is displaying this adapter's + * page views. + */ + public void finishUpdate(ViewGroup container) { + finishUpdate((View) container); + } + + /** + * Called when a change in the shown pages is going to start being made. + * @param container The containing View which is displaying this adapter's + * page views. + * + * @deprecated Use {@link #startUpdate(ViewGroup)} + */ + public void startUpdate(View container) { + } + + /** + * Create the page for the given position. The adapter is responsible + * for adding the view to the container given here, although it only + * must ensure this is done by the time it returns from + * {@link #finishUpdate(ViewGroup)}. + * + * @param container The containing View in which the page will be shown. + * @param position The page position to be instantiated. + * @return Returns an Object representing the new page. This does not + * need to be a View, but can be some other container of the page. + * + * @deprecated Use {@link #instantiateItem(ViewGroup, int)} + */ + public Object instantiateItem(View container, int position) { + throw new UnsupportedOperationException( + "Required method instantiateItem was not overridden"); + } + + /** + * Remove a page for the given position. The adapter is responsible + * for removing the view from its container, although it only must ensure + * this is done by the time it returns from {@link #finishUpdate(View)}. + * + * @param container The containing View from which the page will be removed. + * @param position The page position to be removed. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + * + * @deprecated Use {@link #destroyItem(ViewGroup, int, Object)} + */ + public void destroyItem(View container, int position, Object object) { + throw new UnsupportedOperationException("Required method destroyItem was not overridden"); + } + + /** + * Called to inform the adapter of which item is currently considered to + * be the "primary", that is the one show to the user as the current page. + * + * @param container The containing View from which the page will be removed. + * @param position The page position that is now the primary. + * @param object The same object that was returned by + * {@link #instantiateItem(View, int)}. + * + * @deprecated Use {@link #setPrimaryItem(ViewGroup, int, Object)} + */ + public void setPrimaryItem(View container, int position, Object object) { + } + + /** + * Called when the a change in the shown pages has been completed. At this + * point you must ensure that all of the pages have actually been added or + * removed from the container as appropriate. + * @param container The containing View which is displaying this adapter's + * page views. + * + * @deprecated Use {@link #finishUpdate(ViewGroup)} + */ + public void finishUpdate(View container) { + } + + /** + * Determines whether a page View is associated with a specific key object + * as returned by {@link #instantiateItem(ViewGroup, int)}. This method is + * required for a PagerAdapter to function properly. + * + * @param view Page View to check for association with object + * @param object Object to check for association with view + * @return true if view is associated with the key object object + */ + public abstract boolean isViewFromObject(View view, Object object); + + /** + * Save any instance state associated with this adapter and its pages that should be + * restored if the current UI state needs to be reconstructed. + * + * @return Saved state for this adapter + */ + public Parcelable saveState() { + return null; + } + + /** + * Restore any instance state associated with this adapter and its pages + * that was previously saved by {@link #saveState()}. + * + * @param state State previously saved by a call to {@link #saveState()} + * @param loader A ClassLoader that should be used to instantiate any restored objects + */ + public void restoreState(Parcelable state, ClassLoader loader) { + } + + /** + * Called when the host view is attempting to determine if an item's position + * has changed. Returns {@link #POSITION_UNCHANGED} if the position of the given + * item has not changed or {@link #POSITION_NONE} if the item is no longer present + * in the adapter. + * + *

    The default implementation assumes that items will never + * change position and always returns {@link #POSITION_UNCHANGED}. + * + * @param object Object representing an item, previously returned by a call to + * {@link #instantiateItem(View, int)}. + * @return object's new position index from [0, {@link #getCount()}), + * {@link #POSITION_UNCHANGED} if the object's position has not changed, + * or {@link #POSITION_NONE} if the item is no longer present. + */ + public int getItemPosition(Object object) { + return POSITION_UNCHANGED; + } + + /** + * This method should be called by the application if the data backing this adapter has changed + * and associated views should update. + */ + public void notifyDataSetChanged() { + mObservable.notifyChanged(); + } + + /** + * Register an observer to receive callbacks related to the adapter's data changing. + * + * @param observer The {@link android.database.DataSetObserver} which will receive callbacks. + */ + public void registerDataSetObserver(DataSetObserver observer) { + mObservable.registerObserver(observer); + } + + /** + * Unregister an observer from callbacks related to the adapter's data changing. + * + * @param observer The {@link android.database.DataSetObserver} which will be unregistered. + */ + public void unregisterDataSetObserver(DataSetObserver observer) { + mObservable.unregisterObserver(observer); + } + + /** + * This method may be called by the ViewPager to obtain a title string + * to describe the specified page. This method may return null + * indicating no title for this page. The default implementation returns + * null. + * + * @param position The position of the title requested + * @return A title for the requested page + */ + public CharSequence getPageTitle(int position) { + return null; + } + + /** + * Returns the proportional width of a given page as a percentage of the + * ViewPager's measured width from (0.f-1.f] + * + * @param position The position of the page requested + * @return Proportional width for the given page position + */ + public float getPageWidth(int position) { + return 1.f; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/PagerTabStrip.java b/mobile/google-android-support-v4/src/android/support/v4/view/PagerTabStrip.java new file mode 100644 index 00000000000..834035c5d96 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/PagerTabStrip.java @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorRes; +import android.support.annotation.DrawableRes; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; + +/** + * PagerTabStrip is an interactive indicator of the current, next, + * and previous pages of a {@link ViewPager}. It is intended to be used as a + * child view of a ViewPager widget in your XML layout. + * Add it as a child of a ViewPager in your layout file and set its + * android:layout_gravity to TOP or BOTTOM to pin it to the top or bottom + * of the ViewPager. The title from each page is supplied by the method + * {@link PagerAdapter#getPageTitle(int)} in the adapter supplied to + * the ViewPager. + * + *

    For a non-interactive indicator, see {@link PagerTitleStrip}.

    + */ +public class PagerTabStrip extends PagerTitleStrip { + private static final String TAG = "PagerTabStrip"; + + private static final int INDICATOR_HEIGHT = 3; // dp + private static final int MIN_PADDING_BOTTOM = INDICATOR_HEIGHT + 3; // dp + private static final int TAB_PADDING = 16; // dp + private static final int TAB_SPACING = 32; // dp + private static final int MIN_TEXT_SPACING = TAB_SPACING + TAB_PADDING * 2; // dp + private static final int FULL_UNDERLINE_HEIGHT = 1; // dp + private static final int MIN_STRIP_HEIGHT = 32; // dp + + private int mIndicatorColor; + private int mIndicatorHeight; + + private int mMinPaddingBottom; + private int mMinTextSpacing; + private int mMinStripHeight; + + private int mTabPadding; + + private final Paint mTabPaint = new Paint(); + private final Rect mTempRect = new Rect(); + + private int mTabAlpha = 0xFF; + + private boolean mDrawFullUnderline = false; + private boolean mDrawFullUnderlineSet = false; + private int mFullUnderlineHeight; + + private boolean mIgnoreTap; + private float mInitialMotionX; + private float mInitialMotionY; + private int mTouchSlop; + + public PagerTabStrip(Context context) { + this(context, null); + } + + public PagerTabStrip(Context context, AttributeSet attrs) { + super(context, attrs); + + mIndicatorColor = mTextColor; + mTabPaint.setColor(mIndicatorColor); + + // Note: this follows the rules for Resources#getDimensionPixelOffset/Size: + // sizes round up, offsets round down. + final float density = context.getResources().getDisplayMetrics().density; + mIndicatorHeight = (int) (INDICATOR_HEIGHT * density + 0.5f); + mMinPaddingBottom = (int) (MIN_PADDING_BOTTOM * density + 0.5f); + mMinTextSpacing = (int) (MIN_TEXT_SPACING * density); + mTabPadding = (int) (TAB_PADDING * density + 0.5f); + mFullUnderlineHeight = (int) (FULL_UNDERLINE_HEIGHT * density + 0.5f); + mMinStripHeight = (int) (MIN_STRIP_HEIGHT * density + 0.5f); + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + // Enforce restrictions + setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom()); + setTextSpacing(getTextSpacing()); + + setWillNotDraw(false); + + mPrevText.setFocusable(true); + mPrevText.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mPager.setCurrentItem(mPager.getCurrentItem() - 1); + } + }); + + mNextText.setFocusable(true); + mNextText.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + mPager.setCurrentItem(mPager.getCurrentItem() + 1); + } + }); + + if (getBackground() == null) { + mDrawFullUnderline = true; + } + } + + /** + * Set the color of the tab indicator bar. + * + * @param color Color to set as an 0xRRGGBB value. The high byte (alpha) is ignored. + */ + public void setTabIndicatorColor(int color) { + mIndicatorColor = color; + mTabPaint.setColor(mIndicatorColor); + invalidate(); + } + + /** + * Set the color of the tab indicator bar from a color resource. + * + * @param resId Resource ID of a color resource to load + */ + public void setTabIndicatorColorResource(@ColorRes int resId) { + setTabIndicatorColor(getContext().getResources().getColor(resId)); + } + + /** + * @return The current tab indicator color as an 0xRRGGBB value. + */ + public int getTabIndicatorColor() { + return mIndicatorColor; + } + + @Override + public void setPadding(int left, int top, int right, int bottom) { + if (bottom < mMinPaddingBottom) { + bottom = mMinPaddingBottom; + } + super.setPadding(left, top, right, bottom); + } + + @Override + public void setTextSpacing(int textSpacing) { + if (textSpacing < mMinTextSpacing) { + textSpacing = mMinTextSpacing; + } + super.setTextSpacing(textSpacing); + } + + @Override + public void setBackgroundDrawable(Drawable d) { + super.setBackgroundDrawable(d); + if (!mDrawFullUnderlineSet) { + mDrawFullUnderline = d == null; + } + } + + @Override + public void setBackgroundColor(int color) { + super.setBackgroundColor(color); + if (!mDrawFullUnderlineSet) { + mDrawFullUnderline = (color & 0xFF000000) == 0; + } + } + + @Override + public void setBackgroundResource(@DrawableRes int resId) { + super.setBackgroundResource(resId); + if (!mDrawFullUnderlineSet) { + mDrawFullUnderline = resId == 0; + } + } + + /** + * Set whether this tab strip should draw a full-width underline in the + * current tab indicator color. + * + * @param drawFull true to draw a full-width underline, false otherwise + */ + public void setDrawFullUnderline(boolean drawFull) { + mDrawFullUnderline = drawFull; + mDrawFullUnderlineSet = true; + invalidate(); + } + + /** + * Return whether or not this tab strip will draw a full-width underline. + * This defaults to true if no background is set. + * + * @return true if this tab strip will draw a full-width underline in the + * current tab indicator color. + */ + public boolean getDrawFullUnderline() { + return mDrawFullUnderline; + } + + @Override + int getMinHeight() { + return Math.max(super.getMinHeight(), mMinStripHeight); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + if (action != MotionEvent.ACTION_DOWN && mIgnoreTap) { + return false; + } + + // Any tap within touch slop to either side of the current item + // will scroll to prev/next. + final float x = ev.getX(); + final float y = ev.getY(); + switch (action) { + case MotionEvent.ACTION_DOWN: + mInitialMotionX = x; + mInitialMotionY = y; + mIgnoreTap = false; + break; + + case MotionEvent.ACTION_MOVE: + if (Math.abs(x - mInitialMotionX) > mTouchSlop || + Math.abs(y - mInitialMotionY) > mTouchSlop) { + mIgnoreTap = true; + } + break; + + case MotionEvent.ACTION_UP: + if (x < mCurrText.getLeft() - mTabPadding) { + mPager.setCurrentItem(mPager.getCurrentItem() - 1); + } else if (x > mCurrText.getRight() + mTabPadding) { + mPager.setCurrentItem(mPager.getCurrentItem() + 1); + } + break; + } + + return true; + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + final int height = getHeight(); + final int bottom = height; + final int left = mCurrText.getLeft() - mTabPadding; + final int right = mCurrText.getRight() + mTabPadding; + final int top = bottom - mIndicatorHeight; + + mTabPaint.setColor(mTabAlpha << 24 | (mIndicatorColor & 0xFFFFFF)); + canvas.drawRect(left, top, right, bottom, mTabPaint); + + if (mDrawFullUnderline) { + mTabPaint.setColor(0xFF << 24 | (mIndicatorColor & 0xFFFFFF)); + canvas.drawRect(getPaddingLeft(), height - mFullUnderlineHeight, + getWidth() - getPaddingRight(), height, mTabPaint); + } + } + + @Override + void updateTextPositions(int position, float positionOffset, boolean force) { + final Rect r = mTempRect; + int bottom = getHeight(); + int left = mCurrText.getLeft() - mTabPadding; + int right = mCurrText.getRight() + mTabPadding; + int top = bottom - mIndicatorHeight; + + r.set(left, top, right, bottom); + + super.updateTextPositions(position, positionOffset, force); + mTabAlpha = (int) (Math.abs(positionOffset - 0.5f) * 2 * 0xFF); + + left = mCurrText.getLeft() - mTabPadding; + right = mCurrText.getRight() + mTabPadding; + r.union(left, top, right, bottom); + + invalidate(r); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStrip.java b/mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStrip.java new file mode 100644 index 00000000000..79c771f43f5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStrip.java @@ -0,0 +1,512 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.drawable.Drawable; +import android.text.TextUtils.TruncateAt; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.TextView; + +import java.lang.ref.WeakReference; + +/** + * PagerTitleStrip is a non-interactive indicator of the current, next, + * and previous pages of a {@link ViewPager}. It is intended to be used as a + * child view of a ViewPager widget in your XML layout. + * Add it as a child of a ViewPager in your layout file and set its + * android:layout_gravity to TOP or BOTTOM to pin it to the top or bottom + * of the ViewPager. The title from each page is supplied by the method + * {@link PagerAdapter#getPageTitle(int)} in the adapter supplied to + * the ViewPager. + * + *

    For an interactive indicator, see {@link PagerTabStrip}.

    + */ +public class PagerTitleStrip extends ViewGroup implements ViewPager.Decor { + private static final String TAG = "PagerTitleStrip"; + + ViewPager mPager; + TextView mPrevText; + TextView mCurrText; + TextView mNextText; + + private int mLastKnownCurrentPage = -1; + private float mLastKnownPositionOffset = -1; + private int mScaledTextSpacing; + private int mGravity; + + private boolean mUpdatingText; + private boolean mUpdatingPositions; + + private final PageListener mPageListener = new PageListener(); + + private WeakReference mWatchingAdapter; + + private static final int[] ATTRS = new int[] { + android.R.attr.textAppearance, + android.R.attr.textSize, + android.R.attr.textColor, + android.R.attr.gravity + }; + + private static final int[] TEXT_ATTRS = new int[] { + 0x0101038c // android.R.attr.textAllCaps + }; + + private static final float SIDE_ALPHA = 0.6f; + private static final int TEXT_SPACING = 16; // dip + + private int mNonPrimaryAlpha; + int mTextColor; + + interface PagerTitleStripImpl { + void setSingleLineAllCaps(TextView text); + } + + static class PagerTitleStripImplBase implements PagerTitleStripImpl { + public void setSingleLineAllCaps(TextView text) { + text.setSingleLine(); + } + } + + static class PagerTitleStripImplIcs implements PagerTitleStripImpl { + public void setSingleLineAllCaps(TextView text) { + PagerTitleStripIcs.setSingleLineAllCaps(text); + } + } + + private static final PagerTitleStripImpl IMPL; + static { + if (android.os.Build.VERSION.SDK_INT >= 14) { + IMPL = new PagerTitleStripImplIcs(); + } else { + IMPL = new PagerTitleStripImplBase(); + } + } + + private static void setSingleLineAllCaps(TextView text) { + IMPL.setSingleLineAllCaps(text); + } + + public PagerTitleStrip(Context context) { + this(context, null); + } + + public PagerTitleStrip(Context context, AttributeSet attrs) { + super(context, attrs); + + addView(mPrevText = new TextView(context)); + addView(mCurrText = new TextView(context)); + addView(mNextText = new TextView(context)); + + final TypedArray a = context.obtainStyledAttributes(attrs, ATTRS); + final int textAppearance = a.getResourceId(0, 0); + if (textAppearance != 0) { + mPrevText.setTextAppearance(context, textAppearance); + mCurrText.setTextAppearance(context, textAppearance); + mNextText.setTextAppearance(context, textAppearance); + } + final int textSize = a.getDimensionPixelSize(1, 0); + if (textSize != 0) { + setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize); + } + if (a.hasValue(2)) { + final int textColor = a.getColor(2, 0); + mPrevText.setTextColor(textColor); + mCurrText.setTextColor(textColor); + mNextText.setTextColor(textColor); + } + mGravity = a.getInteger(3, Gravity.BOTTOM); + a.recycle(); + + mTextColor = mCurrText.getTextColors().getDefaultColor(); + setNonPrimaryAlpha(SIDE_ALPHA); + + mPrevText.setEllipsize(TruncateAt.END); + mCurrText.setEllipsize(TruncateAt.END); + mNextText.setEllipsize(TruncateAt.END); + + boolean allCaps = false; + if (textAppearance != 0) { + final TypedArray ta = context.obtainStyledAttributes(textAppearance, TEXT_ATTRS); + allCaps = ta.getBoolean(0, false); + ta.recycle(); + } + + if (allCaps) { + setSingleLineAllCaps(mPrevText); + setSingleLineAllCaps(mCurrText); + setSingleLineAllCaps(mNextText); + } else { + mPrevText.setSingleLine(); + mCurrText.setSingleLine(); + mNextText.setSingleLine(); + } + + final float density = context.getResources().getDisplayMetrics().density; + mScaledTextSpacing = (int) (TEXT_SPACING * density); + } + + /** + * Set the required spacing between title segments. + * + * @param spacingPixels Spacing between each title displayed in pixels + */ + public void setTextSpacing(int spacingPixels) { + mScaledTextSpacing = spacingPixels; + requestLayout(); + } + + /** + * @return The required spacing between title segments in pixels + */ + public int getTextSpacing() { + return mScaledTextSpacing; + } + + /** + * Set the alpha value used for non-primary page titles. + * + * @param alpha Opacity value in the range 0-1f + */ + public void setNonPrimaryAlpha(float alpha) { + mNonPrimaryAlpha = (int) (alpha * 255) & 0xFF; + final int transparentColor = (mNonPrimaryAlpha << 24) | (mTextColor & 0xFFFFFF); + mPrevText.setTextColor(transparentColor); + mNextText.setTextColor(transparentColor); + } + + /** + * Set the color value used as the base color for all displayed page titles. + * Alpha will be ignored for non-primary page titles. See {@link #setNonPrimaryAlpha(float)}. + * + * @param color Color hex code in 0xAARRGGBB format + */ + public void setTextColor(int color) { + mTextColor = color; + mCurrText.setTextColor(color); + final int transparentColor = (mNonPrimaryAlpha << 24) | (mTextColor & 0xFFFFFF); + mPrevText.setTextColor(transparentColor); + mNextText.setTextColor(transparentColor); + } + + /** + * Set the default text size to a given unit and value. + * See {@link TypedValue} for the possible dimension units. + * + *

    Example: to set the text size to 14px, use + * setTextSize(TypedValue.COMPLEX_UNIT_PX, 14);

    + * + * @param unit The desired dimension unit + * @param size The desired size in the given units + */ + public void setTextSize(int unit, float size) { + mPrevText.setTextSize(unit, size); + mCurrText.setTextSize(unit, size); + mNextText.setTextSize(unit, size); + } + + /** + * Set the {@link Gravity} used to position text within the title strip. + * Only the vertical gravity component is used. + * + * @param gravity {@link Gravity} constant for positioning title text + */ + public void setGravity(int gravity) { + mGravity = gravity; + requestLayout(); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + + final ViewParent parent = getParent(); + if (!(parent instanceof ViewPager)) { + throw new IllegalStateException( + "PagerTitleStrip must be a direct child of a ViewPager."); + } + + final ViewPager pager = (ViewPager) parent; + final PagerAdapter adapter = pager.getAdapter(); + + pager.setInternalPageChangeListener(mPageListener); + pager.setOnAdapterChangeListener(mPageListener); + mPager = pager; + updateAdapter(mWatchingAdapter != null ? mWatchingAdapter.get() : null, adapter); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (mPager != null) { + updateAdapter(mPager.getAdapter(), null); + mPager.setInternalPageChangeListener(null); + mPager.setOnAdapterChangeListener(null); + mPager = null; + } + } + + void updateText(int currentItem, PagerAdapter adapter) { + final int itemCount = adapter != null ? adapter.getCount() : 0; + mUpdatingText = true; + + CharSequence text = null; + if (currentItem >= 1 && adapter != null) { + text = adapter.getPageTitle(currentItem - 1); + } + mPrevText.setText(text); + + mCurrText.setText(adapter != null && currentItem < itemCount ? + adapter.getPageTitle(currentItem) : null); + + text = null; + if (currentItem + 1 < itemCount && adapter != null) { + text = adapter.getPageTitle(currentItem + 1); + } + mNextText.setText(text); + + // Measure everything + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + final int childHeight = getHeight() - getPaddingTop() - getPaddingBottom(); + final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (width * 0.8f), + MeasureSpec.AT_MOST); + final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST); + mPrevText.measure(childWidthSpec, childHeightSpec); + mCurrText.measure(childWidthSpec, childHeightSpec); + mNextText.measure(childWidthSpec, childHeightSpec); + + mLastKnownCurrentPage = currentItem; + + if (!mUpdatingPositions) { + updateTextPositions(currentItem, mLastKnownPositionOffset, false); + } + + mUpdatingText = false; + } + + @Override + public void requestLayout() { + if (!mUpdatingText) { + super.requestLayout(); + } + } + + void updateAdapter(PagerAdapter oldAdapter, PagerAdapter newAdapter) { + if (oldAdapter != null) { + oldAdapter.unregisterDataSetObserver(mPageListener); + mWatchingAdapter = null; + } + if (newAdapter != null) { + newAdapter.registerDataSetObserver(mPageListener); + mWatchingAdapter = new WeakReference(newAdapter); + } + if (mPager != null) { + mLastKnownCurrentPage = -1; + mLastKnownPositionOffset = -1; + updateText(mPager.getCurrentItem(), newAdapter); + requestLayout(); + } + } + + void updateTextPositions(int position, float positionOffset, boolean force) { + if (position != mLastKnownCurrentPage) { + updateText(position, mPager.getAdapter()); + } else if (!force && positionOffset == mLastKnownPositionOffset) { + return; + } + + mUpdatingPositions = true; + + final int prevWidth = mPrevText.getMeasuredWidth(); + final int currWidth = mCurrText.getMeasuredWidth(); + final int nextWidth = mNextText.getMeasuredWidth(); + final int halfCurrWidth = currWidth / 2; + + final int stripWidth = getWidth(); + final int stripHeight = getHeight(); + final int paddingLeft = getPaddingLeft(); + final int paddingRight = getPaddingRight(); + final int paddingTop = getPaddingTop(); + final int paddingBottom = getPaddingBottom(); + final int textPaddedLeft = paddingLeft + halfCurrWidth; + final int textPaddedRight = paddingRight + halfCurrWidth; + final int contentWidth = stripWidth - textPaddedLeft - textPaddedRight; + + float currOffset = positionOffset + 0.5f; + if (currOffset > 1.f) { + currOffset -= 1.f; + } + final int currCenter = stripWidth - textPaddedRight - (int) (contentWidth * currOffset); + final int currLeft = currCenter - currWidth / 2; + final int currRight = currLeft + currWidth; + + final int prevBaseline = mPrevText.getBaseline(); + final int currBaseline = mCurrText.getBaseline(); + final int nextBaseline = mNextText.getBaseline(); + final int maxBaseline = Math.max(Math.max(prevBaseline, currBaseline), nextBaseline); + final int prevTopOffset = maxBaseline - prevBaseline; + final int currTopOffset = maxBaseline - currBaseline; + final int nextTopOffset = maxBaseline - nextBaseline; + final int alignedPrevHeight = prevTopOffset + mPrevText.getMeasuredHeight(); + final int alignedCurrHeight = currTopOffset + mCurrText.getMeasuredHeight(); + final int alignedNextHeight = nextTopOffset + mNextText.getMeasuredHeight(); + final int maxTextHeight = Math.max(Math.max(alignedPrevHeight, alignedCurrHeight), + alignedNextHeight); + + final int vgrav = mGravity & Gravity.VERTICAL_GRAVITY_MASK; + + int prevTop; + int currTop; + int nextTop; + switch (vgrav) { + default: + case Gravity.TOP: + prevTop = paddingTop + prevTopOffset; + currTop = paddingTop + currTopOffset; + nextTop = paddingTop + nextTopOffset; + break; + case Gravity.CENTER_VERTICAL: + final int paddedHeight = stripHeight - paddingTop - paddingBottom; + final int centeredTop = (paddedHeight - maxTextHeight) / 2; + prevTop = centeredTop + prevTopOffset; + currTop = centeredTop + currTopOffset; + nextTop = centeredTop + nextTopOffset; + break; + case Gravity.BOTTOM: + final int bottomGravTop = stripHeight - paddingBottom - maxTextHeight; + prevTop = bottomGravTop + prevTopOffset; + currTop = bottomGravTop + currTopOffset; + nextTop = bottomGravTop + nextTopOffset; + break; + } + + mCurrText.layout(currLeft, currTop, currRight, + currTop + mCurrText.getMeasuredHeight()); + + final int prevLeft = Math.min(paddingLeft, currLeft - mScaledTextSpacing - prevWidth); + mPrevText.layout(prevLeft, prevTop, prevLeft + prevWidth, + prevTop + mPrevText.getMeasuredHeight()); + + final int nextLeft = Math.max(stripWidth - paddingRight - nextWidth, + currRight + mScaledTextSpacing); + mNextText.layout(nextLeft, nextTop, nextLeft + nextWidth, + nextTop + mNextText.getMeasuredHeight()); + + mLastKnownPositionOffset = positionOffset; + mUpdatingPositions = false; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + final int widthSize = MeasureSpec.getSize(widthMeasureSpec); + final int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY) { + throw new IllegalStateException("Must measure with an exact width"); + } + + int childHeight = heightSize; + int minHeight = getMinHeight(); + int padding = 0; + padding = getPaddingTop() + getPaddingBottom(); + childHeight -= padding; + + final int childWidthSpec = MeasureSpec.makeMeasureSpec((int) (widthSize * 0.8f), + MeasureSpec.AT_MOST); + final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST); + + mPrevText.measure(childWidthSpec, childHeightSpec); + mCurrText.measure(childWidthSpec, childHeightSpec); + mNextText.measure(childWidthSpec, childHeightSpec); + + if (heightMode == MeasureSpec.EXACTLY) { + setMeasuredDimension(widthSize, heightSize); + } else { + int textHeight = mCurrText.getMeasuredHeight(); + setMeasuredDimension(widthSize, Math.max(minHeight, textHeight + padding)); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + if (mPager != null) { + final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; + updateTextPositions(mLastKnownCurrentPage, offset, true); + } + } + + int getMinHeight() { + int minHeight = 0; + final Drawable bg = getBackground(); + if (bg != null) { + minHeight = bg.getIntrinsicHeight(); + } + return minHeight; + } + + private class PageListener extends DataSetObserver implements ViewPager.OnPageChangeListener, + ViewPager.OnAdapterChangeListener { + private int mScrollState; + + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + if (positionOffset > 0.5f) { + // Consider ourselves to be on the next page when we're 50% of the way there. + position++; + } + updateTextPositions(position, positionOffset, false); + } + + @Override + public void onPageSelected(int position) { + if (mScrollState == ViewPager.SCROLL_STATE_IDLE) { + // Only update the text here if we're not dragging or settling. + updateText(mPager.getCurrentItem(), mPager.getAdapter()); + + final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; + updateTextPositions(mPager.getCurrentItem(), offset, true); + } + } + + @Override + public void onPageScrollStateChanged(int state) { + mScrollState = state; + } + + @Override + public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter) { + updateAdapter(oldAdapter, newAdapter); + } + + @Override + public void onChanged() { + updateText(mPager.getCurrentItem(), mPager.getAdapter()); + + final float offset = mLastKnownPositionOffset >= 0 ? mLastKnownPositionOffset : 0; + updateTextPositions(mPager.getCurrentItem(), offset, true); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStripIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStripIcs.java new file mode 100644 index 00000000000..99cc8677265 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/PagerTitleStripIcs.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.text.method.SingleLineTransformationMethod; +import android.view.View; +import android.widget.TextView; + +import java.util.Locale; + +class PagerTitleStripIcs { + public static void setSingleLineAllCaps(TextView text) { + text.setTransformationMethod(new SingleLineAllCapsTransform(text.getContext())); + } + + private static class SingleLineAllCapsTransform extends SingleLineTransformationMethod { + private static final String TAG = "SingleLineAllCapsTransform"; + + private Locale mLocale; + + public SingleLineAllCapsTransform(Context context) { + mLocale = context.getResources().getConfiguration().locale; + } + + @Override + public CharSequence getTransformation(CharSequence source, View view) { + source = super.getTransformation(source, view); + return source != null ? source.toString().toUpperCase(mLocale) : null; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompat.java new file mode 100644 index 00000000000..ba8d0ec7c29 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompat.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +/** + * Helper for accessing features in ScaleGestureDetector introduced + * after API level 19 (KitKat) in a backwards compatible fashion. + */ +public class ScaleGestureDetectorCompat { + static final ScaleGestureDetectorImpl IMPL; + + interface ScaleGestureDetectorImpl { + + public void setQuickScaleEnabled(Object o, boolean enabled); + + public boolean isQuickScaleEnabled(Object o); + } + + private static class BaseScaleGestureDetectorImpl implements ScaleGestureDetectorImpl { + @Override + public void setQuickScaleEnabled(Object o, boolean enabled) { + // Intentionally blank + } + + @Override + public boolean isQuickScaleEnabled(Object o) { + return false; + } + } + + private static class ScaleGestureDetectorCompatKitKatImpl implements ScaleGestureDetectorImpl { + @Override + public void setQuickScaleEnabled(Object o, boolean enabled) { + ScaleGestureDetectorCompatKitKat.setQuickScaleEnabled(o, enabled); + } + + @Override + public boolean isQuickScaleEnabled(Object o) { + return ScaleGestureDetectorCompatKitKat.isQuickScaleEnabled(o); + } + } + + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 19) { // KitKat + IMPL = new ScaleGestureDetectorCompatKitKatImpl(); + } else { + IMPL = new BaseScaleGestureDetectorImpl(); + } + } + + private ScaleGestureDetectorCompat() {} + + /** + * Set whether the associated OnScaleGestureListener should receive onScale + * callbacks when the user performs a doubleTap followed by a swipe. Note that this is enabled + * by default if the app targets API 19 and newer. + * @param enabled true to enable quick scaling, false to disable + */ + public static void setQuickScaleEnabled(Object scaleGestureDetector, boolean enabled) { + IMPL.setQuickScaleEnabled(scaleGestureDetector, enabled); + } + + /** + * Return whether the quick scale gesture, in which the user performs a double tap followed by a + * swipe, should perform scaling. See setQuickScaleEnabled(Object, boolean). + */ + public static boolean isQuickScaleEnabled(Object scaleGestureDetector) { + return IMPL.isQuickScaleEnabled(scaleGestureDetector); // KitKat + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompatKitKat.java new file mode 100644 index 00000000000..d3f8eb0452f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ScaleGestureDetectorCompatKitKat.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.ScaleGestureDetector; +import android.view.MotionEvent; +import android.content.Context; + +/** + * Implementation of ScaleGestureDetector compatibility that can call KitKat APIs. This class is an + * implementation detail for ScaleGestureDetectorCompat and should not be used directly. + */ +class ScaleGestureDetectorCompatKitKat { + + private ScaleGestureDetectorCompatKitKat() { + } + + public static void setQuickScaleEnabled(Object scaleGestureDetector, boolean enabled) { + ((ScaleGestureDetector) scaleGestureDetector).setQuickScaleEnabled(enabled); + } + + public static boolean isQuickScaleEnabled(Object scaleGestureDetector) { + return ((ScaleGestureDetector) scaleGestureDetector).isQuickScaleEnabled(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ScrollingView.java b/mobile/google-android-support-v4/src/android/support/v4/view/ScrollingView.java new file mode 100644 index 00000000000..4fe0507917e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ScrollingView.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +/** + * An interface that can be implemented by Views to provide scroll related APIs. + */ +public interface ScrollingView { + /** + *

    Compute the horizontal range that the horizontal scrollbar + * represents.

    + * + *

    The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeHorizontalScrollExtent()} and + * {@link #computeHorizontalScrollOffset()}.

    + * + *

    The default range is the drawing width of this view.

    + * + * @return the total horizontal range represented by the horizontal + * scrollbar + * + * @see #computeHorizontalScrollExtent() + * @see #computeHorizontalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeHorizontalScrollRange(); + + /** + *

    Compute the horizontal offset of the horizontal scrollbar's thumb + * within the horizontal range. This value is used to compute the position + * of the thumb within the scrollbar's track.

    + * + *

    The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeHorizontalScrollRange()} and + * {@link #computeHorizontalScrollExtent()}.

    + * + *

    The default offset is the scroll offset of this view.

    + * + * @return the horizontal offset of the scrollbar's thumb + * + * @see #computeHorizontalScrollRange() + * @see #computeHorizontalScrollExtent() + * @see android.widget.ScrollBarDrawable + */ + int computeHorizontalScrollOffset(); + + /** + *

    Compute the horizontal extent of the horizontal scrollbar's thumb + * within the horizontal range. This value is used to compute the length + * of the thumb within the scrollbar's track.

    + * + *

    The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeHorizontalScrollRange()} and + * {@link #computeHorizontalScrollOffset()}.

    + * + *

    The default extent is the drawing width of this view.

    + * + * @return the horizontal extent of the scrollbar's thumb + * + * @see #computeHorizontalScrollRange() + * @see #computeHorizontalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeHorizontalScrollExtent(); + + /** + *

    Compute the vertical range that the vertical scrollbar represents.

    + * + *

    The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeVerticalScrollExtent()} and + * {@link #computeVerticalScrollOffset()}.

    + * + * @return the total vertical range represented by the vertical scrollbar + * + *

    The default range is the drawing height of this view.

    + * + * @see #computeVerticalScrollExtent() + * @see #computeVerticalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeVerticalScrollRange(); + + /** + *

    Compute the vertical offset of the vertical scrollbar's thumb + * within the horizontal range. This value is used to compute the position + * of the thumb within the scrollbar's track.

    + * + *

    The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeVerticalScrollRange()} and + * {@link #computeVerticalScrollExtent()}.

    + * + *

    The default offset is the scroll offset of this view.

    + * + * @return the vertical offset of the scrollbar's thumb + * + * @see #computeVerticalScrollRange() + * @see #computeVerticalScrollExtent() + * @see android.widget.ScrollBarDrawable + */ + int computeVerticalScrollOffset(); + + /** + *

    Compute the vertical extent of the vertical scrollbar's thumb + * within the vertical range. This value is used to compute the length + * of the thumb within the scrollbar's track.

    + * + *

    The range is expressed in arbitrary units that must be the same as the + * units used by {@link #computeVerticalScrollRange()} and + * {@link #computeVerticalScrollOffset()}.

    + * + *

    The default extent is the drawing height of this view.

    + * + * @return the vertical extent of the scrollbar's thumb + * + * @see #computeVerticalScrollRange() + * @see #computeVerticalScrollOffset() + * @see android.widget.ScrollBarDrawable + */ + int computeVerticalScrollExtent(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/TintableBackgroundView.java b/mobile/google-android-support-v4/src/android/support/v4/view/TintableBackgroundView.java new file mode 100644 index 00000000000..83c014b756b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/TintableBackgroundView.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.support.annotation.Nullable; + +/** + * Interface which allows a {@link android.view.View} to receive background tinting calls from + * {@code ViewCompat} when running on API v20 devices or lower. + */ +public interface TintableBackgroundView { + + /** + * Applies a tint to the background drawable. Does not modify the current tint + * mode, which is {@link PorterDuff.Mode#SRC_IN} by default. + *

    + * Subsequent calls to {@code View.setBackground(Drawable)} will automatically + * mutate the drawable and apply the specified tint and tint mode. + * + * @param tint the tint to apply, may be {@code null} to clear tint + * + * @see #getSupportBackgroundTintList() + */ + void setSupportBackgroundTintList(@Nullable ColorStateList tint); + + /** + * Return the tint applied to the background drawable, if specified. + * + * @return the tint applied to the background drawable + */ + @Nullable + ColorStateList getSupportBackgroundTintList(); + + /** + * Specifies the blending mode used to apply the tint specified by + * {@link #setSupportBackgroundTintList(ColorStateList)}} to the background + * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. + * + * @param tintMode the blending mode used to apply the tint, may be + * {@code null} to clear tint + * @see #getSupportBackgroundTintMode() + */ + void setSupportBackgroundTintMode(@Nullable PorterDuff.Mode tintMode); + + /** + * Return the blending mode used to apply the tint to the background + * drawable, if specified. + * + * @return the blending mode used to apply the tint to the background + * drawable + */ + @Nullable + PorterDuff.Mode getSupportBackgroundTintMode(); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompat.java new file mode 100644 index 00000000000..99286d076a0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompat.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.VelocityTracker; + +/** + * Helper for accessing features in {@link VelocityTracker} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class VelocityTrackerCompat { + /** + * Interface for the full API. + */ + interface VelocityTrackerVersionImpl { + public float getXVelocity(VelocityTracker tracker, int pointerId); + public float getYVelocity(VelocityTracker tracker, int pointerId); + } + + /** + * Interface implementation that doesn't use anything about v4 APIs. + */ + static class BaseVelocityTrackerVersionImpl implements VelocityTrackerVersionImpl { + @Override + public float getXVelocity(VelocityTracker tracker, int pointerId) { + return tracker.getXVelocity(); + } + @Override + public float getYVelocity(VelocityTracker tracker, int pointerId) { + return tracker.getYVelocity(); + } + } + + /** + * Interface implementation for devices with at least v11 APIs. + */ + static class HoneycombVelocityTrackerVersionImpl implements VelocityTrackerVersionImpl { + @Override + public float getXVelocity(VelocityTracker tracker, int pointerId) { + return VelocityTrackerCompatHoneycomb.getXVelocity(tracker, pointerId); + } + @Override + public float getYVelocity(VelocityTracker tracker, int pointerId) { + return VelocityTrackerCompatHoneycomb.getYVelocity(tracker, pointerId); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final VelocityTrackerVersionImpl IMPL; + static { + if (android.os.Build.VERSION.SDK_INT >= 11) { + IMPL = new HoneycombVelocityTrackerVersionImpl(); + } else { + IMPL = new BaseVelocityTrackerVersionImpl(); + } + } + + // ------------------------------------------------------------------- + + /** + * Call {@link VelocityTracker#getXVelocity(int)}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} device, + * returns {@link VelocityTracker#getXVelocity()}. + */ + public static float getXVelocity(VelocityTracker tracker, int pointerId) { + return IMPL.getXVelocity(tracker, pointerId); + } + + /** + * Call {@link VelocityTracker#getYVelocity(int)}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#HONEYCOMB} device, + * returns {@link VelocityTracker#getYVelocity()}. + */ + public static float getYVelocity(VelocityTracker tracker, int pointerId) { + return IMPL.getYVelocity(tracker, pointerId); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompatHoneycomb.java new file mode 100644 index 00000000000..4f9d326c982 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/VelocityTrackerCompatHoneycomb.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.VelocityTracker; + +/** + * Implementation of velocity tracker compatibility that can call Honeycomb APIs. + */ +class VelocityTrackerCompatHoneycomb { + public static float getXVelocity(VelocityTracker tracker, int pointerId) { + return tracker.getXVelocity(pointerId); + } + public static float getYVelocity(VelocityTracker tracker, int pointerId) { + return tracker.getYVelocity(pointerId); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompat.java new file mode 100644 index 00000000000..871f633335a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompat.java @@ -0,0 +1,3008 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.res.ColorStateList; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.support.annotation.IdRes; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; +import android.util.Log; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.WeakHashMap; + +/** + * Helper for accessing features in {@link View} introduced after API + * level 4 in a backwards compatible fashion. + */ +public class ViewCompat { + private static final String TAG = "ViewCompat"; + + + /** @hide */ + @IntDef({OVER_SCROLL_ALWAYS, OVER_SCROLL_IF_CONTENT_SCROLLS, OVER_SCROLL_NEVER}) + @Retention(RetentionPolicy.SOURCE) + private @interface OverScroll {} + + /** + * Always allow a user to over-scroll this view, provided it is a + * view that can scroll. + */ + public static final int OVER_SCROLL_ALWAYS = 0; + + /** + * Allow a user to over-scroll this view only if the content is large + * enough to meaningfully scroll, provided it is a view that can scroll. + */ + public static final int OVER_SCROLL_IF_CONTENT_SCROLLS = 1; + + /** + * Never allow a user to over-scroll this view. + */ + public static final int OVER_SCROLL_NEVER = 2; + + private static final long FAKE_FRAME_TIME = 10; + + /** @hide */ + @IntDef({ + IMPORTANT_FOR_ACCESSIBILITY_AUTO, + IMPORTANT_FOR_ACCESSIBILITY_YES, + IMPORTANT_FOR_ACCESSIBILITY_NO, + IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + }) + @Retention(RetentionPolicy.SOURCE) + private @interface ImportantForAccessibility {} + + /** + * Automatically determine whether a view is important for accessibility. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_AUTO = 0x00000000; + + /** + * The view is important for accessibility. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_YES = 0x00000001; + + /** + * The view is not important for accessibility. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_NO = 0x00000002; + + /** + * The view is not important for accessibility, nor are any of its + * descendant views. + */ + public static final int IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS = 0x00000004; + + /** @hide */ + @IntDef({ + ACCESSIBILITY_LIVE_REGION_NONE, + ACCESSIBILITY_LIVE_REGION_POLITE, + ACCESSIBILITY_LIVE_REGION_ASSERTIVE + }) + @Retention(RetentionPolicy.SOURCE) + private @interface AccessibilityLiveRegion {} + + /** + * Live region mode specifying that accessibility services should not + * automatically announce changes to this view. This is the default live + * region mode for most views. + *

    + * Use with {@link ViewCompat#setAccessibilityLiveRegion(View, int)}. + */ + public static final int ACCESSIBILITY_LIVE_REGION_NONE = 0x00000000; + + /** + * Live region mode specifying that accessibility services should announce + * changes to this view. + *

    + * Use with {@link ViewCompat#setAccessibilityLiveRegion(View, int)}. + */ + public static final int ACCESSIBILITY_LIVE_REGION_POLITE = 0x00000001; + + /** + * Live region mode specifying that accessibility services should interrupt + * ongoing speech to immediately announce changes to this view. + *

    + * Use with {@link ViewCompat#setAccessibilityLiveRegion(View, int)}. + */ + public static final int ACCESSIBILITY_LIVE_REGION_ASSERTIVE = 0x00000002; + + /** @hide */ + @IntDef({LAYER_TYPE_NONE, LAYER_TYPE_SOFTWARE, LAYER_TYPE_HARDWARE}) + @Retention(RetentionPolicy.SOURCE) + private @interface LayerType {} + + /** + * Indicates that the view does not have a layer. + */ + public static final int LAYER_TYPE_NONE = 0; + + /** + *

    Indicates that the view has a software layer. A software layer is backed + * by a bitmap and causes the view to be rendered using Android's software + * rendering pipeline, even if hardware acceleration is enabled.

    + * + *

    Software layers have various usages:

    + *

    When the application is not using hardware acceleration, a software layer + * is useful to apply a specific color filter and/or blending mode and/or + * translucency to a view and all its children.

    + *

    When the application is using hardware acceleration, a software layer + * is useful to render drawing primitives not supported by the hardware + * accelerated pipeline. It can also be used to cache a complex view tree + * into a texture and reduce the complexity of drawing operations. For instance, + * when animating a complex view tree with a translation, a software layer can + * be used to render the view tree only once.

    + *

    Software layers should be avoided when the affected view tree updates + * often. Every update will require to re-render the software layer, which can + * potentially be slow (particularly when hardware acceleration is turned on + * since the layer will have to be uploaded into a hardware texture after every + * update.)

    + */ + public static final int LAYER_TYPE_SOFTWARE = 1; + + /** + *

    Indicates that the view has a hardware layer. A hardware layer is backed + * by a hardware specific texture (generally Frame Buffer Objects or FBO on + * OpenGL hardware) and causes the view to be rendered using Android's hardware + * rendering pipeline, but only if hardware acceleration is turned on for the + * view hierarchy. When hardware acceleration is turned off, hardware layers + * behave exactly as {@link #LAYER_TYPE_SOFTWARE software layers}.

    + * + *

    A hardware layer is useful to apply a specific color filter and/or + * blending mode and/or translucency to a view and all its children.

    + *

    A hardware layer can be used to cache a complex view tree into a + * texture and reduce the complexity of drawing operations. For instance, + * when animating a complex view tree with a translation, a hardware layer can + * be used to render the view tree only once.

    + *

    A hardware layer can also be used to increase the rendering quality when + * rotation transformations are applied on a view. It can also be used to + * prevent potential clipping issues when applying 3D transforms on a view.

    + */ + public static final int LAYER_TYPE_HARDWARE = 2; + + /** @hide */ + @IntDef({ + LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL, + LAYOUT_DIRECTION_INHERIT, + LAYOUT_DIRECTION_LOCALE}) + @Retention(RetentionPolicy.SOURCE) + private @interface LayoutDirectionMode {} + + /** @hide */ + @IntDef({ + LAYOUT_DIRECTION_LTR, + LAYOUT_DIRECTION_RTL + }) + @Retention(RetentionPolicy.SOURCE) + private @interface ResolvedLayoutDirectionMode {} + + /** + * Horizontal layout direction of this view is from Left to Right. + */ + public static final int LAYOUT_DIRECTION_LTR = 0; + + /** + * Horizontal layout direction of this view is from Right to Left. + */ + public static final int LAYOUT_DIRECTION_RTL = 1; + + /** + * Horizontal layout direction of this view is inherited from its parent. + * Use with {@link #setLayoutDirection}. + */ + public static final int LAYOUT_DIRECTION_INHERIT = 2; + + /** + * Horizontal layout direction of this view is from deduced from the default language + * script for the locale. Use with {@link #setLayoutDirection}. + */ + public static final int LAYOUT_DIRECTION_LOCALE = 3; + + /** + * Bits of {@link #getMeasuredWidthAndState} and + * {@link #getMeasuredWidthAndState} that provide the actual measured size. + */ + public static final int MEASURED_SIZE_MASK = 0x00ffffff; + + /** + * Bits of {@link #getMeasuredWidthAndState} and + * {@link #getMeasuredWidthAndState} that provide the additional state bits. + */ + public static final int MEASURED_STATE_MASK = 0xff000000; + + /** + * Bit shift of {@link #MEASURED_STATE_MASK} to get to the height bits + * for functions that combine both width and height into a single int, + * such as {@link #getMeasuredState} and the childState argument of + * {@link #resolveSizeAndState(int, int, int)}. + */ + public static final int MEASURED_HEIGHT_STATE_SHIFT = 16; + + /** + * Bit of {@link #getMeasuredWidthAndState} and + * {@link #getMeasuredWidthAndState} that indicates the measured size + * is smaller that the space the view would like to have. + */ + public static final int MEASURED_STATE_TOO_SMALL = 0x01000000; + + /** + * Indicates no axis of view scrolling. + */ + public static final int SCROLL_AXIS_NONE = 0; + + /** + * Indicates scrolling along the horizontal axis. + */ + public static final int SCROLL_AXIS_HORIZONTAL = 1 << 0; + + /** + * Indicates scrolling along the vertical axis. + */ + public static final int SCROLL_AXIS_VERTICAL = 1 << 1; + + interface ViewCompatImpl { + public boolean canScrollHorizontally(View v, int direction); + public boolean canScrollVertically(View v, int direction); + public int getOverScrollMode(View v); + public void setOverScrollMode(View v, int mode); + public void onInitializeAccessibilityEvent(View v, AccessibilityEvent event); + public void onPopulateAccessibilityEvent(View v, AccessibilityEvent event); + public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfoCompat info); + public void setAccessibilityDelegate(View v, @Nullable AccessibilityDelegateCompat delegate); + public boolean hasAccessibilityDelegate(View v); + public boolean hasTransientState(View view); + public void setHasTransientState(View view, boolean hasTransientState); + public void postInvalidateOnAnimation(View view); + public void postInvalidateOnAnimation(View view, int left, int top, int right, int bottom); + public void postOnAnimation(View view, Runnable action); + public void postOnAnimationDelayed(View view, Runnable action, long delayMillis); + public int getImportantForAccessibility(View view); + public void setImportantForAccessibility(View view, int mode); + public boolean isImportantForAccessibility(View view); + public boolean performAccessibilityAction(View view, int action, Bundle arguments); + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View view); + public float getAlpha(View view); + public void setLayerType(View view, int layerType, Paint paint); + public int getLayerType(View view); + public int getLabelFor(View view); + public void setLabelFor(View view, int id); + public void setLayerPaint(View view, Paint paint); + public int getLayoutDirection(View view); + public void setLayoutDirection(View view, int layoutDirection); + public ViewParent getParentForAccessibility(View view); + public boolean isOpaque(View view); + public int resolveSizeAndState(int size, int measureSpec, int childMeasuredState); + public int getMeasuredWidthAndState(View view); + public int getMeasuredHeightAndState(View view); + public int getMeasuredState(View view); + public int getAccessibilityLiveRegion(View view); + public void setAccessibilityLiveRegion(View view, int mode); + public int getPaddingStart(View view); + public int getPaddingEnd(View view); + public void setPaddingRelative(View view, int start, int top, int end, int bottom); + public void dispatchStartTemporaryDetach(View view); + public void dispatchFinishTemporaryDetach(View view); + public float getX(View view); + public float getY(View view); + public float getRotation(View view); + public float getRotationX(View view); + public float getRotationY(View view); + public float getScaleX(View view); + public float getScaleY(View view); + public float getTranslationX(View view); + public float getTranslationY(View view); + public int getMinimumWidth(View view); + public int getMinimumHeight(View view); + public ViewPropertyAnimatorCompat animate(View view); + public void setRotation(View view, float value); + public void setRotationX(View view, float value); + public void setRotationY(View view, float value); + public void setScaleX(View view, float value); + public void setScaleY(View view, float value); + public void setTranslationX(View view, float value); + public void setTranslationY(View view, float value); + public void setX(View view, float value); + public void setY(View view, float value); + public void setAlpha(View view, float value); + public void setPivotX(View view, float value); + public void setPivotY(View view, float value); + public float getPivotX(View view); + public float getPivotY(View view); + public void setElevation(View view, float elevation); + public float getElevation(View view); + public void setTranslationZ(View view, float translationZ); + public float getTranslationZ(View view); + public void setTransitionName(View view, String transitionName); + public String getTransitionName(View view); + public int getWindowSystemUiVisibility(View view); + public void requestApplyInsets(View view); + public void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled); + public boolean getFitsSystemWindows(View view); + void setFitsSystemWindows(View view, boolean fitSystemWindows); + void jumpDrawablesToCurrentState(View v); + void setOnApplyWindowInsetsListener(View view, OnApplyWindowInsetsListener listener); + WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets); + WindowInsetsCompat dispatchApplyWindowInsets(View v, WindowInsetsCompat insets); + void setSaveFromParentEnabled(View view, boolean enabled); + void setActivated(View view, boolean activated); + boolean isPaddingRelative(View view); + ColorStateList getBackgroundTintList(View view); + void setBackgroundTintList(View view, ColorStateList tintList); + PorterDuff.Mode getBackgroundTintMode(View view); + void setBackgroundTintMode(View view, PorterDuff.Mode mode); + void setNestedScrollingEnabled(View view, boolean enabled); + boolean isNestedScrollingEnabled(View view); + boolean startNestedScroll(View view, int axes); + void stopNestedScroll(View view); + boolean hasNestedScrollingParent(View view); + boolean dispatchNestedScroll(View view, int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow); + boolean dispatchNestedPreScroll(View view, int dx, int dy, int[] consumed, + int[] offsetInWindow); + boolean dispatchNestedFling(View view, float velocityX, float velocityY, boolean consumed); + boolean dispatchNestedPreFling(View view, float velocityX, float velocityY); + boolean isLaidOut(View view); + int combineMeasuredStates(int curState, int newState); + public float getZ(View view); + public boolean isAttachedToWindow(View view); + } + + static class BaseViewCompatImpl implements ViewCompatImpl { + private Method mDispatchStartTemporaryDetach; + private Method mDispatchFinishTemporaryDetach; + private boolean mTempDetachBound; + WeakHashMap mViewPropertyAnimatorCompatMap = null; + + + public boolean canScrollHorizontally(View v, int direction) { + return (v instanceof ScrollingView) && + canScrollingViewScrollHorizontally((ScrollingView) v, direction); + } + public boolean canScrollVertically(View v, int direction) { + return (v instanceof ScrollingView) && + canScrollingViewScrollVertically((ScrollingView) v, direction); + } + public int getOverScrollMode(View v) { + return OVER_SCROLL_NEVER; + } + public void setOverScrollMode(View v, int mode) { + // Do nothing; API doesn't exist + } + public void setAccessibilityDelegate(View v, AccessibilityDelegateCompat delegate) { + // Do nothing; API doesn't exist + } + + @Override + public boolean hasAccessibilityDelegate(View v) { + return false; + } + + public void onPopulateAccessibilityEvent(View v, AccessibilityEvent event) { + // Do nothing; API doesn't exist + } + public void onInitializeAccessibilityEvent(View v, AccessibilityEvent event) { + // Do nothing; API doesn't exist + } + public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfoCompat info) { + // Do nothing; API doesn't exist + } + public boolean hasTransientState(View view) { + // A view can't have transient state if transient state wasn't supported. + return false; + } + public void setHasTransientState(View view, boolean hasTransientState) { + // Do nothing; API doesn't exist + } + public void postInvalidateOnAnimation(View view) { + view.invalidate(); + } + public void postInvalidateOnAnimation(View view, int left, int top, int right, int bottom) { + view.invalidate(left, top, right, bottom); + } + public void postOnAnimation(View view, Runnable action) { + view.postDelayed(action, getFrameTime()); + } + public void postOnAnimationDelayed(View view, Runnable action, long delayMillis) { + view.postDelayed(action, getFrameTime() + delayMillis); + } + long getFrameTime() { + return FAKE_FRAME_TIME; + } + public int getImportantForAccessibility(View view) { + return 0; + } + public void setImportantForAccessibility(View view, int mode) { + + } + public boolean isImportantForAccessibility(View view) { + return true; + } + public boolean performAccessibilityAction(View view, int action, Bundle arguments) { + return false; + } + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View view) { + return null; + } + public float getAlpha(View view) { + return 1.0f; + } + public void setLayerType(View view, int layerType, Paint paint) { + // No-op until layers became available (HC) + } + public int getLayerType(View view) { + return LAYER_TYPE_NONE; + } + public int getLabelFor(View view) { + return 0; + } + public void setLabelFor(View view, int id) { + + } + public void setLayerPaint(View view, Paint p) { + // No-op until layers became available (HC) + } + + @Override + public int getLayoutDirection(View view) { + return LAYOUT_DIRECTION_LTR; + } + + @Override + public void setLayoutDirection(View view, int layoutDirection) { + // No-op + } + + @Override + public ViewParent getParentForAccessibility(View view) { + return view.getParent(); + } + + @Override + public boolean isOpaque(View view) { + final Drawable bg = view.getBackground(); + if (bg != null) { + return bg.getOpacity() == PixelFormat.OPAQUE; + } + return false; + } + + public int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { + return View.resolveSize(size, measureSpec); + } + + @Override + public int getMeasuredWidthAndState(View view) { + return view.getMeasuredWidth(); + } + + @Override + public int getMeasuredHeightAndState(View view) { + return view.getMeasuredHeight(); + } + + @Override + public int getMeasuredState(View view) { + return 0; + } + + @Override + public int getAccessibilityLiveRegion(View view) { + return ACCESSIBILITY_LIVE_REGION_NONE; + } + + @Override + public void setAccessibilityLiveRegion(View view, int mode) { + // No-op + } + + @Override + public int getPaddingStart(View view) { + return view.getPaddingLeft(); + } + + @Override + public int getPaddingEnd(View view) { + return view.getPaddingRight(); + } + + @Override + public void setPaddingRelative(View view, int start, int top, int end, int bottom) { + view.setPadding(start, top, end, bottom); + } + + @Override + public void dispatchStartTemporaryDetach(View view) { + if (!mTempDetachBound) { + bindTempDetach(); + } + if (mDispatchStartTemporaryDetach != null) { + try { + mDispatchStartTemporaryDetach.invoke(view); + } catch (Exception e) { + Log.d(TAG, "Error calling dispatchStartTemporaryDetach", e); + } + } else { + // Try this instead + view.onStartTemporaryDetach(); + } + } + + @Override + public void dispatchFinishTemporaryDetach(View view) { + if (!mTempDetachBound) { + bindTempDetach(); + } + if (mDispatchFinishTemporaryDetach != null) { + try { + mDispatchFinishTemporaryDetach.invoke(view); + } catch (Exception e) { + Log.d(TAG, "Error calling dispatchFinishTemporaryDetach", e); + } + } else { + // Try this instead + view.onFinishTemporaryDetach(); + } + } + + private void bindTempDetach() { + try { + mDispatchStartTemporaryDetach = View.class.getDeclaredMethod( + "dispatchStartTemporaryDetach"); + mDispatchFinishTemporaryDetach = View.class.getDeclaredMethod( + "dispatchFinishTemporaryDetach"); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Couldn't find method", e); + } + mTempDetachBound = true; + } + + @Override + public float getTranslationX(View view) { + return 0; + } + + @Override + public float getTranslationY(View view) { + return 0; + } + + @Override + public float getX(View view) { + return 0; + } + + @Override + public float getY(View view) { + return 0; + } + + @Override + public float getRotation(View view) { + return 0; + } + + @Override + public float getRotationX(View view) { + return 0; + } + + @Override + public float getRotationY(View view) { + return 0; + } + + @Override + public float getScaleX(View view) { + return 0; + } + + @Override + public float getScaleY(View view) { + return 0; + } + + @Override + public int getMinimumWidth(View view) { + return ViewCompatBase.getMinimumWidth(view); + } + + @Override + public int getMinimumHeight(View view) { + return ViewCompatBase.getMinimumHeight(view); + } + + @Override + public ViewPropertyAnimatorCompat animate(View view) { + return new ViewPropertyAnimatorCompat(view); + } + + @Override + public void setRotation(View view, float value) { + // noop + } + + @Override + public void setTranslationX(View view, float value) { + // noop + } + + @Override + public void setTranslationY(View view, float value) { + // noop + } + + @Override + public void setAlpha(View view, float value) { + // noop + } + + @Override + public void setRotationX(View view, float value) { + // noop + } + + @Override + public void setRotationY(View view, float value) { + // noop + } + + @Override + public void setScaleX(View view, float value) { + // noop + } + + @Override + public void setScaleY(View view, float value) { + // noop + } + + @Override + public void setX(View view, float value) { + // noop + } + + @Override + public void setY(View view, float value) { + // noop + } + + @Override + public void setPivotX(View view, float value) { + // noop + } + + @Override + public void setPivotY(View view, float value) { + // noop + } + + @Override + public float getPivotX(View view) { + return 0; + } + + @Override + public float getPivotY(View view) { + return 0; + } + + @Override + public void setTransitionName(View view, String transitionName) { + } + + @Override + public String getTransitionName(View view) { + return null; + } + + @Override + public int getWindowSystemUiVisibility(View view) { + return 0; + } + + @Override + public void requestApplyInsets(View view) { + } + + @Override + public void setElevation(View view, float elevation) { + } + + @Override + public float getElevation(View view) { + return 0f; + } + + @Override + public void setTranslationZ(View view, float translationZ) { + } + + @Override + public float getTranslationZ(View view) { + return 0f; + } + + @Override + public void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled) { + // noop + } + + @Override + public boolean getFitsSystemWindows(View view) { + return false; + } + + @Override + public void setFitsSystemWindows(View view, boolean fitSystemWindows) { + // noop + } + + @Override + public void jumpDrawablesToCurrentState(View view) { + // Do nothing; API didn't exist. + } + + @Override + public void setOnApplyWindowInsetsListener(View view, + OnApplyWindowInsetsListener listener) { + // noop + } + + @Override + public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { + return insets; + } + + @Override + public WindowInsetsCompat dispatchApplyWindowInsets(View v, WindowInsetsCompat insets) { + return insets; + } + + @Override + public void setSaveFromParentEnabled(View v, boolean enabled) { + // noop + } + + @Override + public void setActivated(View view, boolean activated) { + // noop + } + + @Override + public boolean isPaddingRelative(View view) { + return false; + } + + public void setNestedScrollingEnabled(View view, boolean enabled) { + if (view instanceof NestedScrollingChild) { + ((NestedScrollingChild) view).setNestedScrollingEnabled(enabled); + } + } + + @Override + public boolean isNestedScrollingEnabled(View view) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).isNestedScrollingEnabled(); + } + return false; + } + + @Override + public ColorStateList getBackgroundTintList(View view) { + return ViewCompatBase.getBackgroundTintList(view); + } + + @Override + public void setBackgroundTintList(View view, ColorStateList tintList) { + ViewCompatBase.setBackgroundTintList(view, tintList); + } + + @Override + public void setBackgroundTintMode(View view, PorterDuff.Mode mode) { + ViewCompatBase.setBackgroundTintMode(view, mode); + } + + @Override + public PorterDuff.Mode getBackgroundTintMode(View view) { + return ViewCompatBase.getBackgroundTintMode(view); + } + + private boolean canScrollingViewScrollHorizontally(ScrollingView view, int direction) { + final int offset = view.computeHorizontalScrollOffset(); + final int range = view.computeHorizontalScrollRange() - + view.computeHorizontalScrollExtent(); + if (range == 0) return false; + if (direction < 0) { + return offset > 0; + } else { + return offset < range - 1; + } + } + + private boolean canScrollingViewScrollVertically(ScrollingView view, int direction) { + final int offset = view.computeVerticalScrollOffset(); + final int range = view.computeVerticalScrollRange() - + view.computeVerticalScrollExtent(); + if (range == 0) return false; + if (direction < 0) { + return offset > 0; + } else { + return offset < range - 1; + } + } + + public boolean startNestedScroll(View view, int axes) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).startNestedScroll(axes); + } + return false; + } + + @Override + public void stopNestedScroll(View view) { + if (view instanceof NestedScrollingChild) { + ((NestedScrollingChild) view).stopNestedScroll(); + } + } + + @Override + public boolean hasNestedScrollingParent(View view) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).hasNestedScrollingParent(); + } + return false; + } + + @Override + public boolean dispatchNestedScroll(View view, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).dispatchNestedScroll(dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, offsetInWindow); + } + return false; + } + + @Override + public boolean dispatchNestedPreScroll(View view, int dx, int dy, + int[] consumed, int[] offsetInWindow) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).dispatchNestedPreScroll(dx, dy, consumed, + offsetInWindow); + } + return false; + } + + @Override + public boolean dispatchNestedFling(View view, float velocityX, float velocityY, + boolean consumed) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).dispatchNestedFling(velocityX, velocityY, + consumed); + } + return false; + } + + @Override + public boolean dispatchNestedPreFling(View view, float velocityX, float velocityY) { + if (view instanceof NestedScrollingChild) { + return ((NestedScrollingChild) view).dispatchNestedPreFling(velocityX, velocityY); + } + return false; + } + + @Override + public boolean isLaidOut(View view) { + return ViewCompatBase.isLaidOut(view); + } + + @Override + public int combineMeasuredStates(int curState, int newState) { + return curState | newState; + } + + @Override + public float getZ(View view) { + return getTranslationZ(view) + getElevation(view); + } + + @Override + public boolean isAttachedToWindow(View view) { + return ViewCompatBase.isAttachedToWindow(view); + } + } + + static class EclairMr1ViewCompatImpl extends BaseViewCompatImpl { + @Override + public boolean isOpaque(View view) { + return ViewCompatEclairMr1.isOpaque(view); + } + + @Override + public void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled) { + ViewCompatEclairMr1.setChildrenDrawingOrderEnabled(viewGroup, enabled); + } + } + + static class GBViewCompatImpl extends EclairMr1ViewCompatImpl { + @Override + public int getOverScrollMode(View v) { + return ViewCompatGingerbread.getOverScrollMode(v); + } + @Override + public void setOverScrollMode(View v, int mode) { + ViewCompatGingerbread.setOverScrollMode(v, mode); + } + } + + static class HCViewCompatImpl extends GBViewCompatImpl { + @Override + long getFrameTime() { + return ViewCompatHC.getFrameTime(); + } + @Override + public float getAlpha(View view) { + return ViewCompatHC.getAlpha(view); + } + @Override + public void setLayerType(View view, int layerType, Paint paint) { + ViewCompatHC.setLayerType(view, layerType, paint); + } + @Override + public int getLayerType(View view) { + return ViewCompatHC.getLayerType(view); + } + @Override + public void setLayerPaint(View view, Paint paint) { + // Make sure the paint is correct; this will be cheap if it's the same + // instance as was used to call setLayerType earlier. + setLayerType(view, getLayerType(view), paint); + // This is expensive, but the only way to accomplish this before JB-MR1. + view.invalidate(); + } + @Override + public int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { + return ViewCompatHC.resolveSizeAndState(size, measureSpec, childMeasuredState); + } + @Override + public int getMeasuredWidthAndState(View view) { + return ViewCompatHC.getMeasuredWidthAndState(view); + } + @Override + public int getMeasuredHeightAndState(View view) { + return ViewCompatHC.getMeasuredHeightAndState(view); + } + @Override + public int getMeasuredState(View view) { + return ViewCompatHC.getMeasuredState(view); + } + @Override + public float getTranslationX(View view) { + return ViewCompatHC.getTranslationX(view); + } + @Override + public float getTranslationY(View view) { + return ViewCompatHC.getTranslationY(view); + } + @Override + public void setTranslationX(View view, float value) { + ViewCompatHC.setTranslationX(view, value); + } + @Override + public void setTranslationY(View view, float value) { + ViewCompatHC.setTranslationY(view, value); + } + @Override + public void setAlpha(View view, float value) { + ViewCompatHC.setAlpha(view, value); + } + @Override + public void setX(View view, float value) { + ViewCompatHC.setX(view, value); + } + @Override + public void setY(View view, float value) { + ViewCompatHC.setY(view, value); + } + @Override + public void setRotation(View view, float value) { + ViewCompatHC.setRotation(view, value); + } + @Override + public void setRotationX(View view, float value) { + ViewCompatHC.setRotationX(view, value); + } + @Override + public void setRotationY(View view, float value) { + ViewCompatHC.setRotationY(view, value); + } + @Override + public void setScaleX(View view, float value) { + ViewCompatHC.setScaleX(view, value); + } + @Override + public void setScaleY(View view, float value) { + ViewCompatHC.setScaleY(view, value); + } + @Override + public void setPivotX(View view, float value) { + ViewCompatHC.setPivotX(view, value); + } + @Override + public void setPivotY(View view, float value) { + ViewCompatHC.setPivotY(view, value); + } + @Override + public float getX(View view) { + return ViewCompatHC.getX(view); + } + + @Override + public float getY(View view) { + return ViewCompatHC.getY(view); + } + + @Override + public float getRotation(View view) { + return ViewCompatHC.getRotation(view); + } + + @Override + public float getRotationX(View view) { + return ViewCompatHC.getRotationX(view); + } + + @Override + public float getRotationY(View view) { + return ViewCompatHC.getRotationY(view); + } + + @Override + public float getScaleX(View view) { + return ViewCompatHC.getScaleX(view); + } + + @Override + public float getScaleY(View view) { + return ViewCompatHC.getScaleY(view); + } + + @Override + public float getPivotX(View view) { + return ViewCompatHC.getPivotX(view); + } + @Override + public float getPivotY(View view) { + return ViewCompatHC.getPivotY(view); + } + @Override + public void jumpDrawablesToCurrentState(View view) { + ViewCompatHC.jumpDrawablesToCurrentState(view); + } + + @Override + public void setSaveFromParentEnabled(View view, boolean enabled) { + ViewCompatHC.setSaveFromParentEnabled(view, enabled); + } + + @Override + public void setActivated(View view, boolean activated) { + ViewCompatHC.setActivated(view, activated); + } + + @Override + public int combineMeasuredStates(int curState, int newState) { + return ViewCompatHC.combineMeasuredStates(curState, newState); + } + } + + static class ICSViewCompatImpl extends HCViewCompatImpl { + static Field mAccessibilityDelegateField; + static boolean accessibilityDelegateCheckFailed = false; + @Override + public boolean canScrollHorizontally(View v, int direction) { + return ViewCompatICS.canScrollHorizontally(v, direction); + } + @Override + public boolean canScrollVertically(View v, int direction) { + return ViewCompatICS.canScrollVertically(v, direction); + } + @Override + public void onPopulateAccessibilityEvent(View v, AccessibilityEvent event) { + ViewCompatICS.onPopulateAccessibilityEvent(v, event); + } + @Override + public void onInitializeAccessibilityEvent(View v, AccessibilityEvent event) { + ViewCompatICS.onInitializeAccessibilityEvent(v, event); + } + @Override + public void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfoCompat info) { + ViewCompatICS.onInitializeAccessibilityNodeInfo(v, info.getInfo()); + } + @Override + public void setAccessibilityDelegate(View v, + @Nullable AccessibilityDelegateCompat delegate) { + ViewCompatICS.setAccessibilityDelegate(v, + delegate == null ? null : delegate.getBridge()); + } + + @Override + public boolean hasAccessibilityDelegate(View v) { + if (accessibilityDelegateCheckFailed) { + return false; // View implementation might have changed. + } + if (mAccessibilityDelegateField == null) { + try { + mAccessibilityDelegateField = View.class + .getDeclaredField("mAccessibilityDelegate"); + mAccessibilityDelegateField.setAccessible(true); + } catch (Throwable t) { + accessibilityDelegateCheckFailed = true; + return false; + } + } + try { + return mAccessibilityDelegateField.get(v) != null; + } catch (Throwable t) { + accessibilityDelegateCheckFailed = true; + return false; + } + } + + @Override + public ViewPropertyAnimatorCompat animate(View view) { + if (mViewPropertyAnimatorCompatMap == null) { + mViewPropertyAnimatorCompatMap = + new WeakHashMap(); + } + ViewPropertyAnimatorCompat vpa = mViewPropertyAnimatorCompatMap.get(view); + if (vpa == null) { + vpa = new ViewPropertyAnimatorCompat(view); + mViewPropertyAnimatorCompatMap.put(view, vpa); + } + return vpa; + } + + @Override + public void setFitsSystemWindows(View view, boolean fitSystemWindows) { + ViewCompatICS.setFitsSystemWindows(view, fitSystemWindows); + } + } + + static class JBViewCompatImpl extends ICSViewCompatImpl { + @Override + public boolean hasTransientState(View view) { + return ViewCompatJB.hasTransientState(view); + } + @Override + public void setHasTransientState(View view, boolean hasTransientState) { + ViewCompatJB.setHasTransientState(view, hasTransientState); + } + @Override + public void postInvalidateOnAnimation(View view) { + ViewCompatJB.postInvalidateOnAnimation(view); + } + @Override + public void postInvalidateOnAnimation(View view, int left, int top, int right, int bottom) { + ViewCompatJB.postInvalidateOnAnimation(view, left, top, right, bottom); + } + @Override + public void postOnAnimation(View view, Runnable action) { + ViewCompatJB.postOnAnimation(view, action); + } + @Override + public void postOnAnimationDelayed(View view, Runnable action, long delayMillis) { + ViewCompatJB.postOnAnimationDelayed(view, action, delayMillis); + } + @Override + public int getImportantForAccessibility(View view) { + return ViewCompatJB.getImportantForAccessibility(view); + } + @Override + public void setImportantForAccessibility(View view, int mode) { + // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS is not available + // on this platform so replace with IMPORTANT_FOR_ACCESSIBILITY_NO + // which is closer semantically. + if (mode == IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) { + mode = IMPORTANT_FOR_ACCESSIBILITY_NO; + } + ViewCompatJB.setImportantForAccessibility(view, mode); + } + @Override + public boolean performAccessibilityAction(View view, int action, Bundle arguments) { + return ViewCompatJB.performAccessibilityAction(view, action, arguments); + } + @Override + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View view) { + Object compat = ViewCompatJB.getAccessibilityNodeProvider(view); + if (compat != null) { + return new AccessibilityNodeProviderCompat(compat); + } + return null; + } + + @Override + public ViewParent getParentForAccessibility(View view) { + return ViewCompatJB.getParentForAccessibility(view); + } + + @Override + public int getMinimumWidth(View view) { + return ViewCompatJB.getMinimumWidth(view); + } + + @Override + public int getMinimumHeight(View view) { + return ViewCompatJB.getMinimumHeight(view); + } + + @Override + public void requestApplyInsets(View view) { + ViewCompatJB.requestApplyInsets(view); + } + + @Override + public boolean getFitsSystemWindows(View view) { + return ViewCompatJB.getFitsSystemWindows(view); + } + } + + static class JbMr1ViewCompatImpl extends JBViewCompatImpl { + + @Override + public int getLabelFor(View view) { + return ViewCompatJellybeanMr1.getLabelFor(view); + } + + @Override + public void setLabelFor(View view, int id) { + ViewCompatJellybeanMr1.setLabelFor(view, id); + } + + @Override + public void setLayerPaint(View view, Paint paint) { + ViewCompatJellybeanMr1.setLayerPaint(view, paint); + } + + @Override + public int getLayoutDirection(View view) { + return ViewCompatJellybeanMr1.getLayoutDirection(view); + } + + @Override + public void setLayoutDirection(View view, int layoutDirection) { + ViewCompatJellybeanMr1.setLayoutDirection(view, layoutDirection); + } + + @Override + public int getPaddingStart(View view) { + return ViewCompatJellybeanMr1.getPaddingStart(view); + } + + @Override + public int getPaddingEnd(View view) { + return ViewCompatJellybeanMr1.getPaddingEnd(view); + } + + @Override + public void setPaddingRelative(View view, int start, int top, int end, int bottom) { + ViewCompatJellybeanMr1.setPaddingRelative(view, start, top, end, bottom); + } + + @Override + public int getWindowSystemUiVisibility(View view) { + return ViewCompatJellybeanMr1.getWindowSystemUiVisibility(view); + } + + @Override + public boolean isPaddingRelative(View view) { + return ViewCompatJellybeanMr1.isPaddingRelative(view); + } + } + + static class KitKatViewCompatImpl extends JbMr1ViewCompatImpl { + @Override + public int getAccessibilityLiveRegion(View view) { + return ViewCompatKitKat.getAccessibilityLiveRegion(view); + } + + @Override + public void setAccessibilityLiveRegion(View view, int mode) { + ViewCompatKitKat.setAccessibilityLiveRegion(view, mode); + } + + @Override + public void setImportantForAccessibility(View view, int mode) { + ViewCompatJB.setImportantForAccessibility(view, mode); + } + + @Override + public boolean isLaidOut(View view) { + return ViewCompatKitKat.isLaidOut(view); + } + + @Override + public boolean isAttachedToWindow(View view) { + return ViewCompatKitKat.isAttachedToWindow(view); + } + } + + static class LollipopViewCompatImpl extends KitKatViewCompatImpl { + @Override + public void setTransitionName(View view, String transitionName) { + ViewCompatLollipop.setTransitionName(view, transitionName); + } + + @Override + public String getTransitionName(View view) { + return ViewCompatLollipop.getTransitionName(view); + } + + @Override + public void requestApplyInsets(View view) { + ViewCompatLollipop.requestApplyInsets(view); + } + + @Override + public void setElevation(View view, float elevation) { + ViewCompatLollipop.setElevation(view, elevation); + } + + @Override + public float getElevation(View view) { + return ViewCompatLollipop.getElevation(view); + } + + @Override + public void setTranslationZ(View view, float translationZ) { + ViewCompatLollipop.setTranslationZ(view, translationZ); + } + + @Override + public float getTranslationZ(View view) { + return ViewCompatLollipop.getTranslationZ(view); + } + + @Override + public void setOnApplyWindowInsetsListener(View view, OnApplyWindowInsetsListener listener) { + ViewCompatLollipop.setOnApplyWindowInsetsListener(view, listener); + } + + @Override + public void setNestedScrollingEnabled(View view, boolean enabled) { + ViewCompatLollipop.setNestedScrollingEnabled(view, enabled); + } + + @Override + public boolean isNestedScrollingEnabled(View view) { + return ViewCompatLollipop.isNestedScrollingEnabled(view); + } + + @Override + public boolean startNestedScroll(View view, int axes) { + return ViewCompatLollipop.startNestedScroll(view, axes); + } + + @Override + public void stopNestedScroll(View view) { + ViewCompatLollipop.stopNestedScroll(view); + } + + @Override + public boolean hasNestedScrollingParent(View view) { + return ViewCompatLollipop.hasNestedScrollingParent(view); + } + + @Override + public boolean dispatchNestedScroll(View view, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + return ViewCompatLollipop.dispatchNestedScroll(view, dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(View view, int dx, int dy, + int[] consumed, int[] offsetInWindow) { + return ViewCompatLollipop.dispatchNestedPreScroll(view, dx, dy, consumed, + offsetInWindow); + } + + @Override + public boolean dispatchNestedFling(View view, float velocityX, float velocityY, + boolean consumed) { + return ViewCompatLollipop.dispatchNestedFling(view, velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(View view, float velocityX, float velocityY) { + return ViewCompatLollipop.dispatchNestedPreFling(view, velocityX, velocityY); + } + + @Override + public boolean isImportantForAccessibility(View view) { + return ViewCompatLollipop.isImportantForAccessibility(view); + } + + @Override + public ColorStateList getBackgroundTintList(View view) { + return ViewCompatLollipop.getBackgroundTintList(view); + } + + @Override + public void setBackgroundTintList(View view, ColorStateList tintList) { + ViewCompatLollipop.setBackgroundTintList(view, tintList); + } + + @Override + public void setBackgroundTintMode(View view, PorterDuff.Mode mode) { + ViewCompatLollipop.setBackgroundTintMode(view, mode); + } + + @Override + public PorterDuff.Mode getBackgroundTintMode(View view) { + return ViewCompatLollipop.getBackgroundTintMode(view); + } + + @Override + public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { + return ViewCompatLollipop.onApplyWindowInsets(v, insets); + } + + @Override + public WindowInsetsCompat dispatchApplyWindowInsets(View v, WindowInsetsCompat insets) { + return ViewCompatLollipop.dispatchApplyWindowInsets(v, insets); + } + + @Override + public float getZ(View view) { + return ViewCompatLollipop.getZ(view); + } + } + + static final ViewCompatImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 21) { + IMPL = new LollipopViewCompatImpl(); + } else if (version >= 19) { + IMPL = new KitKatViewCompatImpl(); + } else if (version >= 17) { + IMPL = new JbMr1ViewCompatImpl(); + } else if (version >= 16) { + IMPL = new JBViewCompatImpl(); + } else if (version >= 14) { + IMPL = new ICSViewCompatImpl(); + } else if (version >= 11) { + IMPL = new HCViewCompatImpl(); + } else if (version >= 9) { + IMPL = new GBViewCompatImpl(); + } else if (version >= 7) { + IMPL = new EclairMr1ViewCompatImpl(); + } else { + IMPL = new BaseViewCompatImpl(); + } + } + + /** + * Check if this view can be scrolled horizontally in a certain direction. + * + * @param v The View against which to invoke the method. + * @param direction Negative to check scrolling left, positive to check scrolling right. + * @return true if this view can be scrolled in the specified direction, false otherwise. + */ + public static boolean canScrollHorizontally(View v, int direction) { + return IMPL.canScrollHorizontally(v, direction); + } + + /** + * Check if this view can be scrolled vertically in a certain direction. + * + * @param v The View against which to invoke the method. + * @param direction Negative to check scrolling up, positive to check scrolling down. + * @return true if this view can be scrolled in the specified direction, false otherwise. + */ + public static boolean canScrollVertically(View v, int direction) { + return IMPL.canScrollVertically(v, direction); + } + + /** + * Returns the over-scroll mode for this view. The result will be + * one of {@link #OVER_SCROLL_ALWAYS} (default), {@link #OVER_SCROLL_IF_CONTENT_SCROLLS} + * (allow over-scrolling only if the view content is larger than the container), + * or {@link #OVER_SCROLL_NEVER}. + * + * @param v The View against which to invoke the method. + * @return This view's over-scroll mode. + */ + @OverScroll + public static int getOverScrollMode(View v) { + return IMPL.getOverScrollMode(v); + } + + /** + * Set the over-scroll mode for this view. Valid over-scroll modes are + * {@link #OVER_SCROLL_ALWAYS} (default), {@link #OVER_SCROLL_IF_CONTENT_SCROLLS} + * (allow over-scrolling only if the view content is larger than the container), + * or {@link #OVER_SCROLL_NEVER}. + * + * Setting the over-scroll mode of a view will have an effect only if the + * view is capable of scrolling. + * + * @param v The View against which to invoke the method. + * @param overScrollMode The new over-scroll mode for this view. + */ + public static void setOverScrollMode(View v, @OverScroll int overScrollMode) { + IMPL.setOverScrollMode(v, overScrollMode); + } + + /** + * Called from {@link View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)} + * giving a chance to this View to populate the accessibility event with its + * text content. While this method is free to modify event + * attributes other than text content, doing so should normally be performed in + * {@link View#onInitializeAccessibilityEvent(AccessibilityEvent)}. + *

    + * Example: Adding formatted date string to an accessibility event in addition + * to the text added by the super implementation: + *

     public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
    +     *     super.onPopulateAccessibilityEvent(event);
    +     *     final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_WEEKDAY;
    +     *     String selectedDateUtterance = DateUtils.formatDateTime(mContext,
    +     *         mCurrentDate.getTimeInMillis(), flags);
    +     *     event.getText().add(selectedDateUtterance);
    +     * }
    + *

    + * If an {@link android.view.View.AccessibilityDelegate} has been specified via calling + * {@link View#setAccessibilityDelegate(android.view.View.AccessibilityDelegate)} its + * {@link android.view.View.AccessibilityDelegate#onPopulateAccessibilityEvent(View, + * AccessibilityEvent)} + * is responsible for handling this call. + *

    + *

    Note: Always call the super implementation before adding + * information to the event, in case the default implementation has basic information to add. + *

    + * + * @param v The View against which to invoke the method. + * @param event The accessibility event which to populate. + * + * @see View#sendAccessibilityEvent(int) + * @see View#dispatchPopulateAccessibilityEvent(AccessibilityEvent) + */ + public static void onPopulateAccessibilityEvent(View v, AccessibilityEvent event) { + IMPL.onPopulateAccessibilityEvent(v, event); + } + + /** + * Initializes an {@link AccessibilityEvent} with information about + * this View which is the event source. In other words, the source of + * an accessibility event is the view whose state change triggered firing + * the event. + *

    + * Example: Setting the password property of an event in addition + * to properties set by the super implementation: + *

     public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
    +     *     super.onInitializeAccessibilityEvent(event);
    +     *     event.setPassword(true);
    +     * }
    + *

    + * If an {@link android.view.View.AccessibilityDelegate} has been specified via calling + * {@link View#setAccessibilityDelegate(android.view.View.AccessibilityDelegate)} its + * {@link android.view.View.AccessibilityDelegate#onInitializeAccessibilityEvent(View, + * AccessibilityEvent)} + * is responsible for handling this call. + *

    + *

    Note: Always call the super implementation before adding + * information to the event, in case the default implementation has basic information to add. + *

    + * + * @param v The View against which to invoke the method. + * @param event The event to initialize. + * + * @see View#sendAccessibilityEvent(int) + * @see View#dispatchPopulateAccessibilityEvent(AccessibilityEvent) + */ + public static void onInitializeAccessibilityEvent(View v, AccessibilityEvent event) { + IMPL.onInitializeAccessibilityEvent(v, event); + } + + /** + * Initializes an {@link android.view.accessibility.AccessibilityNodeInfo} with information + * about this view. The base implementation sets: + *
      + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setParent(View)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setBoundsInParent(Rect)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setBoundsInScreen(Rect)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setPackageName(CharSequence)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setClassName(CharSequence)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setContentDescription(CharSequence)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setEnabled(boolean)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setClickable(boolean)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setFocusable(boolean)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setFocused(boolean)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setLongClickable(boolean)},
    • + *
    • {@link android.view.accessibility.AccessibilityNodeInfo#setSelected(boolean)},
    • + *
    + *

    + * Subclasses should override this method, call the super implementation, + * and set additional attributes. + *

    + *

    + * If an {@link android.view.View.AccessibilityDelegate} has been specified via calling + * {@link View#setAccessibilityDelegate(android.view.View.AccessibilityDelegate)} its + * {@link android.view.View.AccessibilityDelegate#onInitializeAccessibilityNodeInfo(View, + * android.view.accessibility.AccessibilityNodeInfo)} + * is responsible for handling this call. + *

    + * + * @param v The View against which to invoke the method. + * @param info The instance to initialize. + */ + public static void onInitializeAccessibilityNodeInfo(View v, AccessibilityNodeInfoCompat info) { + IMPL.onInitializeAccessibilityNodeInfo(v, info); + } + + /** + * Sets a delegate for implementing accessibility support via compositon as + * opposed to inheritance. The delegate's primary use is for implementing + * backwards compatible widgets. For more details see + * {@link android.view.View.AccessibilityDelegate}. + * + * @param v The View against which to invoke the method. + * @param delegate The delegate instance. + * + * @see android.view.View.AccessibilityDelegate + */ + public static void setAccessibilityDelegate(View v, AccessibilityDelegateCompat delegate) { + IMPL.setAccessibilityDelegate(v, delegate); + } + + /** + * Checks whether provided View has an accessibility delegate attached to it. + * + * @param v The View instance to check + * @return True if the View has an accessibility delegate + */ + public static boolean hasAccessibilityDelegate(View v) { + return IMPL.hasAccessibilityDelegate(v); + } + + /** + * Indicates whether the view is currently tracking transient state that the + * app should not need to concern itself with saving and restoring, but that + * the framework should take special note to preserve when possible. + * + * @param view View to check for transient state + * @return true if the view has transient state + */ + public static boolean hasTransientState(View view) { + return IMPL.hasTransientState(view); + } + + /** + * Set whether this view is currently tracking transient state that the + * framework should attempt to preserve when possible. + * + * @param view View tracking transient state + * @param hasTransientState true if this view has transient state + */ + public static void setHasTransientState(View view, boolean hasTransientState) { + IMPL.setHasTransientState(view, hasTransientState); + } + + /** + *

    Cause an invalidate to happen on the next animation time step, typically the + * next display frame.

    + * + *

    This method can be invoked from outside of the UI thread + * only when this View is attached to a window.

    + * + * @param view View to invalidate + */ + public static void postInvalidateOnAnimation(View view) { + IMPL.postInvalidateOnAnimation(view); + } + + /** + *

    Cause an invalidate of the specified area to happen on the next animation + * time step, typically the next display frame.

    + * + *

    This method can be invoked from outside of the UI thread + * only when this View is attached to a window.

    + * + * @param view View to invalidate + * @param left The left coordinate of the rectangle to invalidate. + * @param top The top coordinate of the rectangle to invalidate. + * @param right The right coordinate of the rectangle to invalidate. + * @param bottom The bottom coordinate of the rectangle to invalidate. + */ + public static void postInvalidateOnAnimation(View view, int left, int top, + int right, int bottom) { + IMPL.postInvalidateOnAnimation(view, left, top, right, bottom); + } + + /** + *

    Causes the Runnable to execute on the next animation time step. + * The runnable will be run on the user interface thread.

    + * + *

    This method can be invoked from outside of the UI thread + * only when this View is attached to a window.

    + * + * @param view View to post this Runnable to + * @param action The Runnable that will be executed. + */ + public static void postOnAnimation(View view, Runnable action) { + IMPL.postOnAnimation(view, action); + } + + /** + *

    Causes the Runnable to execute on the next animation time step, + * after the specified amount of time elapses. + * The runnable will be run on the user interface thread.

    + * + *

    This method can be invoked from outside of the UI thread + * only when this View is attached to a window.

    + * + * @param view The view to post this Runnable to + * @param action The Runnable that will be executed. + * @param delayMillis The delay (in milliseconds) until the Runnable + * will be executed. + */ + public static void postOnAnimationDelayed(View view, Runnable action, long delayMillis) { + IMPL.postOnAnimationDelayed(view, action, delayMillis); + } + + /** + * Gets the mode for determining whether this View is important for accessibility + * which is if it fires accessibility events and if it is reported to + * accessibility services that query the screen. + * + * @param view The view whose property to get. + * @return The mode for determining whether a View is important for accessibility. + * + * @see #IMPORTANT_FOR_ACCESSIBILITY_YES + * @see #IMPORTANT_FOR_ACCESSIBILITY_NO + * @see #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + * @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO + */ + @ImportantForAccessibility + public static int getImportantForAccessibility(View view) { + return IMPL.getImportantForAccessibility(view); + } + + /** + * Sets how to determine whether this view is important for accessibility + * which is if it fires accessibility events and if it is reported to + * accessibility services that query the screen. + *

    + * Note: If the current paltform version does not support the + * {@link #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS} mode, then + * {@link #IMPORTANT_FOR_ACCESSIBILITY_NO} will be used as it is the + * closest terms of semantics. + *

    + * + * @param view The view whose property to set. + * @param mode How to determine whether this view is important for accessibility. + * + * @see #IMPORTANT_FOR_ACCESSIBILITY_YES + * @see #IMPORTANT_FOR_ACCESSIBILITY_NO + * @see #IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + * @see #IMPORTANT_FOR_ACCESSIBILITY_AUTO + */ + public static void setImportantForAccessibility(View view, + @ImportantForAccessibility int mode) { + IMPL.setImportantForAccessibility(view, mode); + } + + /** + * Performs the specified accessibility action on the view. For + * possible accessibility actions look at {@link AccessibilityNodeInfoCompat}. + *

    + * If an {@link AccessibilityDelegateCompat} has been specified via calling + * {@link #setAccessibilityDelegate(View, AccessibilityDelegateCompat)} its + * {@link AccessibilityDelegateCompat#performAccessibilityAction(View, int, Bundle)} + * is responsible for handling this call. + *

    + * + * @param action The action to perform. + * @param arguments Optional action arguments. + * @return Whether the action was performed. + */ + public static boolean performAccessibilityAction(View view, int action, Bundle arguments) { + return IMPL.performAccessibilityAction(view, action, arguments); + } + + /** + * Gets the provider for managing a virtual view hierarchy rooted at this View + * and reported to {@link android.accessibilityservice.AccessibilityService}s + * that explore the window content. + *

    + * If this method returns an instance, this instance is responsible for managing + * {@link AccessibilityNodeInfoCompat}s describing the virtual sub-tree rooted at + * this View including the one representing the View itself. Similarly the returned + * instance is responsible for performing accessibility actions on any virtual + * view or the root view itself. + *

    + *

    + * If an {@link AccessibilityDelegateCompat} has been specified via calling + * {@link #setAccessibilityDelegate(View, AccessibilityDelegateCompat)} its + * {@link AccessibilityDelegateCompat#getAccessibilityNodeProvider(View)} + * is responsible for handling this call. + *

    + * + * @param view The view whose property to get. + * @return The provider. + * + * @see AccessibilityNodeProviderCompat + */ + public static AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View view) { + return IMPL.getAccessibilityNodeProvider(view); + } + + /** + * The opacity of the view. This is a value from 0 to 1, where 0 means the view is + * completely transparent and 1 means the view is completely opaque. + * + *

    By default this is 1.0f. Prior to API 11, the returned value is always 1.0f. + * @return The opacity of the view. + */ + public static float getAlpha(View view) { + return IMPL.getAlpha(view); + } + + /** + *

    Specifies the type of layer backing this view. The layer can be + * {@link #LAYER_TYPE_NONE disabled}, {@link #LAYER_TYPE_SOFTWARE software} or + * {@link #LAYER_TYPE_HARDWARE hardware}.

    + * + *

    A layer is associated with an optional {@link android.graphics.Paint} + * instance that controls how the layer is composed on screen. The following + * properties of the paint are taken into account when composing the layer:

    + *
      + *
    • {@link android.graphics.Paint#getAlpha() Translucency (alpha)}
    • + *
    • {@link android.graphics.Paint#getXfermode() Blending mode}
    • + *
    • {@link android.graphics.Paint#getColorFilter() Color filter}
    • + *
    + * + *

    If this view has an alpha value set to < 1.0 by calling + * setAlpha(float), the alpha value of the layer's paint is replaced by + * this view's alpha value. Calling setAlpha(float) is therefore + * equivalent to setting a hardware layer on this view and providing a paint with + * the desired alpha value.

    + * + *

    Refer to the documentation of {@link #LAYER_TYPE_NONE disabled}, + * {@link #LAYER_TYPE_SOFTWARE software} and {@link #LAYER_TYPE_HARDWARE hardware} + * for more information on when and how to use layers.

    + * + * @param layerType The ype of layer to use with this view, must be one of + * {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or + * {@link #LAYER_TYPE_HARDWARE} + * @param paint The paint used to compose the layer. This argument is optional + * and can be null. It is ignored when the layer type is + * {@link #LAYER_TYPE_NONE} + * + * @param view View to set the layer type for + * @param layerType The type of layer to use with this view, must be one of + * {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or + * {@link #LAYER_TYPE_HARDWARE} + * @param paint The paint used to compose the layer. This argument is optional + * and can be null. It is ignored when the layer type is + * {@link #LAYER_TYPE_NONE} + */ + public static void setLayerType(View view, @LayerType int layerType, Paint paint) { + IMPL.setLayerType(view, layerType, paint); + } + + /** + * Indicates what type of layer is currently associated with this view. By default + * a view does not have a layer, and the layer type is {@link #LAYER_TYPE_NONE}. + * Refer to the documentation of + * {@link #setLayerType(android.view.View, int, android.graphics.Paint)} + * for more information on the different types of layers. + * + * @param view The view to fetch the layer type from + * @return {@link #LAYER_TYPE_NONE}, {@link #LAYER_TYPE_SOFTWARE} or + * {@link #LAYER_TYPE_HARDWARE} + * + * @see #setLayerType(android.view.View, int, android.graphics.Paint) + * @see #LAYER_TYPE_NONE + * @see #LAYER_TYPE_SOFTWARE + * @see #LAYER_TYPE_HARDWARE + */ + @LayerType + public static int getLayerType(View view) { + return IMPL.getLayerType(view); + } + + /** + * Gets the id of a view for which a given view serves as a label for + * accessibility purposes. + * + * @param view The view on which to invoke the corresponding method. + * @return The labeled view id. + */ + public static int getLabelFor(View view) { + return IMPL.getLabelFor(view); + } + + /** + * Sets the id of a view for which a given view serves as a label for + * accessibility purposes. + * + * @param view The view on which to invoke the corresponding method. + * @param labeledId The labeled view id. + */ + public static void setLabelFor(View view, @IdRes int labeledId) { + IMPL.setLabelFor(view, labeledId); + } + + /** + * Updates the {@link Paint} object used with the current layer (used only if the current + * layer type is not set to {@link #LAYER_TYPE_NONE}). Changed properties of the Paint + * provided to {@link #setLayerType(android.view.View, int, android.graphics.Paint)} + * will be used the next time the View is redrawn, but + * {@link #setLayerPaint(android.view.View, android.graphics.Paint)} + * must be called to ensure that the view gets redrawn immediately. + * + *

    A layer is associated with an optional {@link android.graphics.Paint} + * instance that controls how the layer is composed on screen. The following + * properties of the paint are taken into account when composing the layer:

    + *
      + *
    • {@link android.graphics.Paint#getAlpha() Translucency (alpha)}
    • + *
    • {@link android.graphics.Paint#getXfermode() Blending mode}
    • + *
    • {@link android.graphics.Paint#getColorFilter() Color filter}
    • + *
    + * + *

    If this view has an alpha value set to < 1.0 by calling + * View#setAlpha(float), the alpha value of the layer's paint is replaced by + * this view's alpha value. Calling View#setAlpha(float) is therefore + * equivalent to setting a hardware layer on this view and providing a paint with + * the desired alpha value.

    + * + * @param view View to set a layer paint for + * @param paint The paint used to compose the layer. This argument is optional + * and can be null. It is ignored when the layer type is + * {@link #LAYER_TYPE_NONE} + * + * @see #setLayerType(View, int, android.graphics.Paint) + */ + public static void setLayerPaint(View view, Paint paint) { + IMPL.setLayerPaint(view, paint); + } + + /** + * Returns the resolved layout direction for this view. + * + * @param view View to get layout direction for + * @return {@link #LAYOUT_DIRECTION_RTL} if the layout direction is RTL or returns + * {@link #LAYOUT_DIRECTION_LTR} if the layout direction is not RTL. + * + * For compatibility, this will return {@link #LAYOUT_DIRECTION_LTR} if API version + * is lower than Jellybean MR1 (API 17) + */ + @ResolvedLayoutDirectionMode + public static int getLayoutDirection(View view) { + return IMPL.getLayoutDirection(view); + } + + /** + * Set the layout direction for this view. This will propagate a reset of layout direction + * resolution to the view's children and resolve layout direction for this view. + * + * @param view View to set layout direction for + * @param layoutDirection the layout direction to set. Should be one of: + * + * {@link #LAYOUT_DIRECTION_LTR}, + * {@link #LAYOUT_DIRECTION_RTL}, + * {@link #LAYOUT_DIRECTION_INHERIT}, + * {@link #LAYOUT_DIRECTION_LOCALE}. + * + * Resolution will be done if the value is set to LAYOUT_DIRECTION_INHERIT. The resolution + * proceeds up the parent chain of the view to get the value. If there is no parent, then it + * will return the default {@link #LAYOUT_DIRECTION_LTR}. + */ + public static void setLayoutDirection(View view, @LayoutDirectionMode int layoutDirection) { + IMPL.setLayoutDirection(view, layoutDirection); + } + + /** + * Gets the parent for accessibility purposes. Note that the parent for + * accessibility is not necessary the immediate parent. It is the first + * predecessor that is important for accessibility. + * + * @param view View to retrieve parent for + * @return The parent for use in accessibility inspection + */ + public static ViewParent getParentForAccessibility(View view) { + return IMPL.getParentForAccessibility(view); + } + + /** + * Indicates whether this View is opaque. An opaque View guarantees that it will + * draw all the pixels overlapping its bounds using a fully opaque color. + * + * On API 7 and above this will call View's true isOpaque method. On previous platform + * versions it will check the opacity of the view's background drawable if present. + * + * @return True if this View is guaranteed to be fully opaque, false otherwise. + */ + public static boolean isOpaque(View view) { + return IMPL.isOpaque(view); + } + + /** + * Utility to reconcile a desired size and state, with constraints imposed + * by a MeasureSpec. Will take the desired size, unless a different size + * is imposed by the constraints. The returned value is a compound integer, + * with the resolved size in the {@link #MEASURED_SIZE_MASK} bits and + * optionally the bit {@link #MEASURED_STATE_TOO_SMALL} set if the resulting + * size is smaller than the size the view wants to be. + * + * @param size How big the view wants to be + * @param measureSpec Constraints imposed by the parent + * @return Size information bit mask as defined by + * {@link #MEASURED_SIZE_MASK} and {@link #MEASURED_STATE_TOO_SMALL}. + */ + public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { + return IMPL.resolveSizeAndState(size, measureSpec, childMeasuredState); + } + + /** + * Return the full width measurement information for this view as computed + * by the most recent call to {@link android.view.View#measure(int, int)}. + * This result is a bit mask as defined by {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. + * This should be used during measurement and layout calculations only. Use + * {@link android.view.View#getWidth()} to see how wide a view is after layout. + * + * @return The measured width of this view as a bit mask. + */ + public static int getMeasuredWidthAndState(View view) { + return IMPL.getMeasuredWidthAndState(view); + } + + /** + * Return the full height measurement information for this view as computed + * by the most recent call to {@link android.view.View#measure(int, int)}. + * This result is a bit mask as defined by {@link #MEASURED_SIZE_MASK} and + * {@link #MEASURED_STATE_TOO_SMALL}. + * This should be used during measurement and layout calculations only. Use + * {@link android.view.View#getHeight()} to see how wide a view is after layout. + * + * @return The measured width of this view as a bit mask. + */ + public static int getMeasuredHeightAndState(View view) { + return IMPL.getMeasuredHeightAndState(view); + } + + /** + * Return only the state bits of {@link #getMeasuredWidthAndState} + * and {@link #getMeasuredHeightAndState}, combined into one integer. + * The width component is in the regular bits {@link #MEASURED_STATE_MASK} + * and the height component is at the shifted bits + * {@link #MEASURED_HEIGHT_STATE_SHIFT}>>{@link #MEASURED_STATE_MASK}. + */ + public static int getMeasuredState(View view) { + return IMPL.getMeasuredState(view); + } + + /** + * Merge two states as returned by {@link #getMeasuredState(View)}. + * @param curState The current state as returned from a view or the result + * of combining multiple views. + * @param newState The new view state to combine. + * @return Returns a new integer reflecting the combination of the two + * states. + */ + public static int combineMeasuredStates(int curState, int newState) { + return IMPL.combineMeasuredStates(curState, newState); + } + + /** + * Gets the live region mode for the specified View. + * + * @param view The view from which to obtain the live region mode + * @return The live region mode for the view. + * + * @see ViewCompat#setAccessibilityLiveRegion(View, int) + */ + @AccessibilityLiveRegion + public static int getAccessibilityLiveRegion(View view) { + return IMPL.getAccessibilityLiveRegion(view); + } + + /** + * Sets the live region mode for the specified view. This indicates to + * accessibility services whether they should automatically notify the user + * about changes to the view's content description or text, or to the + * content descriptions or text of the view's children (where applicable). + *

    + * For example, in a login screen with a TextView that displays an "incorrect + * password" notification, that view should be marked as a live region with + * mode {@link #ACCESSIBILITY_LIVE_REGION_POLITE}. + *

    + * To disable change notifications for this view, use + * {@link #ACCESSIBILITY_LIVE_REGION_NONE}. This is the default live region + * mode for most views. + *

    + * To indicate that the user should be notified of changes, use + * {@link #ACCESSIBILITY_LIVE_REGION_POLITE}. + *

    + * If the view's changes should interrupt ongoing speech and notify the user + * immediately, use {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE}. + * + * @param view The view on which to set the live region mode + * @param mode The live region mode for this view, one of: + *

      + *
    • {@link #ACCESSIBILITY_LIVE_REGION_NONE} + *
    • {@link #ACCESSIBILITY_LIVE_REGION_POLITE} + *
    • {@link #ACCESSIBILITY_LIVE_REGION_ASSERTIVE} + *
    + */ + public static void setAccessibilityLiveRegion(View view, @AccessibilityLiveRegion int mode) { + IMPL.setAccessibilityLiveRegion(view, mode); + } + + /** + * Returns the start padding of the specified view depending on its resolved layout direction. + * If there are inset and enabled scrollbars, this value may include the space + * required to display the scrollbars as well. + * + * @param view The view to get padding for + * @return the start padding in pixels + */ + public static int getPaddingStart(View view) { + return IMPL.getPaddingStart(view); + } + + /** + * Returns the end padding of the specified view depending on its resolved layout direction. + * If there are inset and enabled scrollbars, this value may include the space + * required to display the scrollbars as well. + * + * @param view The view to get padding for + * @return the end padding in pixels + */ + public static int getPaddingEnd(View view) { + return IMPL.getPaddingEnd(view); + } + + /** + * Sets the relative padding. The view may add on the space required to display + * the scrollbars, depending on the style and visibility of the scrollbars. + * So the values returned from {@link #getPaddingStart}, {@link View#getPaddingTop}, + * {@link #getPaddingEnd} and {@link View#getPaddingBottom} may be different + * from the values set in this call. + * + * @param view The view on which to set relative padding + * @param start the start padding in pixels + * @param top the top padding in pixels + * @param end the end padding in pixels + * @param bottom the bottom padding in pixels + */ + public static void setPaddingRelative(View view, int start, int top, int end, int bottom) { + IMPL.setPaddingRelative(view, start, top, end, bottom); + } + + /** + * Notify a view that it is being temporarily detached. + */ + public static void dispatchStartTemporaryDetach(View view) { + IMPL.dispatchStartTemporaryDetach(view); + } + + /** + * Notify a view that its temporary detach has ended; the view is now reattached. + */ + public static void dispatchFinishTemporaryDetach(View view) { + IMPL.dispatchFinishTemporaryDetach(view); + } + + /** + * The horizontal location of this view relative to its {@link View#getLeft() left} position. + * This position is post-layout, in addition to wherever the object's + * layout placed it. + * + *

    Prior to API 11 this will return 0.

    + * + * @return The horizontal position of this view relative to its left position, in pixels. + */ + public static float getTranslationX(View view) { + return IMPL.getTranslationX(view); + } + + /** + * The vertical location of this view relative to its {@link View#getTop() left} position. + * This position is post-layout, in addition to wherever the object's + * layout placed it. + * + *

    Prior to API 11 this will return 0.

    + * + * @return The vertical position of this view relative to its top position, in pixels. + */ + public static float getTranslationY(View view) { + return IMPL.getTranslationY(view); + } + + /** + * Returns the minimum width of the view. + * + *

    Prior to API 16 this will return 0.

    + * + * @return the minimum width the view will try to be. + */ + public static int getMinimumWidth(View view) { + return IMPL.getMinimumWidth(view); + } + + /** + * Returns the minimum height of the view. + * + *

    Prior to API 16 this will return 0.

    + * + * @return the minimum height the view will try to be. + */ + public static int getMinimumHeight(View view) { + return IMPL.getMinimumHeight(view); + } + + /** + * This method returns a ViewPropertyAnimator object, which can be used to animate + * specific properties on this View. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @return ViewPropertyAnimator The ViewPropertyAnimator associated with this View. + */ + public static ViewPropertyAnimatorCompat animate(View view) { + return IMPL.animate(view); + } + + /** + * Sets the horizontal location of this view relative to its left position. + * This effectively positions the object post-layout, in addition to wherever the object's + * layout placed it. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The horizontal position of this view relative to its left position, + * in pixels. + */ + public static void setTranslationX(View view, float value) { + IMPL.setTranslationX(view, value); + } + + /** + * Sets the vertical location of this view relative to its top position. + * This effectively positions the object post-layout, in addition to wherever the object's + * layout placed it. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The vertical position of this view relative to its top position, + * in pixels. + * + * @attr ref android.R.styleable#View_translationY + */ + public static void setTranslationY(View view, float value) { + IMPL.setTranslationY(view, value); + } + + /** + *

    Sets the opacity of the view. This is a value from 0 to 1, where 0 means the view is + * completely transparent and 1 means the view is completely opaque.

    + * + *

    Note that setting alpha to a translucent value (0 < alpha < 1) can have significant + * performance implications, especially for large views. It is best to use the alpha property + * sparingly and transiently, as in the case of fading animations.

    + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The opacity of the view. + */ + public static void setAlpha(View view, float value) { + IMPL.setAlpha(view, value); + } + + /** + * Sets the visual x position of this view, in pixels. This is equivalent to setting the + * {@link #setTranslationX(View, float) translationX} property to be the difference between + * the x value passed in and the current left property of the view as determined + * by the layout bounds. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The visual x position of this view, in pixels. + */ + public static void setX(View view, float value) { + IMPL.setX(view, value); + } + + /** + * Sets the visual y position of this view, in pixels. This is equivalent to setting the + * {@link #setTranslationY(View, float) translationY} property to be the difference between + * the y value passed in and the current top property of the view as determined by the + * layout bounds. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The visual y position of this view, in pixels. + */ + public static void setY(View view, float value) { + IMPL.setY(view, value); + } + + /** + * Sets the degrees that the view is rotated around the pivot point. Increasing values + * result in clockwise rotation. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The degrees of rotation. + */ + public static void setRotation(View view, float value) { + IMPL.setRotation(view, value); + } + + /** + * Sets the degrees that the view is rotated around the horizontal axis through the pivot point. + * Increasing values result in clockwise rotation from the viewpoint of looking down the + * x axis. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The degrees of X rotation. + */ + public static void setRotationX(View view, float value) { + IMPL.setRotationX(view, value); + } + + /** + * Sets the degrees that the view is rotated around the vertical axis through the pivot point. + * Increasing values result in counter-clockwise rotation from the viewpoint of looking + * down the y axis. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The degrees of Y rotation. + */ + public static void setRotationY(View view, float value) { + IMPL.setRotationY(view, value); + } + + /** + * Sets the amount that the view is scaled in x around the pivot point, as a proportion of + * the view's unscaled width. A value of 1 means that no scaling is applied. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The scaling factor. + */ + public static void setScaleX(View view, float value) { + IMPL.setScaleX(view, value); + } + + /** + * Sets the amount that the view is scaled in Y around the pivot point, as a proportion of + * the view's unscaled width. A value of 1 means that no scaling is applied. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The scaling factor. + */ + public static void setScaleY(View view, float value) { + IMPL.setScaleY(view, value); + } + + /** + * The x location of the point around which the view is + * {@link #setRotation(View, float) rotated} and {@link #setScaleX(View, float) scaled}. + * + *

    Prior to API 11 this will have no effect.

    + * + */ + public static float getPivotX(View view) { + return IMPL.getPivotX(view); + } + + /** + * Sets the x location of the point around which the view is + * {@link #setRotation(View, float) rotated} and {@link #setScaleX(View, float) scaled}. + * By default, the pivot point is centered on the object. + * Setting this property disables this behavior and causes the view to use only the + * explicitly set pivotX and pivotY values. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The x location of the pivot point. + */ + public static void setPivotX(View view, float value) { + IMPL.setPivotX(view, value); + } + + /** + * The y location of the point around which the view is {@link #setRotation(View, + * float) rotated} and {@link #setScaleY(View, float) scaled}. + * + *

    Prior to API 11 this will return 0.

    + * + * @return The y location of the pivot point. + */ + public static float getPivotY(View view) { + return IMPL.getPivotY(view); + } + + /** + * Sets the y location of the point around which the view is + * {@link #setRotation(View, float) rotated} and {@link #setScaleY(View, float) scaled}. + * By default, the pivot point is centered on the object. + * Setting this property disables this behavior and causes the view to use only the + * explicitly set pivotX and pivotY values. + * + *

    Prior to API 11 this will have no effect.

    + * + * @param value The y location of the pivot point. + */ + public static void setPivotY(View view, float value) { + IMPL.setPivotX(view, value); + } + + public static float getRotation(View view) { + return IMPL.getRotation(view); + } + + public static float getRotationX(View view) { + return IMPL.getRotationX(view); + } + + public static float getRotationY(View view) { + return IMPL.getRotationY(view); + } + + public static float getScaleX(View view) { + return IMPL.getScaleX(view); + } + + public static float getScaleY(View view) { + return IMPL.getScaleY(view); + } + + public static float getX(View view) { + return IMPL.getX(view); + } + + public static float getY(View view) { + return IMPL.getY(view); + } + + /** + * Sets the base elevation of this view, in pixels. + */ + public static void setElevation(View view, float elevation) { + IMPL.setElevation(view, elevation); + } + + /** + * The base elevation of this view relative to its parent, in pixels. + * + * @return The base depth position of the view, in pixels. + */ + public static float getElevation(View view) { + return IMPL.getElevation(view); + } + + /** + * Sets the depth location of this view relative to its {@link #getElevation(View) elevation}. + */ + public static void setTranslationZ(View view, float translationZ) { + IMPL.setTranslationZ(view, translationZ); + } + + /** + * The depth location of this view relative to its {@link #getElevation(View) elevation}. + * + * @return The depth of this view relative to its elevation. + */ + public static float getTranslationZ(View view) { + return IMPL.getTranslationZ(view); + } + + /** + * Sets the name of the View to be used to identify Views in Transitions. + * Names should be unique in the View hierarchy. + * + * @param view The View against which to invoke the method. + * @param transitionName The name of the View to uniquely identify it for Transitions. + */ + public static void setTransitionName(View view, String transitionName) { + IMPL.setTransitionName(view, transitionName); + } + + /** + * Returns the name of the View to be used to identify Views in Transitions. + * Names should be unique in the View hierarchy. + * + *

    This returns null if the View has not been given a name.

    + * + * @param view The View against which to invoke the method. + * @return The name used of the View to be used to identify Views in Transitions or null + * if no name has been given. + */ + public static String getTransitionName(View view) { + return IMPL.getTransitionName(view); + } + + /** + * Returns the current system UI visibility that is currently set for the entire window. + */ + public static int getWindowSystemUiVisibility(View view) { + return IMPL.getWindowSystemUiVisibility(view); + } + + /** + * Ask that a new dispatch of {@code View.onApplyWindowInsets(WindowInsets)} be performed. This + * falls back to {@code View.requestFitSystemWindows()} where available. + */ + public static void requestApplyInsets(View view) { + IMPL.requestApplyInsets(view); + } + + /** + * Tells the ViewGroup whether to draw its children in the order defined by the method + * {@code ViewGroup.getChildDrawingOrder(int, int)}. + * + * @param enabled true if the order of the children when drawing is determined by + * {@link ViewGroup#getChildDrawingOrder(int, int)}, false otherwise + * + *

    Prior to API 7 this will have no effect.

    + */ + public static void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled) { + IMPL.setChildrenDrawingOrderEnabled(viewGroup, enabled); + } + + /** + * Returns true if this view should adapt to fit system window insets. This method will always + * return false before API 16 (Jellybean). + */ + public static boolean getFitsSystemWindows(View v) { + return IMPL.getFitsSystemWindows(v); + } + + /** + * Sets whether or not this view should account for system screen decorations + * such as the status bar and inset its content; that is, controlling whether + * the default implementation of {@link View#fitSystemWindows(Rect)} will be + * executed. See that method for more details. + */ + public static void setFitsSystemWindows(View view, boolean fitSystemWindows) { + IMPL.setFitsSystemWindows(view, fitSystemWindows); + } + + /** + * On API 11 devices and above, call Drawable.jumpToCurrentState() + * on all Drawable objects associated with this view. + *

    + * On API 21 and above, also calls StateListAnimator#jumpToCurrentState() + * if there is a StateListAnimator attached to this view. + */ + public static void jumpDrawablesToCurrentState(View v) { + IMPL.jumpDrawablesToCurrentState(v); + } + + /** + * Set an {@link OnApplyWindowInsetsListener} to take over the policy for applying + * window insets to this view. This will only take effect on devices with API 21 or above. + */ + public static void setOnApplyWindowInsetsListener(View v, + OnApplyWindowInsetsListener listener) { + IMPL.setOnApplyWindowInsetsListener(v, listener); + } + + /** + * Called when the view should apply {@link WindowInsetsCompat} according to its internal policy. + * + *

    Clients may supply an {@link OnApplyWindowInsetsListener} to a view. If one is set + * it will be called during dispatch instead of this method. The listener may optionally + * call this method from its own implementation if it wishes to apply the view's default + * insets policy in addition to its own.

    + * + * @param view The View against which to invoke the method. + * @param insets Insets to apply + * @return The supplied insets with any applied insets consumed + */ + public static WindowInsetsCompat onApplyWindowInsets(View view, WindowInsetsCompat insets) { + return IMPL.onApplyWindowInsets(view, insets); + } + + /** + * Request to apply the given window insets to this view or another view in its subtree. + * + *

    This method should be called by clients wishing to apply insets corresponding to areas + * obscured by window decorations or overlays. This can include the status and navigation bars, + * action bars, input methods and more. New inset categories may be added in the future. + * The method returns the insets provided minus any that were applied by this view or its + * children.

    + * + * @param insets Insets to apply + * @return The provided insets minus the insets that were consumed + */ + public static WindowInsetsCompat dispatchApplyWindowInsets(View view, + WindowInsetsCompat insets) { + return IMPL.dispatchApplyWindowInsets(view, insets); + } + + /** + * Controls whether the entire hierarchy under this view will save its + * state when a state saving traversal occurs from its parent. + * + * @param enabled Set to false to disable state saving, or true + * (the default) to allow it. + */ + public static void setSaveFromParentEnabled(View v, boolean enabled) { + IMPL.setSaveFromParentEnabled(v, enabled); + } + + /** + * Changes the activated state of this view. A view can be activated or not. + * Note that activation is not the same as selection. Selection is + * a transient property, representing the view (hierarchy) the user is + * currently interacting with. Activation is a longer-term state that the + * user can move views in and out of. + * + * @param activated true if the view must be activated, false otherwise + */ + public static void setActivated(View view, boolean activated) { + IMPL.setActivated(view, activated); + } + + /** + * Return if the padding as been set through relative values + * {@code View.setPaddingRelative(int, int, int, int)} or thru + * + * @return true if the padding is relative or false if it is not. + */ + public static boolean isPaddingRelative(View view) { + return IMPL.isPaddingRelative(view); + } + + /** + * Return the tint applied to the background drawable, if specified. + *

    + * Only returns meaningful info when running on API v21 or newer, or if {@code view} + * implements the {@code TintableBackgroundView} interface. + */ + public static ColorStateList getBackgroundTintList(View view) { + return IMPL.getBackgroundTintList(view); + } + + /** + * Applies a tint to the background drawable. + *

    + * This will always take effect when running on API v21 or newer. When running on platforms + * previous to API v21, it will only take effect if {@code view} implement the + * {@code TintableBackgroundView} interface. + */ + public static void setBackgroundTintList(View view, ColorStateList tintList) { + IMPL.setBackgroundTintList(view, tintList); + } + + /** + * Return the blending mode used to apply the tint to the background + * drawable, if specified. + *

    + * Only returns meaningful info when running on API v21 or newer, or if {@code view} + * implements the {@code TintableBackgroundView} interface. + */ + public static PorterDuff.Mode getBackgroundTintMode(View view) { + return IMPL.getBackgroundTintMode(view); + } + + /** + * Specifies the blending mode used to apply the tint specified by + * {@link #setBackgroundTintList(android.view.View, android.content.res.ColorStateList)} to + * the background drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}. + *

    + * This will always take effect when running on API v21 or newer. When running on platforms + * previous to API v21, it will only take effect if {@code view} implement the + * {@code TintableBackgroundView} interface. + */ + public static void setBackgroundTintMode(View view, PorterDuff.Mode mode) { + IMPL.setBackgroundTintMode(view, mode); + } + // TODO: getters for various view properties (rotation, etc) + + /** + * Enable or disable nested scrolling for this view. + * + *

    If this property is set to true the view will be permitted to initiate nested + * scrolling operations with a compatible parent view in the current hierarchy. If this + * view does not implement nested scrolling this will have no effect. Disabling nested scrolling + * while a nested scroll is in progress has the effect of + * {@link #stopNestedScroll(View) stopping} the nested scroll.

    + * + * @param enabled true to enable nested scrolling, false to disable + * + * @see #isNestedScrollingEnabled(View) + */ + public static void setNestedScrollingEnabled(View view, boolean enabled) { + IMPL.setNestedScrollingEnabled(view, enabled); + } + + /** + * Returns true if nested scrolling is enabled for this view. + * + *

    If nested scrolling is enabled and this View class implementation supports it, + * this view will act as a nested scrolling child view when applicable, forwarding data + * about the scroll operation in progress to a compatible and cooperating nested scrolling + * parent.

    + * + * @return true if nested scrolling is enabled + * + * @see #setNestedScrollingEnabled(View, boolean) + */ + public static boolean isNestedScrollingEnabled(View view) { + return IMPL.isNestedScrollingEnabled(view); + } + + /** + * Begin a nestable scroll operation along the given axes. + * + *

    A view starting a nested scroll promises to abide by the following contract:

    + * + *

    The view will call startNestedScroll upon initiating a scroll operation. In the case + * of a touch scroll this corresponds to the initial {@link MotionEvent#ACTION_DOWN}. + * In the case of touch scrolling the nested scroll will be terminated automatically in + * the same manner as {@link ViewParent#requestDisallowInterceptTouchEvent(boolean)}. + * In the event of programmatic scrolling the caller must explicitly call + * {@link #stopNestedScroll(View)} to indicate the end of the nested scroll.

    + * + *

    If startNestedScroll returns true, a cooperative parent was found. + * If it returns false the caller may ignore the rest of this contract until the next scroll. + * Calling startNestedScroll while a nested scroll is already in progress will return true.

    + * + *

    At each incremental step of the scroll the caller should invoke + * {@link #dispatchNestedPreScroll(View, int, int, int[], int[]) dispatchNestedPreScroll} + * once it has calculated the requested scrolling delta. If it returns true the nested scrolling + * parent at least partially consumed the scroll and the caller should adjust the amount it + * scrolls by.

    + * + *

    After applying the remainder of the scroll delta the caller should invoke + * {@link #dispatchNestedScroll(View, int, int, int, int, int[]) dispatchNestedScroll}, passing + * both the delta consumed and the delta unconsumed. A nested scrolling parent may treat + * these values differently. See + * {@link NestedScrollingParent#onNestedScroll(View, int, int, int, int)}. + *

    + * + * @param axes Flags consisting of a combination of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL} + * and/or {@link ViewCompat#SCROLL_AXIS_VERTICAL}. + * @return true if a cooperative parent was found and nested scrolling has been enabled for + * the current gesture. + * + * @see #stopNestedScroll(View) + * @see #dispatchNestedPreScroll(View, int, int, int[], int[]) + * @see #dispatchNestedScroll(View, int, int, int, int, int[]) + */ + public static boolean startNestedScroll(View view, int axes) { + return IMPL.startNestedScroll(view, axes); + } + + /** + * Stop a nested scroll in progress. + * + *

    Calling this method when a nested scroll is not currently in progress is harmless.

    + * + * @see #startNestedScroll(View, int) + */ + public static void stopNestedScroll(View view) { + IMPL.stopNestedScroll(view); + } + + /** + * Returns true if this view has a nested scrolling parent. + * + *

    The presence of a nested scrolling parent indicates that this view has initiated + * a nested scroll and it was accepted by an ancestor view further up the view hierarchy.

    + * + * @return whether this view has a nested scrolling parent + */ + public static boolean hasNestedScrollingParent(View view) { + return IMPL.hasNestedScrollingParent(view); + } + + /** + * Dispatch one step of a nested scroll in progress. + * + *

    Implementations of views that support nested scrolling should call this to report + * info about a scroll in progress to the current nested scrolling parent. If a nested scroll + * is not currently in progress or nested scrolling is not + * {@link #isNestedScrollingEnabled(View) enabled} for this view this method does nothing.

    + * + *

    Compatible View implementations should also call + * {@link #dispatchNestedPreScroll(View, int, int, int[], int[]) dispatchNestedPreScroll} before + * consuming a component of the scroll event themselves.

    + * + * @param dxConsumed Horizontal distance in pixels consumed by this view during this scroll step + * @param dyConsumed Vertical distance in pixels consumed by this view during this scroll step + * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param dyUnconsumed Horizontal scroll distance in pixels not consumed by this view + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the event was dispatched, false if it could not be dispatched. + * @see #dispatchNestedPreScroll(View, int, int, int[], int[]) + */ + public static boolean dispatchNestedScroll(View view, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + return IMPL.dispatchNestedScroll(view, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow); + } + + /** + * Dispatch one step of a nested scroll in progress before this view consumes any portion of it. + * + *

    Nested pre-scroll events are to nested scroll events what touch intercept is to touch. + * dispatchNestedPreScroll offers an opportunity for the parent view in a nested + * scrolling operation to consume some or all of the scroll operation before the child view + * consumes it.

    + * + * @param dx Horizontal scroll distance in pixels + * @param dy Vertical scroll distance in pixels + * @param consumed Output. If not null, consumed[0] will contain the consumed component of dx + * and consumed[1] the consumed dy. + * @param offsetInWindow Optional. If not null, on return this will contain the offset + * in local view coordinates of this view from before this operation + * to after it completes. View implementations may use this to adjust + * expected input coordinate tracking. + * @return true if the parent consumed some or all of the scroll delta + * @see #dispatchNestedScroll(View, int, int, int, int, int[]) + */ + public static boolean dispatchNestedPreScroll(View view, int dx, int dy, int[] consumed, + int[] offsetInWindow) { + return IMPL.dispatchNestedPreScroll(view, dx, dy, consumed, offsetInWindow); + } + + /** + * Dispatch a fling to a nested scrolling parent. + * + *

    This method should be used to indicate that a nested scrolling child has detected + * suitable conditions for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

    + * + *

    If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.

    + * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @param consumed true if the child consumed the fling, false otherwise + * @return true if the nested scrolling parent consumed or otherwise reacted to the fling + */ + public static boolean dispatchNestedFling(View view, float velocityX, float velocityY, + boolean consumed) { + return IMPL.dispatchNestedFling(view, velocityX, velocityY, consumed); + } + + /** + * Dispatch a fling to a nested scrolling parent before it is processed by this view. + * + *

    Nested pre-fling events are to nested fling events what touch intercept is to touch + * and what nested pre-scroll is to nested scroll. dispatchNestedPreFling + * offsets an opportunity for the parent view in a nested fling to fully consume the fling + * before the child view consumes it. If this method returns true, a nested + * parent view consumed the fling and this view should not scroll as a result.

    + * + *

    For a better user experience, only one view in a nested scrolling chain should consume + * the fling at a time. If a parent view consumed the fling this method will return false. + * Custom view implementations should account for this in two ways:

    + * + *
      + *
    • If a custom view is paged and needs to settle to a fixed page-point, do not + * call dispatchNestedPreFling; consume the fling and settle to a valid + * position regardless.
    • + *
    • If a nested parent does consume the fling, this view should not scroll at all, + * even to settle back to a valid idle position.
    • + *
    + * + *

    Views should also not offer fling velocities to nested parent views along an axis + * where scrolling is not currently supported; a {@link android.widget.ScrollView ScrollView} + * should not offer a horizontal fling velocity to its parents since scrolling along that + * axis is not permitted and carrying velocity along that motion does not make sense.

    + * + * @param velocityX Horizontal fling velocity in pixels per second + * @param velocityY Vertical fling velocity in pixels per second + * @return true if a nested scrolling parent consumed the fling + */ + public static boolean dispatchNestedPreFling(View view, float velocityX, float velocityY) { + return IMPL.dispatchNestedPreFling(view, velocityX, velocityY); + } + + /** + * Returns true if {@code view} has been through at least one layout since it + * was last attached to or detached from a window. + */ + public static boolean isLaidOut(View view) { + return IMPL.isLaidOut(view); + } + + /** + * The visual z position of this view, in pixels. This is equivalent to the + * {@link #setTranslationZ(View, float) translationZ} property plus the current + * {@link #getElevation(View) elevation} property. + * + * @return The visual z position of this view, in pixels. + */ + public static float getZ(View view) { + return IMPL.getZ(view); + } + + /** + * Offset this view's vertical location by the specified number of pixels. + * + * @param offset the number of pixels to offset the view by + */ + public static void offsetTopAndBottom(View view, int offset) { + view.offsetTopAndBottom(offset); + + if (offset != 0 && Build.VERSION.SDK_INT < 11) { + // We need to manually invalidate pre-honeycomb + view.invalidate(); + } + } + /** + * Offset this view's horizontal location by the specified amount of pixels. + * + * @param offset the number of pixels to offset the view by + */ + public static void offsetLeftAndRight(View view, int offset) { + view.offsetLeftAndRight(offset); + + if (offset != 0 && Build.VERSION.SDK_INT < 11) { + // We need to manually invalidate pre-honeycomb + view.invalidate(); + } + } + + /** + * Returns true if the provided view is currently attached to a window. + */ + public static boolean isAttachedToWindow(View view) { + return IMPL.isAttachedToWindow(view); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatBase.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatBase.java new file mode 100644 index 00000000000..33706592496 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatBase.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.view.View; + +import java.lang.reflect.Field; + +class ViewCompatBase { + + private static final String TAG = "ViewCompatBase"; + + private static Field sMinWidthField; + private static boolean sMinWidthFieldFetched; + private static Field sMinHeightField; + private static boolean sMinHeightFieldFetched; + + static ColorStateList getBackgroundTintList(View view) { + return (view instanceof TintableBackgroundView) + ? ((TintableBackgroundView) view).getSupportBackgroundTintList() + : null; + } + + static void setBackgroundTintList(View view, ColorStateList tintList) { + if (view instanceof TintableBackgroundView) { + ((TintableBackgroundView) view).setSupportBackgroundTintList(tintList); + } + } + + static PorterDuff.Mode getBackgroundTintMode(View view) { + return (view instanceof TintableBackgroundView) + ? ((TintableBackgroundView) view).getSupportBackgroundTintMode() + : null; + } + + static void setBackgroundTintMode(View view, PorterDuff.Mode mode) { + if (view instanceof TintableBackgroundView) { + ((TintableBackgroundView) view).setSupportBackgroundTintMode(mode); + } + } + + static boolean isLaidOut(View view) { + return view.getWidth() > 0 && view.getHeight() > 0; + } + + static int getMinimumWidth(View view) { + if (!sMinWidthFieldFetched) { + try { + sMinWidthField = View.class.getDeclaredField("mMinWidth"); + sMinWidthField.setAccessible(true); + } catch (NoSuchFieldException e) { + // Couldn't find the field. Abort! + } + sMinWidthFieldFetched = true; + } + + if (sMinWidthField != null) { + try { + return (int) sMinWidthField.get(view); + } catch (Exception e) { + // Field get failed. Oh well... + } + } + + // We failed, return 0 + return 0; + } + + static int getMinimumHeight(View view) { + if (!sMinHeightFieldFetched) { + try { + sMinHeightField = View.class.getDeclaredField("mMinHeight"); + sMinHeightField.setAccessible(true); + } catch (NoSuchFieldException e) { + // Couldn't find the field. Abort! + } + sMinHeightFieldFetched = true; + } + + if (sMinHeightField != null) { + try { + return (int) sMinHeightField.get(view); + } catch (Exception e) { + // Field get failed. Oh well... + } + } + + // We failed, return 0 + return 0; + } + + static boolean isAttachedToWindow(View view) { + return view.getWindowToken() != null; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatEclairMr1.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatEclairMr1.java new file mode 100644 index 00000000000..971b70d5f1f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatEclairMr1.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +class ViewCompatEclairMr1 { + public static final String TAG = "ViewCompat"; + + private static Method sChildrenDrawingOrderMethod; + + public static boolean isOpaque(View view) { + return view.isOpaque(); + } + + public static void setChildrenDrawingOrderEnabled(ViewGroup viewGroup, boolean enabled) { + if (sChildrenDrawingOrderMethod == null) { + try { + sChildrenDrawingOrderMethod = ViewGroup.class + .getDeclaredMethod("setChildrenDrawingOrderEnabled", boolean.class); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Unable to find childrenDrawingOrderEnabled", e); + } + sChildrenDrawingOrderMethod.setAccessible(true); + } + try { + sChildrenDrawingOrderMethod.invoke(viewGroup, enabled); + } catch (IllegalAccessException e) { + Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e); + } catch (InvocationTargetException e) { + Log.e(TAG, "Unable to invoke childrenDrawingOrderEnabled", e); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatGingerbread.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatGingerbread.java new file mode 100644 index 00000000000..f410a6941fd --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatGingerbread.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.View; + +class ViewCompatGingerbread { + public static int getOverScrollMode(View v) { + return v.getOverScrollMode(); + } + + public static void setOverScrollMode(View v, int mode) { + v.setOverScrollMode(mode); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatHC.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatHC.java new file mode 100644 index 00000000000..8b212597b71 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatHC.java @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.animation.ValueAnimator; +import android.graphics.Paint; +import android.view.View; + +class ViewCompatHC { + static long getFrameTime() { + return ValueAnimator.getFrameDelay(); + } + + public static float getAlpha(View view) { + return view.getAlpha(); + } + + public static void setLayerType(View view, int layerType, Paint paint) { + view.setLayerType(layerType, paint); + } + + public static int getLayerType(View view) { + return view.getLayerType(); + } + + public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) { + return View.resolveSizeAndState(size, measureSpec, childMeasuredState); + } + + public static int getMeasuredWidthAndState(View view) { + return view.getMeasuredWidthAndState(); + } + + public static int getMeasuredHeightAndState(View view) { + return view.getMeasuredHeightAndState(); + } + + public static int getMeasuredState(View view) { + return view.getMeasuredState(); + } + + public static float getTranslationX(View view) { + return view.getTranslationX(); + } + + public static float getTranslationY(View view) { + return view.getTranslationY(); + } + + public static float getX(View view) { + return view.getX(); + } + + public static float getY(View view) { + return view.getY(); + } + + public static float getRotation(View view) { + return view.getRotation(); + } + + public static float getRotationX(View view) { + return view.getRotationX(); + } + + public static float getRotationY(View view) { + return view.getRotationY(); + } + + public static float getScaleX(View view) { + return view.getScaleX(); + } + + public static float getScaleY(View view) { + return view.getScaleY(); + } + + public static void setTranslationX(View view, float value) { + view.setTranslationX(value); + } + + public static void setTranslationY(View view, float value) { + view.setTranslationY(value); + } + + public static void setAlpha(View view, float value) { + view.setAlpha(value); + } + + public static void setX(View view, float value) { + view.setX(value); + } + + public static void setY(View view, float value) { + view.setY(value); + } + + public static void setRotation(View view, float value) { + view.setRotation(value); + } + + public static void setRotationX(View view, float value) { + view.setRotationX(value); + } + + public static void setRotationY(View view, float value) { + view.setRotationY(value); + } + + public static void setScaleX(View view, float value) { + view.setScaleX(value); + } + + public static void setScaleY(View view, float value) { + view.setScaleY(value); + } + + public static void setPivotX(View view, float value) { + view.setPivotX(value); + } + + public static void setPivotY(View view, float value) { + view.setPivotY(value); + } + + public static float getPivotX(View view) { + return view.getPivotX(); + } + + public static float getPivotY(View view) { + return view.getPivotY(); + } + + public static void jumpDrawablesToCurrentState(View view) { + view.jumpDrawablesToCurrentState(); + } + + public static void setSaveFromParentEnabled(View view, boolean enabled) { + view.setSaveFromParentEnabled(enabled); + } + + public static void setActivated(View view, boolean activated) { + view.setActivated(activated); + } + + public static int combineMeasuredStates(int curState, int newState) { + return View.combineMeasuredStates(curState, newState); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatICS.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatICS.java new file mode 100644 index 00000000000..742c47c5867 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatICS.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.support.annotation.Nullable; +import android.view.View; +import android.view.View.AccessibilityDelegate; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * Helper for accessing newer features in View introduced in ICS. + */ +class ViewCompatICS { + + public static boolean canScrollHorizontally(View v, int direction) { + return v.canScrollHorizontally(direction); + } + + public static boolean canScrollVertically(View v, int direction) { + return v.canScrollVertically(direction); + } + + public static void setAccessibilityDelegate(View v, @Nullable Object delegate) { + v.setAccessibilityDelegate((AccessibilityDelegate) delegate); + } + + public static void onPopulateAccessibilityEvent(View v, AccessibilityEvent event) { + v.onPopulateAccessibilityEvent(event); + } + + public static void onInitializeAccessibilityEvent(View v, AccessibilityEvent event) { + v.onInitializeAccessibilityEvent(event); + } + + public static void onInitializeAccessibilityNodeInfo(View v, Object info) { + v.onInitializeAccessibilityNodeInfo((AccessibilityNodeInfo) info); + } + + public static void setFitsSystemWindows(View view, boolean fitSystemWindows) { + view.setFitsSystemWindows(fitSystemWindows); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJB.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJB.java new file mode 100644 index 00000000000..93735724a38 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJB.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.os.Bundle; +import android.view.View; +import android.view.ViewParent; + +/** + * Jellybean-specific View API access + */ +class ViewCompatJB { + + public static boolean hasTransientState(View view) { + return view.hasTransientState(); + } + + public static void setHasTransientState(View view, boolean hasTransientState) { + view.setHasTransientState(hasTransientState); + } + + public static void postInvalidateOnAnimation(View view) { + view.postInvalidateOnAnimation(); + } + + public static void postInvalidateOnAnimation(View view, int left, int top, + int right, int bottom) { + view.postInvalidate(left, top, right, bottom); + } + + public static void postOnAnimation(View view, Runnable action) { + view.postOnAnimation(action); + } + + public static void postOnAnimationDelayed(View view, Runnable action, long delayMillis) { + view.postOnAnimationDelayed(action, delayMillis); + } + + public static int getImportantForAccessibility(View view) { + return view.getImportantForAccessibility(); + } + + public static void setImportantForAccessibility(View view, int mode) { + view.setImportantForAccessibility(mode); + } + + public static boolean performAccessibilityAction(View view, int action, Bundle arguments) { + return view.performAccessibilityAction(action, arguments); + } + + public static Object getAccessibilityNodeProvider(View view) { + return view.getAccessibilityNodeProvider(); + } + + public static ViewParent getParentForAccessibility(View view) { + return view.getParentForAccessibility(); + } + + public static int getMinimumWidth(View view) { + return view.getMinimumWidth(); + } + + public static int getMinimumHeight(View view) { + return view.getMinimumHeight(); + } + + public static void requestApplyInsets(View view) { + view.requestFitSystemWindows(); + } + + public static boolean getFitsSystemWindows(View view) { + return view.getFitsSystemWindows(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJellybeanMr1.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJellybeanMr1.java new file mode 100644 index 00000000000..4865a8ba4de --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatJellybeanMr1.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.graphics.Paint; +import android.view.View; + +/** + * Jellybean MR1 - specific View API access. + */ +class ViewCompatJellybeanMr1 { + + public static int getLabelFor(View view) { + return view.getLabelFor(); + } + + public static void setLabelFor(View view, int id) { + view.setLabelFor(id); + } + + public static void setLayerPaint(View view, Paint paint) { + view.setLayerPaint(paint); + } + + public static int getLayoutDirection(View view) { + return view.getLayoutDirection(); + } + + public static void setLayoutDirection(View view, int layoutDirection) { + view.setLayoutDirection(layoutDirection); + } + + public static int getPaddingStart(View view) { + return view.getPaddingStart(); + } + + public static int getPaddingEnd(View view) { + return view.getPaddingEnd(); + } + + public static void setPaddingRelative(View view, int start, int top, int end, int bottom) { + view.setPaddingRelative(start, top, end, bottom); + } + + public static int getWindowSystemUiVisibility(View view) { + return view.getWindowSystemUiVisibility(); + } + + public static boolean isPaddingRelative(View view) { + return view.isPaddingRelative(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatKitKat.java new file mode 100644 index 00000000000..cf126d9c625 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatKitKat.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.View; + +/** + * KitKat-specific View API implementation. + */ +class ViewCompatKitKat { + public static int getAccessibilityLiveRegion(View view) { + return view.getAccessibilityLiveRegion(); + } + + public static void setAccessibilityLiveRegion(View view, int mode) { + view.setAccessibilityLiveRegion(mode); + } + + public static boolean isLaidOut(View view) { + return view.isLaidOut(); + } + + public static boolean isAttachedToWindow(View view) { + return view.isAttachedToWindow(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatLollipop.java new file mode 100644 index 00000000000..09b84b3c4bb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewCompatLollipop.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.res.ColorStateList; +import android.graphics.PorterDuff; +import android.view.View; +import android.view.WindowInsets; + +class ViewCompatLollipop { + + public static void setTransitionName(View view, String transitionName) { + view.setTransitionName(transitionName); + } + + public static String getTransitionName(View view) { + return view.getTransitionName(); + } + + public static void requestApplyInsets(View view) { + view.requestApplyInsets(); + } + + public static void setElevation(View view, float elevation) { + view.setElevation(elevation); + } + + public static float getElevation(View view) { + return view.getElevation(); + } + + public static void setTranslationZ(View view, float translationZ) { + view.setTranslationZ(translationZ); + } + + public static float getTranslationZ(View view) { + return view.getTranslationZ(); + } + + public static void setOnApplyWindowInsetsListener(View view, + final OnApplyWindowInsetsListener listener) { + view.setOnApplyWindowInsetsListener(new View.OnApplyWindowInsetsListener() { + @Override + public WindowInsets onApplyWindowInsets(View view, WindowInsets windowInsets) { + // Wrap the framework insets in our wrapper + WindowInsetsCompatApi21 insets = new WindowInsetsCompatApi21(windowInsets); + // Give the listener a chance to use the wrapped insets + insets = (WindowInsetsCompatApi21) listener.onApplyWindowInsets(view, insets); + // Return the unwrapped insets + return insets.unwrap(); + } + }); + } + + public static boolean isImportantForAccessibility(View view) { + return view.isImportantForAccessibility(); + } + + static ColorStateList getBackgroundTintList(View view) { + return view.getBackgroundTintList(); + } + + static void setBackgroundTintList(View view, ColorStateList tintList) { + view.setBackgroundTintList(tintList); + } + + static PorterDuff.Mode getBackgroundTintMode(View view) { + return view.getBackgroundTintMode(); + } + + static void setBackgroundTintMode(View view, PorterDuff.Mode mode) { + view.setBackgroundTintMode(mode); + } + + public static WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) { + if (insets instanceof WindowInsetsCompatApi21) { + // First unwrap the compat version so that we have the framework instance + WindowInsets unwrapped = ((WindowInsetsCompatApi21) insets).unwrap(); + // Now call onApplyWindowInsets + WindowInsets result = v.onApplyWindowInsets(unwrapped); + + if (result != unwrapped) { + // ...and return a newly wrapped compat insets instance if different + insets = new WindowInsetsCompatApi21(result); + } + } + return insets; + } + + public static WindowInsetsCompat dispatchApplyWindowInsets(View v, WindowInsetsCompat insets) { + if (insets instanceof WindowInsetsCompatApi21) { + // First unwrap the compat version so that we have the framework instance + WindowInsets unwrapped = ((WindowInsetsCompatApi21) insets).unwrap(); + // Now call dispatchApplyWindowInsets + WindowInsets result = v.dispatchApplyWindowInsets(unwrapped); + + if (result != unwrapped) { + // ...and return a newly wrapped compat insets instance if different + insets = new WindowInsetsCompatApi21(result); + } + } + return insets; + } + + public static void setNestedScrollingEnabled(View view, boolean enabled) { + view.setNestedScrollingEnabled(enabled); + } + + public static boolean isNestedScrollingEnabled(View view) { + return view.isNestedScrollingEnabled(); + } + + public static boolean startNestedScroll(View view, int axes) { + return view.startNestedScroll(axes); + } + + public static void stopNestedScroll(View view) { + view.stopNestedScroll(); + } + + public static boolean hasNestedScrollingParent(View view) { + return view.hasNestedScrollingParent(); + } + + public static boolean dispatchNestedScroll(View view, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow) { + return view.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow); + } + + public static boolean dispatchNestedPreScroll(View view, int dx, int dy, int[] consumed, + int[] offsetInWindow) { + return view.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + public static boolean dispatchNestedFling(View view, float velocityX, float velocityY, + boolean consumed) { + return view.dispatchNestedFling(velocityX, velocityY, consumed); + } + + public static boolean dispatchNestedPreFling(View view, float velocityX, float velocityY) { + return view.dispatchNestedPreFling(velocityX, velocityY); + } + + public static float getZ(View view) { + return view.getZ(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompat.java new file mode 100644 index 00000000000..86d19bbc736 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompat.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.ViewConfiguration; + +/** + * Helper for accessing features in {@link ViewConfiguration} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ViewConfigurationCompat { + /** + * Interface for the full API. + */ + interface ViewConfigurationVersionImpl { + public int getScaledPagingTouchSlop(ViewConfiguration config); + public boolean hasPermanentMenuKey(ViewConfiguration config); + } + + /** + * Interface implementation that doesn't use anything about v4 APIs. + */ + static class BaseViewConfigurationVersionImpl implements ViewConfigurationVersionImpl { + @Override + public int getScaledPagingTouchSlop(ViewConfiguration config) { + return config.getScaledTouchSlop(); + } + + @Override + public boolean hasPermanentMenuKey(ViewConfiguration config) { + // Pre-HC devices will always have a menu button + return true; + } + } + + /** + * Interface implementation for devices with at least v8 APIs. + */ + static class FroyoViewConfigurationVersionImpl extends BaseViewConfigurationVersionImpl { + @Override + public int getScaledPagingTouchSlop(ViewConfiguration config) { + return ViewConfigurationCompatFroyo.getScaledPagingTouchSlop(config); + } + } + + /** + * Interface implementation for devices with at least v11 APIs. + */ + static class HoneycombViewConfigurationVersionImpl extends FroyoViewConfigurationVersionImpl { + @Override + public boolean hasPermanentMenuKey(ViewConfiguration config) { + // There is no way to check on Honeycomb so we assume false + return false; + } + } + + /** + * Interface implementation for devices with at least v14 APIs. + */ + static class IcsViewConfigurationVersionImpl extends HoneycombViewConfigurationVersionImpl { + @Override + public boolean hasPermanentMenuKey(ViewConfiguration config) { + return ViewConfigurationCompatICS.hasPermanentMenuKey(config); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final ViewConfigurationVersionImpl IMPL; + static { + if (android.os.Build.VERSION.SDK_INT >= 14) { + IMPL = new IcsViewConfigurationVersionImpl(); + } else if (android.os.Build.VERSION.SDK_INT >= 11) { + IMPL = new HoneycombViewConfigurationVersionImpl(); + } else if (android.os.Build.VERSION.SDK_INT >= 8) { + IMPL = new FroyoViewConfigurationVersionImpl(); + } else { + IMPL = new BaseViewConfigurationVersionImpl(); + } + } + + // ------------------------------------------------------------------- + + /** + * Call {@link ViewConfiguration#getScaledPagingTouchSlop()}. + * If running on a pre-{@link android.os.Build.VERSION_CODES#FROYO} device, + * returns {@link ViewConfiguration#getScaledTouchSlop()}. + */ + public static int getScaledPagingTouchSlop(ViewConfiguration config) { + return IMPL.getScaledPagingTouchSlop(config); + } + + /** + * Report if the device has a permanent menu key available to the user, in a backwards + * compatible way. + */ + public static boolean hasPermanentMenuKey(ViewConfiguration config) { + return IMPL.hasPermanentMenuKey(config); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatFroyo.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatFroyo.java new file mode 100644 index 00000000000..71a4b26328e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatFroyo.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.ViewConfiguration; + +/** + * Implementation of menu compatibility that can call Froyo APIs. + */ +class ViewConfigurationCompatFroyo { + public static int getScaledPagingTouchSlop(ViewConfiguration config) { + return config.getScaledPagingTouchSlop(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatICS.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatICS.java new file mode 100644 index 00000000000..13a96471abc --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewConfigurationCompatICS.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.ViewConfiguration; + +/** + * Implementation of menu compatibility that can call ICS APIs. + */ +class ViewConfigurationCompatICS { + static boolean hasPermanentMenuKey(ViewConfiguration config) { + return config.hasPermanentMenuKey(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompat.java new file mode 100644 index 00000000000..2254c4aca97 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompat.java @@ -0,0 +1,267 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.os.Build; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +/** + * Helper for accessing features in {@link ViewGroup} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ViewGroupCompat { + + /** + * This constant is a {@link #setLayoutMode(ViewGroup, int) layoutMode}. + * Clip bounds are the raw values of {@link android.view.View#getLeft() left}, + * {@link android.view.View#getTop() top}, + * {@link android.view.View#getRight() right} and {@link android.view.View#getBottom() bottom}. + */ + public static final int LAYOUT_MODE_CLIP_BOUNDS = 0; + + /** + * This constant is a {@link #setLayoutMode(ViewGroup, int) layoutMode}. + * Optical bounds describe where a widget appears to be. They sit inside the clip + * bounds which need to cover a larger area to allow other effects, + * such as shadows and glows, to be drawn. + */ + public static final int LAYOUT_MODE_OPTICAL_BOUNDS = 1; + + interface ViewGroupCompatImpl { + boolean onRequestSendAccessibilityEvent(ViewGroup group, View child, + AccessibilityEvent event); + void setMotionEventSplittingEnabled(ViewGroup group, boolean split); + int getLayoutMode(ViewGroup group); + void setLayoutMode(ViewGroup group, int mode); + void setTransitionGroup(ViewGroup group, boolean isTransitionGroup); + boolean isTransitionGroup(ViewGroup group); + int getNestedScrollAxes(ViewGroup group); + } + + static class ViewGroupCompatStubImpl implements ViewGroupCompatImpl { + public boolean onRequestSendAccessibilityEvent( + ViewGroup group, View child, AccessibilityEvent event) { + return true; + } + + public void setMotionEventSplittingEnabled(ViewGroup group, boolean split) { + // no-op, didn't exist. + } + + @Override + public int getLayoutMode(ViewGroup group) { + return LAYOUT_MODE_CLIP_BOUNDS; + } + + @Override + public void setLayoutMode(ViewGroup group, int mode) { + // no-op, didn't exist. Views only support clip bounds. + } + + @Override + public void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) { + } + + @Override + public boolean isTransitionGroup(ViewGroup group) { + return false; + } + + @Override + public int getNestedScrollAxes(ViewGroup group) { + if (group instanceof NestedScrollingParent) { + return ((NestedScrollingParent) group).getNestedScrollAxes(); + } + return ViewCompat.SCROLL_AXIS_NONE; + } + } + + static class ViewGroupCompatHCImpl extends ViewGroupCompatStubImpl { + @Override + public void setMotionEventSplittingEnabled(ViewGroup group, boolean split) { + ViewGroupCompatHC.setMotionEventSplittingEnabled(group, split); + } + } + + static class ViewGroupCompatIcsImpl extends ViewGroupCompatHCImpl { + @Override + public boolean onRequestSendAccessibilityEvent( + ViewGroup group, View child, AccessibilityEvent event) { + return ViewGroupCompatIcs.onRequestSendAccessibilityEvent(group, child, event); + } + } + + static class ViewGroupCompatJellybeanMR2Impl extends ViewGroupCompatIcsImpl { + @Override + public int getLayoutMode(ViewGroup group) { + return ViewGroupCompatJellybeanMR2.getLayoutMode(group); + } + + @Override + public void setLayoutMode(ViewGroup group, int mode) { + ViewGroupCompatJellybeanMR2.setLayoutMode(group, mode); + } + } + + static class ViewGroupCompatLollipopImpl extends ViewGroupCompatJellybeanMR2Impl { + @Override + public void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) { + ViewGroupCompatLollipop.setTransitionGroup(group, isTransitionGroup); + } + + @Override + public boolean isTransitionGroup(ViewGroup group) { + return ViewGroupCompatLollipop.isTransitionGroup(group); + } + + @Override + public int getNestedScrollAxes(ViewGroup group) { + return ViewGroupCompatLollipop.getNestedScrollAxes(group); + } + } + + static final ViewGroupCompatImpl IMPL; + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + IMPL = new ViewGroupCompatLollipopImpl(); + } else if (version >= 18) { + IMPL = new ViewGroupCompatJellybeanMR2Impl(); + } else if (version >= 14) { + IMPL = new ViewGroupCompatIcsImpl(); + } else if (version >= 11) { + IMPL = new ViewGroupCompatHCImpl(); + } else { + IMPL = new ViewGroupCompatStubImpl(); + } + } + + /* + * Hide the constructor. + */ + private ViewGroupCompat() { + + } + + /** + * Called when a child has requested sending an {@link AccessibilityEvent} and + * gives an opportunity to its parent to augment the event. + *

    + * If an {@link AccessibilityDelegateCompat} has been specified via calling + * {@link ViewCompat#setAccessibilityDelegate(View, AccessibilityDelegateCompat)} its + * {@link AccessibilityDelegateCompat#onRequestSendAccessibilityEvent(ViewGroup, View, + * AccessibilityEvent)} is responsible for handling this call. + *

    + * + * @param group The group whose method to invoke. + * @param child The child which requests sending the event. + * @param event The event to be sent. + * @return True if the event should be sent. + */ + public static boolean onRequestSendAccessibilityEvent(ViewGroup group, View child, + AccessibilityEvent event) { + return IMPL.onRequestSendAccessibilityEvent(group, child, event); + } + + /** + * Enable or disable the splitting of MotionEvents to multiple children during touch event + * dispatch. This behavior is enabled by default for applications that target an + * SDK version of 11 (Honeycomb) or newer. On earlier platform versions this feature + * was not supported and this method is a no-op. + * + *

    When this option is enabled MotionEvents may be split and dispatched to different child + * views depending on where each pointer initially went down. This allows for user interactions + * such as scrolling two panes of content independently, chording of buttons, and performing + * independent gestures on different pieces of content. + * + * @param group ViewGroup to modify + * @param split true to allow MotionEvents to be split and dispatched to multiple + * child views. false to only allow one child view to be the target of + * any MotionEvent received by this ViewGroup. + */ + public static void setMotionEventSplittingEnabled(ViewGroup group, boolean split) { + IMPL.setMotionEventSplittingEnabled(group, split); + } + + /** + * Returns the basis of alignment during layout operations on this ViewGroup: + * either {@link #LAYOUT_MODE_CLIP_BOUNDS} or {@link #LAYOUT_MODE_OPTICAL_BOUNDS}. + *

    + * If no layoutMode was explicitly set, either programmatically or in an XML resource, + * the method returns the layoutMode of the view's parent ViewGroup if such a parent exists, + * otherwise the method returns a default value of {@link #LAYOUT_MODE_CLIP_BOUNDS}. + * + * @return the layout mode to use during layout operations + * + * @see #setLayoutMode(ViewGroup, int) + */ + public static int getLayoutMode(ViewGroup group) { + return IMPL.getLayoutMode(group); + } + + /** + * Sets the basis of alignment during the layout of this ViewGroup. + * Valid values are either {@link #LAYOUT_MODE_CLIP_BOUNDS} or + * {@link #LAYOUT_MODE_OPTICAL_BOUNDS}. + * + * @param mode the layout mode to use during layout operations + * + * @see #getLayoutMode(ViewGroup) + */ + public static void setLayoutMode(ViewGroup group, int mode) { + IMPL.setLayoutMode(group, mode); + } + + /** + * Changes whether or not this ViewGroup should be treated as a single entity during + * Activity Transitions. + * @param isTransitionGroup Whether or not the ViewGroup should be treated as a unit + * in Activity transitions. If false, the ViewGroup won't transition, + * only its children. If true, the entire ViewGroup will transition + * together. + */ + public static void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) { + IMPL.setTransitionGroup(group, isTransitionGroup); + } + + /** + * Returns true if this ViewGroup should be considered as a single entity for removal + * when executing an Activity transition. If this is false, child elements will move + * individually during the transition. + */ + public static boolean isTransitionGroup(ViewGroup group) { + return IMPL.isTransitionGroup(group); + } + + /** + * Return the current axes of nested scrolling for this ViewGroup. + * + *

    A ViewGroup returning something other than {@link ViewCompat#SCROLL_AXIS_NONE} is + * currently acting as a nested scrolling parent for one or more descendant views in + * the hierarchy.

    + * + * @return Flags indicating the current axes of nested scrolling + * @see ViewCompat#SCROLL_AXIS_HORIZONTAL + * @see ViewCompat#SCROLL_AXIS_VERTICAL + * @see ViewCompat#SCROLL_AXIS_NONE + */ + public static int getNestedScrollAxes(ViewGroup group) { + return IMPL.getNestedScrollAxes(group); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatHC.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatHC.java new file mode 100644 index 00000000000..96349edb889 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatHC.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.view.ViewGroup; + +class ViewGroupCompatHC { + private ViewGroupCompatHC() { + } + + public static void setMotionEventSplittingEnabled(ViewGroup group, boolean split) { + group.setMotionEventSplittingEnabled(split); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatIcs.java new file mode 100644 index 00000000000..73de780023e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatIcs.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityEvent; + +/** + * ICS specific ViewGroup API implementation. + */ +class ViewGroupCompatIcs { + public static boolean onRequestSendAccessibilityEvent(ViewGroup group, View child, + AccessibilityEvent event) { + return group.onRequestSendAccessibilityEvent(child, event); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatJellybeanMR2.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatJellybeanMR2.java new file mode 100644 index 00000000000..086b07004f8 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatJellybeanMR2.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.view.ViewGroup; + +class ViewGroupCompatJellybeanMR2 { + public static int getLayoutMode(ViewGroup group) { + return group.getLayoutMode(); + } + + public static void setLayoutMode(ViewGroup group, int mode) { + group.setLayoutMode(mode); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatLollipop.java new file mode 100644 index 00000000000..1a62404373c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewGroupCompatLollipop.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.ViewGroup; + +class ViewGroupCompatLollipop { + + public static void setTransitionGroup(ViewGroup group, boolean isTransitionGroup) { + group.setTransitionGroup(isTransitionGroup); + } + + public static boolean isTransitionGroup(ViewGroup group) { + return group.isTransitionGroup(); + } + + public static int getNestedScrollAxes(ViewGroup group) { + return group.getNestedScrollAxes(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPager.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPager.java new file mode 100644 index 00000000000..57152c2f0f2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPager.java @@ -0,0 +1,2976 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.database.DataSetObserver; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.DrawableRes; +import android.support.v4.os.ParcelableCompat; +import android.support.v4.os.ParcelableCompatCreatorCallbacks; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.support.v4.widget.EdgeEffectCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.FocusFinder; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.SoundEffectConstants; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +/** + * Layout manager that allows the user to flip left and right + * through pages of data. You supply an implementation of a + * {@link PagerAdapter} to generate the pages that the view shows. + * + *

    Note this class is currently under early design and + * development. The API will likely change in later updates of + * the compatibility library, requiring changes to the source code + * of apps when they are compiled against the newer version.

    + * + *

    ViewPager is most often used in conjunction with {@link android.app.Fragment}, + * which is a convenient way to supply and manage the lifecycle of each page. + * There are standard adapters implemented for using fragments with the ViewPager, + * which cover the most common use cases. These are + * {@link android.support.v4.app.FragmentPagerAdapter} and + * {@link android.support.v4.app.FragmentStatePagerAdapter}; each of these + * classes have simple code showing how to build a full user interface + * with them. + * + *

    For more information about how to use ViewPager, read Creating Swipe Views with + * Tabs.

    + * + *

    Below is a more complicated example of ViewPager, using it in conjunction + * with {@link android.app.ActionBar} tabs. You can find other examples of using + * ViewPager in the API 4+ Support Demos and API 13+ Support Demos sample code. + * + * {@sample development/samples/Support13Demos/src/com/example/android/supportv13/app/ActionBarTabsPager.java + * complete} + */ +public class ViewPager extends ViewGroup { + private static final String TAG = "ViewPager"; + private static final boolean DEBUG = false; + + private static final boolean USE_CACHE = false; + + private static final int DEFAULT_OFFSCREEN_PAGES = 1; + private static final int MAX_SETTLE_DURATION = 600; // ms + private static final int MIN_DISTANCE_FOR_FLING = 25; // dips + + private static final int DEFAULT_GUTTER_SIZE = 16; // dips + + private static final int MIN_FLING_VELOCITY = 400; // dips + + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.layout_gravity + }; + + /** + * Used to track what the expected number of items in the adapter should be. + * If the app changes this when we don't expect it, we'll throw a big obnoxious exception. + */ + private int mExpectedAdapterCount; + + static class ItemInfo { + Object object; + int position; + boolean scrolling; + float widthFactor; + float offset; + } + + private static final Comparator COMPARATOR = new Comparator(){ + @Override + public int compare(ItemInfo lhs, ItemInfo rhs) { + return lhs.position - rhs.position; + } + }; + + private static final Interpolator sInterpolator = new Interpolator() { + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private final ArrayList mItems = new ArrayList(); + private final ItemInfo mTempItem = new ItemInfo(); + + private final Rect mTempRect = new Rect(); + + private PagerAdapter mAdapter; + private int mCurItem; // Index of currently displayed page. + private int mRestoredCurItem = -1; + private Parcelable mRestoredAdapterState = null; + private ClassLoader mRestoredClassLoader = null; + private Scroller mScroller; + private PagerObserver mObserver; + + private int mPageMargin; + private Drawable mMarginDrawable; + private int mTopPageBounds; + private int mBottomPageBounds; + + // Offsets of the first and last items, if known. + // Set during population, used to determine if we are at the beginning + // or end of the pager data set during touch scrolling. + private float mFirstOffset = -Float.MAX_VALUE; + private float mLastOffset = Float.MAX_VALUE; + + private int mChildWidthMeasureSpec; + private int mChildHeightMeasureSpec; + private boolean mInLayout; + + private boolean mScrollingCacheEnabled; + + private boolean mPopulatePending; + private int mOffscreenPageLimit = DEFAULT_OFFSCREEN_PAGES; + + private boolean mIsBeingDragged; + private boolean mIsUnableToDrag; + private int mDefaultGutterSize; + private int mGutterSize; + private int mTouchSlop; + /** + * Position of the last motion event. + */ + private float mLastMotionX; + private float mLastMotionY; + private float mInitialMotionX; + private float mInitialMotionY; + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + private int mMinimumVelocity; + private int mMaximumVelocity; + private int mFlingDistance; + private int mCloseEnough; + + // If the pager is at least this close to its final position, complete the scroll + // on touch down and let the user interact with the content inside instead of + // "catching" the flinging pager. + private static final int CLOSE_ENOUGH = 2; // dp + + private boolean mFakeDragging; + private long mFakeDragBeginTime; + + private EdgeEffectCompat mLeftEdge; + private EdgeEffectCompat mRightEdge; + + private boolean mFirstLayout = true; + private boolean mNeedCalculatePageOffsets = false; + private boolean mCalledSuper; + private int mDecorChildCount; + + private List mOnPageChangeListeners; + private OnPageChangeListener mOnPageChangeListener; + private OnPageChangeListener mInternalPageChangeListener; + private OnAdapterChangeListener mAdapterChangeListener; + private PageTransformer mPageTransformer; + private Method mSetChildrenDrawingOrderEnabled; + + private static final int DRAW_ORDER_DEFAULT = 0; + private static final int DRAW_ORDER_FORWARD = 1; + private static final int DRAW_ORDER_REVERSE = 2; + private int mDrawingOrder; + private ArrayList mDrawingOrderedChildren; + private static final ViewPositionComparator sPositionComparator = new ViewPositionComparator(); + + /** + * Indicates that the pager is in an idle, settled state. The current page + * is fully in view and no animation is in progress. + */ + public static final int SCROLL_STATE_IDLE = 0; + + /** + * Indicates that the pager is currently being dragged by the user. + */ + public static final int SCROLL_STATE_DRAGGING = 1; + + /** + * Indicates that the pager is in the process of settling to a final position. + */ + public static final int SCROLL_STATE_SETTLING = 2; + + private final Runnable mEndScrollRunnable = new Runnable() { + public void run() { + setScrollState(SCROLL_STATE_IDLE); + populate(); + } + }; + + private int mScrollState = SCROLL_STATE_IDLE; + + /** + * Callback interface for responding to changing state of the selected page. + */ + public interface OnPageChangeListener { + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param positionOffset Value from [0, 1) indicating the offset from the page at position. + * @param positionOffsetPixels Value in pixels indicating the offset from position. + */ + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels); + + /** + * This method will be invoked when a new page becomes selected. Animation is not + * necessarily complete. + * + * @param position Position index of the new selected page. + */ + public void onPageSelected(int position); + + /** + * Called when the scroll state changes. Useful for discovering when the user + * begins dragging, when the pager is automatically settling to the current page, + * or when it is fully stopped/idle. + * + * @param state The new scroll state. + * @see ViewPager#SCROLL_STATE_IDLE + * @see ViewPager#SCROLL_STATE_DRAGGING + * @see ViewPager#SCROLL_STATE_SETTLING + */ + public void onPageScrollStateChanged(int state); + } + + /** + * Simple implementation of the {@link OnPageChangeListener} interface with stub + * implementations of each method. Extend this if you do not intend to override + * every method of {@link OnPageChangeListener}. + */ + public static class SimpleOnPageChangeListener implements OnPageChangeListener { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + // This space for rent + } + + @Override + public void onPageSelected(int position) { + // This space for rent + } + + @Override + public void onPageScrollStateChanged(int state) { + // This space for rent + } + } + + /** + * A PageTransformer is invoked whenever a visible/attached page is scrolled. + * This offers an opportunity for the application to apply a custom transformation + * to the page views using animation properties. + * + *

    As property animation is only supported as of Android 3.0 and forward, + * setting a PageTransformer on a ViewPager on earlier platform versions will + * be ignored.

    + */ + public interface PageTransformer { + /** + * Apply a property transformation to the given page. + * + * @param page Apply the transformation to this page + * @param position Position of page relative to the current front-and-center + * position of the pager. 0 is front and center. 1 is one full + * page position to the right, and -1 is one page position to the left. + */ + public void transformPage(View page, float position); + } + + /** + * Used internally to monitor when adapters are switched. + */ + interface OnAdapterChangeListener { + public void onAdapterChanged(PagerAdapter oldAdapter, PagerAdapter newAdapter); + } + + /** + * Used internally to tag special types of child views that should be added as + * pager decorations by default. + */ + interface Decor {} + + public ViewPager(Context context) { + super(context); + initViewPager(); + } + + public ViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + initViewPager(); + } + + void initViewPager() { + setWillNotDraw(false); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setFocusable(true); + final Context context = getContext(); + mScroller = new Scroller(context, sInterpolator); + final ViewConfiguration configuration = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + + mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration); + mMinimumVelocity = (int) (MIN_FLING_VELOCITY * density); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + mLeftEdge = new EdgeEffectCompat(context); + mRightEdge = new EdgeEffectCompat(context); + + mFlingDistance = (int) (MIN_DISTANCE_FOR_FLING * density); + mCloseEnough = (int) (CLOSE_ENOUGH * density); + mDefaultGutterSize = (int) (DEFAULT_GUTTER_SIZE * density); + + ViewCompat.setAccessibilityDelegate(this, new MyAccessibilityDelegate()); + + if (ViewCompat.getImportantForAccessibility(this) + == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + ViewCompat.setImportantForAccessibility(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + } + + @Override + protected void onDetachedFromWindow() { + removeCallbacks(mEndScrollRunnable); + super.onDetachedFromWindow(); + } + + private void setScrollState(int newState) { + if (mScrollState == newState) { + return; + } + + mScrollState = newState; + if (mPageTransformer != null) { + // PageTransformers can do complex things that benefit from hardware layers. + enableLayers(newState != SCROLL_STATE_IDLE); + } + dispatchOnScrollStateChanged(newState); + } + + /** + * Set a PagerAdapter that will supply views for this pager as needed. + * + * @param adapter Adapter to use + */ + public void setAdapter(PagerAdapter adapter) { + if (mAdapter != null) { + mAdapter.unregisterDataSetObserver(mObserver); + mAdapter.startUpdate(this); + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + mAdapter.destroyItem(this, ii.position, ii.object); + } + mAdapter.finishUpdate(this); + mItems.clear(); + removeNonDecorViews(); + mCurItem = 0; + scrollTo(0, 0); + } + + final PagerAdapter oldAdapter = mAdapter; + mAdapter = adapter; + mExpectedAdapterCount = 0; + + if (mAdapter != null) { + if (mObserver == null) { + mObserver = new PagerObserver(); + } + mAdapter.registerDataSetObserver(mObserver); + mPopulatePending = false; + final boolean wasFirstLayout = mFirstLayout; + mFirstLayout = true; + mExpectedAdapterCount = mAdapter.getCount(); + if (mRestoredCurItem >= 0) { + mAdapter.restoreState(mRestoredAdapterState, mRestoredClassLoader); + setCurrentItemInternal(mRestoredCurItem, false, true); + mRestoredCurItem = -1; + mRestoredAdapterState = null; + mRestoredClassLoader = null; + } else if (!wasFirstLayout) { + populate(); + } else { + requestLayout(); + } + } + + if (mAdapterChangeListener != null && oldAdapter != adapter) { + mAdapterChangeListener.onAdapterChanged(oldAdapter, adapter); + } + } + + private void removeNonDecorViews() { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + removeViewAt(i); + i--; + } + } + } + + /** + * Retrieve the current adapter supplying pages. + * + * @return The currently registered PagerAdapter + */ + public PagerAdapter getAdapter() { + return mAdapter; + } + + void setOnAdapterChangeListener(OnAdapterChangeListener listener) { + mAdapterChangeListener = listener; + } + + private int getClientWidth() { + return getMeasuredWidth() - getPaddingLeft() - getPaddingRight(); + } + + /** + * Set the currently selected page. If the ViewPager has already been through its first + * layout with its current adapter there will be a smooth animated transition between + * the current item and the specified item. + * + * @param item Item index to select + */ + public void setCurrentItem(int item) { + mPopulatePending = false; + setCurrentItemInternal(item, !mFirstLayout, false); + } + + /** + * Set the currently selected page. + * + * @param item Item index to select + * @param smoothScroll True to smoothly scroll to the new item, false to transition immediately + */ + public void setCurrentItem(int item, boolean smoothScroll) { + mPopulatePending = false; + setCurrentItemInternal(item, smoothScroll, false); + } + + public int getCurrentItem() { + return mCurItem; + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always) { + setCurrentItemInternal(item, smoothScroll, always, 0); + } + + void setCurrentItemInternal(int item, boolean smoothScroll, boolean always, int velocity) { + if (mAdapter == null || mAdapter.getCount() <= 0) { + setScrollingCacheEnabled(false); + return; + } + if (!always && mCurItem == item && mItems.size() != 0) { + setScrollingCacheEnabled(false); + return; + } + + if (item < 0) { + item = 0; + } else if (item >= mAdapter.getCount()) { + item = mAdapter.getCount() - 1; + } + final int pageLimit = mOffscreenPageLimit; + if (item > (mCurItem + pageLimit) || item < (mCurItem - pageLimit)) { + // We are doing a jump by more than one page. To avoid + // glitches, we want to keep all current pages in the view + // until the scroll ends. + for (int i=0; iComponents that add a listener should take care to remove it when finished. + * Other components that take ownership of a view may call {@link #clearOnPageChangeListeners()} + * to remove all attached listeners.

    + * + * @param listener listener to add + */ + public void addOnPageChangeListener(OnPageChangeListener listener) { + if (mOnPageChangeListeners == null) { + mOnPageChangeListeners = new ArrayList<>(); + } + mOnPageChangeListeners.add(listener); + } + + /** + * Remove a listener that was previously added via + * {@link #addOnPageChangeListener(OnPageChangeListener)}. + * + * @param listener listener to remove + */ + public void removeOnPageChangeListener(OnPageChangeListener listener) { + if (mOnPageChangeListeners != null) { + mOnPageChangeListeners.remove(listener); + } + } + + /** + * Remove all listeners that are notified of any changes in scroll state or position. + */ + public void clearOnPageChangeListeners() { + if (mOnPageChangeListeners != null) { + mOnPageChangeListeners.clear(); + } + } + + /** + * Set a {@link PageTransformer} that will be called for each attached page whenever + * the scroll position is changed. This allows the application to apply custom property + * transformations to each page, overriding the default sliding look and feel. + * + *

    Note: Prior to Android 3.0 the property animation APIs did not exist. + * As a result, setting a PageTransformer prior to Android 3.0 (API 11) will have no effect.

    + * + * @param reverseDrawingOrder true if the supplied PageTransformer requires page views + * to be drawn from last to first instead of first to last. + * @param transformer PageTransformer that will modify each page's animation properties + */ + public void setPageTransformer(boolean reverseDrawingOrder, PageTransformer transformer) { + if (Build.VERSION.SDK_INT >= 11) { + final boolean hasTransformer = transformer != null; + final boolean needsPopulate = hasTransformer != (mPageTransformer != null); + mPageTransformer = transformer; + setChildrenDrawingOrderEnabledCompat(hasTransformer); + if (hasTransformer) { + mDrawingOrder = reverseDrawingOrder ? DRAW_ORDER_REVERSE : DRAW_ORDER_FORWARD; + } else { + mDrawingOrder = DRAW_ORDER_DEFAULT; + } + if (needsPopulate) populate(); + } + } + + void setChildrenDrawingOrderEnabledCompat(boolean enable) { + if (Build.VERSION.SDK_INT >= 7) { + if (mSetChildrenDrawingOrderEnabled == null) { + try { + mSetChildrenDrawingOrderEnabled = ViewGroup.class.getDeclaredMethod( + "setChildrenDrawingOrderEnabled", new Class[] { Boolean.TYPE }); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Can't find setChildrenDrawingOrderEnabled", e); + } + } + try { + mSetChildrenDrawingOrderEnabled.invoke(this, enable); + } catch (Exception e) { + Log.e(TAG, "Error changing children drawing order", e); + } + } + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + final int index = mDrawingOrder == DRAW_ORDER_REVERSE ? childCount - 1 - i : i; + final int result = ((LayoutParams) mDrawingOrderedChildren.get(index).getLayoutParams()).childIndex; + return result; + } + + /** + * Set a separate OnPageChangeListener for internal use by the support library. + * + * @param listener Listener to set + * @return The old listener that was set, if any. + */ + OnPageChangeListener setInternalPageChangeListener(OnPageChangeListener listener) { + OnPageChangeListener oldListener = mInternalPageChangeListener; + mInternalPageChangeListener = listener; + return oldListener; + } + + /** + * Returns the number of pages that will be retained to either side of the + * current page in the view hierarchy in an idle state. Defaults to 1. + * + * @return How many pages will be kept offscreen on either side + * @see #setOffscreenPageLimit(int) + */ + public int getOffscreenPageLimit() { + return mOffscreenPageLimit; + } + + /** + * Set the number of pages that should be retained to either side of the + * current page in the view hierarchy in an idle state. Pages beyond this + * limit will be recreated from the adapter when needed. + * + *

    This is offered as an optimization. If you know in advance the number + * of pages you will need to support or have lazy-loading mechanisms in place + * on your pages, tweaking this setting can have benefits in perceived smoothness + * of paging animations and interaction. If you have a small number of pages (3-4) + * that you can keep active all at once, less time will be spent in layout for + * newly created view subtrees as the user pages back and forth.

    + * + *

    You should keep this limit low, especially if your pages have complex layouts. + * This setting defaults to 1.

    + * + * @param limit How many pages will be kept offscreen in an idle state. + */ + public void setOffscreenPageLimit(int limit) { + if (limit < DEFAULT_OFFSCREEN_PAGES) { + Log.w(TAG, "Requested offscreen page limit " + limit + " too small; defaulting to " + + DEFAULT_OFFSCREEN_PAGES); + limit = DEFAULT_OFFSCREEN_PAGES; + } + if (limit != mOffscreenPageLimit) { + mOffscreenPageLimit = limit; + populate(); + } + } + + /** + * Set the margin between pages. + * + * @param marginPixels Distance between adjacent pages in pixels + * @see #getPageMargin() + * @see #setPageMarginDrawable(Drawable) + * @see #setPageMarginDrawable(int) + */ + public void setPageMargin(int marginPixels) { + final int oldMargin = mPageMargin; + mPageMargin = marginPixels; + + final int width = getWidth(); + recomputeScrollPosition(width, width, marginPixels, oldMargin); + + requestLayout(); + } + + /** + * Return the margin between pages. + * + * @return The size of the margin in pixels + */ + public int getPageMargin() { + return mPageMargin; + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param d Drawable to display between pages + */ + public void setPageMarginDrawable(Drawable d) { + mMarginDrawable = d; + if (d != null) refreshDrawableState(); + setWillNotDraw(d == null); + invalidate(); + } + + /** + * Set a drawable that will be used to fill the margin between pages. + * + * @param resId Resource ID of a drawable to display between pages + */ + public void setPageMarginDrawable(@DrawableRes int resId) { + setPageMarginDrawable(getContext().getResources().getDrawable(resId)); + } + + @Override + protected boolean verifyDrawable(Drawable who) { + return super.verifyDrawable(who) || who == mMarginDrawable; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + final Drawable d = mMarginDrawable; + if (d != null && d.isStateful()) { + d.setState(getDrawableState()); + } + } + + // We want the duration of the page snap animation to be influenced by the distance that + // the screen has to travel, however, we don't want this duration to be effected in a + // purely linear fashion. Instead, we use this method to moderate the effect that the distance + // of travel has on the overall snap duration. + float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + */ + void smoothScrollTo(int x, int y) { + smoothScrollTo(x, y, 0); + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param x the number of pixels to scroll by on the X axis + * @param y the number of pixels to scroll by on the Y axis + * @param velocity the velocity associated with a fling, if applicable. (0 otherwise) + */ + void smoothScrollTo(int x, int y, int velocity) { + if (getChildCount() == 0) { + // Nothing to do. + setScrollingCacheEnabled(false); + return; + } + int sx = getScrollX(); + int sy = getScrollY(); + int dx = x - sx; + int dy = y - sy; + if (dx == 0 && dy == 0) { + completeScroll(false); + populate(); + setScrollState(SCROLL_STATE_IDLE); + return; + } + + setScrollingCacheEnabled(true); + setScrollState(SCROLL_STATE_SETTLING); + + final int width = getClientWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width); + final float distance = halfWidth + halfWidth * + distanceInfluenceForSnapDuration(distanceRatio); + + int duration = 0; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float pageWidth = width * mAdapter.getPageWidth(mCurItem); + final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin); + duration = (int) ((pageDelta + 1) * 100); + } + duration = Math.min(duration, MAX_SETTLE_DURATION); + + mScroller.startScroll(sx, sy, dx, dy, duration); + ViewCompat.postInvalidateOnAnimation(this); + } + + ItemInfo addNewItem(int position, int index) { + ItemInfo ii = new ItemInfo(); + ii.position = position; + ii.object = mAdapter.instantiateItem(this, position); + ii.widthFactor = mAdapter.getPageWidth(position); + if (index < 0 || index >= mItems.size()) { + mItems.add(ii); + } else { + mItems.add(index, ii); + } + return ii; + } + + void dataSetChanged() { + // This method only gets called if our observer is attached, so mAdapter is non-null. + + final int adapterCount = mAdapter.getCount(); + mExpectedAdapterCount = adapterCount; + boolean needPopulate = mItems.size() < mOffscreenPageLimit * 2 + 1 && + mItems.size() < adapterCount; + int newCurrItem = mCurItem; + + boolean isUpdating = false; + for (int i = 0; i < mItems.size(); i++) { + final ItemInfo ii = mItems.get(i); + final int newPos = mAdapter.getItemPosition(ii.object); + + if (newPos == PagerAdapter.POSITION_UNCHANGED) { + continue; + } + + if (newPos == PagerAdapter.POSITION_NONE) { + mItems.remove(i); + i--; + + if (!isUpdating) { + mAdapter.startUpdate(this); + isUpdating = true; + } + + mAdapter.destroyItem(this, ii.position, ii.object); + needPopulate = true; + + if (mCurItem == ii.position) { + // Keep the current item in the valid range + newCurrItem = Math.max(0, Math.min(mCurItem, adapterCount - 1)); + needPopulate = true; + } + continue; + } + + if (ii.position != newPos) { + if (ii.position == mCurItem) { + // Our current item changed position. Follow it. + newCurrItem = newPos; + } + + ii.position = newPos; + needPopulate = true; + } + } + + if (isUpdating) { + mAdapter.finishUpdate(this); + } + + Collections.sort(mItems, COMPARATOR); + + if (needPopulate) { + // Reset our known page widths; populate will recompute them. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) { + lp.widthFactor = 0.f; + } + } + + setCurrentItemInternal(newCurrItem, false, true); + requestLayout(); + } + } + + void populate() { + populate(mCurItem); + } + + void populate(int newCurrentItem) { + ItemInfo oldCurInfo = null; + int focusDirection = View.FOCUS_FORWARD; + if (mCurItem != newCurrentItem) { + focusDirection = mCurItem < newCurrentItem ? View.FOCUS_RIGHT : View.FOCUS_LEFT; + oldCurInfo = infoForPosition(mCurItem); + mCurItem = newCurrentItem; + } + + if (mAdapter == null) { + sortChildDrawingOrder(); + return; + } + + // Bail now if we are waiting to populate. This is to hold off + // on creating views from the time the user releases their finger to + // fling to a new position until we have finished the scroll to + // that position, avoiding glitches from happening at that point. + if (mPopulatePending) { + if (DEBUG) Log.i(TAG, "populate is pending, skipping for now..."); + sortChildDrawingOrder(); + return; + } + + // Also, don't populate until we are attached to a window. This is to + // avoid trying to populate before we have restored our view hierarchy + // state and conflicting with what is restored. + if (getWindowToken() == null) { + return; + } + + mAdapter.startUpdate(this); + + final int pageLimit = mOffscreenPageLimit; + final int startPos = Math.max(0, mCurItem - pageLimit); + final int N = mAdapter.getCount(); + final int endPos = Math.min(N-1, mCurItem + pageLimit); + + if (N != mExpectedAdapterCount) { + String resName; + try { + resName = getResources().getResourceName(getId()); + } catch (Resources.NotFoundException e) { + resName = Integer.toHexString(getId()); + } + throw new IllegalStateException("The application's PagerAdapter changed the adapter's" + + " contents without calling PagerAdapter#notifyDataSetChanged!" + + " Expected adapter item count: " + mExpectedAdapterCount + ", found: " + N + + " Pager id: " + resName + + " Pager class: " + getClass() + + " Problematic adapter: " + mAdapter.getClass()); + } + + // Locate the currently focused item or add it if needed. + int curIndex = -1; + ItemInfo curItem = null; + for (curIndex = 0; curIndex < mItems.size(); curIndex++) { + final ItemInfo ii = mItems.get(curIndex); + if (ii.position >= mCurItem) { + if (ii.position == mCurItem) curItem = ii; + break; + } + } + + if (curItem == null && N > 0) { + curItem = addNewItem(mCurItem, curIndex); + } + + // Fill 3x the available width or up to the number of offscreen + // pages requested to either side, whichever is larger. + // If we have no current item we have no work to do. + if (curItem != null) { + float extraWidthLeft = 0.f; + int itemIndex = curIndex - 1; + ItemInfo ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + final int clientWidth = getClientWidth(); + final float leftWidthNeeded = clientWidth <= 0 ? 0 : + 2.f - curItem.widthFactor + (float) getPaddingLeft() / (float) clientWidth; + for (int pos = mCurItem - 1; pos >= 0; pos--) { + if (extraWidthLeft >= leftWidthNeeded && pos < startPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ((View) ii.object)); + } + itemIndex--; + curIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthLeft += ii.widthFactor; + itemIndex--; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex + 1); + extraWidthLeft += ii.widthFactor; + curIndex++; + ii = itemIndex >= 0 ? mItems.get(itemIndex) : null; + } + } + + float extraWidthRight = curItem.widthFactor; + itemIndex = curIndex + 1; + if (extraWidthRight < 2.f) { + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + final float rightWidthNeeded = clientWidth <= 0 ? 0 : + (float) getPaddingRight() / (float) clientWidth + 2.f; + for (int pos = mCurItem + 1; pos < N; pos++) { + if (extraWidthRight >= rightWidthNeeded && pos > endPos) { + if (ii == null) { + break; + } + if (pos == ii.position && !ii.scrolling) { + mItems.remove(itemIndex); + mAdapter.destroyItem(this, pos, ii.object); + if (DEBUG) { + Log.i(TAG, "populate() - destroyItem() with pos: " + pos + + " view: " + ((View) ii.object)); + } + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } else if (ii != null && pos == ii.position) { + extraWidthRight += ii.widthFactor; + itemIndex++; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } else { + ii = addNewItem(pos, itemIndex); + itemIndex++; + extraWidthRight += ii.widthFactor; + ii = itemIndex < mItems.size() ? mItems.get(itemIndex) : null; + } + } + } + + calculatePageOffsets(curItem, curIndex, oldCurInfo); + } + + if (DEBUG) { + Log.i(TAG, "Current page list:"); + for (int i=0; i(); + } else { + mDrawingOrderedChildren.clear(); + } + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + mDrawingOrderedChildren.add(child); + } + Collections.sort(mDrawingOrderedChildren, sPositionComparator); + } + } + + private void calculatePageOffsets(ItemInfo curItem, int curIndex, ItemInfo oldCurInfo) { + final int N = mAdapter.getCount(); + final int width = getClientWidth(); + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + // Fix up offsets for later layout. + if (oldCurInfo != null) { + final int oldCurPosition = oldCurInfo.position; + // Base offsets off of oldCurInfo. + if (oldCurPosition < curItem.position) { + int itemIndex = 0; + ItemInfo ii = null; + float offset = oldCurInfo.offset + oldCurInfo.widthFactor + marginOffset; + for (int pos = oldCurPosition + 1; + pos <= curItem.position && itemIndex < mItems.size(); pos++) { + ii = mItems.get(itemIndex); + while (pos > ii.position && itemIndex < mItems.size() - 1) { + itemIndex++; + ii = mItems.get(itemIndex); + } + while (pos < ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset += mAdapter.getPageWidth(pos) + marginOffset; + pos++; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + } else if (oldCurPosition > curItem.position) { + int itemIndex = mItems.size() - 1; + ItemInfo ii = null; + float offset = oldCurInfo.offset; + for (int pos = oldCurPosition - 1; + pos >= curItem.position && itemIndex >= 0; pos--) { + ii = mItems.get(itemIndex); + while (pos < ii.position && itemIndex > 0) { + itemIndex--; + ii = mItems.get(itemIndex); + } + while (pos > ii.position) { + // We don't have an item populated for this, + // ask the adapter for an offset. + offset -= mAdapter.getPageWidth(pos) + marginOffset; + pos--; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + } + } + } + + // Base all offsets off of curItem. + final int itemCount = mItems.size(); + float offset = curItem.offset; + int pos = curItem.position - 1; + mFirstOffset = curItem.position == 0 ? curItem.offset : -Float.MAX_VALUE; + mLastOffset = curItem.position == N - 1 ? + curItem.offset + curItem.widthFactor - 1 : Float.MAX_VALUE; + // Previous pages + for (int i = curIndex - 1; i >= 0; i--, pos--) { + final ItemInfo ii = mItems.get(i); + while (pos > ii.position) { + offset -= mAdapter.getPageWidth(pos--) + marginOffset; + } + offset -= ii.widthFactor + marginOffset; + ii.offset = offset; + if (ii.position == 0) mFirstOffset = offset; + } + offset = curItem.offset + curItem.widthFactor + marginOffset; + pos = curItem.position + 1; + // Next pages + for (int i = curIndex + 1; i < itemCount; i++, pos++) { + final ItemInfo ii = mItems.get(i); + while (pos < ii.position) { + offset += mAdapter.getPageWidth(pos++) + marginOffset; + } + if (ii.position == N - 1) { + mLastOffset = offset + ii.widthFactor - 1; + } + ii.offset = offset; + offset += ii.widthFactor + marginOffset; + } + + mNeedCalculatePageOffsets = false; + } + + /** + * This is the persistent state that is saved by ViewPager. Only needed + * if you are creating a sublass of ViewPager that must save its own + * state, in which case it should implement a subclass of this which + * contains that state. + */ + public static class SavedState extends BaseSavedState { + int position; + Parcelable adapterState; + ClassLoader loader; + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(position); + out.writeParcelable(adapterState, flags); + } + + @Override + public String toString() { + return "FragmentPager.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " position=" + position + "}"; + } + + public static final Parcelable.Creator CREATOR + = ParcelableCompat.newCreator(new ParcelableCompatCreatorCallbacks() { + @Override + public SavedState createFromParcel(Parcel in, ClassLoader loader) { + return new SavedState(in, loader); + } + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }); + + SavedState(Parcel in, ClassLoader loader) { + super(in); + if (loader == null) { + loader = getClass().getClassLoader(); + } + position = in.readInt(); + adapterState = in.readParcelable(loader); + this.loader = loader; + } + } + + @Override + public Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.position = mCurItem; + if (mAdapter != null) { + ss.adapterState = mAdapter.saveState(); + } + return ss; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState)state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (mAdapter != null) { + mAdapter.restoreState(ss.adapterState, ss.loader); + setCurrentItemInternal(ss.position, false, true); + } else { + mRestoredCurItem = ss.position; + mRestoredAdapterState = ss.adapterState; + mRestoredClassLoader = ss.loader; + } + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (!checkLayoutParams(params)) { + params = generateLayoutParams(params); + } + final LayoutParams lp = (LayoutParams) params; + lp.isDecor |= child instanceof Decor; + if (mInLayout) { + if (lp != null && lp.isDecor) { + throw new IllegalStateException("Cannot add pager decor view during layout"); + } + lp.needsMeasure = true; + addViewInLayout(child, index, params); + } else { + super.addView(child, index, params); + } + + if (USE_CACHE) { + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(mScrollingCacheEnabled); + } else { + child.setDrawingCacheEnabled(false); + } + } + } + + @Override + public void removeView(View view) { + if (mInLayout) { + removeViewInLayout(view); + } else { + super.removeView(view); + } + } + + ItemInfo infoForChild(View child) { + for (int i=0; i 0 && !mItems.isEmpty()) { + final int widthWithMargin = width - getPaddingLeft() - getPaddingRight() + margin; + final int oldWidthWithMargin = oldWidth - getPaddingLeft() - getPaddingRight() + + oldMargin; + final int xpos = getScrollX(); + final float pageOffset = (float) xpos / oldWidthWithMargin; + final int newOffsetPixels = (int) (pageOffset * widthWithMargin); + + scrollTo(newOffsetPixels, getScrollY()); + if (!mScroller.isFinished()) { + // We now return to your regularly scheduled scroll, already in progress. + final int newDuration = mScroller.getDuration() - mScroller.timePassed(); + ItemInfo targetInfo = infoForPosition(mCurItem); + mScroller.startScroll(newOffsetPixels, 0, + (int) (targetInfo.offset * width), 0, newDuration); + } + } else { + final ItemInfo ii = infoForPosition(mCurItem); + final float scrollOffset = ii != null ? Math.min(ii.offset, mLastOffset) : 0; + final int scrollPos = (int) (scrollOffset * + (width - getPaddingLeft() - getPaddingRight())); + if (scrollPos != getScrollX()) { + completeScroll(false); + scrollTo(scrollPos, getScrollY()); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final int count = getChildCount(); + int width = r - l; + int height = b - t; + int paddingLeft = getPaddingLeft(); + int paddingTop = getPaddingTop(); + int paddingRight = getPaddingRight(); + int paddingBottom = getPaddingBottom(); + final int scrollX = getScrollX(); + + int decorCount = 0; + + // First pass - decor views. We need to do this in two passes so that + // we have the proper offsets for non-decor views later. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + int childLeft = 0; + int childTop = 0; + if (lp.isDecor) { + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getMeasuredWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + switch (vgrav) { + default: + childTop = paddingTop; + break; + case Gravity.TOP: + childTop = paddingTop; + paddingTop += child.getMeasuredHeight(); + break; + case Gravity.CENTER_VERTICAL: + childTop = Math.max((height - child.getMeasuredHeight()) / 2, + paddingTop); + break; + case Gravity.BOTTOM: + childTop = height - paddingBottom - child.getMeasuredHeight(); + paddingBottom += child.getMeasuredHeight(); + break; + } + childLeft += scrollX; + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + decorCount++; + } + } + } + + final int childWidth = width - paddingLeft - paddingRight; + // Page views. Do this once we have the right padding offsets from above. + for (int i = 0; i < count; i++) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + ItemInfo ii; + if (!lp.isDecor && (ii = infoForChild(child)) != null) { + int loff = (int) (childWidth * ii.offset); + int childLeft = paddingLeft + loff; + int childTop = paddingTop; + if (lp.needsMeasure) { + // This was added during layout and needs measurement. + // Do it now that we know what we're working with. + lp.needsMeasure = false; + final int widthSpec = MeasureSpec.makeMeasureSpec( + (int) (childWidth * lp.widthFactor), + MeasureSpec.EXACTLY); + final int heightSpec = MeasureSpec.makeMeasureSpec( + (int) (height - paddingTop - paddingBottom), + MeasureSpec.EXACTLY); + child.measure(widthSpec, heightSpec); + } + if (DEBUG) Log.v(TAG, "Positioning #" + i + " " + child + " f=" + ii.object + + ":" + childLeft + "," + childTop + " " + child.getMeasuredWidth() + + "x" + child.getMeasuredHeight()); + child.layout(childLeft, childTop, + childLeft + child.getMeasuredWidth(), + childTop + child.getMeasuredHeight()); + } + } + } + mTopPageBounds = paddingTop; + mBottomPageBounds = height - paddingBottom; + mDecorChildCount = decorCount; + + if (mFirstLayout) { + scrollToItem(mCurItem, false, 0, false); + } + mFirstLayout = false; + } + + @Override + public void computeScroll() { + if (!mScroller.isFinished() && mScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (!pageScrolled(x)) { + mScroller.abortAnimation(); + scrollTo(0, y); + } + } + + // Keep on drawing until the animation has finished. + ViewCompat.postInvalidateOnAnimation(this); + return; + } + + // Done with scroll, clean up state. + completeScroll(true); + } + + private boolean pageScrolled(int xpos) { + if (mItems.size() == 0) { + mCalledSuper = false; + onPageScrolled(0, 0, 0); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return false; + } + final ItemInfo ii = infoForCurrentScrollPosition(); + final int width = getClientWidth(); + final int widthWithMargin = width + mPageMargin; + final float marginOffset = (float) mPageMargin / width; + final int currentPage = ii.position; + final float pageOffset = (((float) xpos / width) - ii.offset) / + (ii.widthFactor + marginOffset); + final int offsetPixels = (int) (pageOffset * widthWithMargin); + + mCalledSuper = false; + onPageScrolled(currentPage, pageOffset, offsetPixels); + if (!mCalledSuper) { + throw new IllegalStateException( + "onPageScrolled did not call superclass implementation"); + } + return true; + } + + /** + * This method will be invoked when the current page is scrolled, either as part + * of a programmatically initiated smooth scroll or a user initiated touch scroll. + * If you override this method you must call through to the superclass implementation + * (e.g. super.onPageScrolled(position, offset, offsetPixels)) before onPageScrolled + * returns. + * + * @param position Position index of the first page currently being displayed. + * Page position+1 will be visible if positionOffset is nonzero. + * @param offset Value from [0, 1) indicating the offset from the page at position. + * @param offsetPixels Value in pixels indicating the offset from position. + */ + protected void onPageScrolled(int position, float offset, int offsetPixels) { + // Offset any decor views if needed - keep them on-screen at all times. + if (mDecorChildCount > 0) { + final int scrollX = getScrollX(); + int paddingLeft = getPaddingLeft(); + int paddingRight = getPaddingRight(); + final int width = getWidth(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (!lp.isDecor) continue; + + final int hgrav = lp.gravity & Gravity.HORIZONTAL_GRAVITY_MASK; + int childLeft = 0; + switch (hgrav) { + default: + childLeft = paddingLeft; + break; + case Gravity.LEFT: + childLeft = paddingLeft; + paddingLeft += child.getWidth(); + break; + case Gravity.CENTER_HORIZONTAL: + childLeft = Math.max((width - child.getMeasuredWidth()) / 2, + paddingLeft); + break; + case Gravity.RIGHT: + childLeft = width - paddingRight - child.getMeasuredWidth(); + paddingRight += child.getMeasuredWidth(); + break; + } + childLeft += scrollX; + + final int childOffset = childLeft - child.getLeft(); + if (childOffset != 0) { + child.offsetLeftAndRight(childOffset); + } + } + } + + dispatchOnPageScrolled(position, offset, offsetPixels); + + if (mPageTransformer != null) { + final int scrollX = getScrollX(); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (lp.isDecor) continue; + + final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth(); + mPageTransformer.transformPage(child, transformPos); + } + } + + mCalledSuper = true; + } + + private void dispatchOnPageScrolled(int position, float offset, int offsetPixels) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageScrolled(position, offset, offsetPixels); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrolled(position, offset, offsetPixels); + } + } + + private void dispatchOnPageSelected(int position) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageSelected(position); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageSelected(position); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageSelected(position); + } + } + + private void dispatchOnScrollStateChanged(int state) { + if (mOnPageChangeListener != null) { + mOnPageChangeListener.onPageScrollStateChanged(state); + } + if (mOnPageChangeListeners != null) { + for (int i = 0, z = mOnPageChangeListeners.size(); i < z; i++) { + OnPageChangeListener listener = mOnPageChangeListeners.get(i); + if (listener != null) { + listener.onPageScrollStateChanged(state); + } + } + } + if (mInternalPageChangeListener != null) { + mInternalPageChangeListener.onPageScrollStateChanged(state); + } + } + + private void completeScroll(boolean postEvents) { + boolean needPopulate = mScrollState == SCROLL_STATE_SETTLING; + if (needPopulate) { + // Done with scroll, no longer want to cache view drawing. + setScrollingCacheEnabled(false); + mScroller.abortAnimation(); + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + if (oldX != x || oldY != y) { + scrollTo(x, y); + if (x != oldX) { + pageScrolled(x); + } + } + } + mPopulatePending = false; + for (int i=0; i 0) || (x > getWidth() - mGutterSize && dx < 0); + } + + private void enableLayers(boolean enable) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final int layerType = enable ? + ViewCompat.LAYER_TYPE_HARDWARE : ViewCompat.LAYER_TYPE_NONE; + ViewCompat.setLayerType(getChildAt(i), layerType, null); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + final int action = ev.getAction() & MotionEventCompat.ACTION_MASK; + + // Always take care of the touch gesture being complete. + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + // Release the drag. + if (DEBUG) Log.v(TAG, "Intercept done!"); + mIsBeingDragged = false; + mIsUnableToDrag = false; + mActivePointerId = INVALID_POINTER; + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + return false; + } + + // Nothing more to do here if we have decided whether or not we + // are dragging. + if (action != MotionEvent.ACTION_DOWN) { + if (mIsBeingDragged) { + if (DEBUG) Log.v(TAG, "Intercept returning true!"); + return true; + } + if (mIsUnableToDrag) { + if (DEBUG) Log.v(TAG, "Intercept returning false!"); + return false; + } + } + + switch (action) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); + final float x = MotionEventCompat.getX(ev, pointerIndex); + final float dx = x - mLastMotionX; + final float xDiff = Math.abs(dx); + final float y = MotionEventCompat.getY(ev, pointerIndex); + final float yDiff = Math.abs(y - mInitialMotionY); + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + + if (dx != 0 && !isGutterDrag(mLastMotionX, dx) && + canScroll(this, false, (int) dx, (int) x, (int) y)) { + // Nested view has scrollable area under this point. Let it be handled there. + mLastMotionX = x; + mLastMotionY = y; + mIsUnableToDrag = true; + return false; + } + if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollingCacheEnabled(true); + } else if (yDiff > mTouchSlop) { + // The finger has moved enough in the vertical + // direction to be counted as a drag... abort + // any attempt to drag horizontally, to work correctly + // with children that have scrolling containers. + if (DEBUG) Log.v(TAG, "Starting unable to drag!"); + mIsUnableToDrag = true; + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + if (performDrag(x)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mIsUnableToDrag = false; + + mScroller.computeScrollOffset(); + if (mScrollState == SCROLL_STATE_SETTLING && + Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) { + // Let the user 'catch' the pager as it animates. + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + setScrollState(SCROLL_STATE_DRAGGING); + } else { + completeScroll(false); + mIsBeingDragged = false; + } + + if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY + + " mIsBeingDragged=" + mIsBeingDragged + + "mIsUnableToDrag=" + mIsUnableToDrag); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mFakeDragging) { + // A fake drag is in progress already, ignore this real one + // but still eat the touch events. + // (It is likely that the user is multi-touching the screen.) + return true; + } + + if (ev.getAction() == MotionEvent.ACTION_DOWN && ev.getEdgeFlags() != 0) { + // Don't handle edge touches immediately -- they may actually belong to one of our + // descendants. + return false; + } + + if (mAdapter == null || mAdapter.getCount() == 0) { + // Nothing to present or scroll; nothing to touch. + return false; + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + final int action = ev.getAction(); + boolean needsInvalidate = false; + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + mScroller.abortAnimation(); + mPopulatePending = false; + populate(); + + // Remember where the motion event started + mLastMotionX = mInitialMotionX = ev.getX(); + mLastMotionY = mInitialMotionY = ev.getY(); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + break; + } + case MotionEvent.ACTION_MOVE: + if (!mIsBeingDragged) { + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, pointerIndex); + final float xDiff = Math.abs(x - mLastMotionX); + final float y = MotionEventCompat.getY(ev, pointerIndex); + final float yDiff = Math.abs(y - mLastMotionY); + if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff); + if (xDiff > mTouchSlop && xDiff > yDiff) { + if (DEBUG) Log.v(TAG, "Starting drag!"); + mIsBeingDragged = true; + requestParentDisallowInterceptTouchEvent(true); + mLastMotionX = x - mInitialMotionX > 0 ? mInitialMotionX + mTouchSlop : + mInitialMotionX - mTouchSlop; + mLastMotionY = y; + setScrollState(SCROLL_STATE_DRAGGING); + setScrollingCacheEnabled(true); + + // Disallow Parent Intercept, just in case + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + } + // Not else! Note that mIsBeingDragged can be set above. + if (mIsBeingDragged) { + // Scroll to follow the motion event + final int activePointerIndex = MotionEventCompat.findPointerIndex( + ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, activePointerIndex); + needsInvalidate |= performDrag(x); + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( + velocityTracker, mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int activePointerIndex = + MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, activePointerIndex); + final int totalDelta = (int) (x - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + + mActivePointerId = INVALID_POINTER; + endDrag(); + needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged) { + scrollToItem(mCurItem, true, 0, false); + mActivePointerId = INVALID_POINTER; + endDrag(); + needsInvalidate = mLeftEdge.onRelease() | mRightEdge.onRelease(); + } + break; + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + final float x = MotionEventCompat.getX(ev, index); + mLastMotionX = x; + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionX = MotionEventCompat.getX(ev, + MotionEventCompat.findPointerIndex(ev, mActivePointerId)); + break; + } + if (needsInvalidate) { + ViewCompat.postInvalidateOnAnimation(this); + } + return true; + } + + private void requestParentDisallowInterceptTouchEvent(boolean disallowIntercept) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + + private boolean performDrag(float x) { + boolean needsInvalidate = false; + + final float deltaX = mLastMotionX - x; + mLastMotionX = x; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX + deltaX; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + boolean leftAbsolute = true; + boolean rightAbsolute = true; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftAbsolute = false; + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightAbsolute = false; + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + if (leftAbsolute) { + float over = leftBound - scrollX; + needsInvalidate = mLeftEdge.onPull(Math.abs(over) / width); + } + scrollX = leftBound; + } else if (scrollX > rightBound) { + if (rightAbsolute) { + float over = scrollX - rightBound; + needsInvalidate = mRightEdge.onPull(Math.abs(over) / width); + } + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + return needsInvalidate; + } + + /** + * @return Info about the page at the current scroll position. + * This can be synthetic for a missing middle page; the 'object' field can be null. + */ + private ItemInfo infoForCurrentScrollPosition() { + final int width = getClientWidth(); + final float scrollOffset = width > 0 ? (float) getScrollX() / width : 0; + final float marginOffset = width > 0 ? (float) mPageMargin / width : 0; + int lastPos = -1; + float lastOffset = 0.f; + float lastWidth = 0.f; + boolean first = true; + + ItemInfo lastItem = null; + for (int i = 0; i < mItems.size(); i++) { + ItemInfo ii = mItems.get(i); + float offset; + if (!first && ii.position != lastPos + 1) { + // Create a synthetic item for a missing page. + ii = mTempItem; + ii.offset = lastOffset + lastWidth + marginOffset; + ii.position = lastPos + 1; + ii.widthFactor = mAdapter.getPageWidth(ii.position); + i--; + } + offset = ii.offset; + + final float leftBound = offset; + final float rightBound = offset + ii.widthFactor + marginOffset; + if (first || scrollOffset >= leftBound) { + if (scrollOffset < rightBound || i == mItems.size() - 1) { + return ii; + } + } else { + return lastItem; + } + first = false; + lastPos = ii.position; + lastOffset = offset; + lastWidth = ii.widthFactor; + lastItem = ii; + } + + return lastItem; + } + + private int determineTargetPage(int currentPage, float pageOffset, int velocity, int deltaX) { + int targetPage; + if (Math.abs(deltaX) > mFlingDistance && Math.abs(velocity) > mMinimumVelocity) { + targetPage = velocity > 0 ? currentPage : currentPage + 1; + } else { + final float truncator = currentPage >= mCurItem ? 0.4f : 0.6f; + targetPage = (int) (currentPage + pageOffset + truncator); + } + + if (mItems.size() > 0) { + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + + // Only let the user target pages we have items for + targetPage = Math.max(firstItem.position, Math.min(targetPage, lastItem.position)); + } + + return targetPage; + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + boolean needsInvalidate = false; + + final int overScrollMode = ViewCompat.getOverScrollMode(this); + if (overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && + mAdapter != null && mAdapter.getCount() > 1)) { + if (!mLeftEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + final int width = getWidth(); + + canvas.rotate(270); + canvas.translate(-height + getPaddingTop(), mFirstOffset * width); + mLeftEdge.setSize(height, width); + needsInvalidate |= mLeftEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + if (!mRightEdge.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.rotate(90); + canvas.translate(-getPaddingTop(), -(mLastOffset + 1) * width); + mRightEdge.setSize(height, width); + needsInvalidate |= mRightEdge.draw(canvas); + canvas.restoreToCount(restoreCount); + } + } else { + mLeftEdge.finish(); + mRightEdge.finish(); + } + + if (needsInvalidate) { + // Keep animating + ViewCompat.postInvalidateOnAnimation(this); + } + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + // Draw the margin drawable between pages if needed. + if (mPageMargin > 0 && mMarginDrawable != null && mItems.size() > 0 && mAdapter != null) { + final int scrollX = getScrollX(); + final int width = getWidth(); + + final float marginOffset = (float) mPageMargin / width; + int itemIndex = 0; + ItemInfo ii = mItems.get(0); + float offset = ii.offset; + final int itemCount = mItems.size(); + final int firstPos = ii.position; + final int lastPos = mItems.get(itemCount - 1).position; + for (int pos = firstPos; pos < lastPos; pos++) { + while (pos > ii.position && itemIndex < itemCount) { + ii = mItems.get(++itemIndex); + } + + float drawAt; + if (pos == ii.position) { + drawAt = (ii.offset + ii.widthFactor) * width; + offset = ii.offset + ii.widthFactor + marginOffset; + } else { + float widthFactor = mAdapter.getPageWidth(pos); + drawAt = (offset + widthFactor) * width; + offset += widthFactor + marginOffset; + } + + if (drawAt + mPageMargin > scrollX) { + mMarginDrawable.setBounds((int) drawAt, mTopPageBounds, + (int) (drawAt + mPageMargin + 0.5f), mBottomPageBounds); + mMarginDrawable.draw(canvas); + } + + if (drawAt > scrollX + width) { + break; // No more visible, no sense in continuing + } + } + } + } + + /** + * Start a fake drag of the pager. + * + *

    A fake drag can be useful if you want to synchronize the motion of the ViewPager + * with the touch scrolling of another view, while still letting the ViewPager + * control the snapping motion and fling behavior. (e.g. parallax-scrolling tabs.) + * Call {@link #fakeDragBy(float)} to simulate the actual drag motion. Call + * {@link #endFakeDrag()} to complete the fake drag and fling as necessary. + * + *

    During a fake drag the ViewPager will ignore all touch events. If a real drag + * is already in progress, this method will return false. + * + * @return true if the fake drag began successfully, false if it could not be started. + * + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean beginFakeDrag() { + if (mIsBeingDragged) { + return false; + } + mFakeDragging = true; + setScrollState(SCROLL_STATE_DRAGGING); + mInitialMotionX = mLastMotionX = 0; + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(time, time, MotionEvent.ACTION_DOWN, 0, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + mFakeDragBeginTime = time; + return true; + } + + /** + * End a fake drag of the pager. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + */ + public void endFakeDrag() { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) VelocityTrackerCompat.getXVelocity( + velocityTracker, mActivePointerId); + mPopulatePending = true; + final int width = getClientWidth(); + final int scrollX = getScrollX(); + final ItemInfo ii = infoForCurrentScrollPosition(); + final int currentPage = ii.position; + final float pageOffset = (((float) scrollX / width) - ii.offset) / ii.widthFactor; + final int totalDelta = (int) (mLastMotionX - mInitialMotionX); + int nextPage = determineTargetPage(currentPage, pageOffset, initialVelocity, + totalDelta); + setCurrentItemInternal(nextPage, true, true, initialVelocity); + endDrag(); + + mFakeDragging = false; + } + + /** + * Fake drag by an offset in pixels. You must have called {@link #beginFakeDrag()} first. + * + * @param xOffset Offset in pixels to drag by. + * @see #beginFakeDrag() + * @see #endFakeDrag() + */ + public void fakeDragBy(float xOffset) { + if (!mFakeDragging) { + throw new IllegalStateException("No fake drag in progress. Call beginFakeDrag first."); + } + + mLastMotionX += xOffset; + + float oldScrollX = getScrollX(); + float scrollX = oldScrollX - xOffset; + final int width = getClientWidth(); + + float leftBound = width * mFirstOffset; + float rightBound = width * mLastOffset; + + final ItemInfo firstItem = mItems.get(0); + final ItemInfo lastItem = mItems.get(mItems.size() - 1); + if (firstItem.position != 0) { + leftBound = firstItem.offset * width; + } + if (lastItem.position != mAdapter.getCount() - 1) { + rightBound = lastItem.offset * width; + } + + if (scrollX < leftBound) { + scrollX = leftBound; + } else if (scrollX > rightBound) { + scrollX = rightBound; + } + // Don't lose the rounded component + mLastMotionX += scrollX - (int) scrollX; + scrollTo((int) scrollX, getScrollY()); + pageScrolled((int) scrollX); + + // Synthesize an event for the VelocityTracker. + final long time = SystemClock.uptimeMillis(); + final MotionEvent ev = MotionEvent.obtain(mFakeDragBeginTime, time, MotionEvent.ACTION_MOVE, + mLastMotionX, 0, 0); + mVelocityTracker.addMovement(ev); + ev.recycle(); + } + + /** + * Returns true if a fake drag is in progress. + * + * @return true if currently in a fake drag, false otherwise. + * + * @see #beginFakeDrag() + * @see #fakeDragBy(float) + * @see #endFakeDrag() + */ + public boolean isFakeDragging() { + return mFakeDragging; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = MotionEventCompat.getActionIndex(ev); + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionX = MotionEventCompat.getX(ev, newPointerIndex); + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + private void endDrag() { + mIsBeingDragged = false; + mIsUnableToDrag = false; + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + private void setScrollingCacheEnabled(boolean enabled) { + if (mScrollingCacheEnabled != enabled) { + mScrollingCacheEnabled = enabled; + if (USE_CACHE) { + final int size = getChildCount(); + for (int i = 0; i < size; ++i) { + final View child = getChildAt(i); + if (child.getVisibility() != GONE) { + child.setDrawingCacheEnabled(enabled); + } + } + } + } + } + + public boolean canScrollHorizontally(int direction) { + if (mAdapter == null) { + return false; + } + + final int width = getClientWidth(); + final int scrollX = getScrollX(); + if (direction < 0) { + return (scrollX > (int) (width * mFirstOffset)); + } else if (direction > 0) { + return (scrollX < (int) (width * mLastOffset)); + } else { + return false; + } + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && + y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && + canScroll(child, true, dx, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && ViewCompat.canScrollHorizontally(v, -dx); + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_LEFT: + handled = arrowScroll(FOCUS_LEFT); + break; + case KeyEvent.KEYCODE_DPAD_RIGHT: + handled = arrowScroll(FOCUS_RIGHT); + break; + case KeyEvent.KEYCODE_TAB: + if (Build.VERSION.SDK_INT >= 11) { + // The focus finder had a bug handling FOCUS_FORWARD and FOCUS_BACKWARD + // before Android 3.0. Ignore the tab key on those devices. + if (KeyEventCompat.hasNoModifiers(event)) { + handled = arrowScroll(FOCUS_FORWARD); + } else if (KeyEventCompat.hasModifiers(event, KeyEvent.META_SHIFT_ON)) { + handled = arrowScroll(FOCUS_BACKWARD); + } + } + break; + } + } + return handled; + } + + public boolean arrowScroll(int direction) { + View currentFocused = findFocus(); + if (currentFocused == this) { + currentFocused = null; + } else if (currentFocused != null) { + boolean isChild = false; + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + if (parent == this) { + isChild = true; + break; + } + } + if (!isChild) { + // This would cause the focus search down below to fail in fun ways. + final StringBuilder sb = new StringBuilder(); + sb.append(currentFocused.getClass().getSimpleName()); + for (ViewParent parent = currentFocused.getParent(); parent instanceof ViewGroup; + parent = parent.getParent()) { + sb.append(" => ").append(parent.getClass().getSimpleName()); + } + Log.e(TAG, "arrowScroll tried to find focus based on non-child " + + "current focused view " + sb.toString()); + currentFocused = null; + } + } + + boolean handled = false; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, + direction); + if (nextFocused != null && nextFocused != currentFocused) { + if (direction == View.FOCUS_LEFT) { + // If there is nothing to the left, or this is causing us to + // jump to the right, then what we really want to do is page left. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft >= currLeft) { + handled = pageLeft(); + } else { + handled = nextFocused.requestFocus(); + } + } else if (direction == View.FOCUS_RIGHT) { + // If there is nothing to the right, or this is causing us to + // jump to the left, then what we really want to do is page right. + final int nextLeft = getChildRectInPagerCoordinates(mTempRect, nextFocused).left; + final int currLeft = getChildRectInPagerCoordinates(mTempRect, currentFocused).left; + if (currentFocused != null && nextLeft <= currLeft) { + handled = pageRight(); + } else { + handled = nextFocused.requestFocus(); + } + } + } else if (direction == FOCUS_LEFT || direction == FOCUS_BACKWARD) { + // Trying to move left and nothing there; try to page. + handled = pageLeft(); + } else if (direction == FOCUS_RIGHT || direction == FOCUS_FORWARD) { + // Trying to move right and nothing there; try to page. + handled = pageRight(); + } + if (handled) { + playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction)); + } + return handled; + } + + private Rect getChildRectInPagerCoordinates(Rect outRect, View child) { + if (outRect == null) { + outRect = new Rect(); + } + if (child == null) { + outRect.set(0, 0, 0, 0); + return outRect; + } + outRect.left = child.getLeft(); + outRect.right = child.getRight(); + outRect.top = child.getTop(); + outRect.bottom = child.getBottom(); + + ViewParent parent = child.getParent(); + while (parent instanceof ViewGroup && parent != this) { + final ViewGroup group = (ViewGroup) parent; + outRect.left += group.getLeft(); + outRect.right += group.getRight(); + outRect.top += group.getTop(); + outRect.bottom += group.getBottom(); + + parent = group.getParent(); + } + return outRect; + } + + boolean pageLeft() { + if (mCurItem > 0) { + setCurrentItem(mCurItem-1, true); + return true; + } + return false; + } + + boolean pageRight() { + if (mAdapter != null && mCurItem < (mAdapter.getCount()-1)) { + setCurrentItem(mCurItem+1, true); + return true; + } + return false; + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + public void addFocusables(ArrayList views, int direction, int focusableMode) { + final int focusableCount = views.size(); + + final int descendantFocusability = getDescendantFocusability(); + + if (descendantFocusability != FOCUS_BLOCK_DESCENDANTS) { + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addFocusables(views, direction, focusableMode); + } + } + } + } + + // we add ourselves (if focusable) in all cases except for when we are + // FOCUS_AFTER_DESCENDANTS and there are some descendants focusable. this is + // to avoid the focus search finding layouts when a more precise search + // among the focusable children would be more interesting. + if ( + descendantFocusability != FOCUS_AFTER_DESCENDANTS || + // No focusable descendants + (focusableCount == views.size())) { + // Note that we can't call the superclass here, because it will + // add all views in. So we need to do the same thing View does. + if (!isFocusable()) { + return; + } + if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE && + isInTouchMode() && !isFocusableInTouchMode()) { + return; + } + if (views != null) { + views.add(this); + } + } + } + + /** + * We only want the current page that is being shown to be touchable. + */ + @Override + public void addTouchables(ArrayList views) { + // Note that we don't call super.addTouchables(), which means that + // we don't call View.addTouchables(). This is okay because a ViewPager + // is itself not touchable. + for (int i = 0; i < getChildCount(); i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + child.addTouchables(views); + } + } + } + } + + /** + * We only want the current page that is being shown to be focusable. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + int index; + int increment; + int end; + int count = getChildCount(); + if ((direction & FOCUS_FORWARD) != 0) { + index = 0; + increment = 1; + end = count; + } else { + index = count - 1; + increment = -1; + end = -1; + } + for (int i = index; i != end; i += increment) { + View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem) { + if (child.requestFocus(direction, previouslyFocusedRect)) { + return true; + } + } + } + } + return false; + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) { + // Dispatch scroll events from this ViewPager. + if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED) { + return super.dispatchPopulateAccessibilityEvent(event); + } + + // Dispatch all other accessibility events from the current page. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == VISIBLE) { + final ItemInfo ii = infoForChild(child); + if (ii != null && ii.position == mCurItem && + child.dispatchPopulateAccessibilityEvent(event)) { + return true; + } + } + } + + return false; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return generateDefaultLayoutParams(); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + class MyAccessibilityDelegate extends AccessibilityDelegateCompat { + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + event.setClassName(ViewPager.class.getName()); + final AccessibilityRecordCompat recordCompat = AccessibilityRecordCompat.obtain(); + recordCompat.setScrollable(canScroll()); + if (event.getEventType() == AccessibilityEventCompat.TYPE_VIEW_SCROLLED + && mAdapter != null) { + recordCompat.setItemCount(mAdapter.getCount()); + recordCompat.setFromIndex(mCurItem); + recordCompat.setToIndex(mCurItem); + } + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + info.setClassName(ViewPager.class.getName()); + info.setScrollable(canScroll()); + if (canScrollHorizontally(1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } + if (canScrollHorizontally(-1)) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (super.performAccessibilityAction(host, action, args)) { + return true; + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { + if (canScrollHorizontally(1)) { + setCurrentItem(mCurItem + 1); + return true; + } + } return false; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { + if (canScrollHorizontally(-1)) { + setCurrentItem(mCurItem - 1); + return true; + } + } return false; + } + return false; + } + + private boolean canScroll() { + return (mAdapter != null) && (mAdapter.getCount() > 1); + } + } + + private class PagerObserver extends DataSetObserver { + @Override + public void onChanged() { + dataSetChanged(); + } + @Override + public void onInvalidated() { + dataSetChanged(); + } + } + + /** + * Layout parameters that should be supplied for views added to a + * ViewPager. + */ + public static class LayoutParams extends ViewGroup.LayoutParams { + /** + * true if this view is a decoration on the pager itself and not + * a view supplied by the adapter. + */ + public boolean isDecor; + + /** + * Gravity setting for use on decor views only: + * Where to position the view page within the overall ViewPager + * container; constants are defined in {@link android.view.Gravity}. + */ + public int gravity; + + /** + * Width as a 0-1 multiplier of the measured pager width + */ + float widthFactor = 0.f; + + /** + * true if this view was added during layout and needs to be measured + * before being positioned. + */ + boolean needsMeasure; + + /** + * Adapter position this view is for if !isDecor + */ + int position; + + /** + * Current child index within the ViewPager that this view occupies + */ + int childIndex; + + public LayoutParams() { + super(FILL_PARENT, FILL_PARENT); + } + + public LayoutParams(Context context, AttributeSet attrs) { + super(context, attrs); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + gravity = a.getInteger(0, Gravity.TOP); + a.recycle(); + } + } + + static class ViewPositionComparator implements Comparator { + @Override + public int compare(View lhs, View rhs) { + final LayoutParams llp = (LayoutParams) lhs.getLayoutParams(); + final LayoutParams rlp = (LayoutParams) rhs.getLayoutParams(); + if (llp.isDecor != rlp.isDecor) { + return llp.isDecor ? 1 : -1; + } + return llp.position - rlp.position; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompat.java new file mode 100644 index 00000000000..b95b332fabb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompat.java @@ -0,0 +1,427 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.content.Context; +import android.os.Build; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +/** + * Helper for accessing features in {@link ViewParent} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class ViewParentCompat { + + interface ViewParentCompatImpl { + public boolean requestSendAccessibilityEvent( + ViewParent parent, View child, AccessibilityEvent event); + boolean onStartNestedScroll(ViewParent parent, View child, View target, + int nestedScrollAxes); + void onNestedScrollAccepted(ViewParent parent, View child, View target, + int nestedScrollAxes); + void onStopNestedScroll(ViewParent parent, View target); + void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed); + void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, int[] consumed); + boolean onNestedFling(ViewParent parent, View target, float velocityX, float velocityY, + boolean consumed); + boolean onNestedPreFling(ViewParent parent, View target, float velocityX, float velocityY); + void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child, + View source, int changeType); + } + + static class ViewParentCompatStubImpl implements ViewParentCompatImpl { + @Override + public boolean requestSendAccessibilityEvent( + ViewParent parent, View child, AccessibilityEvent event) { + // Emulate what ViewRootImpl does in ICS and above. + if (child == null) { + return false; + } + final AccessibilityManager manager = (AccessibilityManager) child.getContext() + .getSystemService(Context.ACCESSIBILITY_SERVICE); + manager.sendAccessibilityEvent(event); + return true; + } + + @Override + public boolean onStartNestedScroll(ViewParent parent, View child, View target, + int nestedScrollAxes) { + if (parent instanceof NestedScrollingParent) { + return ((NestedScrollingParent) parent).onStartNestedScroll(child, target, + nestedScrollAxes); + } + return false; + } + + @Override + public void onNestedScrollAccepted(ViewParent parent, View child, View target, + int nestedScrollAxes) { + if (parent instanceof NestedScrollingParent) { + ((NestedScrollingParent) parent).onNestedScrollAccepted(child, target, + nestedScrollAxes); + } + } + + @Override + public void onStopNestedScroll(ViewParent parent, View target) { + if (parent instanceof NestedScrollingParent) { + ((NestedScrollingParent) parent).onStopNestedScroll(target); + } + } + + @Override + public void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed) { + if (parent instanceof NestedScrollingParent) { + ((NestedScrollingParent) parent).onNestedScroll(target, dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed); + } + } + + @Override + public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, + int[] consumed) { + if (parent instanceof NestedScrollingParent) { + ((NestedScrollingParent) parent).onNestedPreScroll(target, dx, dy, consumed); + } + } + + @Override + public boolean onNestedFling(ViewParent parent, View target, float velocityX, + float velocityY, boolean consumed) { + if (parent instanceof NestedScrollingParent) { + return ((NestedScrollingParent) parent).onNestedFling(target, velocityX, velocityY, + consumed); + } + return false; + } + + @Override + public boolean onNestedPreFling(ViewParent parent, View target, float velocityX, + float velocityY) { + if (parent instanceof NestedScrollingParent) { + return ((NestedScrollingParent) parent).onNestedPreFling(target, velocityX, + velocityY); + } + return false; + } + + @Override + public void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child, + View source, int changeType) { + } + } + + static class ViewParentCompatICSImpl extends ViewParentCompatStubImpl { + @Override + public boolean requestSendAccessibilityEvent( + ViewParent parent, View child, AccessibilityEvent event) { + return ViewParentCompatICS.requestSendAccessibilityEvent(parent, child, event); + } + } + + static class ViewParentCompatKitKatImpl extends ViewParentCompatICSImpl { + + @Override + public void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child, + View source, int changeType) { + ViewParentCompatKitKat.notifySubtreeAccessibilityStateChanged(parent, child, + source, changeType); + } + } + + static class ViewParentCompatLollipopImpl extends ViewParentCompatKitKatImpl { + @Override + public boolean onStartNestedScroll(ViewParent parent, View child, View target, + int nestedScrollAxes) { + return ViewParentCompatLollipop.onStartNestedScroll(parent, child, target, + nestedScrollAxes); + } + + @Override + public void onNestedScrollAccepted(ViewParent parent, View child, View target, + int nestedScrollAxes) { + ViewParentCompatLollipop.onNestedScrollAccepted(parent, child, target, + nestedScrollAxes); + } + + @Override + public void onStopNestedScroll(ViewParent parent, View target) { + ViewParentCompatLollipop.onStopNestedScroll(parent, target); + } + + @Override + public void onNestedScroll(ViewParent parent, View target, int dxConsumed, int dyConsumed, + int dxUnconsumed, int dyUnconsumed) { + ViewParentCompatLollipop.onNestedScroll(parent, target, dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed); + } + + @Override + public void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, + int[] consumed) { + ViewParentCompatLollipop.onNestedPreScroll(parent, target, dx, dy, consumed); + } + + @Override + public boolean onNestedFling(ViewParent parent, View target, float velocityX, + float velocityY, boolean consumed) { + return ViewParentCompatLollipop.onNestedFling(parent, target, velocityX, velocityY, + consumed); + } + + @Override + public boolean onNestedPreFling(ViewParent parent, View target, float velocityX, + float velocityY) { + return ViewParentCompatLollipop.onNestedPreFling(parent, target, velocityX, velocityY); + } + } + + static final ViewParentCompatImpl IMPL; + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + IMPL = new ViewParentCompatLollipopImpl(); + } else if (version >= 19) { + IMPL = new ViewParentCompatKitKatImpl(); + } else if (version >= 14) { + IMPL = new ViewParentCompatICSImpl(); + } else { + IMPL = new ViewParentCompatStubImpl(); + } + } + + /* + * Hide the constructor. + */ + private ViewParentCompat() { + + } + + /** + * Called by a child to request from its parent to send an {@link AccessibilityEvent}. + * The child has already populated a record for itself in the event and is delegating + * to its parent to send the event. The parent can optionally add a record for itself. + *

    + * Note: An accessibility event is fired by an individual view which populates the + * event with a record for its state and requests from its parent to perform + * the sending. The parent can optionally add a record for itself before + * dispatching the request to its parent. A parent can also choose not to + * respect the request for sending the event. The accessibility event is sent + * by the topmost view in the view tree.

    + * + * @param parent The parent whose method to invoke. + * @param child The child which requests sending the event. + * @param event The event to be sent. + * @return True if the event was sent. + */ + public static boolean requestSendAccessibilityEvent( + ViewParent parent, View child, AccessibilityEvent event) { + return IMPL.requestSendAccessibilityEvent(parent, child, event); + } + + /** + * React to a descendant view initiating a nestable scroll operation, claiming the + * nested scroll operation if appropriate. + * + *

    This method will be called in response to a descendant view invoking + * {@link ViewCompat#startNestedScroll(View, int)}. Each parent up the view hierarchy will be + * given an opportunity to respond and claim the nested scrolling operation by returning + * true.

    + * + *

    This method may be overridden by ViewParent implementations to indicate when the view + * is willing to support a nested scrolling operation that is about to begin. If it returns + * true, this ViewParent will become the target view's nested scrolling parent for the duration + * of the scroll operation in progress. When the nested scroll is finished this ViewParent + * will receive a call to {@link #onStopNestedScroll(ViewParent, View)}. + *

    + * + * @param child Direct child of this ViewParent containing target + * @param target View that initiated the nested scroll + * @param nestedScrollAxes Flags consisting of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, + * {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both + * @return true if this ViewParent accepts the nested scroll operation + */ + public static boolean onStartNestedScroll(ViewParent parent, View child, View target, + int nestedScrollAxes) { + return IMPL.onStartNestedScroll(parent, child, target, nestedScrollAxes); + } + + /** + * React to the successful claiming of a nested scroll operation. + * + *

    This method will be called after + * {@link #onStartNestedScroll(ViewParent, View, View, int) onStartNestedScroll} returns true. + * It offers an opportunity for the view and its superclasses to perform initial configuration + * for the nested scroll. Implementations of this method should always call their superclass's + * implementation of this method if one is present.

    + * + * @param child Direct child of this ViewParent containing target + * @param target View that initiated the nested scroll + * @param nestedScrollAxes Flags consisting of {@link ViewCompat#SCROLL_AXIS_HORIZONTAL}, + * {@link ViewCompat#SCROLL_AXIS_VERTICAL} or both + * @see #onStartNestedScroll(ViewParent, View, View, int) + * @see #onStopNestedScroll(ViewParent, View) + */ + public static void onNestedScrollAccepted(ViewParent parent, View child, View target, + int nestedScrollAxes) { + IMPL.onNestedScrollAccepted(parent, child, target, nestedScrollAxes); + } + + /** + * React to a nested scroll operation ending. + * + *

    Perform cleanup after a nested scrolling operation. + * This method will be called when a nested scroll stops, for example when a nested touch + * scroll ends with a {@link MotionEvent#ACTION_UP} or {@link MotionEvent#ACTION_CANCEL} event. + * Implementations of this method should always call their superclass's implementation of this + * method if one is present.

    + * + * @param target View that initiated the nested scroll + */ + public static void onStopNestedScroll(ViewParent parent, View target) { + IMPL.onStopNestedScroll(parent, target); + } + + /** + * React to a nested scroll in progress. + * + *

    This method will be called when the ViewParent's current nested scrolling child view + * dispatches a nested scroll event. To receive calls to this method the ViewParent must have + * previously returned true for a call to + * {@link #onStartNestedScroll(ViewParent, View, View, int)}.

    + * + *

    Both the consumed and unconsumed portions of the scroll distance are reported to the + * ViewParent. An implementation may choose to use the consumed portion to match or chase scroll + * position of multiple child elements, for example. The unconsumed portion may be used to + * allow continuous dragging of multiple scrolling or draggable elements, such as scrolling + * a list within a vertical drawer where the drawer begins dragging once the edge of inner + * scrolling content is reached.

    + * + * @param target The descendent view controlling the nested scroll + * @param dxConsumed Horizontal scroll distance in pixels already consumed by target + * @param dyConsumed Vertical scroll distance in pixels already consumed by target + * @param dxUnconsumed Horizontal scroll distance in pixels not consumed by target + * @param dyUnconsumed Vertical scroll distance in pixels not consumed by target + */ + public static void onNestedScroll(ViewParent parent, View target, int dxConsumed, + int dyConsumed, int dxUnconsumed, int dyUnconsumed) { + IMPL.onNestedScroll(parent, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); + } + + /** + * React to a nested scroll in progress before the target view consumes a portion of the scroll. + * + *

    When working with nested scrolling often the parent view may want an opportunity + * to consume the scroll before the nested scrolling child does. An example of this is a + * drawer that contains a scrollable list. The user will want to be able to scroll the list + * fully into view before the list itself begins scrolling.

    + * + *

    onNestedPreScroll is called when a nested scrolling child invokes + * {@link ViewCompat#dispatchNestedPreScroll(View, int, int, int[], int[])}. The implementation + * should report how any pixels of the scroll reported by dx, dy were consumed in the + * consumed array. Index 0 corresponds to dx and index 1 corresponds to dy. + * This parameter will never be null. Initial values for consumed[0] and consumed[1] + * will always be 0.

    + * + * @param target View that initiated the nested scroll + * @param dx Horizontal scroll distance in pixels + * @param dy Vertical scroll distance in pixels + * @param consumed Output. The horizontal and vertical scroll distance consumed by this parent + */ + public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, + int[] consumed) { + IMPL.onNestedPreScroll(parent, target, dx, dy, consumed); + } + + /** + * Request a fling from a nested scroll. + * + *

    This method signifies that a nested scrolling child has detected suitable conditions + * for a fling. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

    + * + *

    If a nested scrolling child view would normally fling but it is at the edge of + * its own content, it can use this method to delegate the fling to its nested scrolling + * parent instead. The parent may optionally consume the fling or observe a child fling.

    + * + * @param target View that initiated the nested scroll + * @param velocityX Horizontal velocity in pixels per second + * @param velocityY Vertical velocity in pixels per second + * @param consumed true if the child consumed the fling, false otherwise + * @return true if this parent consumed or otherwise reacted to the fling + */ + public static boolean onNestedFling(ViewParent parent, View target, float velocityX, + float velocityY, boolean consumed) { + return IMPL.onNestedFling(parent, target, velocityX, velocityY, consumed); + } + + /** + * React to a nested fling before the target view consumes it. + * + *

    This method siginfies that a nested scrolling child has detected a fling with the given + * velocity along each axis. Generally this means that a touch scroll has ended with a + * {@link VelocityTracker velocity} in the direction of scrolling that meets or exceeds + * the {@link ViewConfiguration#getScaledMinimumFlingVelocity() minimum fling velocity} + * along a scrollable axis.

    + * + *

    If a nested scrolling parent is consuming motion as part of a + * {@link #onNestedPreScroll(ViewParent, View, int, int, int[]) pre-scroll}, it may be + * appropriate for it to also consume the pre-fling to complete that same motion. By returning + * true from this method, the parent indicates that the child should not + * fling its own internal content as well.

    + * + * @param target View that initiated the nested scroll + * @param velocityX Horizontal velocity in pixels per second + * @param velocityY Vertical velocity in pixels per second + * @return true if this parent consumed the fling ahead of the target view + */ + public static boolean onNestedPreFling(ViewParent parent, View target, float velocityX, + float velocityY) { + return IMPL.onNestedPreFling(parent, target, velocityX, velocityY); + } + + /** + * Notifies a view parent that the accessibility state of one of its + * descendants has changed and that the structure of the subtree is + * different. + * @param child The direct child whose subtree has changed. + * @param source The descendant view that changed. + * @param changeType A bit mask of the types of changes that occurred. One + * or more of: + *
      + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION} + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE} + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT} + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED} + *
    + */ + public static void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child, + View source, int changeType) { + IMPL.notifySubtreeAccessibilityStateChanged(parent, child, source, changeType); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatICS.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatICS.java new file mode 100644 index 00000000000..251de0e0d08 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatICS.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.View; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; + +/** + * ICS-specific ViewParent API implementation. + */ +public class ViewParentCompatICS { + public static boolean requestSendAccessibilityEvent( + ViewParent parent, View child, AccessibilityEvent event) { + return parent.requestSendAccessibilityEvent(child, event); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatKitKat.java new file mode 100644 index 00000000000..5e60a9bb0e4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatKitKat.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.View; +import android.view.ViewParent; + +class ViewParentCompatKitKat { + public static void notifySubtreeAccessibilityStateChanged(ViewParent parent, View child, + View source, int changeType) { + parent.notifySubtreeAccessibilityStateChanged(child, source, changeType); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatLollipop.java new file mode 100644 index 00000000000..7dbcf61b6d0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewParentCompatLollipop.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.view; + +import android.util.Log; +import android.view.View; +import android.view.ViewParent; + +class ViewParentCompatLollipop { + private static final String TAG = "ViewParentCompat"; + + public static boolean onStartNestedScroll(ViewParent parent, View child, View target, + int nestedScrollAxes) { + try { + return parent.onStartNestedScroll(child, target, nestedScrollAxes); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onStartNestedScroll", e); + return false; + } + } + + public static void onNestedScrollAccepted(ViewParent parent, View child, View target, + int nestedScrollAxes) { + try { + parent.onNestedScrollAccepted(child, target, nestedScrollAxes); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onNestedScrollAccepted", e); + } + } + + public static void onStopNestedScroll(ViewParent parent, View target) { + try { + parent.onStopNestedScroll(target); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onStopNestedScroll", e); + } + } + + public static void onNestedScroll(ViewParent parent, View target, int dxConsumed, + int dyConsumed, int dxUnconsumed, int dyUnconsumed) { + try { + parent.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onNestedScroll", e); + } + } + + public static void onNestedPreScroll(ViewParent parent, View target, int dx, int dy, + int[] consumed) { + try { + parent.onNestedPreScroll(target, dx, dy, consumed); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onNestedPreScroll", e); + } + } + + public static boolean onNestedFling(ViewParent parent, View target, float velocityX, + float velocityY, boolean consumed) { + try { + return parent.onNestedFling(target, velocityX, velocityY, consumed); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onNestedFling", e); + return false; + } + } + + public static boolean onNestedPreFling(ViewParent parent, View target, float velocityX, + float velocityY) { + try { + return parent.onNestedPreFling(target, velocityX, velocityY); + } catch (AbstractMethodError e) { + Log.e(TAG, "ViewParent " + parent + " does not implement interface " + + "method onNestedPreFling", e); + return false; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompat.java new file mode 100644 index 00000000000..c4a21454b59 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompat.java @@ -0,0 +1,1338 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.view; + +import android.view.View; +import android.view.animation.Interpolator; + +import java.lang.ref.WeakReference; +import java.util.WeakHashMap; + +public class ViewPropertyAnimatorCompat { + private static final String TAG = "ViewAnimatorCompat"; + private WeakReference mView; + private Runnable mStartAction = null; + private Runnable mEndAction = null; + private int mOldLayerType = -1; + // HACK ALERT! Choosing this id knowing that the framework does not use it anywhere + // internally and apps should use ids higher than it + static final int LISTENER_TAG_ID = 0x7e000000; + + + ViewPropertyAnimatorCompat(View view) { + mView = new WeakReference(view); + } + + interface ViewPropertyAnimatorCompatImpl { + public void setDuration(ViewPropertyAnimatorCompat vpa, View view, long value); + public long getDuration(ViewPropertyAnimatorCompat vpa, View view); + public void setInterpolator(ViewPropertyAnimatorCompat vpa, View view, Interpolator value); + public Interpolator getInterpolator(ViewPropertyAnimatorCompat vpa, View view); + public void setStartDelay(ViewPropertyAnimatorCompat vpa, View view, long value); + public long getStartDelay(ViewPropertyAnimatorCompat vpa, View view); + public void alpha(ViewPropertyAnimatorCompat vpa, View view, float value); + public void alphaBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void rotation(ViewPropertyAnimatorCompat vpa, View view, float value); + public void rotationBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void rotationX(ViewPropertyAnimatorCompat vpa, View view, float value); + public void rotationXBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void rotationY(ViewPropertyAnimatorCompat vpa, View view, float value); + public void rotationYBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void scaleX(ViewPropertyAnimatorCompat vpa, View view, float value); + public void scaleXBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void scaleY(ViewPropertyAnimatorCompat vpa, View view, float value); + public void scaleYBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void cancel(ViewPropertyAnimatorCompat vpa, View view); + public void x(ViewPropertyAnimatorCompat vpa, View view, float value); + public void xBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void y(ViewPropertyAnimatorCompat vpa, View view, float value); + public void yBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void z(ViewPropertyAnimatorCompat vpa, View view, float value); + public void zBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void translationX(ViewPropertyAnimatorCompat vpa, View view, float value); + public void translationXBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void translationY(ViewPropertyAnimatorCompat vpa, View view, float value); + public void translationYBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void translationZ(ViewPropertyAnimatorCompat vpa, View view, float value); + public void translationZBy(ViewPropertyAnimatorCompat vpa, View view, float value); + public void start(ViewPropertyAnimatorCompat vpa, View view); + public void withLayer(ViewPropertyAnimatorCompat vpa, View view); + public void withStartAction(ViewPropertyAnimatorCompat vpa, View view, Runnable runnable); + public void withEndAction(ViewPropertyAnimatorCompat vpa, View view, Runnable runnable); + public void setListener(ViewPropertyAnimatorCompat vpa, View view, + ViewPropertyAnimatorListener listener); + public void setUpdateListener(ViewPropertyAnimatorCompat vpa, View view, + ViewPropertyAnimatorUpdateListener listener); + }; + + static class BaseViewPropertyAnimatorCompatImpl implements ViewPropertyAnimatorCompatImpl { + WeakHashMap mStarterMap = null; + + @Override + public void setDuration(ViewPropertyAnimatorCompat vpa, View view, long value) { + // noop on versions prior to ICS + } + + @Override + public void alpha(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void translationX(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void translationY(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void withEndAction(ViewPropertyAnimatorCompat vpa, View view, Runnable runnable) { + vpa.mEndAction = runnable; + postStartMessage(vpa, view); + } + + @Override + public long getDuration(ViewPropertyAnimatorCompat vpa, View view) { + return 0; + } + + @Override + public void setInterpolator(ViewPropertyAnimatorCompat vpa, View view, Interpolator value) { + // noop on versions prior to ICS + } + + @Override + public Interpolator getInterpolator(ViewPropertyAnimatorCompat vpa, View view) { + return null; + } + + @Override + public void setStartDelay(ViewPropertyAnimatorCompat vpa, View view, long value) { + // noop on versions prior to ICS + } + + @Override + public long getStartDelay(ViewPropertyAnimatorCompat vpa, View view) { + return 0; + } + + @Override + public void alphaBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void rotation(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void rotationBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void rotationX(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void rotationXBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void rotationY(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void rotationYBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void scaleX(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void scaleXBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void scaleY(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void scaleYBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void cancel(ViewPropertyAnimatorCompat vpa, View view) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void x(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void xBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void y(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void yBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void z(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to Lollipop + } + + @Override + public void zBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to Lollipop + } + + @Override + public void translationXBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void translationYBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to ICS + postStartMessage(vpa, view); + } + + @Override + public void translationZ(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to Lollipop + } + + @Override + public void translationZBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + // noop on versions prior to Lollipop + } + + @Override + public void start(ViewPropertyAnimatorCompat vpa, View view) { + removeStartMessage(view); + startAnimation(vpa, view); + } + + @Override + public void withLayer(ViewPropertyAnimatorCompat vpa, View view) { + // noop on versions prior to ICS + } + + @Override + public void withStartAction(ViewPropertyAnimatorCompat vpa, View view, Runnable runnable) { + vpa.mStartAction = runnable; + postStartMessage(vpa, view); + } + + @Override + public void setListener(ViewPropertyAnimatorCompat vpa, View view, ViewPropertyAnimatorListener listener) { + view.setTag(LISTENER_TAG_ID, listener); + } + + @Override + public void setUpdateListener(ViewPropertyAnimatorCompat vpa, View view, ViewPropertyAnimatorUpdateListener listener) { + // noop + } + + private void startAnimation(ViewPropertyAnimatorCompat vpa, View view) { + Object listenerTag = view.getTag(LISTENER_TAG_ID); + ViewPropertyAnimatorListener listener = null; + if (listenerTag instanceof ViewPropertyAnimatorListener) { + listener = (ViewPropertyAnimatorListener) listenerTag; + } + Runnable startAction = vpa.mStartAction; + Runnable endAction = vpa.mEndAction; + if (startAction != null) { + startAction.run(); + } + if (listener != null) { + listener.onAnimationStart(view); + listener.onAnimationEnd(view); + } + if (endAction != null) { + endAction.run(); + } + if (mStarterMap != null) { + mStarterMap.remove(view); + } + } + + class Starter implements Runnable { + WeakReference mViewRef; + ViewPropertyAnimatorCompat mVpa; + + private Starter(ViewPropertyAnimatorCompat vpa, View view) { + mViewRef = new WeakReference(view); + mVpa = vpa; + } + + @Override + public void run() { + final View view = mViewRef.get(); + if (view != null) { + startAnimation(mVpa, view); + } + } + }; + + private void removeStartMessage(View view) { + Runnable starter = null; + if (mStarterMap != null) { + starter = mStarterMap.get(view); + if (starter != null) { + view.removeCallbacks(starter); + } + } + } + + private void postStartMessage(ViewPropertyAnimatorCompat vpa, View view) { + Runnable starter = null; + if (mStarterMap != null) { + starter = mStarterMap.get(view); + } + if (starter == null) { + starter = new Starter(vpa, view); + if (mStarterMap == null) { + mStarterMap = new WeakHashMap(); + } + mStarterMap.put(view, starter); + } + view.removeCallbacks(starter); + view.post(starter); + } + + } + + static class ICSViewPropertyAnimatorCompatImpl extends BaseViewPropertyAnimatorCompatImpl { + WeakHashMap mLayerMap = null; + + @Override + public void setDuration(ViewPropertyAnimatorCompat vpa, View view, long value) { + ViewPropertyAnimatorCompatICS.setDuration(view, value); + } + + @Override + public void alpha(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.alpha(view, value); + } + + @Override + public void translationX(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.translationX(view, value); + } + + @Override + public void translationY(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.translationY(view, value); + } + + @Override + public long getDuration(ViewPropertyAnimatorCompat vpa, View view) { + return ViewPropertyAnimatorCompatICS.getDuration(view); + } + + @Override + public void setInterpolator(ViewPropertyAnimatorCompat vpa, View view, Interpolator value) { + ViewPropertyAnimatorCompatICS.setInterpolator(view, value); + } + + @Override + public void setStartDelay(ViewPropertyAnimatorCompat vpa, View view, long value) { + ViewPropertyAnimatorCompatICS.setStartDelay(view, value); + } + + @Override + public long getStartDelay(ViewPropertyAnimatorCompat vpa, View view) { + return ViewPropertyAnimatorCompatICS.getStartDelay(view); + } + + @Override + public void alphaBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.alphaBy(view, value); + } + + @Override + public void rotation(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.rotation(view, value); + } + + @Override + public void rotationBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.rotationBy(view, value); + } + + @Override + public void rotationX(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.rotationX(view, value); + } + + @Override + public void rotationXBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.rotationXBy(view, value); + } + + @Override + public void rotationY(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.rotationY(view, value); + } + + @Override + public void rotationYBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.rotationYBy(view, value); + } + + @Override + public void scaleX(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.scaleX(view, value); + } + + @Override + public void scaleXBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.scaleXBy(view, value); + } + + @Override + public void scaleY(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.scaleY(view, value); + } + + @Override + public void scaleYBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.scaleYBy(view, value); + } + + @Override + public void cancel(ViewPropertyAnimatorCompat vpa, View view) { + ViewPropertyAnimatorCompatICS.cancel(view); + } + + @Override + public void x(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.x(view, value); + } + + @Override + public void xBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.xBy(view, value); + } + + @Override + public void y(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.y(view, value); + } + + @Override + public void yBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.yBy(view, value); + } + + @Override + public void translationXBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.translationXBy(view, value); + } + + @Override + public void translationYBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatICS.translationYBy(view, value); + } + + @Override + public void start(ViewPropertyAnimatorCompat vpa, View view) { + ViewPropertyAnimatorCompatICS.start(view); + } + + @Override + public void setListener(ViewPropertyAnimatorCompat vpa, View view, ViewPropertyAnimatorListener listener) { + view.setTag(LISTENER_TAG_ID, listener); + ViewPropertyAnimatorCompatICS.setListener(view, new MyVpaListener(vpa)); + } + + @Override + public void withEndAction(ViewPropertyAnimatorCompat vpa, View view, final Runnable runnable) { + ViewPropertyAnimatorCompatICS.setListener(view, new MyVpaListener(vpa)); + vpa.mEndAction = runnable; + } + + @Override + public void withStartAction(ViewPropertyAnimatorCompat vpa, View view, final Runnable runnable) { + ViewPropertyAnimatorCompatICS.setListener(view, new MyVpaListener(vpa)); + vpa.mStartAction = runnable; + } + + @Override + public void withLayer(ViewPropertyAnimatorCompat vpa, View view) { + vpa.mOldLayerType = ViewCompat.getLayerType(view); + ViewPropertyAnimatorCompatICS.setListener(view, new MyVpaListener(vpa)); + } + + static class MyVpaListener implements ViewPropertyAnimatorListener { + + ViewPropertyAnimatorCompat mVpa; + + MyVpaListener(ViewPropertyAnimatorCompat vpa) { + mVpa = vpa; + } + + @Override + public void onAnimationStart(View view) { + if (mVpa.mOldLayerType >= 0) { + ViewCompat.setLayerType(view, ViewCompat.LAYER_TYPE_HARDWARE, null); + } + if (mVpa.mStartAction != null) { + mVpa.mStartAction.run(); + } + Object listenerTag = view.getTag(LISTENER_TAG_ID); + ViewPropertyAnimatorListener listener = null; + if (listenerTag instanceof ViewPropertyAnimatorListener) { + listener = (ViewPropertyAnimatorListener) listenerTag; + } + if (listener != null) { + listener.onAnimationStart(view); + } + } + + @Override + public void onAnimationEnd(View view) { + if (mVpa.mOldLayerType >= 0) { + ViewCompat.setLayerType(view, mVpa.mOldLayerType, null); + mVpa.mOldLayerType = -1; + } + if (mVpa.mEndAction != null) { + mVpa.mEndAction.run(); + } + Object listenerTag = view.getTag(LISTENER_TAG_ID); + ViewPropertyAnimatorListener listener = null; + if (listenerTag instanceof ViewPropertyAnimatorListener) { + listener = (ViewPropertyAnimatorListener) listenerTag; + } + if (listener != null) { + listener.onAnimationEnd(view); + } + } + + @Override + public void onAnimationCancel(View view) { + Object listenerTag = view.getTag(LISTENER_TAG_ID); + ViewPropertyAnimatorListener listener = null; + if (listenerTag instanceof ViewPropertyAnimatorListener) { + listener = (ViewPropertyAnimatorListener) listenerTag; + } + if (listener != null) { + listener.onAnimationCancel(view); + } + } + }; + } + + static class JBViewPropertyAnimatorCompatImpl extends ICSViewPropertyAnimatorCompatImpl { + + @Override + public void setListener(ViewPropertyAnimatorCompat vpa, View view, ViewPropertyAnimatorListener listener) { + ViewPropertyAnimatorCompatJB.setListener(view, listener); + } + + @Override + public void withStartAction(ViewPropertyAnimatorCompat vpa, View view, Runnable runnable) { + ViewPropertyAnimatorCompatJB.withStartAction(view, runnable); + } + + @Override + public void withEndAction(ViewPropertyAnimatorCompat vpa, View view, Runnable runnable) { + ViewPropertyAnimatorCompatJB.withEndAction(view, runnable); + } + + @Override + public void withLayer(ViewPropertyAnimatorCompat vpa, View view) { + ViewPropertyAnimatorCompatJB.withLayer(view); + } + } + + static class JBMr2ViewPropertyAnimatorCompatImpl extends JBViewPropertyAnimatorCompatImpl { + + @Override + public Interpolator getInterpolator(ViewPropertyAnimatorCompat vpa, View view) { + return (Interpolator) ViewPropertyAnimatorCompatJellybeanMr2.getInterpolator(view); + } + } + + static class KitKatViewPropertyAnimatorCompatImpl extends JBMr2ViewPropertyAnimatorCompatImpl { + @Override + public void setUpdateListener(ViewPropertyAnimatorCompat vpa, View view, ViewPropertyAnimatorUpdateListener listener) { + ViewPropertyAnimatorCompatKK.setUpdateListener(view, listener); + } + } + + static class LollipopViewPropertyAnimatorCompatImpl extends KitKatViewPropertyAnimatorCompatImpl { + @Override + public void translationZ(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatLollipop.translationZ(view, value); + } + + @Override + public void translationZBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatLollipop.translationZBy(view, value); + } + + @Override + public void z(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatLollipop.z(view, value); + } + + @Override + public void zBy(ViewPropertyAnimatorCompat vpa, View view, float value) { + ViewPropertyAnimatorCompatLollipop.zBy(view, value); + } + } + + static final ViewPropertyAnimatorCompatImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 21) { + IMPL = new LollipopViewPropertyAnimatorCompatImpl(); + } else if (version >= 19) { + IMPL = new KitKatViewPropertyAnimatorCompatImpl(); + } else if (version >= 18) { + IMPL = new JBMr2ViewPropertyAnimatorCompatImpl(); + } else if (version >= 16) { + IMPL = new JBViewPropertyAnimatorCompatImpl(); + } else if (version >= 14) { + IMPL = new ICSViewPropertyAnimatorCompatImpl(); + } else { + IMPL = new BaseViewPropertyAnimatorCompatImpl(); + } + } + + /** + * Sets the duration for the underlying animator that animates the requested properties. + * By default, the animator uses the default value for ValueAnimator. Calling this method + * will cause the declared value to be used instead. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The length of ensuing property animations, in milliseconds. The value + * cannot be negative. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat setDuration(long value) { + View view; + if ((view = mView.get()) != null) { + IMPL.setDuration(this, view, value); + } + return this; + } + + /** + * This method will cause the View's alpha property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat alpha(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.alpha(this, view, value); + } + return this; + } + + /** + * This method will cause the View's alpha property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat alphaBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.alphaBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's translationX property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat translationX(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.translationX(this, view, value); + } + return this; + } + + /** + * This method will cause the View's translationY property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat translationY(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.translationY(this, view, value); + } + return this; + } + + /** + * Specifies an action to take place when the next animation ends. The action is only + * run if the animation ends normally; if the ViewPropertyAnimator is canceled during + * that animation, the runnable will not run. + * This method, along with {@link #withStartAction(Runnable)}, is intended to help facilitate + * choreographing ViewPropertyAnimator animations with other animations or actions + * in the application. + * + *

    For example, the following code animates a view to x=200 and then back to 0:

    + *
    +     *     Runnable endAction = new Runnable() {
    +     *         public void run() {
    +     *             view.animate().x(0);
    +     *         }
    +     *     };
    +     *     view.animate().x(200).withEndAction(endAction);
    +     * 
    + * + *

    Prior to API 14, this method will run the action immediately.

    + * + *

    For API 14 and 15, this method will run by setting + * a listener on the ViewPropertyAnimatorCompat object and running the action + * in that listener's {@link ViewPropertyAnimatorListener#onAnimationEnd(View)} method.

    + * + * @param runnable The action to run when the next animation ends. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat withEndAction(Runnable runnable) { + View view; + if ((view = mView.get()) != null) { + IMPL.withEndAction(this, view, runnable); + } + return this; + } + + /** + * Returns the current duration of property animations. If the duration was set on this + * object, that value is returned. Otherwise, the default value of the underlying Animator + * is returned. + * + *

    Prior to API 14, this method will return 0.

    + * + * @see #setDuration(long) + * @return The duration of animations, in milliseconds. + */ + public long getDuration() { + View view; + if ((view = mView.get()) != null) { + return IMPL.getDuration(this, view); + } else { + return 0; + } + } + + /** + * Sets the interpolator for the underlying animator that animates the requested properties. + * By default, the animator uses the default interpolator for ValueAnimator. Calling this method + * will cause the declared object to be used instead. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The TimeInterpolator to be used for ensuing property animations. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat setInterpolator(Interpolator value) { + View view; + if ((view = mView.get()) != null) { + IMPL.setInterpolator(this, view, value); + } + return this; + } + + /** + * Returns the timing interpolator that this animation uses. + * + *

    Prior to API 14, this method will return null.

    + * + * @return The timing interpolator for this animation. + */ + public Interpolator getInterpolator() { + View view; + if ((view = mView.get()) != null) { + return IMPL.getInterpolator(this, view); + } + else return null; + } + + /** + * Sets the startDelay for the underlying animator that animates the requested properties. + * By default, the animator uses the default value for ValueAnimator. Calling this method + * will cause the declared value to be used instead. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The delay of ensuing property animations, in milliseconds. The value + * cannot be negative. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat setStartDelay(long value) { + View view; + if ((view = mView.get()) != null) { + IMPL.setStartDelay(this, view, value); + } + return this; + } + + /** + * Returns the current startDelay of property animations. If the startDelay was set on this + * object, that value is returned. Otherwise, the default value of the underlying Animator + * is returned. + * + *

    Prior to API 14, this method will return 0.

    + * + * @see #setStartDelay(long) + * @return The startDelay of animations, in milliseconds. + */ + public long getStartDelay() { + View view; + if ((view = mView.get()) != null) { + return IMPL.getStartDelay(this, view); + } else { + return 0; + } + } + + /** + * This method will cause the View's rotation property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat rotation(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.rotation(this, view, value); + } + return this; + } + + /** + * This method will cause the View's rotation property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat rotationBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.rotationBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's rotationX property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat rotationX(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.rotationX(this, view, value); + } + return this; + } + + /** + * This method will cause the View's rotationX property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat rotationXBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.rotationXBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's rotationY property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat rotationY(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.rotationY(this, view, value); + } + return this; + } + + /** + * This method will cause the View's rotationY property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat rotationYBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.rotationYBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's scaleX property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat scaleX(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.scaleX(this, view, value); + } + return this; + } + + /** + * This method will cause the View's scaleX property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat scaleXBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.scaleXBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's scaleY property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat scaleY(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.scaleY(this, view, value); + } + return this; + } + + /** + * This method will cause the View's scaleY property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat scaleYBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.scaleYBy(this, view, value); + } + return this; + } + + /** + * Cancels all property animations that are currently running or pending. + */ + public void cancel() { + View view; + if ((view = mView.get()) != null) { + IMPL.cancel(this, view); + } + } + + /** + * This method will cause the View's x property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat x(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.x(this, view, value); + } + return this; + } + + /** + * This method will cause the View's x property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat xBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.xBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's y property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The value to be animated to. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat y(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.y(this, view, value); + } + return this; + } + + /** + * This method will cause the View's y property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat yBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.yBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's translationX property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat translationXBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.translationXBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's translationY property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat translationYBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.translationYBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's translationZ property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 21, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat translationZBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.translationZBy(this, view, value); + } + return this; + } + + /** + * This method will cause the View's translationZ property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 21, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat translationZ(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.translationZ(this, view, value); + } + return this; + } + + /** + * This method will cause the View's z property to be animated to the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 21, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat z(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.z(this, view, value); + } + return this; + } + + /** + * This method will cause the View's z property to be animated by the + * specified value. Animations already running on the property will be canceled. + * + *

    Prior to API 21, this method will do nothing.

    + * + * @param value The amount to be animated by, as an offset from the current value. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat zBy(float value) { + View view; + if ((view = mView.get()) != null) { + IMPL.zBy(this, view, value); + } + return this; + } + + /** + * Starts the currently pending property animations immediately. Calling start() + * is optional because all animations start automatically at the next opportunity. However, + * if the animations are needed to start immediately and synchronously (not at the time when + * the next event is processed by the hierarchy, which is when the animations would begin + * otherwise), then this method can be used. + * + *

    Prior to API 14, this method will do nothing.

    + */ + public void start() { + View view; + if ((view = mView.get()) != null) { + IMPL.start(this, view); + } + } + + /** + * The View associated with this ViewPropertyAnimator will have its + * {@link ViewCompat#setLayerType(View, int, android.graphics.Paint) layer type} set to + * {@link ViewCompat#LAYER_TYPE_HARDWARE} for the duration of the next animation. + * As stated in the documentation for {@link ViewCompat#LAYER_TYPE_HARDWARE}, + * the actual type of layer used internally depends on the runtime situation of the + * view. If the activity and this view are hardware-accelerated, then the layer will be + * accelerated as well. If the activity or the view is not accelerated, then the layer will + * effectively be the same as {@link ViewCompat#LAYER_TYPE_SOFTWARE}. + * + *

    This state is not persistent, either on the View or on this ViewPropertyAnimator: the + * layer type of the View will be restored when the animation ends to what it was when this + * method was called, and this setting on ViewPropertyAnimator is only valid for the next + * animation. Note that calling this method and then independently setting the layer type of + * the View (by a direct call to + * {@link ViewCompat#setLayerType(View, int, android.graphics.Paint)}) will result in some + * inconsistency, including having the layer type restored to its pre-withLayer() + * value when the animation ends.

    + * + *

    Prior to API 14, this method will do nothing.

    + * + *

    For API 14 and 15, this method will run by setting + * a listener on the ViewPropertyAnimatorCompat object, setting a hardware layer in + * the listener's {@link ViewPropertyAnimatorListener#onAnimationStart(View)} method, + * and then restoring the orignal layer type in the listener's + * {@link ViewPropertyAnimatorListener#onAnimationEnd(View)} method.

    + * + * @see View#setLayerType(int, android.graphics.Paint) + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat withLayer() { + View view; + if ((view = mView.get()) != null) { + IMPL.withLayer(this, view); + } + return this; + } + + /** + * Specifies an action to take place when the next animation runs. If there is a + * {@link #setStartDelay(long) startDelay} set on this ViewPropertyAnimator, then the + * action will run after that startDelay expires, when the actual animation begins. + * This method, along with {@link #withEndAction(Runnable)}, is intended to help facilitate + * choreographing ViewPropertyAnimator animations with other animations or actions + * in the application. + * + *

    Prior to API 14, this method will run the action immediately.

    + * + *

    For API 14 and 15, this method will run by setting + * a listener on the ViewPropertyAnimatorCompat object and running the action + * in that listener's {@link ViewPropertyAnimatorListener#onAnimationStart(View)} method.

    + * + * @param runnable The action to run when the next animation starts. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat withStartAction(Runnable runnable) { + View view; + if ((view = mView.get()) != null) { + IMPL.withStartAction(this, view, runnable); + } + return this; + } + + /** + * Sets a listener for events in the underlying Animators that run the property + * animations. + * + *

    Prior to API 14, this method will do nothing.

    + * + * @param listener The listener to be called with AnimatorListener events. A value of + * null removes any existing listener. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat setListener(ViewPropertyAnimatorListener listener) { + View view; + if ((view = mView.get()) != null) { + IMPL.setListener(this, view, listener); + } + return this; + } + + /** + * Sets a listener for update events in the underlying Animator that runs + * the property animations. + * + *

    Prior to API 19, this method will do nothing.

    + * + * @param listener The listener to be called with update events. A value of + * null removes any existing listener. + * @return This object, allowing calls to methods in this class to be chained. + */ + public ViewPropertyAnimatorCompat setUpdateListener( + ViewPropertyAnimatorUpdateListener listener) { + View view; + if ((view = mView.get()) != null) { + IMPL.setUpdateListener(this, view, listener); + } + return this; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatICS.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatICS.java new file mode 100644 index 00000000000..1fd7f3ebc9f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatICS.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.view; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.view.View; +import android.view.animation.Interpolator; + +class ViewPropertyAnimatorCompatICS { + + public static void setDuration(View view, long value) { + view.animate().setDuration(value); + } + + public static void alpha(View view, float value) { + view.animate().alpha(value); + } + + public static void translationX(View view, float value) { + view.animate().translationX(value); + } + + public static void translationY(View view, float value) { + view.animate().translationY(value); + } + + public static long getDuration(View view) { + return view.animate().getDuration(); + } + + public static void setInterpolator(View view, Interpolator value) { + view.animate().setInterpolator(value); + } + + public static void setStartDelay(View view, long value) { + view.animate().setStartDelay(value); + } + + public static long getStartDelay(View view) { + return view.animate().getStartDelay(); + } + + public static void alphaBy(View view, float value) { + view.animate().alphaBy(value); + } + + public static void rotation(View view, float value) { + view.animate().rotation(value); + } + + public static void rotationBy(View view, float value) { + view.animate().rotationBy(value); + } + + public static void rotationX(View view, float value) { + view.animate().rotationX(value); + } + + public static void rotationXBy(View view, float value) { + view.animate().rotationXBy(value); + } + + public static void rotationY(View view, float value) { + view.animate().rotationY(value); + } + + public static void rotationYBy(View view, float value) { + view.animate().rotationYBy(value); + } + + public static void scaleX(View view, float value) { + view.animate().scaleX(value); + } + + public static void scaleXBy(View view, float value) { + view.animate().scaleXBy(value); + } + + public static void scaleY(View view, float value) { + view.animate().scaleY(value); + } + + public static void scaleYBy(View view, float value) { + view.animate().scaleYBy(value); + } + + public static void cancel(View view) { + view.animate().cancel(); + } + + public static void x(View view, float value) { + view.animate().x(value); + } + + public static void xBy(View view, float value) { + view.animate().xBy(value); + } + + public static void y(View view, float value) { + view.animate().y(value); + } + + public static void yBy(View view, float value) { + view.animate().yBy(value); + } + + public static void translationXBy(View view, float value) { + view.animate().translationXBy(value); + } + + public static void translationYBy(View view, float value) { + view.animate().translationYBy(value); + } + + public static void start(View view) { + view.animate().start(); + } + + public static void setListener(final View view, + final ViewPropertyAnimatorListener listener) { + if (listener != null) { + view.animate().setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + listener.onAnimationCancel(view); + } + + @Override + public void onAnimationEnd(Animator animation) { + listener.onAnimationEnd(view); + } + + @Override + public void onAnimationStart(Animator animation) { + listener.onAnimationStart(view); + } + }); + } else { + view.animate().setListener(null); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJB.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJB.java new file mode 100644 index 00000000000..6107c42c4f4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJB.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.view; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.view.View; + +class ViewPropertyAnimatorCompatJB { + + public static void withStartAction(View view, Runnable runnable) { + view.animate().withStartAction(runnable); + } + + public static void withEndAction(View view, Runnable runnable) { + view.animate().withEndAction(runnable); + } + + public static void withLayer(View view) { + view.animate().withLayer(); + } + + public static void setListener(final View view, + final ViewPropertyAnimatorListener listener) { + if (listener != null) { + view.animate().setListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationCancel(Animator animation) { + listener.onAnimationCancel(view); + } + + @Override + public void onAnimationEnd(Animator animation) { + listener.onAnimationEnd(view); + } + + @Override + public void onAnimationStart(Animator animation) { + listener.onAnimationStart(view); + } + }); + } else { + view.animate().setListener(null); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJellybeanMr2.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJellybeanMr2.java new file mode 100644 index 00000000000..e9a29ee0127 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatJellybeanMr2.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.view; + +import android.view.View; +import android.view.animation.Interpolator; + +class ViewPropertyAnimatorCompatJellybeanMr2 { + public static Interpolator getInterpolator(View view) { + return (Interpolator) view.animate().getInterpolator(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatKK.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatKK.java new file mode 100644 index 00000000000..5439972d655 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatKK.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.view; + +import android.animation.ValueAnimator; +import android.view.View; + +class ViewPropertyAnimatorCompatKK { + + public static void setUpdateListener(final View view, + final ViewPropertyAnimatorUpdateListener listener) { + view.animate().setUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + listener.onAnimationUpdate(view); + } + }); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatLollipop.java new file mode 100644 index 00000000000..3bfc427e77a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorCompatLollipop.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.View; + +class ViewPropertyAnimatorCompatLollipop { + + public static void translationZ(View view, float value) { + view.animate().translationZ(value); + } + + public static void translationZBy(View view, float value) { + view.animate().translationZBy(value); + } + + public static void z(View view, float value) { + view.animate().z(value); + } + + public static void zBy(View view, float value) { + view.animate().zBy(value); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListener.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListener.java new file mode 100644 index 00000000000..72f1daf9c23 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListener.java @@ -0,0 +1,47 @@ +package android.support.v4.view;/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.view.View; + +/** + *

    An animation listener receives notifications from an animation. + * Notifications indicate animation related events, such as the end or the + * start of the animation.

    + */ +public interface ViewPropertyAnimatorListener { + /** + *

    Notifies the start of the animation.

    + * + * @param view The view associated with the ViewPropertyAnimator + */ + void onAnimationStart(View view); + + /** + *

    Notifies the end of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

    + * + * @param view The view associated with the ViewPropertyAnimator + */ + void onAnimationEnd(View view); + + /** + *

    Notifies the cancellation of the animation. This callback is not invoked + * for animations with repeat count set to INFINITE.

    + * + * @param view The view associated with the ViewPropertyAnimator + */ + void onAnimationCancel(View view); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListenerAdapter.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListenerAdapter.java new file mode 100644 index 00000000000..4f58912824e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorListenerAdapter.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.View; + +/** + * This adapter class provides empty implementations of the methods from + * {@link ViewPropertyAnimatorListener}. Any custom listener that cares only about a subset of + * the methods of this listener can simply subclass this adapter class instead of implementing + * the interface directly. + */ +public class ViewPropertyAnimatorListenerAdapter implements ViewPropertyAnimatorListener { + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationStart(View view) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationEnd(View view) { + } + + /** + * {@inheritDoc} + */ + @Override + public void onAnimationCancel(View view) { + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorUpdateListener.java b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorUpdateListener.java new file mode 100644 index 00000000000..b189d8a8699 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/ViewPropertyAnimatorUpdateListener.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.View; + +/** + * Implementors of this interface can add themselves as update listeners + * to an ViewPropertyAnimatorCompat instance to receive callbacks on every animation + * frame, after the current frame's values have been calculated for that + * ViewPropertyAnimatorCompat. + */ +public interface ViewPropertyAnimatorUpdateListener { + + /** + *

    Notifies the occurrence of another frame of the animation.

    + * + * @param view The view associated with the ViewPropertyAnimatorCompat + */ + void onAnimationUpdate(View view); + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/WindowCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/WindowCompat.java new file mode 100644 index 00000000000..15fe7154b74 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/WindowCompat.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.view.View; +import android.view.Window; + +/** + * Helper for accessing features in {@link Window} introduced after API + * level 4 in a backwards compatible fashion. + */ +public class WindowCompat { + /** + * Flag for enabling the Action Bar. + * This is enabled by default for some devices. The Action Bar + * replaces the title bar and provides an alternate location + * for an on-screen menu button on some devices. + */ + public static final int FEATURE_ACTION_BAR = 8; + + /** + * Flag for requesting an Action Bar that overlays window content. + * Normally an Action Bar will sit in the space above window content, but if this + * feature is requested along with {@link #FEATURE_ACTION_BAR} it will be layered over + * the window content itself. This is useful if you would like your app to have more control + * over how the Action Bar is displayed, such as letting application content scroll beneath + * an Action Bar with a transparent background or otherwise displaying a transparent/translucent + * Action Bar over application content. + * + *

    This mode is especially useful with {@link View#SYSTEM_UI_FLAG_FULLSCREEN + * View.SYSTEM_UI_FLAG_FULLSCREEN}, which allows you to seamlessly hide the + * action bar in conjunction with other screen decorations. + * + *

    As of {@link android.os.Build.VERSION_CODES#JELLY_BEAN}, when an + * ActionBar is in this mode it will adjust the insets provided to + * {@link View#fitSystemWindows(android.graphics.Rect) View.fitSystemWindows(Rect)} + * to include the content covered by the action bar, so you can do layout within + * that space. + */ + public static final int FEATURE_ACTION_BAR_OVERLAY = 9; + + /** + * Flag for specifying the behavior of action modes when an Action Bar is not present. + * If overlay is enabled, the action mode UI will be allowed to cover existing window content. + */ + public static final int FEATURE_ACTION_MODE_OVERLAY = 10; +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompat.java new file mode 100644 index 00000000000..45959a9768e --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompat.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.graphics.Rect; + +/** + * Describes a set of insets for window content. + * + *

    WindowInsetsCompats are immutable and may be expanded to include more inset types in the + * future. To adjust insets, use one of the supplied clone methods to obtain a new + * WindowInsetsCompat instance with the adjusted properties.

    + */ +public class WindowInsetsCompat { + + /** Private ctor */ + WindowInsetsCompat() {} + + /** + * Returns the left system window inset in pixels. + * + *

    The system window inset represents the area of a full-screen window that is + * partially or fully obscured by the status bar, navigation bar, IME or other system windows. + *

    + * + * @return The left system window inset + */ + public int getSystemWindowInsetLeft() { + return 0; + } + + /** + * Returns the top system window inset in pixels. + * + *

    The system window inset represents the area of a full-screen window that is + * partially or fully obscured by the status bar, navigation bar, IME or other system windows. + *

    + * + * @return The top system window inset + */ + public int getSystemWindowInsetTop() { + return 0; + } + + /** + * Returns the right system window inset in pixels. + * + *

    The system window inset represents the area of a full-screen window that is + * partially or fully obscured by the status bar, navigation bar, IME or other system windows. + *

    + * + * @return The right system window inset + */ + public int getSystemWindowInsetRight() { + return 0; + } + + /** + * Returns the bottom system window inset in pixels. + * + *

    The system window inset represents the area of a full-screen window that is + * partially or fully obscured by the status bar, navigation bar, IME or other system windows. + *

    + * + * @return The bottom system window inset + */ + public int getSystemWindowInsetBottom() { + return 0; + } + + /** + * Returns true if this WindowInsets has nonzero system window insets. + * + *

    The system window inset represents the area of a full-screen window that is + * partially or fully obscured by the status bar, navigation bar, IME or other system windows. + *

    + * + * @return true if any of the system window inset values are nonzero + */ + public boolean hasSystemWindowInsets() { + return false; + } + + /** + * Returns true if this WindowInsets has any nonzero insets. + * + * @return true if any inset values are nonzero + */ + public boolean hasInsets() { + return false; + } + + /** + * Check if these insets have been fully consumed. + * + *

    Insets are considered "consumed" if the applicable consume* methods + * have been called such that all insets have been set to zero. This affects propagation of + * insets through the view hierarchy; insets that have not been fully consumed will continue + * to propagate down to child views.

    + * + *

    The result of this method is equivalent to the return value of + * {@link android.view.View#fitSystemWindows(android.graphics.Rect)}.

    + * + * @return true if the insets have been fully consumed. + */ + public boolean isConsumed() { + return false; + } + + /** + * Returns true if the associated window has a round shape. + * + *

    A round window's left, top, right and bottom edges reach all the way to the + * associated edges of the window but the corners may not be visible. Views responding + * to round insets should take care to not lay out critical elements within the corners + * where they may not be accessible.

    + * + * @return True if the window is round + */ + public boolean isRound() { + return false; + } + + /** + * Returns a copy of this WindowInsets with the system window insets fully consumed. + * + * @return A modified copy of this WindowInsets + */ + public WindowInsetsCompat consumeSystemWindowInsets() { + return this; + } + + /** + * Returns a copy of this WindowInsets with selected system window insets replaced + * with new values. + * + * @param left New left inset in pixels + * @param top New top inset in pixels + * @param right New right inset in pixels + * @param bottom New bottom inset in pixels + * @return A modified copy of this WindowInsets + */ + public WindowInsetsCompat replaceSystemWindowInsets(int left, int top, int right, int bottom) { + return this; + } + + /** + * Returns a copy of this WindowInsets with selected system window insets replaced + * with new values. + * + * @param systemWindowInsets New system window insets. Each field is the inset in pixels + * for that edge + * @return A modified copy of this WindowInsets + */ + public WindowInsetsCompat replaceSystemWindowInsets(Rect systemWindowInsets) { + return this; + } + + /** + * Returns the top stable inset in pixels. + * + *

    The stable inset represents the area of a full-screen window that may be + * partially or fully obscured by the system UI elements. This value does not change + * based on the visibility state of those elements; for example, if the status bar is + * normally shown, but temporarily hidden, the stable inset will still provide the inset + * associated with the status bar being shown.

    + * + * @return The top stable inset + */ + public int getStableInsetTop() { + return 0; + } + + + /** + * Returns the left stable inset in pixels. + * + *

    The stable inset represents the area of a full-screen window that may be + * partially or fully obscured by the system UI elements. This value does not change + * based on the visibility state of those elements; for example, if the status bar is + * normally shown, but temporarily hidden, the stable inset will still provide the inset + * associated with the status bar being shown.

    + * + * @return The left stable inset + */ + public int getStableInsetLeft() { + return 0; + } + + /** + * Returns the right stable inset in pixels. + * + *

    The stable inset represents the area of a full-screen window that may be + * partially or fully obscured by the system UI elements. This value does not change + * based on the visibility state of those elements; for example, if the status bar is + * normally shown, but temporarily hidden, the stable inset will still provide the inset + * associated with the status bar being shown.

    + * + * @return The right stable inset + */ + public int getStableInsetRight() { + return 0; + } + + + /** + * Returns the bottom stable inset in pixels. + * + *

    The stable inset represents the area of a full-screen window that may be + * partially or fully obscured by the system UI elements. This value does not change + * based on the visibility state of those elements; for example, if the status bar is + * normally shown, but temporarily hidden, the stable inset will still provide the inset + * associated with the status bar being shown.

    + * + * @return The bottom stable inset + */ + public int getStableInsetBottom() { + return 0; + } + + /** + * Returns true if this WindowInsets has nonzero stable insets. + * + *

    The stable inset represents the area of a full-screen window that may be + * partially or fully obscured by the system UI elements. This value does not change + * based on the visibility state of those elements; for example, if the status bar is + * normally shown, but temporarily hidden, the stable inset will still provide the inset + * associated with the status bar being shown.

    + * + * @return true if any of the stable inset values are nonzero + */ + public boolean hasStableInsets() { + return false; + } + + /** + * Returns a copy of this WindowInsets with the stable insets fully consumed. + * + * @return A modified copy of this WindowInsetsCompat + */ + public WindowInsetsCompat consumeStableInsets() { + return this; + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompatApi21.java new file mode 100644 index 00000000000..c3237a4d49d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/WindowInsetsCompatApi21.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view; + +import android.graphics.Rect; +import android.view.WindowInsets; + +class WindowInsetsCompatApi21 extends WindowInsetsCompat { + + private final WindowInsets mSource; + + WindowInsetsCompatApi21(WindowInsets source) { + mSource = source; + } + + @Override + public int getSystemWindowInsetLeft() { + return mSource.getSystemWindowInsetLeft(); + } + + @Override + public int getSystemWindowInsetTop() { + return mSource.getSystemWindowInsetTop(); + } + + @Override + public int getSystemWindowInsetRight() { + return mSource.getSystemWindowInsetRight(); + } + + @Override + public int getSystemWindowInsetBottom() { + return mSource.getSystemWindowInsetBottom(); + } + + @Override + public boolean hasSystemWindowInsets() { + return mSource.hasSystemWindowInsets(); + } + + @Override + public boolean hasInsets() { + return mSource.hasInsets(); + } + + @Override + public boolean isConsumed() { + return mSource.isConsumed(); + } + + @Override + public boolean isRound() { + return mSource.isRound(); + } + + @Override + public WindowInsetsCompat consumeSystemWindowInsets() { + return new WindowInsetsCompatApi21(mSource.consumeSystemWindowInsets()); + } + + @Override + public WindowInsetsCompat replaceSystemWindowInsets(int left, int top, int right, int bottom) { + return new WindowInsetsCompatApi21(mSource.replaceSystemWindowInsets(left, top, right, bottom)); + } + + @Override + public WindowInsetsCompat replaceSystemWindowInsets(Rect systemWindowInsets) { + return new WindowInsetsCompatApi21(mSource.replaceSystemWindowInsets(systemWindowInsets)); + } + + @Override + public int getStableInsetTop() { + return mSource.getStableInsetTop(); + } + + @Override + public int getStableInsetLeft() { + return mSource.getStableInsetLeft(); + } + + @Override + public int getStableInsetRight() { + return mSource.getStableInsetRight(); + } + + @Override + public int getStableInsetBottom() { + return mSource.getStableInsetBottom(); + } + + @Override + public boolean hasStableInsets() { + return mSource.hasStableInsets(); + } + + @Override + public WindowInsetsCompat consumeStableInsets() { + return new WindowInsetsCompatApi21(mSource.consumeStableInsets()); + } + + WindowInsets unwrap() { + return mSource; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompat.java new file mode 100644 index 00000000000..297d6464dc7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompat.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.os.Build; +import android.view.accessibility.AccessibilityEvent; + +/** + * Helper for accessing features in {@link AccessibilityEvent} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AccessibilityEventCompat { + + static interface AccessibilityEventVersionImpl { + int getRecordCount(AccessibilityEvent event); + void appendRecord(AccessibilityEvent event, Object record); + Object getRecord(AccessibilityEvent event, int index); + void setContentChangeTypes(AccessibilityEvent event, int types); + int getContentChangeTypes(AccessibilityEvent event); + } + + static class AccessibilityEventStubImpl implements AccessibilityEventVersionImpl { + + @Override + public void appendRecord(AccessibilityEvent event, Object record) { + + } + + @Override + public Object getRecord(AccessibilityEvent event, int index) { + return null; + } + + @Override + public void setContentChangeTypes(AccessibilityEvent event, int types) { + + } + + @Override + public int getRecordCount(AccessibilityEvent event) { + return 0; + } + + @Override + public int getContentChangeTypes(AccessibilityEvent event) { + return 0; + } + } + + static class AccessibilityEventIcsImpl extends AccessibilityEventStubImpl { + + @Override + public void appendRecord(AccessibilityEvent event, Object record) { + AccessibilityEventCompatIcs.appendRecord(event, record); + } + + @Override + public Object getRecord(AccessibilityEvent event, int index) { + return AccessibilityEventCompatIcs.getRecord(event, index); + } + + @Override + public int getRecordCount(AccessibilityEvent event) { + return AccessibilityEventCompatIcs.getRecordCount(event); + } + } + + static class AccessibilityEventKitKatImpl extends AccessibilityEventIcsImpl { + + @Override + public void setContentChangeTypes(AccessibilityEvent event, int types) { + AccessibilityEventCompatKitKat.setContentChangeTypes(event, types); + } + + @Override + public int getContentChangeTypes(AccessibilityEvent event) { + return AccessibilityEventCompatKitKat.getContentChangeTypes(event); + } + } + + private final static AccessibilityEventVersionImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 19) { // KitKat + IMPL = new AccessibilityEventKitKatImpl(); + } else if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new AccessibilityEventIcsImpl(); + } else { + IMPL = new AccessibilityEventStubImpl(); + } + } + + /** + * Represents the event of a hover enter over a {@link android.view.View}. + */ + public static final int TYPE_VIEW_HOVER_ENTER = 0x00000080; + + /** + * Represents the event of a hover exit over a {@link android.view.View}. + */ + public static final int TYPE_VIEW_HOVER_EXIT = 0x00000100; + + /** + * Represents the event of starting a touch exploration gesture. + */ + public static final int TYPE_TOUCH_EXPLORATION_GESTURE_START = 0x00000200; + + /** + * Represents the event of ending a touch exploration gesture. + */ + public static final int TYPE_TOUCH_EXPLORATION_GESTURE_END = 0x00000400; + + /** + * Represents the event of changing the content of a window. + */ + public static final int TYPE_WINDOW_CONTENT_CHANGED = 0x00000800; + + /** + * Represents the event of scrolling a view. + */ + public static final int TYPE_VIEW_SCROLLED = 0x00001000; + + /** + * Represents the event of changing the selection in an {@link android.widget.EditText}. + */ + public static final int TYPE_VIEW_TEXT_SELECTION_CHANGED = 0x00002000; + + /** + * Represents the event of an application making an announcement. + */ + public static final int TYPE_ANNOUNCEMENT = 0x00004000; + + /** + * Represents the event of gaining accessibility focus. + */ + public static final int TYPE_VIEW_ACCESSIBILITY_FOCUSED = 0x00008000; + + /** + * Represents the event of clearing accessibility focus. + */ + public static final int TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED = 0x00010000; + + /** + * Represents the event of traversing the text of a view at a given movement granularity. + */ + public static final int TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY = 0x00020000; + + /** + * Represents the event of beginning gesture detection. + */ + public static final int TYPE_GESTURE_DETECTION_START = 0x00040000; + + /** + * Represents the event of ending gesture detection. + */ + public static final int TYPE_GESTURE_DETECTION_END = 0x00080000; + + /** + * Represents the event of the user starting to touch the screen. + */ + public static final int TYPE_TOUCH_INTERACTION_START = 0x00100000; + + /** + * Represents the event of the user ending to touch the screen. + */ + public static final int TYPE_TOUCH_INTERACTION_END = 0x00200000; + + /** + * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: + * The type of change is not defined. + */ + public static final int CONTENT_CHANGE_TYPE_UNDEFINED = 0x00000000; + + /** + * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: + * A node in the subtree rooted at the source node was added or removed. + */ + public static final int CONTENT_CHANGE_TYPE_SUBTREE = 0x00000001; + + /** + * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: + * The node's text changed. + */ + public static final int CONTENT_CHANGE_TYPE_TEXT = 0x00000002; + + /** + * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: + * The node's content description changed. + */ + public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004; + + /** + * Mask for {@link AccessibilityEvent} all types. + * + * @see AccessibilityEvent#TYPE_VIEW_CLICKED + * @see AccessibilityEvent#TYPE_VIEW_LONG_CLICKED + * @see AccessibilityEvent#TYPE_VIEW_SELECTED + * @see AccessibilityEvent#TYPE_VIEW_FOCUSED + * @see AccessibilityEvent#TYPE_VIEW_TEXT_CHANGED + * @see AccessibilityEvent#TYPE_WINDOW_STATE_CHANGED + * @see AccessibilityEvent#TYPE_NOTIFICATION_STATE_CHANGED + * @see #TYPE_VIEW_HOVER_ENTER + * @see #TYPE_VIEW_HOVER_EXIT + * @see #TYPE_TOUCH_EXPLORATION_GESTURE_START + * @see #TYPE_TOUCH_EXPLORATION_GESTURE_END + * @see #TYPE_WINDOW_CONTENT_CHANGED + * @see #TYPE_VIEW_SCROLLED + * @see #TYPE_VIEW_TEXT_SELECTION_CHANGED + * @see #TYPE_ANNOUNCEMENT + * @see #TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY + * @see #TYPE_GESTURE_DETECTION_START + * @see #TYPE_GESTURE_DETECTION_END + * @see #TYPE_TOUCH_INTERACTION_START + * @see #TYPE_TOUCH_INTERACTION_END + */ + public static final int TYPES_ALL_MASK = 0xFFFFFFFF; + + /* + * Hide constructor from clients. + */ + private AccessibilityEventCompat() { + + } + + /** + * Gets the number of records contained in the event. + * + * @return The number of records. + */ + public static int getRecordCount(AccessibilityEvent event) { + return IMPL.getRecordCount(event); + } + + /** + * Appends an {@link android.view.accessibility.AccessibilityRecord} to the end of + * event records. + * + * @param record The record to append. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public static void appendRecord(AccessibilityEvent event, AccessibilityRecordCompat record) { + IMPL.appendRecord(event, record.getImpl()); + } + + /** + * Gets the record at a given index. + * + * @param index The index. + * @return The record at the specified index. + */ + public static AccessibilityRecordCompat getRecord(AccessibilityEvent event, int index) { + return new AccessibilityRecordCompat(IMPL.getRecord(event, index)); + } + + /** + * Creates an {@link AccessibilityRecordCompat} from an {@link AccessibilityEvent} + * that can be used to manipulate the event properties defined in + * {@link android.view.accessibility.AccessibilityRecord}. + *

    + * Note: Do not call {@link AccessibilityRecordCompat#recycle()} on the + * returned {@link AccessibilityRecordCompat}. Call {@link AccessibilityEvent#recycle()} + * in case you want to recycle the event. + *

    + * + * @param event The from which to create a record. + * @return An {@link AccessibilityRecordCompat}. + */ + public static AccessibilityRecordCompat asRecord(AccessibilityEvent event) { + return new AccessibilityRecordCompat(event); + } + + /** + * Sets the bit mask of node tree changes signaled by an + * {@link #TYPE_WINDOW_CONTENT_CHANGED} event. + * + * @param changeTypes The bit mask of change types. + * @throws IllegalStateException If called from an AccessibilityService. + * @see #getContentChangeTypes(AccessibilityEvent) + */ + public static void setContentChangeTypes(AccessibilityEvent event, int changeTypes) { + IMPL.setContentChangeTypes(event, changeTypes); + } + + /** + * Gets the bit mask of change types signaled by an + * {@link #TYPE_WINDOW_CONTENT_CHANGED} event. A single event may represent + * multiple change types. + * + * @return The bit mask of change types. One or more of: + *
      + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION} + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_SUBTREE} + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_TEXT} + *
    • {@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED} + *
    + */ + public static int getContentChangeTypes(AccessibilityEvent event) { + return IMPL.getContentChangeTypes(event); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatIcs.java new file mode 100644 index 00000000000..632c4d12447 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatIcs.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityRecord; + +/** + * ICS specific AccessibilityEvent API implementation. + */ +class AccessibilityEventCompatIcs { + + public static int getRecordCount(AccessibilityEvent event) { + return event.getRecordCount(); + } + + public static void appendRecord(AccessibilityEvent event, Object record) { + event.appendRecord((AccessibilityRecord) record); + } + + public static Object getRecord(AccessibilityEvent event, int index) { + return event.getRecord(index); + } + + public static void setScrollable(AccessibilityEvent event, boolean scrollable) { + event.setScrollable(scrollable); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatKitKat.java new file mode 100644 index 00000000000..1a2cd941dcb --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityEventCompatKitKat.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.view.accessibility.AccessibilityEvent; + +class AccessibilityEventCompatKitKat { + public static void setContentChangeTypes(AccessibilityEvent event, int changeTypes) { + event.setContentChangeTypes(changeTypes); + } + + public static int getContentChangeTypes(AccessibilityEvent event) { + return event.getContentChangeTypes(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompat.java new file mode 100644 index 00000000000..ee2da3312c7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompat.java @@ -0,0 +1,216 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.os.Build; +import android.support.v4.view.accessibility.AccessibilityManagerCompatIcs.AccessibilityStateChangeListenerBridge; +import android.view.accessibility.AccessibilityManager; + +import java.util.Collections; +import java.util.List; + +/** + * Helper for accessing features in {@link AccessibilityManager} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AccessibilityManagerCompat { + + interface AccessibilityManagerVersionImpl { + public Object newAccessiblityStateChangeListener( + AccessibilityStateChangeListenerCompat listener); + public boolean addAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener); + public boolean removeAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener); + public List getEnabledAccessibilityServiceList( + AccessibilityManager manager,int feedbackTypeFlags); + public List getInstalledAccessibilityServiceList( + AccessibilityManager manager); + public boolean isTouchExplorationEnabled(AccessibilityManager manager); + } + + static class AccessibilityManagerStubImpl implements AccessibilityManagerVersionImpl { + public Object newAccessiblityStateChangeListener( + AccessibilityStateChangeListenerCompat listener) { + return null; + } + + public boolean addAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener) { + return false; + } + + public boolean removeAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener) { + return false; + } + + public List getEnabledAccessibilityServiceList( + AccessibilityManager manager, int feedbackTypeFlags) { + return Collections.emptyList(); + } + + public List getInstalledAccessibilityServiceList( + AccessibilityManager manager) { + return Collections.emptyList(); + } + + public boolean isTouchExplorationEnabled(AccessibilityManager manager) { + return false; + } + } + + static class AccessibilityManagerIcsImpl extends AccessibilityManagerStubImpl { + + @Override + public Object newAccessiblityStateChangeListener( + final AccessibilityStateChangeListenerCompat listener) { + return AccessibilityManagerCompatIcs.newAccessibilityStateChangeListener( + new AccessibilityStateChangeListenerBridge() { + public void onAccessibilityStateChanged(boolean enabled) { + listener.onAccessibilityStateChanged(enabled); + } + }); + } + + @Override + public boolean addAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener) { + return AccessibilityManagerCompatIcs.addAccessibilityStateChangeListener(manager, + listener.mListener); + } + + @Override + public boolean removeAccessibilityStateChangeListener(AccessibilityManager manager, + final AccessibilityStateChangeListenerCompat listener) { + return AccessibilityManagerCompatIcs.removeAccessibilityStateChangeListener(manager, + listener.mListener); + } + + @Override + public List getEnabledAccessibilityServiceList( + AccessibilityManager manager, int feedbackTypeFlags) { + return AccessibilityManagerCompatIcs.getEnabledAccessibilityServiceList(manager, + feedbackTypeFlags); + } + + @Override + public List getInstalledAccessibilityServiceList( + AccessibilityManager manager) { + return AccessibilityManagerCompatIcs.getInstalledAccessibilityServiceList(manager); + } + + @Override + public boolean isTouchExplorationEnabled(AccessibilityManager manager) { + return AccessibilityManagerCompatIcs.isTouchExplorationEnabled(manager); + } + } + + static { + if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new AccessibilityManagerIcsImpl(); + } else { + IMPL = new AccessibilityManagerStubImpl(); + } + } + + private static final AccessibilityManagerVersionImpl IMPL; + + /** + * Registers an {@link AccessibilityManager.AccessibilityStateChangeListener} for changes in + * the global accessibility state of the system. + * + * @param manager The accessibility manager. + * @param listener The listener. + * @return True if successfully registered. + */ + public static boolean addAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener) { + return IMPL.addAccessibilityStateChangeListener(manager, listener); + } + + /** + * Unregisters an {@link AccessibilityManager.AccessibilityStateChangeListener}. + * + * @param manager The accessibility manager. + * @param listener The listener. + * @return True if successfully unregistered. + */ + public static boolean removeAccessibilityStateChangeListener(AccessibilityManager manager, + AccessibilityStateChangeListenerCompat listener) { + return IMPL.removeAccessibilityStateChangeListener(manager, listener); + } + + /** + * Returns the {@link AccessibilityServiceInfo}s of the installed accessibility services. + * + * @param manager The accessibility manager. + * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. + */ + public static List getInstalledAccessibilityServiceList( + AccessibilityManager manager) { + return IMPL.getInstalledAccessibilityServiceList(manager); + } + + /** + * Returns the {@link AccessibilityServiceInfo}s of the enabled accessibility services + * for a given feedback type. + * + * @param manager The accessibility manager. + * @param feedbackTypeFlags The feedback type flags. + * @return An unmodifiable list with {@link AccessibilityServiceInfo}s. + * + * @see AccessibilityServiceInfo#FEEDBACK_AUDIBLE + * @see AccessibilityServiceInfo#FEEDBACK_GENERIC + * @see AccessibilityServiceInfo#FEEDBACK_HAPTIC + * @see AccessibilityServiceInfo#FEEDBACK_SPOKEN + * @see AccessibilityServiceInfo#FEEDBACK_VISUAL + */ + public static List getEnabledAccessibilityServiceList( + AccessibilityManager manager, int feedbackTypeFlags) { + return IMPL.getEnabledAccessibilityServiceList(manager, feedbackTypeFlags); + } + + /** + * Returns if the touch exploration in the system is enabled. + * + * @param manager The accessibility manager. + * @return True if touch exploration is enabled, false otherwise. + */ + public static boolean isTouchExplorationEnabled(AccessibilityManager manager) { + return IMPL.isTouchExplorationEnabled(manager); + } + + /** + * Listener for the accessibility state. + */ + public static abstract class AccessibilityStateChangeListenerCompat { + final Object mListener; + + public AccessibilityStateChangeListenerCompat() { + mListener = IMPL.newAccessiblityStateChangeListener(this); + } + + /** + * Called back on change in the accessibility state. + * + * @param enabled Whether accessibility is enabled. + */ + public abstract void onAccessibilityStateChanged(boolean enabled); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompatIcs.java new file mode 100644 index 00000000000..4d4e024af02 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityManagerCompatIcs.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; + +import java.util.List; + +/** + * ICS specific AccessibilityManager API implementation. + */ +class AccessibilityManagerCompatIcs { + + interface AccessibilityStateChangeListenerBridge { + public void onAccessibilityStateChanged(boolean enabled); + } + + public static Object newAccessibilityStateChangeListener( + final AccessibilityStateChangeListenerBridge bridge) { + return new AccessibilityStateChangeListener() { + @Override + public void onAccessibilityStateChanged(boolean enabled) { + bridge.onAccessibilityStateChanged(enabled); + } + }; + } + + public static boolean addAccessibilityStateChangeListener(AccessibilityManager manager, + Object listener) { + return manager.addAccessibilityStateChangeListener( + (AccessibilityStateChangeListener)listener); + } + + public static boolean removeAccessibilityStateChangeListener(AccessibilityManager manager, + Object listener) { + return manager.removeAccessibilityStateChangeListener( + (AccessibilityStateChangeListener)listener); + } + + public static List getEnabledAccessibilityServiceList( + AccessibilityManager manager,int feedbackTypeFlags) { + return manager.getEnabledAccessibilityServiceList(feedbackTypeFlags); + } + + public static List getInstalledAccessibilityServiceList( + AccessibilityManager manager) { + return manager.getInstalledAccessibilityServiceList(); + } + + public static boolean isTouchExplorationEnabled(AccessibilityManager manager) { + return manager.isTouchExplorationEnabled(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java new file mode 100644 index 00000000000..1b239a73241 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompat.java @@ -0,0 +1,2787 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.graphics.Rect; +import android.os.Build; +import android.os.Bundle; +import android.support.v4.accessibilityservice.AccessibilityServiceInfoCompat; +import android.support.v4.view.ViewCompat; +import android.view.View; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Helper for accessing {@link android.view.accessibility.AccessibilityNodeInfo} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AccessibilityNodeInfoCompat { + + public static class AccessibilityActionCompat { + private final Object mAction; + + /** + * Creates a new instance. + * + * @param actionId The action id. + * @param label The action label. + */ + public AccessibilityActionCompat(int actionId, CharSequence label) { + this(IMPL.newAccessibilityAction(actionId, label)); + } + + private AccessibilityActionCompat(Object action) { + mAction = action; + } + + /** + * Gets the id for this action. + * + * @return The action id. + */ + public int getId() { + return IMPL.getAccessibilityActionId(mAction); + } + + /** + * Gets the label for this action. Its purpose is to describe the + * action to user. + * + * @return The label. + */ + public CharSequence getLabel() { + return IMPL.getAccessibilityActionLabel(mAction); + } + } + + public static class CollectionInfoCompat { + /** Selection mode where items are not selectable. */ + public static final int SELECTION_MODE_NONE = 0; + + /** Selection mode where a single item may be selected. */ + public static final int SELECTION_MODE_SINGLE = 1; + + /** Selection mode where multiple items may be selected. */ + public static final int SELECTION_MODE_MULTIPLE = 2; + + final Object mInfo; + + /** + * Returns a cached instance if such is available otherwise a new one. + * + * @return An instance. + */ + public static CollectionInfoCompat obtain(int rowCount, int columnCount, + boolean hierarchical, int selectionMode) { + return new CollectionInfoCompat(IMPL.obtainCollectionInfo(rowCount, columnCount, + hierarchical, selectionMode)); + } + + private CollectionInfoCompat(Object info) { + mInfo = info; + } + + public int getColumnCount() { + return IMPL.getCollectionInfoColumnCount(mInfo); + } + + public int getRowCount() { + return IMPL.getCollectionInfoRowCount(mInfo); + } + + public boolean isHierarchical() { + return IMPL.isCollectionInfoHierarchical(mInfo); + } + } + + public static class CollectionItemInfoCompat { + + private final Object mInfo; + + /** + * Returns a cached instance if such is available otherwise a new one. + * + * @return An instance. + */ + public static CollectionItemInfoCompat obtain(int rowIndex, int rowSpan, + int columnIndex, int columnSpan, boolean heading, boolean selected) { + return new CollectionItemInfoCompat(IMPL.obtainCollectionItemInfo(rowIndex, rowSpan, + columnIndex, columnSpan, heading, selected)); + } + + private CollectionItemInfoCompat(Object info) { + mInfo = info; + } + + public int getColumnIndex() { + return IMPL.getCollectionItemColumnIndex(mInfo); + } + + public int getColumnSpan() { + return IMPL.getCollectionItemColumnSpan(mInfo); + } + + public int getRowIndex() { + return IMPL.getCollectionItemRowIndex(mInfo); + } + + public int getRowSpan() { + return IMPL.getCollectionItemRowSpan(mInfo); + } + + public boolean isHeading() { + return IMPL.isCollectionItemHeading(mInfo); + } + + public boolean isSelected() { + return IMPL.isCollectionItemSelected(mInfo); + } + } + + public static class RangeInfoCompat { + /** Range type: integer. */ + public static final int RANGE_TYPE_INT = 0; + /** Range type: float. */ + public static final int RANGE_TYPE_FLOAT = 1; + /** Range type: percent with values from zero to one.*/ + public static final int RANGE_TYPE_PERCENT = 2; + + private final Object mInfo; + + private RangeInfoCompat(Object info) { + mInfo = info; + } + + public float getCurrent() { + return AccessibilityNodeInfoCompatKitKat.RangeInfo.getCurrent(mInfo); + } + + public float getMax() { + return AccessibilityNodeInfoCompatKitKat.RangeInfo.getMax(mInfo); + } + + public float getMin() { + return AccessibilityNodeInfoCompatKitKat.RangeInfo.getMin(mInfo); + } + + public int getType() { + return AccessibilityNodeInfoCompatKitKat.RangeInfo.getType(mInfo); + } + } + + static interface AccessibilityNodeInfoImpl { + public Object newAccessibilityAction(int actionId, CharSequence label); + public Object obtain(); + public Object obtain(View source); + public Object obtain(Object info); + public Object obtain(View root, int virtualDescendantId); + public void setSource(Object info, View source); + public void setSource(Object info, View root, int virtualDescendantId); + public Object findFocus(Object info, int focus); + public Object focusSearch(Object info, int direction); + public int getWindowId(Object info); + public int getChildCount(Object info); + public Object getChild(Object info, int index); + public void addChild(Object info, View child); + public void addChild(Object info, View child, int virtualDescendantId); + public int getActions(Object info); + public void addAction(Object info, int action); + public void addAction(Object info, Object action); + public int getAccessibilityActionId(Object action); + public CharSequence getAccessibilityActionLabel(Object action); + public boolean performAction(Object info, int action); + public boolean performAction(Object info, int action, Bundle arguments); + public void setMovementGranularities(Object info, int granularities); + public int getMovementGranularities(Object info); + public List findAccessibilityNodeInfosByText(Object info, String text); + public Object getParent(Object info); + public void setParent(Object info, View root, int virtualDescendantId); + public void setParent(Object info, View parent); + public void getBoundsInParent(Object info, Rect outBounds); + public void setBoundsInParent(Object info, Rect bounds); + public void getBoundsInScreen(Object info, Rect outBounds); + public void setBoundsInScreen(Object info, Rect bounds); + public boolean isCheckable(Object info); + public void setCheckable(Object info, boolean checkable); + public boolean isChecked(Object info); + public void setChecked(Object info, boolean checked); + public boolean isFocusable(Object info); + public void setFocusable(Object info, boolean focusable); + public boolean isFocused(Object info); + public void setFocused(Object info, boolean focused); + public boolean isVisibleToUser(Object info); + public void setVisibleToUser(Object info, boolean visibleToUser); + public boolean isAccessibilityFocused(Object info); + public void setAccessibilityFocused(Object info, boolean focused); + public boolean isSelected(Object info); + public void setSelected(Object info, boolean selected); + public boolean isClickable(Object info); + public void setClickable(Object info, boolean clickable); + public boolean isLongClickable(Object info); + public void setLongClickable(Object info, boolean longClickable); + public boolean isEnabled(Object info); + public void setEnabled(Object info, boolean enabled); + public boolean isPassword(Object info); + public void setPassword(Object info, boolean password); + public boolean isScrollable(Object info); + public void setScrollable(Object info, boolean scrollable); + public CharSequence getPackageName(Object info); + public void setPackageName(Object info, CharSequence packageName); + public CharSequence getClassName(Object info); + public void setClassName(Object info, CharSequence className); + public CharSequence getText(Object info); + public void setText(Object info, CharSequence text); + public CharSequence getContentDescription(Object info); + public void setContentDescription(Object info, CharSequence contentDescription); + public void recycle(Object info); + public String getViewIdResourceName(Object info); + public void setViewIdResourceName(Object info, String viewId); + public int getLiveRegion(Object info); + public void setLiveRegion(Object info, int mode); + public Object getCollectionInfo(Object info); + public void setCollectionInfo(Object info, Object collectionInfo); + public Object getCollectionItemInfo(Object info); + public void setCollectionItemInfo(Object info, Object collectionItemInfo); + public Object getRangeInfo(Object info); + public List getActionList(Object info); + public Object obtainCollectionInfo(int rowCount, int columnCount, boolean hierarchical, + int selectionMode); + public int getCollectionInfoColumnCount(Object info); + public int getCollectionInfoRowCount(Object info); + public boolean isCollectionInfoHierarchical(Object info); + public Object obtainCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, + int columnSpan, boolean heading, boolean selected); + public int getCollectionItemColumnIndex(Object info); + public int getCollectionItemColumnSpan(Object info); + public int getCollectionItemRowIndex(Object info); + public int getCollectionItemRowSpan(Object info); + public boolean isCollectionItemHeading(Object info); + public boolean isCollectionItemSelected(Object info); + public AccessibilityNodeInfoCompat getTraversalBefore(Object info); + public void setTraversalBefore(Object info, View view); + public void setTraversalBefore(Object info, View root, int virtualDescendantId); + public AccessibilityNodeInfoCompat getTraversalAfter(Object info); + public void setTraversalAfter(Object info, View view); + public void setTraversalAfter(Object info, View root, int virtualDescendantId); + public void setContentInvalid(Object info, boolean contentInvalid); + public boolean isContentInvalid(Object info); + public void setError(Object info, CharSequence error); + public CharSequence getError(Object info); + public void setLabelFor(Object info, View labeled); + public void setLabelFor(Object info, View root, int virtualDescendantId); + } + + static class AccessibilityNodeInfoStubImpl implements AccessibilityNodeInfoImpl { + @Override + public Object newAccessibilityAction(int actionId, CharSequence label) { + return null; + } + + @Override + public Object obtain() { + return null; + } + + @Override + public Object obtain(View source) { + return null; + } + + @Override + public Object obtain(View root, int virtualDescendantId) { + return null; + } + + @Override + public Object obtain(Object info) { + return null; + } + + @Override + public void addAction(Object info, int action) { + + } + + @Override + public void addAction(Object info, Object action) { + + } + + @Override + public int getAccessibilityActionId(Object action) { + return 0; + } + + @Override + public CharSequence getAccessibilityActionLabel(Object action) { + return null; + } + + @Override + public void addChild(Object info, View child) { + + } + + @Override + public void addChild(Object info, View child, int virtualDescendantId) { + + } + + @Override + public List findAccessibilityNodeInfosByText(Object info, String text) { + return Collections.emptyList(); + } + + @Override + public int getActions(Object info) { + return 0; + } + + @Override + public void getBoundsInParent(Object info, Rect outBounds) { + + } + + @Override + public void getBoundsInScreen(Object info, Rect outBounds) { + + } + + @Override + public Object getChild(Object info, int index) { + return null; + } + + @Override + public int getChildCount(Object info) { + return 0; + } + + @Override + public CharSequence getClassName(Object info) { + return null; + } + + @Override + public CharSequence getContentDescription(Object info) { + return null; + } + + @Override + public CharSequence getPackageName(Object info) { + return null; + } + + @Override + public Object getParent(Object info) { + return null; + } + + @Override + public CharSequence getText(Object info) { + return null; + } + + @Override + public int getWindowId(Object info) { + return 0; + } + + @Override + public boolean isCheckable(Object info) { + return false; + } + + @Override + public boolean isChecked(Object info) { + return false; + } + + @Override + public boolean isClickable(Object info) { + return false; + } + + @Override + public boolean isEnabled(Object info) { + return false; + } + + @Override + public boolean isFocusable(Object info) { + return false; + } + + @Override + public boolean isFocused(Object info) { + return false; + } + + @Override + public boolean isVisibleToUser(Object info) { + return false; + } + + @Override + public boolean isAccessibilityFocused(Object info) { + return false; + } + + @Override + public boolean isLongClickable(Object info) { + return false; + } + + @Override + public boolean isPassword(Object info) { + return false; + } + + @Override + public boolean isScrollable(Object info) { + return false; + } + + @Override + public boolean isSelected(Object info) { + return false; + } + + @Override + public boolean performAction(Object info, int action) { + return false; + } + + @Override + public boolean performAction(Object info, int action, Bundle arguments) { + return false; + } + + @Override + public void setMovementGranularities(Object info, int granularities) { + + } + + @Override + public int getMovementGranularities(Object info) { + return 0; + } + + @Override + public void setBoundsInParent(Object info, Rect bounds) { + + } + + @Override + public void setBoundsInScreen(Object info, Rect bounds) { + + } + + @Override + public void setCheckable(Object info, boolean checkable) { + + } + + @Override + public void setChecked(Object info, boolean checked) { + + } + + @Override + public void setClassName(Object info, CharSequence className) { + + } + + @Override + public void setClickable(Object info, boolean clickable) { + + } + + @Override + public void setContentDescription(Object info, CharSequence contentDescription) { + + } + + @Override + public void setEnabled(Object info, boolean enabled) { + + } + + @Override + public void setFocusable(Object info, boolean focusable) { + + } + + @Override + public void setFocused(Object info, boolean focused) { + + } + + @Override + public void setVisibleToUser(Object info, boolean visibleToUser) { + + } + + @Override + public void setAccessibilityFocused(Object info, boolean focused) { + + } + + @Override + public void setLongClickable(Object info, boolean longClickable) { + + } + + @Override + public void setPackageName(Object info, CharSequence packageName) { + + } + + @Override + public void setParent(Object info, View parent) { + + } + + @Override + public void setPassword(Object info, boolean password) { + + } + + @Override + public void setScrollable(Object info, boolean scrollable) { + + } + + @Override + public void setSelected(Object info, boolean selected) { + + } + + @Override + public void setSource(Object info, View source) { + + } + + @Override + public void setSource(Object info, View root, int virtualDescendantId) { + + } + + @Override + public Object findFocus(Object info, int focus) { + return null; + } + + @Override + public Object focusSearch(Object info, int direction) { + return null; + } + + @Override + public void setText(Object info, CharSequence text) { + + } + + @Override + public void recycle(Object info) { + + } + + @Override + public void setParent(Object info, View root, int virtualDescendantId) { + + } + + @Override + public String getViewIdResourceName(Object info) { + return null; + } + + @Override + public void setViewIdResourceName(Object info, String viewId) { + + } + + @Override + public int getLiveRegion(Object info) { + return ViewCompat.ACCESSIBILITY_LIVE_REGION_NONE; + } + + @Override + public void setLiveRegion(Object info, int mode) { + // No-op + } + + @Override + public Object getCollectionInfo(Object info) { + return null; + } + + @Override + public void setCollectionInfo(Object info, Object collectionInfo) { + } + + @Override + public Object getCollectionItemInfo(Object info) { + return null; + } + + @Override + public void setCollectionItemInfo(Object info, Object collectionItemInfo) { + } + + @Override + public Object getRangeInfo(Object info) { + return null; + } + + @Override + public List getActionList(Object info) { + return null; + } + + @Override + public Object obtainCollectionInfo(int rowCount, int columnCount, boolean hierarchical, + int selectionMode) { + return null; + } + + @Override + public int getCollectionInfoColumnCount(Object info) { + return 0; + } + + @Override + public int getCollectionInfoRowCount(Object info) { + return 0; + } + + @Override + public boolean isCollectionInfoHierarchical(Object info) { + return false; + } + + @Override + public Object obtainCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, + int columnSpan, boolean heading, boolean selected) { + return null; + } + + @Override + public int getCollectionItemColumnIndex(Object info) { + return 0; + } + + @Override + public int getCollectionItemColumnSpan(Object info) { + return 0; + } + + @Override + public int getCollectionItemRowIndex(Object info) { + return 0; + } + + @Override + public int getCollectionItemRowSpan(Object info) { + return 0; + } + + @Override + public boolean isCollectionItemHeading(Object info) { + return false; + } + + @Override + public boolean isCollectionItemSelected(Object info) { + return false; + } + + @Override + public AccessibilityNodeInfoCompat getTraversalBefore(Object info) { + return null; + } + + @Override + public void setTraversalBefore(Object info, View view) { + } + + @Override + public void setTraversalBefore(Object info, View root, int virtualDescendantId) { + } + + @Override + public AccessibilityNodeInfoCompat getTraversalAfter(Object info) { + return null; + } + + @Override + public void setTraversalAfter(Object info, View view) { + } + + @Override + public void setTraversalAfter(Object info, View root, int virtualDescendantId) { + } + + @Override + public void setContentInvalid(Object info, boolean contentInvalid) { + } + + @Override + public boolean isContentInvalid(Object info) { + return false; + } + + @Override + public void setError(Object info, CharSequence error) { + } + + @Override + public CharSequence getError(Object info) { + return null; + } + + @Override + public void setLabelFor(Object info, View labeled) { + } + + @Override + public void setLabelFor(Object info, View root, int virtualDescendantId) { + } + } + + static class AccessibilityNodeInfoIcsImpl extends AccessibilityNodeInfoStubImpl { + @Override + public Object obtain() { + return AccessibilityNodeInfoCompatIcs.obtain(); + } + + @Override + public Object obtain(View source) { + return AccessibilityNodeInfoCompatIcs.obtain(source); + } + + @Override + public Object obtain(Object info) { + return AccessibilityNodeInfoCompatIcs.obtain(info); + } + + @Override + public void addAction(Object info, int action) { + AccessibilityNodeInfoCompatIcs.addAction(info, action); + } + + @Override + public void addChild(Object info, View child) { + AccessibilityNodeInfoCompatIcs.addChild(info, child); + } + + @Override + public List findAccessibilityNodeInfosByText(Object info, String text) { + return AccessibilityNodeInfoCompatIcs.findAccessibilityNodeInfosByText(info, text); + } + + @Override + public int getActions(Object info) { + return AccessibilityNodeInfoCompatIcs.getActions(info); + } + + @Override + public void getBoundsInParent(Object info, Rect outBounds) { + AccessibilityNodeInfoCompatIcs.getBoundsInParent(info, outBounds); + } + + @Override + public void getBoundsInScreen(Object info, Rect outBounds) { + AccessibilityNodeInfoCompatIcs.getBoundsInScreen(info, outBounds); + } + + @Override + public Object getChild(Object info, int index) { + return AccessibilityNodeInfoCompatIcs.getChild(info, index); + } + + @Override + public int getChildCount(Object info) { + return AccessibilityNodeInfoCompatIcs.getChildCount(info); + } + + @Override + public CharSequence getClassName(Object info) { + return AccessibilityNodeInfoCompatIcs.getClassName(info); + } + + @Override + public CharSequence getContentDescription(Object info) { + return AccessibilityNodeInfoCompatIcs.getContentDescription(info); + } + + @Override + public CharSequence getPackageName(Object info) { + return AccessibilityNodeInfoCompatIcs.getPackageName(info); + } + + @Override + public Object getParent(Object info) { + return AccessibilityNodeInfoCompatIcs.getParent(info); + } + + @Override + public CharSequence getText(Object info) { + return AccessibilityNodeInfoCompatIcs.getText(info); + } + + @Override + public int getWindowId(Object info) { + return AccessibilityNodeInfoCompatIcs.getWindowId(info); + } + + @Override + public boolean isCheckable(Object info) { + return AccessibilityNodeInfoCompatIcs.isCheckable(info); + } + + @Override + public boolean isChecked(Object info) { + return AccessibilityNodeInfoCompatIcs.isChecked(info); + } + + @Override + public boolean isClickable(Object info) { + return AccessibilityNodeInfoCompatIcs.isClickable(info); + } + + @Override + public boolean isEnabled(Object info) { + return AccessibilityNodeInfoCompatIcs.isEnabled(info); + } + + @Override + public boolean isFocusable(Object info) { + return AccessibilityNodeInfoCompatIcs.isFocusable(info); + } + + @Override + public boolean isFocused(Object info) { + return AccessibilityNodeInfoCompatIcs.isFocused(info); + } + + @Override + public boolean isLongClickable(Object info) { + return AccessibilityNodeInfoCompatIcs.isLongClickable(info); + } + + @Override + public boolean isPassword(Object info) { + return AccessibilityNodeInfoCompatIcs.isPassword(info); + } + + @Override + public boolean isScrollable(Object info) { + return AccessibilityNodeInfoCompatIcs.isScrollable(info); + } + + @Override + public boolean isSelected(Object info) { + return AccessibilityNodeInfoCompatIcs.isSelected(info); + } + + @Override + public boolean performAction(Object info, int action) { + return AccessibilityNodeInfoCompatIcs.performAction(info, action); + } + + @Override + public void setBoundsInParent(Object info, Rect bounds) { + AccessibilityNodeInfoCompatIcs.setBoundsInParent(info, bounds); + } + + @Override + public void setBoundsInScreen(Object info, Rect bounds) { + AccessibilityNodeInfoCompatIcs.setBoundsInScreen(info, bounds); + } + + @Override + public void setCheckable(Object info, boolean checkable) { + AccessibilityNodeInfoCompatIcs.setCheckable(info, checkable); + } + + @Override + public void setChecked(Object info, boolean checked) { + AccessibilityNodeInfoCompatIcs.setChecked(info, checked); + } + + @Override + public void setClassName(Object info, CharSequence className) { + AccessibilityNodeInfoCompatIcs.setClassName(info, className); + } + + @Override + public void setClickable(Object info, boolean clickable) { + AccessibilityNodeInfoCompatIcs.setClickable(info, clickable); + } + + @Override + public void setContentDescription(Object info, CharSequence contentDescription) { + AccessibilityNodeInfoCompatIcs.setContentDescription(info, contentDescription); + } + + @Override + public void setEnabled(Object info, boolean enabled) { + AccessibilityNodeInfoCompatIcs.setEnabled(info, enabled); + } + + @Override + public void setFocusable(Object info, boolean focusable) { + AccessibilityNodeInfoCompatIcs.setFocusable(info, focusable); + } + + @Override + public void setFocused(Object info, boolean focused) { + AccessibilityNodeInfoCompatIcs.setFocused(info, focused); + } + + @Override + public void setLongClickable(Object info, boolean longClickable) { + AccessibilityNodeInfoCompatIcs.setLongClickable(info, longClickable); + } + + @Override + public void setPackageName(Object info, CharSequence packageName) { + AccessibilityNodeInfoCompatIcs.setPackageName(info, packageName); + } + + @Override + public void setParent(Object info, View parent) { + AccessibilityNodeInfoCompatIcs.setParent(info, parent); + } + + @Override + public void setPassword(Object info, boolean password) { + AccessibilityNodeInfoCompatIcs.setPassword(info, password); + } + + @Override + public void setScrollable(Object info, boolean scrollable) { + AccessibilityNodeInfoCompatIcs.setScrollable(info, scrollable); + } + + @Override + public void setSelected(Object info, boolean selected) { + AccessibilityNodeInfoCompatIcs.setSelected(info, selected); + } + + @Override + public void setSource(Object info, View source) { + AccessibilityNodeInfoCompatIcs.setSource(info, source); + } + + @Override + public void setText(Object info, CharSequence text) { + AccessibilityNodeInfoCompatIcs.setText(info, text); + } + + @Override + public void recycle(Object info) { + AccessibilityNodeInfoCompatIcs.recycle(info); + } + } + + static class AccessibilityNodeInfoJellybeanImpl extends AccessibilityNodeInfoIcsImpl { + @Override + public Object obtain(View root, int virtualDescendantId) { + return AccessibilityNodeInfoCompatJellyBean.obtain(root, virtualDescendantId); + } + + @Override + public Object findFocus(Object info, int focus) { + return AccessibilityNodeInfoCompatJellyBean.findFocus(info, focus); + } + + @Override + public Object focusSearch(Object info, int direction) { + return AccessibilityNodeInfoCompatJellyBean.focusSearch(info, direction); + } + + @Override + public void addChild(Object info, View child, int virtualDescendantId) { + AccessibilityNodeInfoCompatJellyBean.addChild(info, child, virtualDescendantId); + } + + @Override + public void setSource(Object info, View root, int virtualDescendantId) { + AccessibilityNodeInfoCompatJellyBean.setSource(info, root, virtualDescendantId); + } + + @Override + public boolean isVisibleToUser(Object info) { + return AccessibilityNodeInfoCompatJellyBean.isVisibleToUser(info); + } + + @Override + public void setVisibleToUser(Object info, boolean visibleToUser) { + AccessibilityNodeInfoCompatJellyBean.setVisibleToUser(info, visibleToUser); + } + + @Override + public boolean isAccessibilityFocused(Object info) { + return AccessibilityNodeInfoCompatJellyBean.isAccessibilityFocused(info); + } + + @Override + public void setAccessibilityFocused(Object info, boolean focused) { + AccessibilityNodeInfoCompatJellyBean.setAccesibilityFocused(info, focused); + } + + @Override + public boolean performAction(Object info, int action, Bundle arguments) { + return AccessibilityNodeInfoCompatJellyBean.performAction(info, action, arguments); + } + + @Override + public void setMovementGranularities(Object info, int granularities) { + AccessibilityNodeInfoCompatJellyBean.setMovementGranularities(info, granularities); + } + + @Override + public int getMovementGranularities(Object info) { + return AccessibilityNodeInfoCompatJellyBean.getMovementGranularities(info); + } + + @Override + public void setParent(Object info, View root, int virtualDescendantId) { + AccessibilityNodeInfoCompatJellyBean.setParent(info, root, virtualDescendantId); + } + } + + static class AccessibilityNodeInfoJellybeanMr2Impl extends AccessibilityNodeInfoJellybeanImpl { + + @Override + public String getViewIdResourceName(Object info) { + return AccessibilityNodeInfoCompatJellybeanMr2.getViewIdResourceName(info); + } + + @Override + public void setViewIdResourceName(Object info, String viewId) { + AccessibilityNodeInfoCompatJellybeanMr2.setViewIdResourceName(info, viewId); + } + } + + static class AccessibilityNodeInfoKitKatImpl extends AccessibilityNodeInfoJellybeanMr2Impl { + @Override + public int getLiveRegion(Object info) { + return AccessibilityNodeInfoCompatKitKat.getLiveRegion(info); + } + + @Override + public void setLiveRegion(Object info, int mode) { + AccessibilityNodeInfoCompatKitKat.setLiveRegion(info, mode); + } + + @Override + public Object getCollectionInfo(Object info) { + return AccessibilityNodeInfoCompatKitKat.getCollectionInfo(info); + } + + @Override + public void setCollectionInfo(Object info, Object collectionInfo) { + AccessibilityNodeInfoCompatKitKat.setCollectionInfo(info, collectionInfo); + } + + @Override + public Object obtainCollectionInfo(int rowCount, int columnCount, + boolean hierarchical, int selectionMode) { + return AccessibilityNodeInfoCompatKitKat.obtainCollectionInfo(rowCount, columnCount, + hierarchical, selectionMode); + } + + @Override + public Object obtainCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, + int columnSpan, boolean heading, boolean selected) { + return AccessibilityNodeInfoCompatKitKat + .obtainCollectionItemInfo(rowIndex, rowSpan, columnIndex, columnSpan, heading); + } + + @Override + public int getCollectionInfoColumnCount(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionInfo.getColumnCount(info); + } + + @Override + public int getCollectionInfoRowCount(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionInfo.getRowCount(info); + } + + @Override + public boolean isCollectionInfoHierarchical(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionInfo.isHierarchical(info); + } + + @Override + public Object getCollectionItemInfo(Object info) { + return AccessibilityNodeInfoCompatKitKat.getCollectionItemInfo(info); + } + + @Override + public Object getRangeInfo(Object info) { + return AccessibilityNodeInfoCompatKitKat.getRangeInfo(info); + } + + @Override + public int getCollectionItemColumnIndex(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.getColumnIndex(info); + } + + @Override + public int getCollectionItemColumnSpan(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.getColumnSpan(info); + } + + @Override + public int getCollectionItemRowIndex(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.getRowIndex(info); + } + + @Override + public int getCollectionItemRowSpan(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.getRowSpan(info); + } + + @Override + public boolean isCollectionItemHeading(Object info) { + return AccessibilityNodeInfoCompatKitKat.CollectionItemInfo.isHeading(info); + } + + @Override + public void setCollectionItemInfo(Object info, Object collectionItemInfo) { + AccessibilityNodeInfoCompatKitKat.setCollectionItemInfo(info, collectionItemInfo); + } + + @Override + public void setContentInvalid(Object info, boolean contentInvalid) { + AccessibilityNodeInfoCompatKitKat.setContentInvalid(info, contentInvalid); + } + + @Override + public boolean isContentInvalid(Object info) { + return AccessibilityNodeInfoCompatKitKat.isContentInvalid(info); + } + } + + static class AccessibilityNodeInfoApi21Impl extends AccessibilityNodeInfoKitKatImpl { + @Override + public Object newAccessibilityAction(int actionId, CharSequence label) { + return AccessibilityNodeInfoCompatApi21.newAccessibilityAction(actionId, label); + } + + @Override + public List getActionList(Object info) { + return AccessibilityNodeInfoCompatApi21.getActionList(info); + } + + @Override + public Object obtainCollectionInfo(int rowCount, int columnCount, boolean hierarchical, + int selectionMode) { + return AccessibilityNodeInfoCompatApi21.obtainCollectionInfo(rowCount, columnCount, + hierarchical, selectionMode); + } + + @Override + public void addAction(Object info, Object action) { + AccessibilityNodeInfoCompatApi21.addAction(info, action); + } + + @Override + public int getAccessibilityActionId(Object action) { + return AccessibilityNodeInfoCompatApi21.getAccessibilityActionId(action); + } + + @Override + public CharSequence getAccessibilityActionLabel(Object action) { + return AccessibilityNodeInfoCompatApi21.getAccessibilityActionLabel(action); + } + + @Override + public Object obtainCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, + int columnSpan, boolean heading, boolean selected) { + return AccessibilityNodeInfoCompatApi21.obtainCollectionItemInfo(rowIndex, rowSpan, + columnIndex, columnSpan, heading, selected); + } + + @Override + public boolean isCollectionItemSelected(Object info) { + return AccessibilityNodeInfoCompatApi21.CollectionItemInfo.isSelected(info); + } + + @Override + public CharSequence getError(Object info) { + return AccessibilityNodeInfoCompatApi21.getError(info); + } + + @Override + public void setError(Object info, CharSequence error) { + AccessibilityNodeInfoCompatApi21.setError(info, error); + } + + @Override + public void setLabelFor(Object info, View labeled) { + AccessibilityNodeInfoCompatApi21.setLabelFor(info, labeled); + } + + @Override + public void setLabelFor(Object info, View root, int virtualDescendantId) { + AccessibilityNodeInfoCompatApi21.setLabelFor(info, root, virtualDescendantId); + } + } + + static class AccessibilityNodeInfoApi22Impl extends AccessibilityNodeInfoApi21Impl { + @Override + public AccessibilityNodeInfoCompat getTraversalBefore(Object info) { + Object nodeInfo = AccessibilityNodeInfoCompatApi22.getTraversalBefore(info); + if (nodeInfo == null) { + return null; + } + + return new AccessibilityNodeInfoCompat(nodeInfo); + } + + @Override + public void setTraversalBefore(Object info, View view) { + AccessibilityNodeInfoCompatApi22.setTraversalBefore(info, view); + } + + @Override + public void setTraversalBefore(Object info, View root, int virtualDescendantId) { + AccessibilityNodeInfoCompatApi22.setTraversalBefore(info, root, virtualDescendantId); + } + + @Override + public AccessibilityNodeInfoCompat getTraversalAfter(Object info) { + Object nodeInfo = AccessibilityNodeInfoCompatApi22.getTraversalAfter(info); + if (nodeInfo == null) { + return null; + } + + return new AccessibilityNodeInfoCompat(nodeInfo); + } + + @Override + public void setTraversalAfter(Object info, View view) { + AccessibilityNodeInfoCompatApi22.setTraversalAfter(info, view); + } + + @Override + public void setTraversalAfter(Object info, View root, int virtualDescendantId) { + AccessibilityNodeInfoCompatApi22.setTraversalAfter(info, root, virtualDescendantId); + } + } + + static { + if (Build.VERSION.SDK_INT >= 22) { + IMPL = new AccessibilityNodeInfoApi22Impl(); + } else if (Build.VERSION.SDK_INT >= 21) { + IMPL = new AccessibilityNodeInfoApi21Impl(); + } else if (Build.VERSION.SDK_INT >= 19) { // KitKat + IMPL = new AccessibilityNodeInfoKitKatImpl(); + } else if (Build.VERSION.SDK_INT >= 18) { // JellyBean MR2 + IMPL = new AccessibilityNodeInfoJellybeanMr2Impl(); + } else if (Build.VERSION.SDK_INT >= 16) { // JellyBean + IMPL = new AccessibilityNodeInfoJellybeanImpl(); + } else if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new AccessibilityNodeInfoIcsImpl(); + } else { + IMPL = new AccessibilityNodeInfoStubImpl(); + } + } + + private static final AccessibilityNodeInfoImpl IMPL; + + private final Object mInfo; + + // Actions introduced in IceCreamSandwich + + /** + * Action that focuses the node. + */ + public static final int ACTION_FOCUS = 0x00000001; + + /** + * Action that unfocuses the node. + */ + public static final int ACTION_CLEAR_FOCUS = 0x00000002; + + /** + * Action that selects the node. + */ + public static final int ACTION_SELECT = 0x00000004; + + /** + * Action that unselects the node. + */ + public static final int ACTION_CLEAR_SELECTION = 0x00000008; + + /** + * Action that clicks on the node info. + */ + public static final int ACTION_CLICK = 0x00000010; + + /** + * Action that long clicks on the node. + */ + public static final int ACTION_LONG_CLICK = 0x00000020; + + // Actions introduced in JellyBean + + /** + * Action that gives accessibility focus to the node. + */ + public static final int ACTION_ACCESSIBILITY_FOCUS = 0x00000040; + + /** + * Action that clears accessibility focus of the node. + */ + public static final int ACTION_CLEAR_ACCESSIBILITY_FOCUS = 0x00000080; + + /** + * Action that requests to go to the next entity in this node's text + * at a given movement granularity. For example, move to the next character, + * word, etc. + *

    + * Arguments: {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<, + * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}
    + * Example: Move to the previous character and do not extend selection. + *

    + * Bundle arguments = new Bundle(); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER); + * arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, + * false); + * info.performAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY, arguments); + *

    + *

    + * + * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT + * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * + * @see #setMovementGranularities(int) + * @see #getMovementGranularities() + * + * @see #MOVEMENT_GRANULARITY_CHARACTER + * @see #MOVEMENT_GRANULARITY_WORD + * @see #MOVEMENT_GRANULARITY_LINE + * @see #MOVEMENT_GRANULARITY_PARAGRAPH + * @see #MOVEMENT_GRANULARITY_PAGE + */ + public static final int ACTION_NEXT_AT_MOVEMENT_GRANULARITY = 0x00000100; + + /** + * Action that requests to go to the previous entity in this node's text + * at a given movement granularity. For example, move to the next character, + * word, etc. + *

    + * Arguments: {@link #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT}<, + * {@link #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN}
    + * Example: Move to the next character and do not extend selection. + *

    + * Bundle arguments = new Bundle(); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT, + * AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER); + * arguments.putBoolean(AccessibilityNodeInfo.ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN, + * false); + * info.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY, + * arguments); + *

    + *

    + * + * @see #ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT + * @see #ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN + * + * @see #setMovementGranularities(int) + * @see #getMovementGranularities() + * + * @see #MOVEMENT_GRANULARITY_CHARACTER + * @see #MOVEMENT_GRANULARITY_WORD + * @see #MOVEMENT_GRANULARITY_LINE + * @see #MOVEMENT_GRANULARITY_PARAGRAPH + * @see #MOVEMENT_GRANULARITY_PAGE + */ + public static final int ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY = 0x00000200; + + /** + * Action to move to the next HTML element of a given type. For example, move + * to the BUTTON, INPUT, TABLE, etc. + *

    + * Arguments: {@link #ACTION_ARGUMENT_HTML_ELEMENT_STRING}
    + * Example: + *

    + * Bundle arguments = new Bundle(); + * arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON"); + * info.performAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT, arguments); + *

    + *

    + */ + public static final int ACTION_NEXT_HTML_ELEMENT = 0x00000400; + + /** + * Action to move to the previous HTML element of a given type. For example, move + * to the BUTTON, INPUT, TABLE, etc. + *

    + * Arguments: {@link #ACTION_ARGUMENT_HTML_ELEMENT_STRING}
    + * Example: + *

    + * Bundle arguments = new Bundle(); + * arguments.putString(AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING, "BUTTON"); + * info.performAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT, arguments); + *

    + *

    + */ + public static final int ACTION_PREVIOUS_HTML_ELEMENT = 0x00000800; + + /** + * Action to scroll the node content forward. + */ + public static final int ACTION_SCROLL_FORWARD = 0x00001000; + + /** + * Action to scroll the node content backward. + */ + public static final int ACTION_SCROLL_BACKWARD = 0x00002000; + + // Actions introduced in JellyBeanMr2 + + /** + * Action to copy the current selection to the clipboard. + */ + public static final int ACTION_COPY = 0x00004000; + + /** + * Action to paste the current clipboard content. + */ + public static final int ACTION_PASTE = 0x00008000; + + /** + * Action to cut the current selection and place it to the clipboard. + */ + public static final int ACTION_CUT = 0x00010000; + + /** + * Action to set the selection. Performing this action with no arguments + * clears the selection. + *

    + * Arguments: {@link #ACTION_ARGUMENT_SELECTION_START_INT}, + * {@link #ACTION_ARGUMENT_SELECTION_END_INT}
    + * Example: + *

    + * Bundle arguments = new Bundle(); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, 1); + * arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, 2); + * info.performAction(AccessibilityNodeInfo.ACTION_SET_SELECTION, arguments); + *

    + *

    + * + * @see #ACTION_ARGUMENT_SELECTION_START_INT + * @see #ACTION_ARGUMENT_SELECTION_END_INT + */ + public static final int ACTION_SET_SELECTION = 0x00020000; + + /** + * Action that sets the text of the node. Performing the action without argument, using + * null or empty {@link CharSequence} will clear the text. This action will also put the + * cursor at the end of text. + *

    + * Arguments: {@link #ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE}
    + * Example: + *

    + * Bundle arguments = new Bundle(); + * arguments.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, + * "android"); + * info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, arguments); + *

    + */ + public static final int ACTION_SET_TEXT = 0x00200000; + + // Action arguments + + /** + * Argument for which movement granularity to be used when traversing the node text. + *

    + * Type: int
    + * Actions: {@link #ACTION_NEXT_AT_MOVEMENT_GRANULARITY}, + * {@link #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY} + *

    + */ + public static final String ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT = + "ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT"; + + /** + * Argument for which HTML element to get moving to the next/previous HTML element. + *

    + * Type: String
    + * Actions: {@link #ACTION_NEXT_HTML_ELEMENT}, + * {@link #ACTION_PREVIOUS_HTML_ELEMENT} + *

    + */ + public static final String ACTION_ARGUMENT_HTML_ELEMENT_STRING = + "ACTION_ARGUMENT_HTML_ELEMENT_STRING"; + + /** + * Argument for whether when moving at granularity to extend the selection + * or to move it otherwise. + *

    + * Type: boolean
    + * Actions: {@link #ACTION_NEXT_AT_MOVEMENT_GRANULARITY}, + * {@link #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY} + *

    + * + * @see #ACTION_NEXT_AT_MOVEMENT_GRANULARITY + * @see #ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY + */ + public static final String ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN = + "ACTION_ARGUMENT_EXTEND_SELECTION_BOOLEAN"; + + /** + * Argument for specifying the selection start. + *

    + * Type: int
    + * Actions: {@link #ACTION_SET_SELECTION} + *

    + * + * @see #ACTION_SET_SELECTION + */ + public static final String ACTION_ARGUMENT_SELECTION_START_INT = + "ACTION_ARGUMENT_SELECTION_START_INT"; + + /** + * Argument for specifying the selection end. + *

    + * Type: int
    + * Actions: {@link #ACTION_SET_SELECTION} + *

    + * + * @see #ACTION_SET_SELECTION + */ + public static final String ACTION_ARGUMENT_SELECTION_END_INT = + "ACTION_ARGUMENT_SELECTION_END_INT"; + + /** + * Argument for specifying the text content to set + *

    + * Type: CharSequence
    + * Actions: {@link #ACTION_SET_TEXT} + *

    + * + * @see #ACTION_SET_TEXT + */ + public static final String ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE = + "ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE"; + + // Focus types + + /** + * The input focus. + */ + public static final int FOCUS_INPUT = 1; + + /** + * The accessibility focus. + */ + public static final int FOCUS_ACCESSIBILITY = 2; + + // Movement granularities + + /** + * Movement granularity bit for traversing the text of a node by character. + */ + public static final int MOVEMENT_GRANULARITY_CHARACTER = 0x00000001; + + /** + * Movement granularity bit for traversing the text of a node by word. + */ + public static final int MOVEMENT_GRANULARITY_WORD = 0x00000002; + + /** + * Movement granularity bit for traversing the text of a node by line. + */ + public static final int MOVEMENT_GRANULARITY_LINE = 0x00000004; + + /** + * Movement granularity bit for traversing the text of a node by paragraph. + */ + public static final int MOVEMENT_GRANULARITY_PARAGRAPH = 0x00000008; + + /** + * Movement granularity bit for traversing the text of a node by page. + */ + public static final int MOVEMENT_GRANULARITY_PAGE = 0x00000010; + + /** + * Creates a wrapper for info implementation. + * + * @param object The info to wrap. + * @return A wrapper for if the object is not null, null otherwise. + */ + static AccessibilityNodeInfoCompat wrapNonNullInstance(Object object) { + if (object != null) { + return new AccessibilityNodeInfoCompat(object); + } + return null; + } + + /** + * Creates a new instance wrapping an + * {@link android.view.accessibility.AccessibilityNodeInfo}. + * + * @param info The info. + */ + public AccessibilityNodeInfoCompat(Object info) { + mInfo = info; + } + + /** + * @return The wrapped {@link android.view.accessibility.AccessibilityNodeInfo}. + */ + public Object getInfo() { + return mInfo; + } + + /** + * Returns a cached instance if such is available otherwise a new one and + * sets the source. + * + * @return An instance. + * @see #setSource(View) + */ + public static AccessibilityNodeInfoCompat obtain(View source) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.obtain(source)); + } + + /** + * Returns a cached instance if such is available otherwise a new one + * and sets the source. + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + * @return An instance. + * + * @see #setSource(View, int) + */ + public static AccessibilityNodeInfoCompat obtain(View root, int virtualDescendantId) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance( + IMPL.obtain(root, virtualDescendantId)); + } + + /** + * Returns a cached instance if such is available otherwise a new one. + * + * @return An instance. + */ + public static AccessibilityNodeInfoCompat obtain() { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.obtain()); + } + + /** + * Returns a cached instance if such is available or a new one is create. + * The returned instance is initialized from the given info. + * + * @param info The other info. + * @return An instance. + */ + public static AccessibilityNodeInfoCompat obtain(AccessibilityNodeInfoCompat info) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.obtain(info.mInfo)); + } + + /** + * Sets the source. + * + * @param source The info source. + */ + public void setSource(View source) { + IMPL.setSource(mInfo, source); + } + + /** + * Sets the source to be a virtual descendant of the given root. + * If virtualDescendantId is {@link View#NO_ID} the root + * is set as the source. + *

    + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report themselves as a tree of virtual views, thus conveying their + * logical structure. + *

    + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setSource(View root, int virtualDescendantId) { + IMPL.setSource(mInfo, root, virtualDescendantId); + } + + /** + * Find the view that has the specified focus type. The search starts from + * the view represented by this node info. + * + * @param focus The focus to find. One of {@link #FOCUS_INPUT} or + * {@link #FOCUS_ACCESSIBILITY}. + * @return The node info of the focused view or null. + * + * @see #FOCUS_INPUT + * @see #FOCUS_ACCESSIBILITY + */ + public AccessibilityNodeInfoCompat findFocus(int focus) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.findFocus(mInfo, focus)); + } + + /** + * Searches for the nearest view in the specified direction that can take + * input focus. + * + * @param direction The direction. Can be one of: + * {@link View#FOCUS_DOWN}, + * {@link View#FOCUS_UP}, + * {@link View#FOCUS_LEFT}, + * {@link View#FOCUS_RIGHT}, + * {@link View#FOCUS_FORWARD}, + * {@link View#FOCUS_BACKWARD}. + * + * @return The node info for the view that can take accessibility focus. + */ + public AccessibilityNodeInfoCompat focusSearch(int direction) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.focusSearch(mInfo, direction)); + } + + /** + * Gets the id of the window from which the info comes from. + * + * @return The window id. + */ + public int getWindowId() { + return IMPL.getWindowId(mInfo); + } + + /** + * Gets the number of children. + * + * @return The child count. + */ + public int getChildCount() { + return IMPL.getChildCount(mInfo); + } + + /** + * Get the child at given index. + *

    + * Note: It is a client responsibility to recycle the + * received info by calling {@link AccessibilityNodeInfoCompat#recycle()} to + * avoid creating of multiple instances. + *

    + * + * @param index The child index. + * @return The child node. + * @throws IllegalStateException If called outside of an + * AccessibilityService. + */ + public AccessibilityNodeInfoCompat getChild(int index) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.getChild(mInfo, index)); + } + + /** + * Adds a child. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param child The child. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void addChild(View child) { + IMPL.addChild(mInfo, child); + } + + /** + * Adds a virtual child which is a descendant of the given root. + * If virtualDescendantId is {@link View#NO_ID} the root + * is added as a child. + *

    + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + *

    + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual child. + */ + public void addChild(View root, int virtualDescendantId) { + IMPL.addChild(mInfo, root, virtualDescendantId); + } + + /** + * Gets the actions that can be performed on the node. + * + * @return The bit mask of with actions. + * @see android.view.accessibility.AccessibilityNodeInfo#ACTION_FOCUS + * @see android.view.accessibility.AccessibilityNodeInfo#ACTION_CLEAR_FOCUS + * @see android.view.accessibility.AccessibilityNodeInfo#ACTION_SELECT + * @see android.view.accessibility.AccessibilityNodeInfo#ACTION_CLEAR_SELECTION + */ + public int getActions() { + return IMPL.getActions(mInfo); + } + + /** + * Adds an action that can be performed on the node. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param action The action. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void addAction(int action) { + IMPL.addAction(mInfo, action); + } + + /** + * Adds an action that can be performed on the node. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param action The action. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void addAction(AccessibilityActionCompat action) { + IMPL.addAction(mInfo, action.mAction); + } + + /** + * Performs an action on the node. + *

    + * Note: An action can be performed only if the request is + * made from an {@link android.accessibilityservice.AccessibilityService}. + *

    + * + * @param action The action to perform. + * @return True if the action was performed. + * @throws IllegalStateException If called outside of an + * AccessibilityService. + */ + public boolean performAction(int action) { + return IMPL.performAction(mInfo, action); + } + + /** + * Performs an action on the node. + *

    + * Note: An action can be performed only if the request is made + * from an {@link android.accessibilityservice.AccessibilityService}. + *

    + * + * @param action The action to perform. + * @param arguments A bundle with additional arguments. + * @return True if the action was performed. + * + * @throws IllegalStateException If called outside of an AccessibilityService. + */ + public boolean performAction(int action, Bundle arguments) { + return IMPL.performAction(mInfo, action, arguments); + } + + /** + * Sets the movement granularities for traversing the text of this node. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param granularities The bit mask with granularities. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setMovementGranularities(int granularities) { + IMPL.setMovementGranularities(mInfo, granularities); + } + + /** + * Gets the movement granularities for traversing the text of this node. + * + * @return The bit mask with granularities. + */ + public int getMovementGranularities() { + return IMPL.getMovementGranularities(mInfo); + } + + /** + * Finds {@link android.view.accessibility.AccessibilityNodeInfo}s by text. The match + * is case insensitive containment. The search is relative to this info i.e. this + * info is the root of the traversed tree. + *

    + * Note: It is a client responsibility to recycle the + * received info by calling {@link android.view.accessibility.AccessibilityNodeInfo#recycle()} + * to avoid creating of multiple instances. + *

    + * + * @param text The searched text. + * @return A list of node info. + */ + public List findAccessibilityNodeInfosByText(String text) { + List result = new ArrayList(); + List infos = IMPL.findAccessibilityNodeInfosByText(mInfo, text); + final int infoCount = infos.size(); + for (int i = 0; i < infoCount; i++) { + Object info = infos.get(i); + result.add(new AccessibilityNodeInfoCompat(info)); + } + return result; + } + + /** + * Gets the parent. + *

    + * Note: It is a client responsibility to recycle the + * received info by calling {@link android.view.accessibility.AccessibilityNodeInfo#recycle()} + * to avoid creating of multiple instances. + *

    + * + * @return The parent. + */ + public AccessibilityNodeInfoCompat getParent() { + return AccessibilityNodeInfoCompat.wrapNonNullInstance(IMPL.getParent(mInfo)); + } + + /** + * Sets the parent. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param parent The parent. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setParent(View parent) { + IMPL.setParent(mInfo, parent); + } + + /** + * Sets the parent to be a virtual descendant of the given root. + * If virtualDescendantId equals to {@link View#NO_ID} the root + * is set as the parent. + *

    + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + *

    + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setParent(View root, int virtualDescendantId) { + IMPL.setParent(mInfo, root, virtualDescendantId); + } + + /** + * Gets the node bounds in parent coordinates. + * + * @param outBounds The output node bounds. + */ + public void getBoundsInParent(Rect outBounds) { + IMPL.getBoundsInParent(mInfo, outBounds); + } + + /** + * Sets the node bounds in parent coordinates. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param bounds The node bounds. + *@throws IllegalStateException If called from an AccessibilityService. + */ + public void setBoundsInParent(Rect bounds) { + IMPL.setBoundsInParent(mInfo, bounds); + } + + /** + * Gets the node bounds in screen coordinates. + * + * @param outBounds The output node bounds. + */ + public void getBoundsInScreen(Rect outBounds) { + IMPL.getBoundsInScreen(mInfo, outBounds); + } + + /** + * Sets the node bounds in screen coordinates. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param bounds The node bounds. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setBoundsInScreen(Rect bounds) { + IMPL.setBoundsInScreen(mInfo, bounds); + } + + /** + * Gets whether this node is checkable. + * + * @return True if the node is checkable. + */ + public boolean isCheckable() { + return IMPL.isCheckable(mInfo); + } + + /** + * Sets whether this node is checkable. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param checkable True if the node is checkable. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setCheckable(boolean checkable) { + IMPL.setCheckable(mInfo, checkable); + } + + /** + * Gets whether this node is checked. + * + * @return True if the node is checked. + */ + public boolean isChecked() { + return IMPL.isChecked(mInfo); + } + + /** + * Sets whether this node is checked. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param checked True if the node is checked. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setChecked(boolean checked) { + IMPL.setChecked(mInfo, checked); + } + + /** + * Gets whether this node is focusable. + * + * @return True if the node is focusable. + */ + public boolean isFocusable() { + return IMPL.isFocusable(mInfo); + } + + /** + * Sets whether this node is focusable. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param focusable True if the node is focusable. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFocusable(boolean focusable) { + IMPL.setFocusable(mInfo, focusable); + } + + /** + * Gets whether this node is focused. + * + * @return True if the node is focused. + */ + public boolean isFocused() { + return IMPL.isFocused(mInfo); + } + + /** + * Sets whether this node is focused. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param focused True if the node is focused. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFocused(boolean focused) { + IMPL.setFocused(mInfo, focused); + } + + /** + * Sets whether this node is visible to the user. + * + * @return Whether the node is visible to the user. + */ + public boolean isVisibleToUser() { + return IMPL.isVisibleToUser(mInfo); + } + + /** + * Sets whether this node is visible to the user. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param visibleToUser Whether the node is visible to the user. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setVisibleToUser(boolean visibleToUser) { + IMPL.setVisibleToUser(mInfo, visibleToUser); + } + + /** + * Gets whether this node is accessibility focused. + * + * @return True if the node is accessibility focused. + */ + public boolean isAccessibilityFocused() { + return IMPL.isAccessibilityFocused(mInfo); + } + + /** + * Sets whether this node is accessibility focused. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param focused True if the node is accessibility focused. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setAccessibilityFocused(boolean focused) { + IMPL.setAccessibilityFocused(mInfo, focused); + } + + /** + * Gets whether this node is selected. + * + * @return True if the node is selected. + */ + public boolean isSelected() { + return IMPL.isSelected(mInfo); + } + + /** + * Sets whether this node is selected. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param selected True if the node is selected. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setSelected(boolean selected) { + IMPL.setSelected(mInfo, selected); + } + + /** + * Gets whether this node is clickable. + * + * @return True if the node is clickable. + */ + public boolean isClickable() { + return IMPL.isClickable(mInfo); + } + + /** + * Sets whether this node is clickable. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param clickable True if the node is clickable. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setClickable(boolean clickable) { + IMPL.setClickable(mInfo, clickable); + } + + /** + * Gets whether this node is long clickable. + * + * @return True if the node is long clickable. + */ + public boolean isLongClickable() { + return IMPL.isLongClickable(mInfo); + } + + /** + * Sets whether this node is long clickable. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param longClickable True if the node is long clickable. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setLongClickable(boolean longClickable) { + IMPL.setLongClickable(mInfo, longClickable); + } + + /** + * Gets whether this node is enabled. + * + * @return True if the node is enabled. + */ + public boolean isEnabled() { + return IMPL.isEnabled(mInfo); + } + + /** + * Sets whether this node is enabled. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param enabled True if the node is enabled. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setEnabled(boolean enabled) { + IMPL.setEnabled(mInfo, enabled); + } + + /** + * Gets whether this node is a password. + * + * @return True if the node is a password. + */ + public boolean isPassword() { + return IMPL.isPassword(mInfo); + } + + /** + * Sets whether this node is a password. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param password True if the node is a password. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setPassword(boolean password) { + IMPL.setPassword(mInfo, password); + } + + /** + * Gets if the node is scrollable. + * + * @return True if the node is scrollable, false otherwise. + */ + public boolean isScrollable() { + return IMPL.isScrollable(mInfo); + } + + /** + * Sets if the node is scrollable. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param scrollable True if the node is scrollable, false otherwise. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setScrollable(boolean scrollable) { + IMPL.setScrollable(mInfo, scrollable); + } + + /** + * Gets the package this node comes from. + * + * @return The package name. + */ + public CharSequence getPackageName() { + return IMPL.getPackageName(mInfo); + } + + /** + * Sets the package this node comes from. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param packageName The package name. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setPackageName(CharSequence packageName) { + IMPL.setPackageName(mInfo, packageName); + } + + /** + * Gets the class this node comes from. + * + * @return The class name. + */ + public CharSequence getClassName() { + return IMPL.getClassName(mInfo); + } + + /** + * Sets the class this node comes from. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param className The class name. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setClassName(CharSequence className) { + IMPL.setClassName(mInfo, className); + } + + /** + * Gets the text of this node. + * + * @return The text. + */ + public CharSequence getText() { + return IMPL.getText(mInfo); + } + + /** + * Sets the text of this node. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param text The text. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setText(CharSequence text) { + IMPL.setText(mInfo, text); + } + + /** + * Gets the content description of this node. + * + * @return The content description. + */ + public CharSequence getContentDescription() { + return IMPL.getContentDescription(mInfo); + } + + /** + * Sets the content description of this node. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + *

    + * + * @param contentDescription The content description. + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setContentDescription(CharSequence contentDescription) { + IMPL.setContentDescription(mInfo, contentDescription); + } + + /** + * Return an instance back to be reused. + *

    + * Note: You must not touch the object after calling this function. + * + * @throws IllegalStateException If the info is already recycled. + */ + public void recycle() { + IMPL.recycle(mInfo); + } + + /** + * Sets the fully qualified resource name of the source view's id. + * + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param viewId The id resource name. + */ + public void setViewIdResourceName(String viewId) { + IMPL.setViewIdResourceName(mInfo, viewId); + } + + /** + * Gets the fully qualified resource name of the source view's id. + * + *

    + * Note: The primary usage of this API is for UI test automation + * and in order to report the source view id of an {@link AccessibilityNodeInfoCompat} + * the client has to set the {@link AccessibilityServiceInfoCompat#FLAG_REPORT_VIEW_IDS} + * flag when configuring his {@link android.accessibilityservice.AccessibilityService}. + *

    + * + * @return The id resource name. + */ + public String getViewIdResourceName() { + return IMPL.getViewIdResourceName(mInfo); + } + + /** + * Gets the node's live region mode. + *

    + * A live region is a node that contains information that is important for + * the user and when it changes the user should be notified. For example, + * in a login screen with a TextView that displays an "incorrect password" + * notification, that view should be marked as a live region with mode + * {@link ViewCompat#ACCESSIBILITY_LIVE_REGION_POLITE}. + *

    + * It is the responsibility of the accessibility service to monitor + * {@link AccessibilityEventCompat#TYPE_WINDOW_CONTENT_CHANGED} events + * indicating changes to live region nodes and their children. + * + * @return The live region mode, or + * {@link ViewCompat#ACCESSIBILITY_LIVE_REGION_NONE} if the view is + * not a live region. + * @see ViewCompat#getAccessibilityLiveRegion(View) + */ + public int getLiveRegion() { + return IMPL.getLiveRegion(mInfo); + } + + /** + * Sets the node's live region mode. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. This class is + * made immutable before being delivered to an AccessibilityService. + * + * @param mode The live region mode, or + * {@link ViewCompat#ACCESSIBILITY_LIVE_REGION_NONE} if the view is + * not a live region. + * @see ViewCompat#setAccessibilityLiveRegion(View, int) + */ + public void setLiveRegion(int mode) { + IMPL.setLiveRegion(mInfo, mode); + } + + /** + * Gets the collection info if the node is a collection. A collection + * child is always a collection item. + * + * @return The collection info. + */ + public CollectionInfoCompat getCollectionInfo() { + Object info = IMPL.getCollectionInfo(mInfo); + if (info == null) return null; + return new CollectionInfoCompat(info); + } + + public void setCollectionInfo(Object collectionInfo) { + IMPL.setCollectionInfo(mInfo, ((CollectionInfoCompat) collectionInfo).mInfo); + } + + public void setCollectionItemInfo(Object collectionItemInfo) { + IMPL.setCollectionItemInfo(mInfo, ((CollectionItemInfoCompat) collectionItemInfo).mInfo); + } + + /** + * Gets the collection item info if the node is a collection item. A collection + * item is always a child of a collection. + * + * @return The collection item info. + */ + public CollectionItemInfoCompat getCollectionItemInfo() { + Object info = IMPL.getCollectionItemInfo(mInfo); + if (info == null) return null; + return new CollectionItemInfoCompat(info); + } + + /** + * Gets the range info if this node is a range. + * + * @return The range. + */ + public RangeInfoCompat getRangeInfo() { + Object info = IMPL.getRangeInfo(mInfo); + if (info == null) return null; + return new RangeInfoCompat(info); + } + + /** + * Gets the actions that can be performed on the node. + * + * @return A list of AccessibilityActions. + */ + public List getActionList() { + List actions = IMPL.getActionList(mInfo); + if (actions != null) { + List result = new ArrayList(); + final int actionCount = actions.size(); + for (int i = 0; i < actionCount; i++) { + Object action = actions.get(i); + result.add(new AccessibilityActionCompat(action)); + } + return result; + } else { + return Collections.emptyList(); + } + } + + /** + * Sets if the content of this node is invalid. For example, + * a date is not well-formed. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param contentInvalid If the node content is invalid. + */ + public void setContentInvalid(boolean contentInvalid) { + IMPL.setContentInvalid(mInfo, contentInvalid); + } + + /** + * Gets if the content of this node is invalid. For example, + * a date is not well-formed. + * + * @return If the node content is invalid. + */ + public boolean isContentInvalid() { + return IMPL.isContentInvalid(mInfo); + } + + /** + * Sets the error text of this node. + *

    + * Note: Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + *

    + * + * @param error The error text. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setError(CharSequence error) { + IMPL.setError(mInfo, error); + } + + /** + * Gets the error text of this node. + * + * @return The error text. + */ + public CharSequence getError() { + return IMPL.getError(mInfo); + } + + /** + * Sets the view for which the view represented by this info serves as a + * label for accessibility purposes. + * + * @param labeled The view for which this info serves as a label. + */ + public void setLabelFor(View labeled) { + IMPL.setLabelFor(mInfo, labeled); + } + + /** + * Sets the view for which the view represented by this info serves as a + * label for accessibility purposes. If virtualDescendantId + * is {@link View#NO_ID} the root is set as the labeled. + *

    + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report themselves as a tree of virtual views, thus conveying their + * logical structure. + *

    + * + * @param root The root whose virtual descendant serves as a label. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setLabelFor(View root, int virtualDescendantId) { + IMPL.setLabelFor(mInfo, root, virtualDescendantId); + } + + @Override + public int hashCode() { + return (mInfo == null) ? 0 : mInfo.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AccessibilityNodeInfoCompat other = (AccessibilityNodeInfoCompat) obj; + if (mInfo == null) { + if (other.mInfo != null) { + return false; + } + } else if (!mInfo.equals(other.mInfo)) { + return false; + } + return true; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(super.toString()); + + Rect bounds = new Rect(); + + getBoundsInParent(bounds); + builder.append("; boundsInParent: " + bounds); + + getBoundsInScreen(bounds); + builder.append("; boundsInScreen: " + bounds); + + builder.append("; packageName: ").append(getPackageName()); + builder.append("; className: ").append(getClassName()); + builder.append("; text: ").append(getText()); + builder.append("; contentDescription: ").append(getContentDescription()); + builder.append("; viewId: ").append(getViewIdResourceName()); + + builder.append("; checkable: ").append(isCheckable()); + builder.append("; checked: ").append(isChecked()); + builder.append("; focusable: ").append(isFocusable()); + builder.append("; focused: ").append(isFocused()); + builder.append("; selected: ").append(isSelected()); + builder.append("; clickable: ").append(isClickable()); + builder.append("; longClickable: ").append(isLongClickable()); + builder.append("; enabled: ").append(isEnabled()); + builder.append("; password: ").append(isPassword()); + builder.append("; scrollable: " + isScrollable()); + + builder.append("; ["); + for (int actionBits = getActions(); actionBits != 0;) { + final int action = 1 << Integer.numberOfTrailingZeros(actionBits); + actionBits &= ~action; + builder.append(getActionSymbolicName(action)); + if (actionBits != 0) { + builder.append(", "); + } + } + builder.append("]"); + + return builder.toString(); + } + + private static String getActionSymbolicName(int action) { + switch (action) { + case ACTION_FOCUS: + return "ACTION_FOCUS"; + case ACTION_CLEAR_FOCUS: + return "ACTION_CLEAR_FOCUS"; + case ACTION_SELECT: + return "ACTION_SELECT"; + case ACTION_CLEAR_SELECTION: + return "ACTION_CLEAR_SELECTION"; + case ACTION_CLICK: + return "ACTION_CLICK"; + case ACTION_LONG_CLICK: + return "ACTION_LONG_CLICK"; + case ACTION_ACCESSIBILITY_FOCUS: + return "ACTION_ACCESSIBILITY_FOCUS"; + case ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return "ACTION_CLEAR_ACCESSIBILITY_FOCUS"; + case ACTION_NEXT_AT_MOVEMENT_GRANULARITY: + return "ACTION_NEXT_AT_MOVEMENT_GRANULARITY"; + case ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: + return "ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY"; + case ACTION_NEXT_HTML_ELEMENT: + return "ACTION_NEXT_HTML_ELEMENT"; + case ACTION_PREVIOUS_HTML_ELEMENT: + return "ACTION_PREVIOUS_HTML_ELEMENT"; + case ACTION_SCROLL_FORWARD: + return "ACTION_SCROLL_FORWARD"; + case ACTION_SCROLL_BACKWARD: + return "ACTION_SCROLL_BACKWARD"; + case ACTION_CUT: + return "ACTION_CUT"; + case ACTION_COPY: + return "ACTION_COPY"; + case ACTION_PASTE: + return "ACTION_PASTE"; + case ACTION_SET_SELECTION: + return "ACTION_SET_SELECTION"; + default: + return"ACTION_UNKNOWN"; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java new file mode 100644 index 00000000000..ce6abf3078f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi21.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; + +import java.util.List; + +/** + * Api21-specific AccessibilityNodeInfo API implementation. + */ +class AccessibilityNodeInfoCompatApi21 { + static List getActionList(Object info) { + Object result = ((AccessibilityNodeInfo) info).getActionList(); + return (List) result; + } + + static void addAction(Object info, Object action) { + ((AccessibilityNodeInfo) info).addAction((AccessibilityAction) action); + } + + public static Object obtainCollectionInfo(int rowCount, int columnCount, + boolean hierarchical, int selectionMode) { + return AccessibilityNodeInfo.CollectionInfo.obtain(rowCount, columnCount, hierarchical, + selectionMode); + } + + public static Object obtainCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, + int columnSpan, boolean heading, boolean selected) { + return AccessibilityNodeInfo.CollectionItemInfo.obtain(rowIndex, rowSpan, columnIndex, + columnSpan, heading, selected); + } + + public static CharSequence getError(Object info) { + return ((AccessibilityNodeInfo) info).getError(); + } + + public static void setError(Object info, CharSequence error) { + ((AccessibilityNodeInfo) info).setError(error); + } + + public static void setLabelFor(Object info, View labeled) { + ((AccessibilityNodeInfo) info).setLabelFor(labeled); + } + + public static void setLabelFor(Object info, View root, int virtualDescendantId) { + ((AccessibilityNodeInfo) info).setLabelFor(root, virtualDescendantId); + } + + static class CollectionItemInfo { + public static boolean isSelected(Object info) { + return ((AccessibilityNodeInfo.CollectionItemInfo) info).isSelected(); + } + } + + static Object newAccessibilityAction(int actionId, CharSequence label) { + return new AccessibilityAction(actionId, label); + } + + static int getAccessibilityActionId(Object action) { + return ((AccessibilityNodeInfo.AccessibilityAction) action).getId(); + } + + static CharSequence getAccessibilityActionLabel(Object action) { + return ((AccessibilityNodeInfo.AccessibilityAction) action).getLabel(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi22.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi22.java new file mode 100644 index 00000000000..786318d9974 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatApi22.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.View; + +/** + * Api22-specific AccessibilityNodeInfo API implementation. + */ +class AccessibilityNodeInfoCompatApi22 { + + public static Object getTraversalBefore(Object info) { + return ((AccessibilityNodeInfo) info).getTraversalBefore(); + } + + public static void setTraversalBefore(Object info, View view) { + ((AccessibilityNodeInfo) info).setTraversalBefore(view); + } + + public static void setTraversalBefore(Object info, View root, int virtualDescendantId) { + ((AccessibilityNodeInfo) info).setTraversalBefore(root, virtualDescendantId); + } + + public static Object getTraversalAfter(Object info) { + return ((AccessibilityNodeInfo) info).getTraversalAfter(); + } + + public static void setTraversalAfter(Object info, View view) { + ((AccessibilityNodeInfo) info).setTraversalAfter(view); + } + + public static void setTraversalAfter(Object info, View root, int virtualDescendantId) { + ((AccessibilityNodeInfo) info).setTraversalAfter(root, virtualDescendantId); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatIcs.java new file mode 100644 index 00000000000..367f995b03f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatIcs.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.graphics.Rect; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; + +import java.util.List; + +/** + * ICS specific AccessibilityNodeInfo API implementation. + */ +class AccessibilityNodeInfoCompatIcs { + + public static Object obtain() { + return AccessibilityNodeInfo.obtain(); + } + + public static Object obtain(View source) { + return AccessibilityNodeInfo.obtain(source); + } + + public static Object obtain(Object info) { + return AccessibilityNodeInfo.obtain((AccessibilityNodeInfo) info); + } + + public static void addAction(Object info, int action) { + ((AccessibilityNodeInfo) info).addAction(action); + } + + public static void addChild(Object info, View child) { + ((AccessibilityNodeInfo) info).addChild(child); + } + + @SuppressWarnings("unchecked") + public static List findAccessibilityNodeInfosByText(Object info, String text) { + Object result = ((AccessibilityNodeInfo) info).findAccessibilityNodeInfosByText(text); + return (List) result; + } + + public static int getActions(Object info) { + return ((AccessibilityNodeInfo) info).getActions(); + } + + public static void getBoundsInParent(Object info, Rect outBounds) { + ((AccessibilityNodeInfo) info).getBoundsInParent(outBounds); + } + + public static void getBoundsInScreen(Object info, Rect outBounds) { + ((AccessibilityNodeInfo) info).getBoundsInScreen(outBounds); + } + + public static Object getChild(Object info, int index) { + return ((AccessibilityNodeInfo) info).getChild(index); + } + + public static int getChildCount(Object info) { + return ((AccessibilityNodeInfo) info).getChildCount(); + } + + public static CharSequence getClassName(Object info) { + return ((AccessibilityNodeInfo) info).getClassName(); + } + + public static CharSequence getContentDescription(Object info) { + return ((AccessibilityNodeInfo) info).getContentDescription(); + } + + public static CharSequence getPackageName(Object info) { + return ((AccessibilityNodeInfo) info).getPackageName(); + } + + public static Object getParent(Object info) { + return ((AccessibilityNodeInfo) info).getParent(); + } + + public static CharSequence getText(Object info) { + return ((AccessibilityNodeInfo) info).getText(); + } + + public static int getWindowId(Object info) { + return ((AccessibilityNodeInfo) info).getWindowId(); + } + + public static boolean isCheckable(Object info) { + return ((AccessibilityNodeInfo) info).isCheckable(); + } + + public static boolean isChecked(Object info) { + return ((AccessibilityNodeInfo) info).isChecked(); + } + + public static boolean isClickable(Object info) { + return ((AccessibilityNodeInfo) info).isClickable(); + } + + public static boolean isEnabled(Object info) { + return ((AccessibilityNodeInfo) info).isEnabled(); + } + + public static boolean isFocusable(Object info) { + return ((AccessibilityNodeInfo) info).isFocusable(); + } + + public static boolean isFocused(Object info) { + return ((AccessibilityNodeInfo) info).isFocused(); + } + + public static boolean isLongClickable(Object info) { + return ((AccessibilityNodeInfo) info).isLongClickable(); + } + + public static boolean isPassword(Object info) { + return ((AccessibilityNodeInfo) info).isPassword(); + } + + public static boolean isScrollable(Object info) { + return ((AccessibilityNodeInfo) info).isScrollable(); + } + + public static boolean isSelected(Object info) { + return ((AccessibilityNodeInfo) info).isSelected(); + } + + public static boolean performAction(Object info, int action) { + return ((AccessibilityNodeInfo) info).performAction(action); + } + + public static void setBoundsInParent(Object info, Rect bounds) { + ((AccessibilityNodeInfo) info).setBoundsInParent(bounds); + } + + public static void setBoundsInScreen(Object info, Rect bounds) { + ((AccessibilityNodeInfo) info).setBoundsInScreen(bounds); + } + + public static void setCheckable(Object info, boolean checkable) { + ((AccessibilityNodeInfo) info).setCheckable(checkable); + } + + public static void setChecked(Object info, boolean checked) { + ((AccessibilityNodeInfo) info).setChecked(checked); + } + + public static void setClassName(Object info, CharSequence className) { + ((AccessibilityNodeInfo) info).setClassName(className); + } + + public static void setClickable(Object info, boolean clickable) { + ((AccessibilityNodeInfo) info).setClickable(clickable); + } + + public static void setContentDescription(Object info, CharSequence contentDescription) { + ((AccessibilityNodeInfo) info).setContentDescription(contentDescription); + } + + public static void setEnabled(Object info, boolean enabled) { + ((AccessibilityNodeInfo) info).setEnabled(enabled); + } + + public static void setFocusable(Object info, boolean focusable) { + ((AccessibilityNodeInfo) info).setFocusable(focusable); + } + + public static void setFocused(Object info, boolean focused) { + ((AccessibilityNodeInfo) info).setFocused(focused); + } + + public static void setLongClickable(Object info, boolean longClickable) { + ((AccessibilityNodeInfo) info).setLongClickable(longClickable); + } + + public static void setPackageName(Object info, CharSequence packageName) { + ((AccessibilityNodeInfo) info).setPackageName(packageName); + } + + public static void setParent(Object info, View parent) { + ((AccessibilityNodeInfo) info).setParent(parent); + } + + public static void setPassword(Object info, boolean password) { + ((AccessibilityNodeInfo) info).setPassword(password); + } + + public static void setScrollable(Object info, boolean scrollable) { + ((AccessibilityNodeInfo) info).setScrollable(scrollable); + } + + public static void setSelected(Object info, boolean selected) { + ((AccessibilityNodeInfo) info).setSelected(selected); + } + + public static void setSource(Object info, View source) { + ((AccessibilityNodeInfo) info).setSource(source); + } + + public static void setText(Object info, CharSequence text) { + ((AccessibilityNodeInfo) info).setText(text); + } + + public static void recycle(Object info) { + ((AccessibilityNodeInfo) info).recycle(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellyBean.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellyBean.java new file mode 100644 index 00000000000..c2f4d938fa5 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellyBean.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.os.Bundle; +import android.view.View; +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * JellyBean specific AccessibilityNodeInfo API implementation. + */ +class AccessibilityNodeInfoCompatJellyBean { + + public static void addChild(Object info, View child, int virtualDescendantId) { + ((AccessibilityNodeInfo) info).addChild(child, virtualDescendantId); + } + + public static void setSource(Object info, View root, int virtualDescendantId) { + ((AccessibilityNodeInfo) info).setSource(root, virtualDescendantId); + } + + public static boolean isVisibleToUser(Object info) { + return ((AccessibilityNodeInfo) info).isVisibleToUser(); + } + + public static void setVisibleToUser(Object info, boolean visibleToUser) { + ((AccessibilityNodeInfo) info).setVisibleToUser(visibleToUser); + } + + public static boolean performAction(Object info, int action, Bundle arguments) { + return ((AccessibilityNodeInfo) info).performAction(action, arguments); + } + + public static void setMovementGranularities(Object info, int granularities) { + ((AccessibilityNodeInfo) info).setMovementGranularities(granularities); + } + + public static int getMovementGranularities(Object info) { + return ((AccessibilityNodeInfo) info).getMovementGranularities(); + } + + public static Object obtain(View root, int virtualDescendantId) { + return AccessibilityNodeInfo.obtain(root, virtualDescendantId); + } + + public static Object findFocus(Object info, int focus) { + return ((AccessibilityNodeInfo) info).findFocus(focus); + } + + public static Object focusSearch(Object info, int direction) { + return ((AccessibilityNodeInfo) info).focusSearch(direction); + } + + public static void setParent(Object info, View root, int virtualDescendantId) { + ((AccessibilityNodeInfo) info).setParent(root, virtualDescendantId); + } + + public static boolean isAccessibilityFocused(Object info) { + return ((AccessibilityNodeInfo) info).isAccessibilityFocused(); + } + + public static void setAccesibilityFocused(Object info, boolean focused) { + ((AccessibilityNodeInfo) info).setAccessibilityFocused(focused); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellybeanMr2.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellybeanMr2.java new file mode 100644 index 00000000000..2b76ea0a10b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatJellybeanMr2.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.view.accessibility.AccessibilityNodeInfo; + +class AccessibilityNodeInfoCompatJellybeanMr2 { + + public static void setViewIdResourceName(Object info, String viewId) { + ((AccessibilityNodeInfo) info).setViewIdResourceName(viewId); + } + + public static String getViewIdResourceName(Object info) { + return ((AccessibilityNodeInfo) info).getViewIdResourceName(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java new file mode 100644 index 00000000000..32fb86fb3bd --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeInfoCompatKitKat.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.view.accessibility.AccessibilityNodeInfo; + +/** + * KitKat-specific AccessibilityNodeInfo API implementation. + */ +class AccessibilityNodeInfoCompatKitKat { + static int getLiveRegion(Object info) { + return ((AccessibilityNodeInfo) info).getLiveRegion(); + } + + static void setLiveRegion(Object info, int mode) { + ((AccessibilityNodeInfo) info).setLiveRegion(mode); + } + + static Object getCollectionInfo(Object info) { + return ((AccessibilityNodeInfo) info).getCollectionInfo(); + } + + static Object getCollectionItemInfo(Object info) { + return ((AccessibilityNodeInfo) info).getCollectionItemInfo(); + } + + public static void setCollectionInfo(Object info, Object collectionInfo) { + ((AccessibilityNodeInfo) info).setCollectionInfo( + (AccessibilityNodeInfo.CollectionInfo)collectionInfo); + } + + public static void setCollectionItemInfo(Object info, Object collectionItemInfo) { + ((AccessibilityNodeInfo) info).setCollectionItemInfo( + (AccessibilityNodeInfo.CollectionItemInfo) collectionItemInfo); + } + + static Object getRangeInfo(Object info) { + return ((AccessibilityNodeInfo) info).getRangeInfo(); + } + + public static Object obtainCollectionInfo(int rowCount, int columnCount, + boolean hierarchical, int selectionMode) { + return AccessibilityNodeInfo.CollectionInfo.obtain(rowCount, columnCount, hierarchical); + } + + public static Object obtainCollectionItemInfo(int rowIndex, int rowSpan, int columnIndex, + int columnSpan, boolean heading) { + return AccessibilityNodeInfo.CollectionItemInfo.obtain(rowIndex, rowSpan, columnIndex, + columnSpan, heading); + } + + public static void setContentInvalid(Object info, boolean contentInvalid) { + ((AccessibilityNodeInfo) info).setContentInvalid(contentInvalid); + } + + public static boolean isContentInvalid(Object info) { + return ((AccessibilityNodeInfo) info).isContentInvalid(); + } + + static class CollectionInfo { + static int getColumnCount(Object info) { + return ((AccessibilityNodeInfo.CollectionInfo) info).getColumnCount(); + } + + static int getRowCount(Object info) { + return ((AccessibilityNodeInfo.CollectionInfo) info).getRowCount(); + } + + static boolean isHierarchical(Object info) { + return ((AccessibilityNodeInfo.CollectionInfo) info).isHierarchical(); + } + } + + static class CollectionItemInfo { + static int getColumnIndex(Object info) { + return ((AccessibilityNodeInfo.CollectionItemInfo) info).getColumnIndex(); + } + + static int getColumnSpan(Object info) { + return ((AccessibilityNodeInfo.CollectionItemInfo) info).getColumnSpan(); + } + + static int getRowIndex(Object info) { + return ((AccessibilityNodeInfo.CollectionItemInfo) info).getRowIndex(); + } + + static int getRowSpan(Object info) { + return ((AccessibilityNodeInfo.CollectionItemInfo) info).getRowSpan(); + } + + static boolean isHeading(Object info) { + return ((AccessibilityNodeInfo.CollectionItemInfo) info).isHeading(); + } + } + + static class RangeInfo { + static float getCurrent(Object info) { + return ((AccessibilityNodeInfo.RangeInfo) info).getCurrent(); + } + + static float getMax(Object info) { + return ((AccessibilityNodeInfo.RangeInfo) info).getMax(); + } + + static float getMin(Object info) { + return ((AccessibilityNodeInfo.RangeInfo) info).getMin(); + } + + static int getType(Object info) { + return ((AccessibilityNodeInfo.RangeInfo) info).getType(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompat.java new file mode 100644 index 00000000000..e6da5f1a0bd --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompat.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.os.Build; +import android.os.Bundle; +import android.view.View; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper for accessing {@link android.view.accessibility.AccessibilityNodeProvider} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AccessibilityNodeProviderCompat { + + interface AccessibilityNodeProviderImpl { + public Object newAccessibilityNodeProviderBridge(AccessibilityNodeProviderCompat compat); + } + + static class AccessibilityNodeProviderStubImpl implements AccessibilityNodeProviderImpl { + @Override + public Object newAccessibilityNodeProviderBridge(AccessibilityNodeProviderCompat compat) { + return null; + } + } + + static class AccessibilityNodeProviderJellyBeanImpl extends AccessibilityNodeProviderStubImpl { + @Override + public Object newAccessibilityNodeProviderBridge( + final AccessibilityNodeProviderCompat compat) { + return AccessibilityNodeProviderCompatJellyBean.newAccessibilityNodeProviderBridge( + new AccessibilityNodeProviderCompatJellyBean.AccessibilityNodeInfoBridge() { + @Override + public boolean performAction(int virtualViewId, int action, + Bundle arguments) { + return compat.performAction(virtualViewId, action, arguments); + } + + @Override + public List findAccessibilityNodeInfosByText( + String text, int virtualViewId) { + List compatInfos = + compat.findAccessibilityNodeInfosByText(text, virtualViewId); + List infos = new ArrayList(); + final int infoCount = compatInfos.size(); + for (int i = 0; i < infoCount; i++) { + AccessibilityNodeInfoCompat infoCompat = compatInfos.get(i); + infos.add(infoCompat.getInfo()); + } + return infos; + } + + @Override + public Object createAccessibilityNodeInfo( + int virtualViewId) { + final AccessibilityNodeInfoCompat compatInfo = compat + .createAccessibilityNodeInfo(virtualViewId); + if (compatInfo == null) { + return null; + } else { + return compatInfo.getInfo(); + } + } + }); + } + } + + static class AccessibilityNodeProviderKitKatImpl extends AccessibilityNodeProviderStubImpl { + @Override + public Object newAccessibilityNodeProviderBridge( + final AccessibilityNodeProviderCompat compat) { + return AccessibilityNodeProviderCompatKitKat.newAccessibilityNodeProviderBridge( + new AccessibilityNodeProviderCompatKitKat.AccessibilityNodeInfoBridge() { + @Override + public boolean performAction( + int virtualViewId, int action, Bundle arguments) { + return compat.performAction(virtualViewId, action, arguments); + } + + @Override + public List findAccessibilityNodeInfosByText( + String text, int virtualViewId) { + List compatInfos = + compat.findAccessibilityNodeInfosByText(text, virtualViewId); + List infos = new ArrayList(); + final int infoCount = compatInfos.size(); + for (int i = 0; i < infoCount; i++) { + AccessibilityNodeInfoCompat infoCompat = compatInfos.get(i); + infos.add(infoCompat.getInfo()); + } + return infos; + } + + @Override + public Object createAccessibilityNodeInfo(int virtualViewId) { + final AccessibilityNodeInfoCompat compatInfo = + compat.createAccessibilityNodeInfo(virtualViewId); + if (compatInfo == null) { + return null; + } else { + return compatInfo.getInfo(); + } + } + + @Override + public Object findFocus(int focus) { + final AccessibilityNodeInfoCompat compatInfo = compat.findFocus(focus); + if (compatInfo == null) { + return null; + } else { + return compatInfo.getInfo(); + } + } + }); + } + } + + private static final AccessibilityNodeProviderImpl IMPL; + + private final Object mProvider; + + static { + if (Build.VERSION.SDK_INT >= 19) { // KitKat + IMPL = new AccessibilityNodeProviderKitKatImpl(); + } else if (Build.VERSION.SDK_INT >= 16) { // JellyBean + IMPL = new AccessibilityNodeProviderJellyBeanImpl(); + } else { + IMPL = new AccessibilityNodeProviderStubImpl(); + } + } + + /** + * Creates a new instance. + */ + public AccessibilityNodeProviderCompat() { + mProvider = IMPL.newAccessibilityNodeProviderBridge(this); + } + + /** + * Creates a new instance wrapping an + * {@link android.view.accessibility.AccessibilityNodeProvider}. + * + * @param provider The provider. + */ + public AccessibilityNodeProviderCompat(Object provider) { + mProvider = provider; + } + + /** + * @return The wrapped {@link android.view.accessibility.AccessibilityNodeProvider}. + */ + public Object getProvider() { + return mProvider; + } + + /** + * Returns an {@link AccessibilityNodeInfoCompat} representing a virtual view, + * i.e. a descendant of the host View, with the given virtualViewId + * or the host View itself if virtualViewId equals to {@link View#NO_ID}. + *

    + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + *

    + *

    + * The implementer is responsible for obtaining an accessibility node info from the + * pool of reusable instances and setting the desired properties of the node info + * before returning it. + *

    + * + * @param virtualViewId A client defined virtual view id. + * @return A populated {@link AccessibilityNodeInfoCompat} for a virtual descendant + * or the host View. + * + * @see AccessibilityNodeInfoCompat + */ + public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) { + return null; + } + + /** + * Performs an accessibility action on a virtual view, i.e. a descendant of the + * host View, with the given virtualViewId or the host View itself + * if virtualViewId equals to {@link View#NO_ID}. + * + * @param virtualViewId A client defined virtual view id. + * @param action The action to perform. + * @param arguments Optional arguments. + * @return True if the action was performed. + * + * @see #createAccessibilityNodeInfo(int) + * @see AccessibilityNodeInfoCompat + */ + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return false; + } + + /** + * Finds {@link AccessibilityNodeInfoCompat}s by text. The match is case insensitive + * containment. The search is relative to the virtual view, i.e. a descendant of the + * host View, with the given virtualViewId or the host View itself + * virtualViewId equals to {@link View#NO_ID}. + * + * @param virtualViewId A client defined virtual view id which defined + * the root of the tree in which to perform the search. + * @param text The searched text. + * @return A list of node info. + * + * @see #createAccessibilityNodeInfo(int) + * @see AccessibilityNodeInfoCompat + */ + public List findAccessibilityNodeInfosByText(String text, + int virtualViewId) { + return null; + } + + /** + * Find the virtual view, i.e. a descendant of the host View, that has the + * specified focus type. + * + * @param focus The focus to find. One of + * {@link AccessibilityNodeInfoCompat#FOCUS_INPUT} or + * {@link AccessibilityNodeInfoCompat#FOCUS_ACCESSIBILITY}. + * @return The node info of the focused view or null. + * @see AccessibilityNodeInfoCompat#FOCUS_INPUT + * @see AccessibilityNodeInfoCompat#FOCUS_ACCESSIBILITY + */ + public AccessibilityNodeInfoCompat findFocus(int focus) { + return null; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatJellyBean.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatJellyBean.java new file mode 100644 index 00000000000..cb8a3b4ece8 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatJellyBean.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.os.Bundle; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +import java.util.List; + +/** + * JellyBean specific AccessibilityNodeProvider API implementation. + */ +class AccessibilityNodeProviderCompatJellyBean { + interface AccessibilityNodeInfoBridge { + public Object createAccessibilityNodeInfo(int virtualViewId); + public boolean performAction(int virtualViewId, int action, Bundle arguments); + public List findAccessibilityNodeInfosByText(String text, + int virtualViewId); + } + + public static Object newAccessibilityNodeProviderBridge( + final AccessibilityNodeInfoBridge bridge) { + return new AccessibilityNodeProvider() { + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + return (AccessibilityNodeInfo) bridge.createAccessibilityNodeInfo(virtualViewId); + } + + @Override + @SuppressWarnings("unchecked") + public List findAccessibilityNodeInfosByText( + String text, int virtualViewId) { + // Use some voodoo to avoid creating intermediary instances. + return (List) (List) + bridge.findAccessibilityNodeInfosByText(text, virtualViewId); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return bridge.performAction(virtualViewId, action, arguments); + } + }; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatKitKat.java new file mode 100644 index 00000000000..3aa475dfd2b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityNodeProviderCompatKitKat.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.os.Bundle; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityNodeProvider; + +import java.util.List; + +/** + * KitKat-specific AccessibilityNodeProvider API implementation. + */ +class AccessibilityNodeProviderCompatKitKat { + interface AccessibilityNodeInfoBridge { + public Object createAccessibilityNodeInfo(int virtualViewId); + public boolean performAction(int virtualViewId, int action, Bundle arguments); + public List findAccessibilityNodeInfosByText(String text, + int virtualViewId); + public Object findFocus(int focus); + } + + public static Object newAccessibilityNodeProviderBridge( + final AccessibilityNodeInfoBridge bridge) { + return new AccessibilityNodeProvider() { + @Override + public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) { + return (AccessibilityNodeInfo) bridge.createAccessibilityNodeInfo(virtualViewId); + } + + @Override + @SuppressWarnings("unchecked") + public List findAccessibilityNodeInfosByText( + String text, int virtualViewId) { + // Use some voodoo to avoid creating intermediary instances. + return (List) (List) + bridge.findAccessibilityNodeInfosByText(text, virtualViewId); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return bridge.performAction(virtualViewId, action, arguments); + } + + @Override + public AccessibilityNodeInfo findFocus(int focus) { + return (AccessibilityNodeInfo) bridge.findFocus(focus); + } + }; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompat.java new file mode 100644 index 00000000000..f5655ccda15 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompat.java @@ -0,0 +1,1044 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.os.Build; +import android.os.Parcelable; +import android.view.View; +import android.view.accessibility.AccessibilityEvent; + +import java.util.Collections; +import java.util.List; + +/** + * Helper for accessing {@link android.view.accessibility.AccessibilityRecord} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class AccessibilityRecordCompat { + + static interface AccessibilityRecordImpl { + public Object obtain(); + public Object obtain(Object record); + public void setSource(Object record, View source); + public void setSource(Object record, View root, int virtualDescendantId); + public AccessibilityNodeInfoCompat getSource(Object record); + public int getWindowId(Object record); + public boolean isChecked(Object record); + public void setChecked(Object record, boolean isChecked); + public boolean isEnabled(Object record); + public void setEnabled(Object record, boolean isEnabled); + public boolean isPassword(Object record); + public void setPassword(Object record, boolean isPassword); + public boolean isFullScreen(Object record); + public void setFullScreen(Object record, boolean isFullScreen); + public boolean isScrollable(Object record); + public void setScrollable(Object record, boolean scrollable); + public int getItemCount(Object record); + public void setItemCount(Object record, int itemCount); + public int getCurrentItemIndex(Object record); + public void setCurrentItemIndex(Object record, int currentItemIndex); + public int getFromIndex(Object record); + public void setFromIndex(Object record, int fromIndex); + public int getToIndex(Object record); + public void setToIndex(Object record, int toIndex); + public int getScrollX(Object record); + public void setScrollX(Object record, int scrollX); + public int getScrollY(Object record); + public void setScrollY(Object record, int scrollY); + public int getMaxScrollX(Object record); + public void setMaxScrollX(Object record, int maxScrollX); + public int getMaxScrollY(Object record); + public void setMaxScrollY(Object record, int maxScrollY); + public int getAddedCount(Object record); + public void setAddedCount(Object record, int addedCount); + public int getRemovedCount(Object record); + public void setRemovedCount(Object record, int removedCount); + public CharSequence getClassName(Object record); + public void setClassName(Object record, CharSequence className); + public List getText(Object record); + public CharSequence getBeforeText(Object record); + public void setBeforeText(Object record, CharSequence beforeText); + public CharSequence getContentDescription(Object record); + public void setContentDescription(Object record, CharSequence contentDescription); + public Parcelable getParcelableData(Object record); + public void setParcelableData(Object record, Parcelable parcelableData); + public void recycle(Object record); + } + + static class AccessibilityRecordStubImpl implements AccessibilityRecordImpl { + public Object obtain() { + return null; + } + + public Object obtain(Object record) { + return null; + } + + public int getAddedCount(Object record) { + return 0; + } + + public CharSequence getBeforeText(Object record) { + return null; + } + + public CharSequence getClassName(Object record) { + return null; + } + + public CharSequence getContentDescription(Object record) { + return null; + } + + public int getCurrentItemIndex(Object record) { + return 0; + } + + public int getFromIndex(Object record) { + return 0; + } + + public int getItemCount(Object record) { + return 0; + } + + public int getMaxScrollX(Object record) { + return 0; + } + + public int getMaxScrollY(Object record) { + return 0; + } + + public Parcelable getParcelableData(Object record) { + return null; + } + + public int getRemovedCount(Object record) { + return 0; + } + + public int getScrollX(Object record) { + return 0; + } + + public int getScrollY(Object record) { + return 0; + } + + public AccessibilityNodeInfoCompat getSource(Object record) { + return null; + } + + public List getText(Object record) { + return Collections.emptyList(); + } + + public int getToIndex(Object record) { + return 0; + } + + public int getWindowId(Object record) { + return 0; + } + + public boolean isChecked(Object record) { + return false; + } + + public boolean isEnabled(Object record) { + return false; + } + + public boolean isFullScreen(Object record) { + return false; + } + + public boolean isPassword(Object record) { + return false; + } + + public boolean isScrollable(Object record) { + return false; + } + + public void recycle(Object record) { + + } + + public void setAddedCount(Object record, int addedCount) { + + } + + public void setBeforeText(Object record, CharSequence beforeText) { + + } + + public void setChecked(Object record, boolean isChecked) { + + } + + public void setClassName(Object record, CharSequence className) { + + } + + public void setContentDescription(Object record, CharSequence contentDescription) { + + } + + public void setCurrentItemIndex(Object record, int currentItemIndex) { + + } + + public void setEnabled(Object record, boolean isEnabled) { + + } + + public void setFromIndex(Object record, int fromIndex) { + + } + + public void setFullScreen(Object record, boolean isFullScreen) { + + } + + public void setItemCount(Object record, int itemCount) { + + } + + public void setMaxScrollX(Object record, int maxScrollX) { + + } + + public void setMaxScrollY(Object record, int maxScrollY) { + + } + + public void setParcelableData(Object record, Parcelable parcelableData) { + + } + + public void setPassword(Object record, boolean isPassword) { + + } + + public void setRemovedCount(Object record, int removedCount) { + + } + + public void setScrollX(Object record, int scrollX) { + + } + + public void setScrollY(Object record, int scrollY) { + + } + + public void setScrollable(Object record, boolean scrollable) { + + } + + public void setSource(Object record, View source) { + + } + + public void setSource(Object record, View root, int virtualDescendantId) { + + } + + public void setToIndex(Object record, int toIndex) { + + } + } + + static class AccessibilityRecordIcsImpl extends AccessibilityRecordStubImpl { + @Override + public Object obtain() { + return AccessibilityRecordCompatIcs.obtain(); + } + + @Override + public Object obtain(Object record) { + return AccessibilityRecordCompatIcs.obtain(record); + } + + @Override + public int getAddedCount(Object record) { + return AccessibilityRecordCompatIcs.getAddedCount(record); + } + + @Override + public CharSequence getBeforeText(Object record) { + return AccessibilityRecordCompatIcs.getBeforeText(record); + } + + @Override + public CharSequence getClassName(Object record) { + return AccessibilityRecordCompatIcs.getClassName(record); + } + + @Override + public CharSequence getContentDescription(Object record) { + return AccessibilityRecordCompatIcs.getContentDescription(record); + } + + @Override + public int getCurrentItemIndex(Object record) { + return AccessibilityRecordCompatIcs.getCurrentItemIndex(record); + } + + @Override + public int getFromIndex(Object record) { + return AccessibilityRecordCompatIcs.getFromIndex(record); + } + + @Override + public int getItemCount(Object record) { + return AccessibilityRecordCompatIcs.getItemCount(record); + } + + @Override + public Parcelable getParcelableData(Object record) { + return AccessibilityRecordCompatIcs.getParcelableData(record); + } + + @Override + public int getRemovedCount(Object record) { + return AccessibilityRecordCompatIcs.getRemovedCount(record); + } + + @Override + public int getScrollX(Object record) { + return AccessibilityRecordCompatIcs.getScrollX(record); + } + + @Override + public int getScrollY(Object record) { + return AccessibilityRecordCompatIcs.getScrollY(record); + } + + @Override + public AccessibilityNodeInfoCompat getSource(Object record) { + return AccessibilityNodeInfoCompat.wrapNonNullInstance( + AccessibilityRecordCompatIcs.getSource(record)); + } + + @Override + public List getText(Object record) { + return AccessibilityRecordCompatIcs.getText(record); + } + + @Override + public int getToIndex(Object record) { + return AccessibilityRecordCompatIcs.getToIndex(record); + } + + @Override + public int getWindowId(Object record) { + return AccessibilityRecordCompatIcs.getWindowId(record); + } + + @Override + public boolean isChecked(Object record) { + return AccessibilityRecordCompatIcs.isChecked(record); + } + + @Override + public boolean isEnabled(Object record) { + return AccessibilityRecordCompatIcs.isEnabled(record); + } + + @Override + public boolean isFullScreen(Object record) { + return AccessibilityRecordCompatIcs.isFullScreen(record); + } + + @Override + public boolean isPassword(Object record) { + return AccessibilityRecordCompatIcs.isPassword(record); + } + + @Override + public boolean isScrollable(Object record) { + return AccessibilityRecordCompatIcs.isScrollable(record); + } + + @Override + public void recycle(Object record) { + AccessibilityRecordCompatIcs.recycle(record); + } + + @Override + public void setAddedCount(Object record, int addedCount) { + AccessibilityRecordCompatIcs.setAddedCount(record, addedCount); + } + + @Override + public void setBeforeText(Object record, CharSequence beforeText) { + AccessibilityRecordCompatIcs.setBeforeText(record, beforeText); + } + + @Override + public void setChecked(Object record, boolean isChecked) { + AccessibilityRecordCompatIcs.setChecked(record, isChecked); + } + + @Override + public void setClassName(Object record, CharSequence className) { + AccessibilityRecordCompatIcs.setClassName(record, className); + } + + @Override + public void setContentDescription(Object record, CharSequence contentDescription) { + AccessibilityRecordCompatIcs.setContentDescription(record, contentDescription); + } + + @Override + public void setCurrentItemIndex(Object record, int currentItemIndex) { + AccessibilityRecordCompatIcs.setCurrentItemIndex(record, currentItemIndex); + } + + @Override + public void setEnabled(Object record, boolean isEnabled) { + AccessibilityRecordCompatIcs.setEnabled(record, isEnabled); + } + + @Override + public void setFromIndex(Object record, int fromIndex) { + AccessibilityRecordCompatIcs.setFromIndex(record, fromIndex); + } + + @Override + public void setFullScreen(Object record, boolean isFullScreen) { + AccessibilityRecordCompatIcs.setFullScreen(record, isFullScreen); + } + + @Override + public void setItemCount(Object record, int itemCount) { + AccessibilityRecordCompatIcs.setItemCount(record, itemCount); + } + + @Override + public void setParcelableData(Object record, Parcelable parcelableData) { + AccessibilityRecordCompatIcs.setParcelableData(record, parcelableData); + } + + @Override + public void setPassword(Object record, boolean isPassword) { + AccessibilityRecordCompatIcs.setPassword(record, isPassword); + } + + @Override + public void setRemovedCount(Object record, int removedCount) { + AccessibilityRecordCompatIcs.setRemovedCount(record, removedCount); + } + + @Override + public void setScrollX(Object record, int scrollX) { + AccessibilityRecordCompatIcs.setScrollX(record, scrollX); + } + + @Override + public void setScrollY(Object record, int scrollY) { + AccessibilityRecordCompatIcs.setScrollY(record, scrollY); + } + + @Override + public void setScrollable(Object record, boolean scrollable) { + AccessibilityRecordCompatIcs.setScrollable(record, scrollable); + } + + @Override + public void setSource(Object record, View source) { + AccessibilityRecordCompatIcs.setSource(record, source); + } + + @Override + public void setToIndex(Object record, int toIndex) { + AccessibilityRecordCompatIcs.setToIndex(record, toIndex); + } + } + + static class AccessibilityRecordIcsMr1Impl extends AccessibilityRecordIcsImpl { + @Override + public int getMaxScrollX(Object record) { + return AccessibilityRecordCompatIcsMr1.getMaxScrollX(record); + } + + @Override + public int getMaxScrollY(Object record) { + return AccessibilityRecordCompatIcsMr1.getMaxScrollY(record); + } + + @Override + public void setMaxScrollX(Object record, int maxScrollX) { + AccessibilityRecordCompatIcsMr1.setMaxScrollX(record, maxScrollX); + } + + @Override + public void setMaxScrollY(Object record, int maxScrollY) { + AccessibilityRecordCompatIcsMr1.setMaxScrollY(record, maxScrollY); + } + } + + static class AccessibilityRecordJellyBeanImpl extends AccessibilityRecordIcsMr1Impl { + @Override + public void setSource(Object record, View root, int virtualDescendantId) { + AccessibilityRecordCompatJellyBean.setSource(record, root, virtualDescendantId); + } + } + + static { + if (Build.VERSION.SDK_INT >= 16) { // JellyBean + IMPL = new AccessibilityRecordJellyBeanImpl(); + } else if (Build.VERSION.SDK_INT >= 15) { // ICS MR1 + IMPL = new AccessibilityRecordIcsMr1Impl(); + } else if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new AccessibilityRecordIcsImpl(); + } else { + IMPL = new AccessibilityRecordStubImpl(); + } + } + + private static final AccessibilityRecordImpl IMPL; + + private final Object mRecord; + + /** + * @deprecated This is not type safe. If you want to modify an + * {@link AccessibilityEvent}'s properties defined in + * {@link android.view.accessibility.AccessibilityRecord} use + * {@link AccessibilityEventCompat#asRecord(AccessibilityEvent)}. This method will be removed + * in a subsequent release of the support library. + */ + public AccessibilityRecordCompat(Object record) { + mRecord = record; + } + + /** + * @return The wrapped implementation. + * + * @deprecated This method will be removed in a subsequent release of + * the support library. + */ + public Object getImpl() { + return mRecord; + } + + /** + * Returns a cached instance if such is available or a new one is + * instantiated. The instance is initialized with data from the + * given record. + * + * @return An instance. + */ + public static AccessibilityRecordCompat obtain(AccessibilityRecordCompat record) { + return new AccessibilityRecordCompat(IMPL.obtain(record.mRecord)); + } + + /** + * Returns a cached instance if such is available or a new one is + * instantiated. + * + * @return An instance. + */ + public static AccessibilityRecordCompat obtain() { + return new AccessibilityRecordCompat(IMPL.obtain()); + } + + /** + * Sets the event source. + * + * @param source The source. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setSource(View source) { + IMPL.setSource(mRecord, source); + } + + /** + * Sets the source to be a virtual descendant of the given root. + * If virtualDescendantId equals to {@link View#NO_ID} the root + * is set as the source. + *

    + * A virtual descendant is an imaginary View that is reported as a part of the view + * hierarchy for accessibility purposes. This enables custom views that draw complex + * content to report them selves as a tree of virtual views, thus conveying their + * logical structure. + *

    + * + * @param root The root of the virtual subtree. + * @param virtualDescendantId The id of the virtual descendant. + */ + public void setSource(View root, int virtualDescendantId) { + IMPL.setSource(mRecord, root, virtualDescendantId); + } + + /** + * Gets the {@link android.view.accessibility.AccessibilityNodeInfo} of + * the event source. + *

    + * Note: It is a client responsibility to recycle the + * received info by calling + * {@link android.view.accessibility.AccessibilityNodeInfo#recycle() + * AccessibilityNodeInfo#recycle()} to avoid creating of multiple instances. + *

    + * + * @return The info of the source. + */ + public AccessibilityNodeInfoCompat getSource() { + return IMPL.getSource(mRecord); + } + + /** + * Gets the id of the window from which the event comes from. + * + * @return The window id. + */ + public int getWindowId() { + return IMPL.getWindowId(mRecord); + } + + /** + * Gets if the source is checked. + * + * @return True if the view is checked, false otherwise. + */ + public boolean isChecked() { + return IMPL.isChecked(mRecord); + } + + /** + * Sets if the source is checked. + * + * @param isChecked True if the view is checked, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setChecked(boolean isChecked) { + IMPL.setChecked(mRecord, isChecked); + } + + /** + * Gets if the source is enabled. + * + * @return True if the view is enabled, false otherwise. + */ + public boolean isEnabled() { + return IMPL.isEnabled(mRecord); + } + + /** + * Sets if the source is enabled. + * + * @param isEnabled True if the view is enabled, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setEnabled(boolean isEnabled) { + IMPL.setEnabled(mRecord, isEnabled); + } + + /** + * Gets if the source is a password field. + * + * @return True if the view is a password field, false otherwise. + */ + public boolean isPassword() { + return IMPL.isPassword(mRecord); + } + + /** + * Sets if the source is a password field. + * + * @param isPassword True if the view is a password field, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setPassword(boolean isPassword) { + IMPL.setPassword(mRecord, isPassword); + } + + /** + * Gets if the source is taking the entire screen. + * + * @return True if the source is full screen, false otherwise. + */ + public boolean isFullScreen() { + return IMPL.isFullScreen(mRecord); + } + + /** + * Sets if the source is taking the entire screen. + * + * @param isFullScreen True if the source is full screen, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFullScreen(boolean isFullScreen) { + IMPL.setFullScreen(mRecord, isFullScreen); + } + + /** + * Gets if the source is scrollable. + * + * @return True if the source is scrollable, false otherwise. + */ + public boolean isScrollable() { + return IMPL.isScrollable(mRecord); + } + + /** + * Sets if the source is scrollable. + * + * @param scrollable True if the source is scrollable, false otherwise. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setScrollable(boolean scrollable) { + IMPL.setScrollable(mRecord, scrollable); + } + + /** + * Gets the number of items that can be visited. + * + * @return The number of items. + */ + public int getItemCount() { + return IMPL.getItemCount(mRecord); + } + + /** + * Sets the number of items that can be visited. + * + * @param itemCount The number of items. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setItemCount(int itemCount) { + IMPL.setItemCount(mRecord, itemCount); + } + + /** + * Gets the index of the source in the list of items the can be visited. + * + * @return The current item index. + */ + public int getCurrentItemIndex() { + return IMPL.getCurrentItemIndex(mRecord); + } + + /** + * Sets the index of the source in the list of items that can be visited. + * + * @param currentItemIndex The current item index. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setCurrentItemIndex(int currentItemIndex) { + IMPL.setCurrentItemIndex(mRecord, currentItemIndex); + } + + /** + * Gets the index of the first character of the changed sequence, + * or the beginning of a text selection or the index of the first + * visible item when scrolling. + * + * @return The index of the first character or selection + * start or the first visible item. + */ + public int getFromIndex() { + return IMPL.getFromIndex(mRecord); + } + + /** + * Sets the index of the first character of the changed sequence + * or the beginning of a text selection or the index of the first + * visible item when scrolling. + * + * @param fromIndex The index of the first character or selection + * start or the first visible item. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setFromIndex(int fromIndex) { + IMPL.setFromIndex(mRecord, fromIndex); + } + + /** + * Gets the index of text selection end or the index of the last + * visible item when scrolling. + * + * @return The index of selection end or last item index. + */ + public int getToIndex() { + return IMPL.getToIndex(mRecord); + } + + /** + * Sets the index of text selection end or the index of the last + * visible item when scrolling. + * + * @param toIndex The index of selection end or last item index. + */ + public void setToIndex(int toIndex) { + IMPL.setToIndex(mRecord, toIndex); + } + + /** + * Gets the scroll offset of the source left edge in pixels. + * + * @return The scroll. + */ + public int getScrollX() { + return IMPL.getScrollX(mRecord); + } + + /** + * Sets the scroll offset of the source left edge in pixels. + * + * @param scrollX The scroll. + */ + public void setScrollX(int scrollX) { + IMPL.setScrollX(mRecord, scrollX); + } + + /** + * Gets the scroll offset of the source top edge in pixels. + * + * @return The scroll. + */ + public int getScrollY() { + return IMPL.getScrollY(mRecord); + } + + /** + * Sets the scroll offset of the source top edge in pixels. + * + * @param scrollY The scroll. + */ + public void setScrollY(int scrollY) { + IMPL.setScrollY(mRecord, scrollY); + } + + /** + * Gets the max scroll offset of the source left edge in pixels. + * + * @return The max scroll. + */ + public int getMaxScrollX() { + return IMPL.getMaxScrollX(mRecord); + } + /** + * Sets the max scroll offset of the source left edge in pixels. + * + * @param maxScrollX The max scroll. + */ + public void setMaxScrollX(int maxScrollX) { + IMPL.setMaxScrollX(mRecord, maxScrollX); + } + + /** + * Gets the max scroll offset of the source top edge in pixels. + * + * @return The max scroll. + */ + public int getMaxScrollY() { + return IMPL.getMaxScrollY(mRecord); + } + + /** + * Sets the max scroll offset of the source top edge in pixels. + * + * @param maxScrollY The max scroll. + */ + public void setMaxScrollY(int maxScrollY) { + IMPL.setMaxScrollY(mRecord, maxScrollY); + } + + /** + * Gets the number of added characters. + * + * @return The number of added characters. + */ + public int getAddedCount() { + return IMPL.getAddedCount(mRecord); + } + + /** + * Sets the number of added characters. + * + * @param addedCount The number of added characters. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setAddedCount(int addedCount) { + IMPL.setAddedCount(mRecord, addedCount); + } + + /** + * Gets the number of removed characters. + * + * @return The number of removed characters. + */ + public int getRemovedCount() { + return IMPL.getRemovedCount(mRecord); + } + + /** + * Sets the number of removed characters. + * + * @param removedCount The number of removed characters. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setRemovedCount(int removedCount) { + IMPL.setRemovedCount(mRecord, removedCount); + } + + /** + * Gets the class name of the source. + * + * @return The class name. + */ + public CharSequence getClassName() { + return IMPL.getClassName(mRecord); + } + + /** + * Sets the class name of the source. + * + * @param className The lass name. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setClassName(CharSequence className) { + IMPL.setClassName(mRecord, className); + } + + /** + * Gets the text of the event. The index in the list represents the priority + * of the text. Specifically, the lower the index the higher the priority. + * + * @return The text. + */ + public List getText() { + return IMPL.getText(mRecord); + } + + /** + * Sets the text before a change. + * + * @return The text before the change. + */ + public CharSequence getBeforeText() { + return IMPL.getBeforeText(mRecord); + } + + /** + * Sets the text before a change. + * + * @param beforeText The text before the change. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setBeforeText(CharSequence beforeText) { + IMPL.setBeforeText(mRecord, beforeText); + } + + /** + * Gets the description of the source. + * + * @return The description. + */ + public CharSequence getContentDescription() { + return IMPL.getContentDescription(mRecord); + } + + /** + * Sets the description of the source. + * + * @param contentDescription The description. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setContentDescription(CharSequence contentDescription) { + IMPL.setContentDescription(mRecord, contentDescription); + } + + /** + * Gets the {@link Parcelable} data. + * + * @return The parcelable data. + */ + public Parcelable getParcelableData() { + return IMPL.getParcelableData(mRecord); + } + + /** + * Sets the {@link Parcelable} data of the event. + * + * @param parcelableData The parcelable data. + * + * @throws IllegalStateException If called from an AccessibilityService. + */ + public void setParcelableData(Parcelable parcelableData) { + IMPL.setParcelableData(mRecord, parcelableData); + } + + /** + * Return an instance back to be reused. + *

    + * Note: You must not touch the object after calling this + * function. + *

    + * + * @throws IllegalStateException If the record is already recycled. + */ + public void recycle() { + IMPL.recycle(mRecord); + } + + @Override + public int hashCode() { + return (mRecord == null) ? 0 : mRecord.hashCode(); + } + + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + AccessibilityRecordCompat other = (AccessibilityRecordCompat) obj; + if (mRecord == null) { + if (other.mRecord != null) { + return false; + } + } else if (!mRecord.equals(other.mRecord)) { + return false; + } + return true; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcs.java new file mode 100644 index 00000000000..0ebcc6a2db9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcs.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.os.Parcelable; +import android.view.View; +import android.view.accessibility.AccessibilityRecord; + +import java.util.List; + +/** + * ICS specific AccessibilityRecord API implementation. + */ +class AccessibilityRecordCompatIcs { + + public static Object obtain() { + return AccessibilityRecord.obtain(); + } + + public static Object obtain(Object record) { + return AccessibilityRecord.obtain((AccessibilityRecord) record); + } + + public static int getAddedCount(Object record) { + return ((AccessibilityRecord) record).getAddedCount(); + } + + public static CharSequence getBeforeText(Object record) { + return ((AccessibilityRecord) record).getBeforeText(); + } + + public static CharSequence getClassName(Object record) { + return ((AccessibilityRecord) record).getClassName(); + } + + public static CharSequence getContentDescription(Object record) { + return ((AccessibilityRecord) record).getContentDescription(); + } + + public static int getCurrentItemIndex(Object record) { + return ((AccessibilityRecord) record).getCurrentItemIndex(); + } + + public static int getFromIndex(Object record) { + return ((AccessibilityRecord) record).getFromIndex(); + } + + public static int getItemCount(Object record) { + return ((AccessibilityRecord) record).getItemCount(); + } + + public static Parcelable getParcelableData(Object record) { + return ((AccessibilityRecord) record).getParcelableData(); + } + + public static int getRemovedCount(Object record) { + return ((AccessibilityRecord) record).getRemovedCount(); + } + + public static int getScrollX(Object record) { + return ((AccessibilityRecord) record).getScrollX(); + } + + public static int getScrollY(Object record) { + return ((AccessibilityRecord) record).getScrollY(); + } + + public static Object getSource(Object record) { + return ((AccessibilityRecord) record).getSource(); + } + + public static List getText(Object record) { + return ((AccessibilityRecord) record).getText(); + } + + public static int getToIndex(Object record) { + return ((AccessibilityRecord) record).getToIndex(); + } + + public static int getWindowId(Object record) { + return ((AccessibilityRecord) record).getWindowId(); + } + + public static boolean isChecked(Object record) { + return ((AccessibilityRecord) record).isChecked(); + } + + public static boolean isEnabled(Object record) { + return ((AccessibilityRecord) record).isEnabled(); + } + + public static boolean isFullScreen(Object record) { + return ((AccessibilityRecord) record).isFullScreen(); + } + + public static boolean isPassword(Object record) { + return ((AccessibilityRecord) record).isPassword(); + } + + public static boolean isScrollable(Object record) { + return ((AccessibilityRecord) record).isScrollable(); + } + + public static void recycle(Object record) { + ((AccessibilityRecord) record).recycle(); + } + + public static void setAddedCount(Object record, int addedCount) { + ((AccessibilityRecord) record).setAddedCount(addedCount); + } + + public static void setBeforeText(Object record, CharSequence beforeText) { + ((AccessibilityRecord) record).setBeforeText(beforeText); + } + + public static void setChecked(Object record, boolean isChecked) { + ((AccessibilityRecord) record).setChecked(isChecked); + } + + public static void setClassName(Object record, CharSequence className) { + ((AccessibilityRecord) record).setClassName(className); + } + + public static void setContentDescription(Object record, CharSequence contentDescription) { + ((AccessibilityRecord) record).setContentDescription(contentDescription); + } + + public static void setCurrentItemIndex(Object record, int currentItemIndex) { + ((AccessibilityRecord) record).setCurrentItemIndex(currentItemIndex); + } + + public static void setEnabled(Object record, boolean isEnabled) { + ((AccessibilityRecord) record).setEnabled(isEnabled); + } + + public static void setFromIndex(Object record, int fromIndex) { + ((AccessibilityRecord) record).setFromIndex(fromIndex); + } + + public static void setFullScreen(Object record, boolean isFullScreen) { + ((AccessibilityRecord) record).setFullScreen(isFullScreen); + } + + public static void setItemCount(Object record, int itemCount) { + ((AccessibilityRecord) record).setItemCount(itemCount); + } + + public static void setParcelableData(Object record, Parcelable parcelableData) { + ((AccessibilityRecord) record).setParcelableData(parcelableData); + } + + public static void setPassword(Object record, boolean isPassword) { + ((AccessibilityRecord) record).setPassword(isPassword); + } + + public static void setRemovedCount(Object record, int removedCount) { + ((AccessibilityRecord) record).setRemovedCount(removedCount); + } + + public static void setScrollX(Object record, int scrollX) { + ((AccessibilityRecord) record).setScrollX(scrollX); + } + + public static void setScrollY(Object record, int scrollY) { + ((AccessibilityRecord) record).setScrollY(scrollY); + } + + public static void setScrollable(Object record, boolean scrollable) { + ((AccessibilityRecord) record).setScrollable(scrollable); + } + + public static void setSource(Object record, View source) { + ((AccessibilityRecord) record).setSource(source); + } + + public static void setToIndex(Object record, int toIndex) { + ((AccessibilityRecord) record).setToIndex(toIndex); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcsMr1.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcsMr1.java new file mode 100644 index 00000000000..94164d74e00 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatIcsMr1.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.view.accessibility.AccessibilityRecord; + +/** + * ICS MR1 specific AccessibilityRecord API implementation. + */ +class AccessibilityRecordCompatIcsMr1 { + + public static int getMaxScrollX(Object record) { + return ((AccessibilityRecord) record).getMaxScrollX(); + } + + public static int getMaxScrollY(Object record) { + return ((AccessibilityRecord) record).getMaxScrollY(); + } + public static void setMaxScrollX(Object record, int maxScrollX) { + ((AccessibilityRecord) record).setMaxScrollX(maxScrollX); + } + + public static void setMaxScrollY(Object record, int maxScrollY) { + ((AccessibilityRecord) record).setMaxScrollY(maxScrollY); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatJellyBean.java b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatJellyBean.java new file mode 100644 index 00000000000..dc9d5188bf7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/AccessibilityRecordCompatJellyBean.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.accessibility; + +import android.view.View; +import android.view.accessibility.AccessibilityRecord; + +/** + * JellyBean specific AccessibilityRecord API implementation. + */ +class AccessibilityRecordCompatJellyBean { + + public static void setSource(Object record, View root, int virtualDescendantId) { + ((AccessibilityRecord) record).setSource(root, virtualDescendantId); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/package.html b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/package.html new file mode 100644 index 00000000000..57b084f61e9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/accessibility/package.html @@ -0,0 +1,5 @@ + + +Support classes to access some of the android.view.accessibility package features introduced after API level 4 in a backwards compatible fashion. + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutLinearInInterpolator.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutLinearInInterpolator.java new file mode 100644 index 00000000000..1f39aa8390a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutLinearInInterpolator.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.animation; + +/** + * Interpolator corresponding to {@link android.R.interpolator#fast_out_linear_in}. + * + * Uses a lookup table for the Bezier curve from (0,0) to (1,1) with control points: + * P0 (0, 0) + * P1 (0.4, 0) + * P2 (1.0, 1.0) + * P3 (1.0, 1.0) + */ +public class FastOutLinearInInterpolator extends LookupTableInterpolator { + + /** + * Lookup table values sampled with x at regular intervals between 0 and 1 for a total of + * 201 points. + */ + private static final float[] VALUES = new float[] { + 0.0000f, 0.0001f, 0.0002f, 0.0005f, 0.0008f, 0.0013f, 0.0018f, + 0.0024f, 0.0032f, 0.0040f, 0.0049f, 0.0059f, 0.0069f, 0.0081f, + 0.0093f, 0.0106f, 0.0120f, 0.0135f, 0.0151f, 0.0167f, 0.0184f, + 0.0201f, 0.0220f, 0.0239f, 0.0259f, 0.0279f, 0.0300f, 0.0322f, + 0.0345f, 0.0368f, 0.0391f, 0.0416f, 0.0441f, 0.0466f, 0.0492f, + 0.0519f, 0.0547f, 0.0574f, 0.0603f, 0.0632f, 0.0662f, 0.0692f, + 0.0722f, 0.0754f, 0.0785f, 0.0817f, 0.0850f, 0.0884f, 0.0917f, + 0.0952f, 0.0986f, 0.1021f, 0.1057f, 0.1093f, 0.1130f, 0.1167f, + 0.1205f, 0.1243f, 0.1281f, 0.1320f, 0.1359f, 0.1399f, 0.1439f, + 0.1480f, 0.1521f, 0.1562f, 0.1604f, 0.1647f, 0.1689f, 0.1732f, + 0.1776f, 0.1820f, 0.1864f, 0.1909f, 0.1954f, 0.1999f, 0.2045f, + 0.2091f, 0.2138f, 0.2184f, 0.2232f, 0.2279f, 0.2327f, 0.2376f, + 0.2424f, 0.2473f, 0.2523f, 0.2572f, 0.2622f, 0.2673f, 0.2723f, + 0.2774f, 0.2826f, 0.2877f, 0.2929f, 0.2982f, 0.3034f, 0.3087f, + 0.3141f, 0.3194f, 0.3248f, 0.3302f, 0.3357f, 0.3412f, 0.3467f, + 0.3522f, 0.3578f, 0.3634f, 0.3690f, 0.3747f, 0.3804f, 0.3861f, + 0.3918f, 0.3976f, 0.4034f, 0.4092f, 0.4151f, 0.4210f, 0.4269f, + 0.4329f, 0.4388f, 0.4448f, 0.4508f, 0.4569f, 0.4630f, 0.4691f, + 0.4752f, 0.4814f, 0.4876f, 0.4938f, 0.5000f, 0.5063f, 0.5126f, + 0.5189f, 0.5252f, 0.5316f, 0.5380f, 0.5444f, 0.5508f, 0.5573f, + 0.5638f, 0.5703f, 0.5768f, 0.5834f, 0.5900f, 0.5966f, 0.6033f, + 0.6099f, 0.6166f, 0.6233f, 0.6301f, 0.6369f, 0.6436f, 0.6505f, + 0.6573f, 0.6642f, 0.6710f, 0.6780f, 0.6849f, 0.6919f, 0.6988f, + 0.7059f, 0.7129f, 0.7199f, 0.7270f, 0.7341f, 0.7413f, 0.7484f, + 0.7556f, 0.7628f, 0.7700f, 0.7773f, 0.7846f, 0.7919f, 0.7992f, + 0.8066f, 0.8140f, 0.8214f, 0.8288f, 0.8363f, 0.8437f, 0.8513f, + 0.8588f, 0.8664f, 0.8740f, 0.8816f, 0.8892f, 0.8969f, 0.9046f, + 0.9124f, 0.9201f, 0.9280f, 0.9358f, 0.9437f, 0.9516f, 0.9595f, + 0.9675f, 0.9755f, 0.9836f, 0.9918f, 1.0000f + }; + + public FastOutLinearInInterpolator() { + super(VALUES); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutSlowInInterpolator.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutSlowInInterpolator.java new file mode 100644 index 00000000000..a21d1310e67 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/FastOutSlowInInterpolator.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.animation; + +/** + * Interpolator corresponding to {@link android.R.interpolator#fast_out_slow_in}. + * + * Uses a lookup table for the Bezier curve from (0,0) to (1,1) with control points: + * P0 (0, 0) + * P1 (0.4, 0) + * P2 (0.2, 1.0) + * P3 (1.0, 1.0) + */ +public class FastOutSlowInInterpolator extends LookupTableInterpolator { + + /** + * Lookup table values sampled with x at regular intervals between 0 and 1 for a total of + * 201 points. + */ + private static final float[] VALUES = new float[] { + 0.0000f, 0.0001f, 0.0002f, 0.0005f, 0.0009f, 0.0014f, 0.0020f, + 0.0027f, 0.0036f, 0.0046f, 0.0058f, 0.0071f, 0.0085f, 0.0101f, + 0.0118f, 0.0137f, 0.0158f, 0.0180f, 0.0205f, 0.0231f, 0.0259f, + 0.0289f, 0.0321f, 0.0355f, 0.0391f, 0.0430f, 0.0471f, 0.0514f, + 0.0560f, 0.0608f, 0.0660f, 0.0714f, 0.0771f, 0.0830f, 0.0893f, + 0.0959f, 0.1029f, 0.1101f, 0.1177f, 0.1257f, 0.1339f, 0.1426f, + 0.1516f, 0.1610f, 0.1707f, 0.1808f, 0.1913f, 0.2021f, 0.2133f, + 0.2248f, 0.2366f, 0.2487f, 0.2611f, 0.2738f, 0.2867f, 0.2998f, + 0.3131f, 0.3265f, 0.3400f, 0.3536f, 0.3673f, 0.3810f, 0.3946f, + 0.4082f, 0.4217f, 0.4352f, 0.4485f, 0.4616f, 0.4746f, 0.4874f, + 0.5000f, 0.5124f, 0.5246f, 0.5365f, 0.5482f, 0.5597f, 0.5710f, + 0.5820f, 0.5928f, 0.6033f, 0.6136f, 0.6237f, 0.6335f, 0.6431f, + 0.6525f, 0.6616f, 0.6706f, 0.6793f, 0.6878f, 0.6961f, 0.7043f, + 0.7122f, 0.7199f, 0.7275f, 0.7349f, 0.7421f, 0.7491f, 0.7559f, + 0.7626f, 0.7692f, 0.7756f, 0.7818f, 0.7879f, 0.7938f, 0.7996f, + 0.8053f, 0.8108f, 0.8162f, 0.8215f, 0.8266f, 0.8317f, 0.8366f, + 0.8414f, 0.8461f, 0.8507f, 0.8551f, 0.8595f, 0.8638f, 0.8679f, + 0.8720f, 0.8760f, 0.8798f, 0.8836f, 0.8873f, 0.8909f, 0.8945f, + 0.8979f, 0.9013f, 0.9046f, 0.9078f, 0.9109f, 0.9139f, 0.9169f, + 0.9198f, 0.9227f, 0.9254f, 0.9281f, 0.9307f, 0.9333f, 0.9358f, + 0.9382f, 0.9406f, 0.9429f, 0.9452f, 0.9474f, 0.9495f, 0.9516f, + 0.9536f, 0.9556f, 0.9575f, 0.9594f, 0.9612f, 0.9629f, 0.9646f, + 0.9663f, 0.9679f, 0.9695f, 0.9710f, 0.9725f, 0.9739f, 0.9753f, + 0.9766f, 0.9779f, 0.9791f, 0.9803f, 0.9815f, 0.9826f, 0.9837f, + 0.9848f, 0.9858f, 0.9867f, 0.9877f, 0.9885f, 0.9894f, 0.9902f, + 0.9910f, 0.9917f, 0.9924f, 0.9931f, 0.9937f, 0.9944f, 0.9949f, + 0.9955f, 0.9960f, 0.9964f, 0.9969f, 0.9973f, 0.9977f, 0.9980f, + 0.9984f, 0.9986f, 0.9989f, 0.9991f, 0.9993f, 0.9995f, 0.9997f, + 0.9998f, 0.9999f, 0.9999f, 1.0000f, 1.0000f + }; + + public FastOutSlowInInterpolator() { + super(VALUES); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/LinearOutSlowInInterpolator.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/LinearOutSlowInInterpolator.java new file mode 100644 index 00000000000..41f4cd6a52f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/LinearOutSlowInInterpolator.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.animation; + +/** + * Interpolator corresponding to {@link android.R.interpolator#linear_out_slow_in}. + * + * Uses a lookup table for the Bezier curve from (0,0) to (1,1) with control points: + * P0 (0, 0) + * P1 (0, 0) + * P2 (0.2, 1.0) + * P3 (1.0, 1.0) + */ +public class LinearOutSlowInInterpolator extends LookupTableInterpolator { + + /** + * Lookup table values sampled with x at regular intervals between 0 and 1 for a total of + * 201 points. + */ + private static final float[] VALUES = new float[] { + 0.0000f, 0.0222f, 0.0424f, 0.0613f, 0.0793f, 0.0966f, 0.1132f, + 0.1293f, 0.1449f, 0.1600f, 0.1747f, 0.1890f, 0.2029f, 0.2165f, + 0.2298f, 0.2428f, 0.2555f, 0.2680f, 0.2802f, 0.2921f, 0.3038f, + 0.3153f, 0.3266f, 0.3377f, 0.3486f, 0.3592f, 0.3697f, 0.3801f, + 0.3902f, 0.4002f, 0.4100f, 0.4196f, 0.4291f, 0.4385f, 0.4477f, + 0.4567f, 0.4656f, 0.4744f, 0.4831f, 0.4916f, 0.5000f, 0.5083f, + 0.5164f, 0.5245f, 0.5324f, 0.5402f, 0.5479f, 0.5555f, 0.5629f, + 0.5703f, 0.5776f, 0.5847f, 0.5918f, 0.5988f, 0.6057f, 0.6124f, + 0.6191f, 0.6257f, 0.6322f, 0.6387f, 0.6450f, 0.6512f, 0.6574f, + 0.6635f, 0.6695f, 0.6754f, 0.6812f, 0.6870f, 0.6927f, 0.6983f, + 0.7038f, 0.7093f, 0.7147f, 0.7200f, 0.7252f, 0.7304f, 0.7355f, + 0.7406f, 0.7455f, 0.7504f, 0.7553f, 0.7600f, 0.7647f, 0.7694f, + 0.7740f, 0.7785f, 0.7829f, 0.7873f, 0.7917f, 0.7959f, 0.8002f, + 0.8043f, 0.8084f, 0.8125f, 0.8165f, 0.8204f, 0.8243f, 0.8281f, + 0.8319f, 0.8356f, 0.8392f, 0.8429f, 0.8464f, 0.8499f, 0.8534f, + 0.8568f, 0.8601f, 0.8634f, 0.8667f, 0.8699f, 0.8731f, 0.8762f, + 0.8792f, 0.8823f, 0.8852f, 0.8882f, 0.8910f, 0.8939f, 0.8967f, + 0.8994f, 0.9021f, 0.9048f, 0.9074f, 0.9100f, 0.9125f, 0.9150f, + 0.9174f, 0.9198f, 0.9222f, 0.9245f, 0.9268f, 0.9290f, 0.9312f, + 0.9334f, 0.9355f, 0.9376f, 0.9396f, 0.9416f, 0.9436f, 0.9455f, + 0.9474f, 0.9492f, 0.9510f, 0.9528f, 0.9545f, 0.9562f, 0.9579f, + 0.9595f, 0.9611f, 0.9627f, 0.9642f, 0.9657f, 0.9672f, 0.9686f, + 0.9700f, 0.9713f, 0.9726f, 0.9739f, 0.9752f, 0.9764f, 0.9776f, + 0.9787f, 0.9798f, 0.9809f, 0.9820f, 0.9830f, 0.9840f, 0.9849f, + 0.9859f, 0.9868f, 0.9876f, 0.9885f, 0.9893f, 0.9900f, 0.9908f, + 0.9915f, 0.9922f, 0.9928f, 0.9934f, 0.9940f, 0.9946f, 0.9951f, + 0.9956f, 0.9961f, 0.9966f, 0.9970f, 0.9974f, 0.9977f, 0.9981f, + 0.9984f, 0.9987f, 0.9989f, 0.9992f, 0.9994f, 0.9995f, 0.9997f, + 0.9998f, 0.9999f, 0.9999f, 1.0000f, 1.0000f + }; + + public LinearOutSlowInInterpolator() { + super(VALUES); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/LookupTableInterpolator.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/LookupTableInterpolator.java new file mode 100644 index 00000000000..c2341771303 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/LookupTableInterpolator.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.animation; + +import android.view.animation.Interpolator; + +/** + * An {@link Interpolator} that uses a lookup table to compute an interpolation based on a + * given input. + */ +abstract class LookupTableInterpolator implements Interpolator { + + private final float[] mValues; + private final float mStepSize; + + public LookupTableInterpolator(float[] values) { + mValues = values; + mStepSize = 1f / (mValues.length - 1); + } + + @Override + public float getInterpolation(float input) { + if (input >= 1.0f) { + return 1.0f; + } + if (input <= 0f) { + return 0f; + } + + // Calculate index - We use min with length - 2 to avoid IndexOutOfBoundsException when + // we lerp (linearly interpolate) in the return statement + int position = Math.min((int) (input * (mValues.length - 1)), mValues.length - 2); + + // Calculate values to account for small offsets as the lookup table has discrete values + float quantized = position * mStepSize; + float diff = input - quantized; + float weight = diff / mStepSize; + + // Linearly interpolate between the table values + return mValues[position] + weight * (mValues[position + 1] - mValues[position]); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompat.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompat.java new file mode 100644 index 00000000000..2e06132b3dd --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompat.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.animation; + +import android.graphics.Path; +import android.os.Build; +import android.view.animation.Interpolator; + +/** + * Helper for creating path-based {@link Interpolator} instances. On API 21 or newer, the + * platform implementation will be used and on older platforms a compatible alternative + * implementation will be used. + */ +public class PathInterpolatorCompat { + + private PathInterpolatorCompat() { + // prevent instantiation + } + + /** + * Create an {@link Interpolator} for an arbitrary {@link Path}. The {@link Path} + * must begin at {@code (0, 0)} and end at {@code (1, 1)}. The x-coordinate along the + * {@link Path} is the input value and the output is the y coordinate of the line at that + * point. This means that the Path must conform to a function {@code y = f(x)}. + *

    + * The {@link Path} must not have gaps in the x direction and must not + * loop back on itself such that there can be two points sharing the same x coordinate. + * + * @param path the {@link Path} to use to make the line representing the {@link Interpolator} + * @return the {@link Interpolator} representing the {@link Path} + */ + public static Interpolator create(Path path) { + if (Build.VERSION.SDK_INT >= 21) { + return PathInterpolatorCompatApi21.create(path); + } + return PathInterpolatorCompatBase.create(path); + } + + /** + * Create an {@link Interpolator} for a quadratic Bezier curve. The end points + * {@code (0, 0)} and {@code (1, 1)} are assumed. + * + * @param controlX the x coordinate of the quadratic Bezier control point + * @param controlY the y coordinate of the quadratic Bezier control point + * @return the {@link Interpolator} representing the quadratic Bezier curve + */ + public static Interpolator create(float controlX, float controlY) { + if (Build.VERSION.SDK_INT >= 21) { + return PathInterpolatorCompatApi21.create(controlX, controlY); + } + return PathInterpolatorCompatBase.create(controlX, controlY); + } + + /** + * Create an {@link Interpolator} for a cubic Bezier curve. The end points + * {@code (0, 0)} and {@code (1, 1)} are assumed. + * + * @param controlX1 the x coordinate of the first control point of the cubic Bezier + * @param controlY1 the y coordinate of the first control point of the cubic Bezier + * @param controlX2 the x coordinate of the second control point of the cubic Bezier + * @param controlY2 the y coordinate of the second control point of the cubic Bezier + * @return the {@link Interpolator} representing the cubic Bezier curve + */ + public static Interpolator create(float controlX1, float controlY1, + float controlX2, float controlY2) { + if (Build.VERSION.SDK_INT >= 21) { + return PathInterpolatorCompatApi21.create(controlX1, controlY1, controlX2, controlY2); + } + return PathInterpolatorCompatBase.create(controlX1, controlY1, controlX2, controlY2); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatApi21.java new file mode 100644 index 00000000000..3767f2734d9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatApi21.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.animation; + +import android.graphics.Path; +import android.view.animation.Interpolator; +import android.view.animation.PathInterpolator; + +/** + * API 21+ implementation for path interpolator compatibility. + */ +class PathInterpolatorCompatApi21 { + + private PathInterpolatorCompatApi21() { + // prevent instantiation + } + + public static Interpolator create(Path path) { + return new PathInterpolator(path); + } + + public static Interpolator create(float controlX, float controlY) { + return new PathInterpolator(controlX, controlY); + } + + public static Interpolator create(float controlX1, float controlY1, + float controlX2, float controlY2) { + return new PathInterpolator(controlX1, controlY1, controlX2, controlY2); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatBase.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatBase.java new file mode 100644 index 00000000000..9bd4c94d74d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorCompatBase.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.animation; + +import android.graphics.Path; +import android.view.animation.Interpolator; + +/** + * Base implementation for path interpolator compatibility. + */ +class PathInterpolatorCompatBase { + + private PathInterpolatorCompatBase() { + // prevent instantiation + } + + public static Interpolator create(Path path) { + return new PathInterpolatorDonut(path); + } + + public static Interpolator create(float controlX, float controlY) { + return new PathInterpolatorDonut(controlX, controlY); + } + + public static Interpolator create(float controlX1, float controlY1, + float controlX2, float controlY2) { + return new PathInterpolatorDonut(controlX1, controlY1, controlX2, controlY2); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorDonut.java b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorDonut.java new file mode 100644 index 00000000000..1240e1f0081 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/animation/PathInterpolatorDonut.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.view.animation; + +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.view.animation.Interpolator; + +/** + * A path interpolator implementation compatible with API 4+. + */ +class PathInterpolatorDonut implements Interpolator { + + /** + * Governs the accuracy of the approximation of the {@link Path}. + */ + private static final float PRECISION = 0.002f; + + private final float[] mX; + private final float[] mY; + + public PathInterpolatorDonut(Path path) { + final PathMeasure pathMeasure = new PathMeasure(path, false /* forceClosed */); + + final float pathLength = pathMeasure.getLength(); + final int numPoints = (int) (pathLength / PRECISION) + 1; + + mX = new float[numPoints]; + mY = new float[numPoints]; + + final float[] position = new float[2]; + for (int i = 0; i < numPoints; ++i) { + final float distance = (i * pathLength) / (numPoints - 1); + pathMeasure.getPosTan(distance, position, null /* tangent */); + + mX[i] = position[0]; + mY[i] = position[1]; + } + } + + public PathInterpolatorDonut(float controlX, float controlY) { + this(createQuad(controlX, controlY)); + } + + public PathInterpolatorDonut(float controlX1, float controlY1, + float controlX2, float controlY2) { + this(createCubic(controlX1, controlY1, controlX2, controlY2)); + } + + @Override + public float getInterpolation(float t) { + if (t <= 0.0f) { + return 0.0f; + } else if (t >= 1.0f) { + return 1.0f; + } + + // Do a binary search for the correct x to interpolate between. + int startIndex = 0; + int endIndex = mX.length - 1; + while (endIndex - startIndex > 1) { + int midIndex = (startIndex + endIndex) / 2; + if (t < mX[midIndex]) { + endIndex = midIndex; + } else { + startIndex = midIndex; + } + } + + final float xRange = mX[endIndex] - mX[startIndex]; + if (xRange == 0) { + return mY[startIndex]; + } + + final float tInRange = t - mX[startIndex]; + final float fraction = tInRange / xRange; + + final float startY = mY[startIndex]; + final float endY = mY[endIndex]; + + return startY + (fraction * (endY - startY)); + } + + private static Path createQuad(float controlX, float controlY) { + final Path path = new Path(); + path.moveTo(0.0f, 0.0f); + path.quadTo(controlX, controlY, 1.0f, 1.0f); + return path; + } + + private static Path createCubic(float controlX1, float controlY1, + float controlX2, float controlY2) { + final Path path = new Path(); + path.moveTo(0.0f, 0.0f); + path.cubicTo(controlX1, controlY1, controlX2, controlY2, 1.0f, 1.0f); + return path; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/view/package.html b/mobile/google-android-support-v4/src/android/support/v4/view/package.html new file mode 100644 index 00000000000..d80ef708b8a --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/view/package.html @@ -0,0 +1,11 @@ + + +Support android.util classes to assist with development of applications for +android API level 4 or later. The main features here are a variety of classes +for handling backwards compatibility with views (for example +{@link android.support.v4.view.MotionEventCompat} allows retrieving multi-touch +data if available), and a new +{@link android.support.v4.view.ViewPager} widget (which at some point should be moved over +to the widget package). + + diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/AutoScrollHelper.java b/mobile/google-android-support-v4/src/android/support/v4/widget/AutoScrollHelper.java new file mode 100644 index 00000000000..32e2d45e10f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/AutoScrollHelper.java @@ -0,0 +1,867 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.res.Resources; +import android.os.SystemClock; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.util.DisplayMetrics; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + +/** + * AutoScrollHelper is a utility class for adding automatic edge-triggered + * scrolling to Views. + *

    + * Note: Implementing classes are responsible for overriding the + * {@link #scrollTargetBy}, {@link #canTargetScrollHorizontally}, and + * {@link #canTargetScrollVertically} methods. See + * {@link ListViewAutoScrollHelper} for a {@link android.widget.ListView} + * -specific implementation. + *

    + *

    Activation

    Automatic scrolling starts when the user touches within + * an activation area. By default, activation areas are defined as the top, + * left, right, and bottom 20% of the host view's total area. Touching within + * the top activation area scrolls up, left scrolls to the left, and so on. + *

    + * As the user touches closer to the extreme edge of the activation area, + * scrolling accelerates up to a maximum velocity. When using the default edge + * type, {@link #EDGE_TYPE_INSIDE_EXTEND}, moving outside of the view bounds + * will scroll at the maximum velocity. + *

    + * The following activation properties may be configured: + *

      + *
    • Delay after entering activation area before auto-scrolling begins, see + * {@link #setActivationDelay}. Default value is + * {@link ViewConfiguration#getTapTimeout()} to avoid conflicting with taps. + *
    • Location of activation areas, see {@link #setEdgeType}. Default value is + * {@link #EDGE_TYPE_INSIDE_EXTEND}. + *
    • Size of activation areas relative to view size, see + * {@link #setRelativeEdges}. Default value is 20% for both vertical and + * horizontal edges. + *
    • Maximum size used to constrain relative size, see + * {@link #setMaximumEdges}. Default value is {@link #NO_MAX}. + *
    + *

    Scrolling

    When automatic scrolling is active, the helper will + * repeatedly call {@link #scrollTargetBy} to apply new scrolling offsets. + *

    + * The following scrolling properties may be configured: + *

      + *
    • Acceleration ramp-up duration, see {@link #setRampUpDuration}. Default + * value is 500 milliseconds. + *
    • Acceleration ramp-down duration, see {@link #setRampDownDuration}. + * Default value is 500 milliseconds. + *
    • Target velocity relative to view size, see {@link #setRelativeVelocity}. + * Default value is 100% per second for both vertical and horizontal. + *
    • Minimum velocity used to constrain relative velocity, see + * {@link #setMinimumVelocity}. When set, scrolling will accelerate to the + * larger of either this value or the relative target value. Default value is + * approximately 5 centimeters or 315 dips per second. + *
    • Maximum velocity used to constrain relative velocity, see + * {@link #setMaximumVelocity}. Default value is approximately 25 centimeters or + * 1575 dips per second. + *
    + */ +public abstract class AutoScrollHelper implements View.OnTouchListener { + /** + * Constant passed to {@link #setRelativeEdges} or + * {@link #setRelativeVelocity}. Using this value ensures that the computed + * relative value is ignored and the absolute maximum value is always used. + */ + public static final float RELATIVE_UNSPECIFIED = 0; + + /** + * Constant passed to {@link #setMaximumEdges}, {@link #setMaximumVelocity}, + * or {@link #setMinimumVelocity}. Using this value ensures that the + * computed relative value is always used without constraining to a + * particular minimum or maximum value. + */ + public static final float NO_MAX = Float.MAX_VALUE; + + /** + * Constant passed to {@link #setMaximumEdges}, or + * {@link #setMaximumVelocity}, or {@link #setMinimumVelocity}. Using this + * value ensures that the computed relative value is always used without + * constraining to a particular minimum or maximum value. + */ + public static final float NO_MIN = 0; + + /** + * Edge type that specifies an activation area starting at the view bounds + * and extending inward. Moving outside the view bounds will stop scrolling. + * + * @see #setEdgeType + */ + public static final int EDGE_TYPE_INSIDE = 0; + + /** + * Edge type that specifies an activation area starting at the view bounds + * and extending inward. After activation begins, moving outside the view + * bounds will continue scrolling. + * + * @see #setEdgeType + */ + public static final int EDGE_TYPE_INSIDE_EXTEND = 1; + + /** + * Edge type that specifies an activation area starting at the view bounds + * and extending outward. Moving inside the view bounds will stop scrolling. + * + * @see #setEdgeType + */ + public static final int EDGE_TYPE_OUTSIDE = 2; + + private static final int HORIZONTAL = 0; + private static final int VERTICAL = 1; + + /** Scroller used to control acceleration toward maximum velocity. */ + private final ClampedScroller mScroller = new ClampedScroller(); + + /** Interpolator used to scale velocity with touch position. */ + private final Interpolator mEdgeInterpolator = new AccelerateInterpolator(); + + /** The view to auto-scroll. Might not be the source of touch events. */ + private final View mTarget; + + /** Runnable used to animate scrolling. */ + private Runnable mRunnable; + + /** Edge insets used to activate auto-scrolling. */ + private float[] mRelativeEdges = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED }; + + /** Clamping values for edge insets used to activate auto-scrolling. */ + private float[] mMaximumEdges = new float[] { NO_MAX, NO_MAX }; + + /** The type of edge being used. */ + private int mEdgeType; + + /** Delay after entering an activation edge before auto-scrolling begins. */ + private int mActivationDelay; + + /** Relative scrolling velocity at maximum edge distance. */ + private float[] mRelativeVelocity = new float[] { RELATIVE_UNSPECIFIED, RELATIVE_UNSPECIFIED }; + + /** Clamping values used for scrolling velocity. */ + private float[] mMinimumVelocity = new float[] { NO_MIN, NO_MIN }; + + /** Clamping values used for scrolling velocity. */ + private float[] mMaximumVelocity = new float[] { NO_MAX, NO_MAX }; + + /** Whether to start activation immediately. */ + private boolean mAlreadyDelayed; + + /** Whether to reset the scroller start time on the next animation. */ + private boolean mNeedsReset; + + /** Whether to send a cancel motion event to the target view. */ + private boolean mNeedsCancel; + + /** Whether the auto-scroller is actively scrolling. */ + private boolean mAnimating; + + /** Whether the auto-scroller is enabled. */ + private boolean mEnabled; + + /** Whether the auto-scroller consumes events when scrolling. */ + private boolean mExclusive; + + // Default values. + private static final int DEFAULT_EDGE_TYPE = EDGE_TYPE_INSIDE_EXTEND; + private static final int DEFAULT_MINIMUM_VELOCITY_DIPS = 315; + private static final int DEFAULT_MAXIMUM_VELOCITY_DIPS = 1575; + private static final float DEFAULT_MAXIMUM_EDGE = NO_MAX; + private static final float DEFAULT_RELATIVE_EDGE = 0.2f; + private static final float DEFAULT_RELATIVE_VELOCITY = 1f; + private static final int DEFAULT_ACTIVATION_DELAY = ViewConfiguration.getTapTimeout(); + private static final int DEFAULT_RAMP_UP_DURATION = 500; + private static final int DEFAULT_RAMP_DOWN_DURATION = 500; + + /** + * Creates a new helper for scrolling the specified target view. + *

    + * The resulting helper may be configured by chaining setter calls and + * should be set as a touch listener on the target view. + *

    + * By default, the helper is disabled and will not respond to touch events + * until it is enabled using {@link #setEnabled}. + * + * @param target The view to automatically scroll. + */ + public AutoScrollHelper(View target) { + mTarget = target; + + final DisplayMetrics metrics = Resources.getSystem().getDisplayMetrics(); + final int maxVelocity = (int) (DEFAULT_MAXIMUM_VELOCITY_DIPS * metrics.density + 0.5f); + final int minVelocity = (int) (DEFAULT_MINIMUM_VELOCITY_DIPS * metrics.density + 0.5f); + setMaximumVelocity(maxVelocity, maxVelocity); + setMinimumVelocity(minVelocity, minVelocity); + + setEdgeType(DEFAULT_EDGE_TYPE); + setMaximumEdges(DEFAULT_MAXIMUM_EDGE, DEFAULT_MAXIMUM_EDGE); + setRelativeEdges(DEFAULT_RELATIVE_EDGE, DEFAULT_RELATIVE_EDGE); + setRelativeVelocity(DEFAULT_RELATIVE_VELOCITY, DEFAULT_RELATIVE_VELOCITY); + setActivationDelay(DEFAULT_ACTIVATION_DELAY); + setRampUpDuration(DEFAULT_RAMP_UP_DURATION); + setRampDownDuration(DEFAULT_RAMP_DOWN_DURATION); + } + + /** + * Sets whether the scroll helper is enabled and should respond to touch + * events. + * + * @param enabled Whether the scroll helper is enabled. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setEnabled(boolean enabled) { + if (mEnabled && !enabled) { + requestStop(); + } + + mEnabled = enabled; + return this; + } + + /** + * @return True if this helper is enabled and responding to touch events. + */ + public boolean isEnabled() { + return mEnabled; + } + + /** + * Enables or disables exclusive handling of touch events during scrolling. + * By default, exclusive handling is disabled and the target view receives + * all touch events. + *

    + * When enabled, {@link #onTouch} will return true if the helper is + * currently scrolling and false otherwise. + * + * @param exclusive True to exclusively handle touch events during scrolling, + * false to allow the target view to receive all touch events. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setExclusive(boolean exclusive) { + mExclusive = exclusive; + return this; + } + + /** + * Indicates whether the scroll helper handles touch events exclusively + * during scrolling. + * + * @return True if exclusive handling of touch events during scrolling is + * enabled, false otherwise. + * @see #setExclusive(boolean) + */ + public boolean isExclusive() { + return mExclusive; + } + + /** + * Sets the absolute maximum scrolling velocity. + *

    + * If relative velocity is not specified, scrolling will always reach the + * same maximum velocity. If both relative and maximum velocities are + * specified, the maximum velocity will be used to clamp the calculated + * relative velocity. + * + * @param horizontalMax The maximum horizontal scrolling velocity, or + * {@link #NO_MAX} to leave the relative value unconstrained. + * @param verticalMax The maximum vertical scrolling velocity, or + * {@link #NO_MAX} to leave the relative value unconstrained. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setMaximumVelocity(float horizontalMax, float verticalMax) { + mMaximumVelocity[HORIZONTAL] = horizontalMax / 1000f; + mMaximumVelocity[VERTICAL] = verticalMax / 1000f; + return this; + } + + /** + * Sets the absolute minimum scrolling velocity. + *

    + * If both relative and minimum velocities are specified, the minimum + * velocity will be used to clamp the calculated relative velocity. + * + * @param horizontalMin The minimum horizontal scrolling velocity, or + * {@link #NO_MIN} to leave the relative value unconstrained. + * @param verticalMin The minimum vertical scrolling velocity, or + * {@link #NO_MIN} to leave the relative value unconstrained. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setMinimumVelocity(float horizontalMin, float verticalMin) { + mMinimumVelocity[HORIZONTAL] = horizontalMin / 1000f; + mMinimumVelocity[VERTICAL] = verticalMin / 1000f; + return this; + } + + /** + * Sets the target scrolling velocity relative to the host view's + * dimensions. + *

    + * If both relative and maximum velocities are specified, the maximum + * velocity will be used to clamp the calculated relative velocity. + * + * @param horizontal The target horizontal velocity as a fraction of the + * host view width per second, or {@link #RELATIVE_UNSPECIFIED} + * to ignore. + * @param vertical The target vertical velocity as a fraction of the host + * view height per second, or {@link #RELATIVE_UNSPECIFIED} to + * ignore. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRelativeVelocity(float horizontal, float vertical) { + mRelativeVelocity[HORIZONTAL] = horizontal / 1000f; + mRelativeVelocity[VERTICAL] = vertical / 1000f; + return this; + } + + /** + * Sets the activation edge type, one of: + *

      + *
    • {@link #EDGE_TYPE_INSIDE} for edges that respond to touches inside + * the bounds of the host view. If touch moves outside the bounds, scrolling + * will stop. + *
    • {@link #EDGE_TYPE_INSIDE_EXTEND} for inside edges that continued to + * scroll when touch moves outside the bounds of the host view. + *
    • {@link #EDGE_TYPE_OUTSIDE} for edges that only respond to touches + * that move outside the bounds of the host view. + *
    + * + * @param type The type of edge to use. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setEdgeType(int type) { + mEdgeType = type; + return this; + } + + /** + * Sets the activation edge size relative to the host view's dimensions. + *

    + * If both relative and maximum edges are specified, the maximum edge will + * be used to constrain the calculated relative edge size. + * + * @param horizontal The horizontal edge size as a fraction of the host view + * width, or {@link #RELATIVE_UNSPECIFIED} to always use the + * maximum value. + * @param vertical The vertical edge size as a fraction of the host view + * height, or {@link #RELATIVE_UNSPECIFIED} to always use the + * maximum value. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRelativeEdges(float horizontal, float vertical) { + mRelativeEdges[HORIZONTAL] = horizontal; + mRelativeEdges[VERTICAL] = vertical; + return this; + } + + /** + * Sets the absolute maximum edge size. + *

    + * If relative edge size is not specified, activation edges will always be + * the maximum edge size. If both relative and maximum edges are specified, + * the maximum edge will be used to constrain the calculated relative edge + * size. + * + * @param horizontalMax The maximum horizontal edge size in pixels, or + * {@link #NO_MAX} to use the unconstrained calculated relative + * value. + * @param verticalMax The maximum vertical edge size in pixels, or + * {@link #NO_MAX} to use the unconstrained calculated relative + * value. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setMaximumEdges(float horizontalMax, float verticalMax) { + mMaximumEdges[HORIZONTAL] = horizontalMax; + mMaximumEdges[VERTICAL] = verticalMax; + return this; + } + + /** + * Sets the delay after entering an activation edge before activation of + * auto-scrolling. By default, the activation delay is set to + * {@link ViewConfiguration#getTapTimeout()}. + *

    + * Specifying a delay of zero will start auto-scrolling immediately after + * the touch position enters an activation edge. + * + * @param delayMillis The activation delay in milliseconds. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setActivationDelay(int delayMillis) { + mActivationDelay = delayMillis; + return this; + } + + /** + * Sets the amount of time after activation of auto-scrolling that is takes + * to reach target velocity for the current touch position. + *

    + * Specifying a duration greater than zero prevents sudden jumps in + * velocity. + * + * @param durationMillis The ramp-up duration in milliseconds. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRampUpDuration(int durationMillis) { + mScroller.setRampUpDuration(durationMillis); + return this; + } + + /** + * Sets the amount of time after de-activation of auto-scrolling that is + * takes to slow to a stop. + *

    + * Specifying a duration greater than zero prevents sudden jumps in + * velocity. + * + * @param durationMillis The ramp-down duration in milliseconds. + * @return The scroll helper, which may used to chain setter calls. + */ + public AutoScrollHelper setRampDownDuration(int durationMillis) { + mScroller.setRampDownDuration(durationMillis); + return this; + } + + /** + * Handles touch events by activating automatic scrolling, adjusting scroll + * velocity, or stopping. + *

    + * If {@link #isExclusive()} is false, always returns false so that + * the host view may handle touch events. Otherwise, returns true when + * automatic scrolling is active and false otherwise. + */ + @Override + public boolean onTouch(View v, MotionEvent event) { + if (!mEnabled) { + return false; + } + + final int action = MotionEventCompat.getActionMasked(event); + switch (action) { + case MotionEvent.ACTION_DOWN: + mNeedsCancel = true; + mAlreadyDelayed = false; + // $FALL-THROUGH$ + case MotionEvent.ACTION_MOVE: + final float xTargetVelocity = computeTargetVelocity( + HORIZONTAL, event.getX(), v.getWidth(), mTarget.getWidth()); + final float yTargetVelocity = computeTargetVelocity( + VERTICAL, event.getY(), v.getHeight(), mTarget.getHeight()); + mScroller.setTargetVelocity(xTargetVelocity, yTargetVelocity); + + // If the auto scroller was not previously active, but it should + // be, then update the state and start animations. + if (!mAnimating && shouldAnimate()) { + startAnimating(); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + requestStop(); + break; + } + + return mExclusive && mAnimating; + } + + /** + * @return whether the target is able to scroll in the requested direction + */ + private boolean shouldAnimate() { + final ClampedScroller scroller = mScroller; + final int verticalDirection = scroller.getVerticalDirection(); + final int horizontalDirection = scroller.getHorizontalDirection(); + + return verticalDirection != 0 && canTargetScrollVertically(verticalDirection) + || horizontalDirection != 0 && canTargetScrollHorizontally(horizontalDirection); + } + + /** + * Starts the scroll animation. + */ + private void startAnimating() { + if (mRunnable == null) { + mRunnable = new ScrollAnimationRunnable(); + } + + mAnimating = true; + mNeedsReset = true; + + if (!mAlreadyDelayed && mActivationDelay > 0) { + ViewCompat.postOnAnimationDelayed(mTarget, mRunnable, mActivationDelay); + } else { + mRunnable.run(); + } + + // If we start animating again before the user lifts their finger, we + // already know it's not a tap and don't need an activation delay. + mAlreadyDelayed = true; + } + + /** + * Requests that the scroll animation slow to a stop. If there is an + * activation delay, this may occur between posting the animation and + * actually running it. + */ + private void requestStop() { + if (mNeedsReset) { + // The animation has been posted, but hasn't run yet. Manually + // stopping animation will prevent it from running. + mAnimating = false; + } else { + mScroller.requestStop(); + } + } + + private float computeTargetVelocity( + int direction, float coordinate, float srcSize, float dstSize) { + final float relativeEdge = mRelativeEdges[direction]; + final float maximumEdge = mMaximumEdges[direction]; + final float value = getEdgeValue(relativeEdge, srcSize, maximumEdge, coordinate); + if (value == 0) { + // The edge in this direction is not activated. + return 0; + } + + final float relativeVelocity = mRelativeVelocity[direction]; + final float minimumVelocity = mMinimumVelocity[direction]; + final float maximumVelocity = mMaximumVelocity[direction]; + final float targetVelocity = relativeVelocity * dstSize; + + // Target velocity is adjusted for interpolated edge position, then + // clamped to the minimum and maximum values. Later, this value will be + // adjusted for time-based acceleration. + if (value > 0) { + return constrain(value * targetVelocity, minimumVelocity, maximumVelocity); + } else { + return -constrain(-value * targetVelocity, minimumVelocity, maximumVelocity); + } + } + + /** + * Override this method to scroll the target view by the specified number of + * pixels. + * + * @param deltaX The number of pixels to scroll by horizontally. + * @param deltaY The number of pixels to scroll by vertically. + */ + public abstract void scrollTargetBy(int deltaX, int deltaY); + + /** + * Override this method to return whether the target view can be scrolled + * horizontally in a certain direction. + * + * @param direction Negative to check scrolling left, positive to check + * scrolling right. + * @return true if the target view is able to horizontally scroll in the + * specified direction. + */ + public abstract boolean canTargetScrollHorizontally(int direction); + + /** + * Override this method to return whether the target view can be scrolled + * vertically in a certain direction. + * + * @param direction Negative to check scrolling up, positive to check + * scrolling down. + * @return true if the target view is able to vertically scroll in the + * specified direction. + */ + public abstract boolean canTargetScrollVertically(int direction); + + /** + * Returns the interpolated position of a touch point relative to an edge + * defined by its relative inset, its maximum absolute inset, and the edge + * interpolator. + * + * @param relativeValue The size of the inset relative to the total size. + * @param size Total size. + * @param maxValue The maximum size of the inset, used to clamp (relative * + * total). + * @param current Touch position within within the total size. + * @return Interpolated value of the touch position within the edge. + */ + private float getEdgeValue(float relativeValue, float size, float maxValue, float current) { + // For now, leading and trailing edges are always the same size. + final float edgeSize = constrain(relativeValue * size, NO_MIN, maxValue); + final float valueLeading = constrainEdgeValue(current, edgeSize); + final float valueTrailing = constrainEdgeValue(size - current, edgeSize); + final float value = (valueTrailing - valueLeading); + final float interpolated; + if (value < 0) { + interpolated = -mEdgeInterpolator.getInterpolation(-value); + } else if (value > 0) { + interpolated = mEdgeInterpolator.getInterpolation(value); + } else { + return 0; + } + + return constrain(interpolated, -1, 1); + } + + private float constrainEdgeValue(float current, float leading) { + if (leading == 0) { + return 0; + } + + switch (mEdgeType) { + case EDGE_TYPE_INSIDE: + case EDGE_TYPE_INSIDE_EXTEND: + if (current < leading) { + if (current >= 0) { + // Movement up to the edge is scaled. + return 1f - current / leading; + } else if (mAnimating && (mEdgeType == EDGE_TYPE_INSIDE_EXTEND)) { + // Movement beyond the edge is always maximum. + return 1f; + } + } + break; + case EDGE_TYPE_OUTSIDE: + if (current < 0) { + // Movement beyond the edge is scaled. + return current / -leading; + } + break; + } + + return 0; + } + + private static int constrain(int value, int min, int max) { + if (value > max) { + return max; + } else if (value < min) { + return min; + } else { + return value; + } + } + + private static float constrain(float value, float min, float max) { + if (value > max) { + return max; + } else if (value < min) { + return min; + } else { + return value; + } + } + + /** + * Sends a {@link MotionEvent#ACTION_CANCEL} event to the target view, + * canceling any ongoing touch events. + */ + private void cancelTargetTouch() { + final long eventTime = SystemClock.uptimeMillis(); + final MotionEvent cancel = MotionEvent.obtain( + eventTime, eventTime, MotionEvent.ACTION_CANCEL, 0, 0, 0); + mTarget.onTouchEvent(cancel); + cancel.recycle(); + } + + private class ScrollAnimationRunnable implements Runnable { + @Override + public void run() { + if (!mAnimating) { + return; + } + + if (mNeedsReset) { + mNeedsReset = false; + mScroller.start(); + } + + final ClampedScroller scroller = mScroller; + if (scroller.isFinished() || !shouldAnimate()) { + mAnimating = false; + return; + } + + if (mNeedsCancel) { + mNeedsCancel = false; + cancelTargetTouch(); + } + + scroller.computeScrollDelta(); + + final int deltaX = scroller.getDeltaX(); + final int deltaY = scroller.getDeltaY(); + scrollTargetBy(deltaX, deltaY); + + // Keep going until the scroller has permanently stopped. + ViewCompat.postOnAnimation(mTarget, this); + } + } + + /** + * Scroller whose velocity follows the curve of an {@link Interpolator} and + * is clamped to the interpolated 0f value before starting and the + * interpolated 1f value after a specified duration. + */ + private static class ClampedScroller { + private int mRampUpDuration; + private int mRampDownDuration; + private float mTargetVelocityX; + private float mTargetVelocityY; + + private long mStartTime; + + private long mDeltaTime; + private int mDeltaX; + private int mDeltaY; + + private long mStopTime; + private float mStopValue; + private int mEffectiveRampDown; + + /** + * Creates a new ramp-up scroller that reaches full velocity after a + * specified duration. + */ + public ClampedScroller() { + mStartTime = Long.MIN_VALUE; + mStopTime = -1; + mDeltaTime = 0; + mDeltaX = 0; + mDeltaY = 0; + } + + public void setRampUpDuration(int durationMillis) { + mRampUpDuration = durationMillis; + } + + public void setRampDownDuration(int durationMillis) { + mRampDownDuration = durationMillis; + } + + /** + * Starts the scroller at the current animation time. + */ + public void start() { + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mStopTime = -1; + mDeltaTime = mStartTime; + mStopValue = 0.5f; + mDeltaX = 0; + mDeltaY = 0; + } + + /** + * Stops the scroller at the current animation time. + */ + public void requestStop() { + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + mEffectiveRampDown = constrain((int) (currentTime - mStartTime), 0, mRampDownDuration); + mStopValue = getValueAt(currentTime); + mStopTime = currentTime; + } + + public boolean isFinished() { + return mStopTime > 0 + && AnimationUtils.currentAnimationTimeMillis() > mStopTime + mEffectiveRampDown; + } + + private float getValueAt(long currentTime) { + if (currentTime < mStartTime) { + return 0f; + } else if (mStopTime < 0 || currentTime < mStopTime) { + final long elapsedSinceStart = currentTime - mStartTime; + return 0.5f * constrain(elapsedSinceStart / (float) mRampUpDuration, 0, 1); + } else { + final long elapsedSinceEnd = currentTime - mStopTime; + return (1 - mStopValue) + mStopValue + * constrain(elapsedSinceEnd / (float) mEffectiveRampDown, 0, 1); + } + } + + /** + * Interpolates the value along a parabolic curve corresponding to the equation + * y = -4x * (x-1). + * + * @param value The value to interpolate, between 0 and 1. + * @return the interpolated value, between 0 and 1. + */ + private float interpolateValue(float value) { + return -4 * value * value + 4 * value; + } + + /** + * Computes the current scroll deltas. This usually only be called after + * starting the scroller with {@link #start()}. + * + * @see #getDeltaX() + * @see #getDeltaY() + */ + public void computeScrollDelta() { + if (mDeltaTime == 0) { + throw new RuntimeException("Cannot compute scroll delta before calling start()"); + } + + final long currentTime = AnimationUtils.currentAnimationTimeMillis(); + final float value = getValueAt(currentTime); + final float scale = interpolateValue(value); + final long elapsedSinceDelta = currentTime - mDeltaTime; + + mDeltaTime = currentTime; + mDeltaX = (int) (elapsedSinceDelta * scale * mTargetVelocityX); + mDeltaY = (int) (elapsedSinceDelta * scale * mTargetVelocityY); + } + + /** + * Sets the target velocity for this scroller. + * + * @param x The target X velocity in pixels per millisecond. + * @param y The target Y velocity in pixels per millisecond. + */ + public void setTargetVelocity(float x, float y) { + mTargetVelocityX = x; + mTargetVelocityY = y; + } + + public int getHorizontalDirection() { + return (int) (mTargetVelocityX / Math.abs(mTargetVelocityX)); + } + + public int getVerticalDirection() { + return (int) (mTargetVelocityY / Math.abs(mTargetVelocityY)); + } + + /** + * The distance traveled in the X-coordinate computed by the last call + * to {@link #computeScrollDelta()}. + */ + public int getDeltaX() { + return mDeltaX; + } + + /** + * The distance traveled in the Y-coordinate computed by the last call + * to {@link #computeScrollDelta()}. + */ + public int getDeltaY() { + return mDeltaY; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/CircleImageView.java b/mobile/google-android-support-v4/src/android/support/v4/widget/CircleImageView.java new file mode 100644 index 00000000000..246ffa746f3 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/CircleImageView.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.RadialGradient; +import android.graphics.Shader; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.OvalShape; +import android.support.v4.view.ViewCompat; +import android.view.animation.Animation; +import android.widget.ImageView; + +/** + * Private class created to work around issues with AnimationListeners being + * called before the animation is actually complete and support shadows on older + * platforms. + * + * @hide + */ +class CircleImageView extends ImageView { + + private static final int KEY_SHADOW_COLOR = 0x1E000000; + private static final int FILL_SHADOW_COLOR = 0x3D000000; + // PX + private static final float X_OFFSET = 0f; + private static final float Y_OFFSET = 1.75f; + private static final float SHADOW_RADIUS = 3.5f; + private static final int SHADOW_ELEVATION = 4; + + private Animation.AnimationListener mListener; + private int mShadowRadius; + + public CircleImageView(Context context, int color, final float radius) { + super(context); + final float density = getContext().getResources().getDisplayMetrics().density; + final int diameter = (int) (radius * density * 2); + final int shadowYOffset = (int) (density * Y_OFFSET); + final int shadowXOffset = (int) (density * X_OFFSET); + + mShadowRadius = (int) (density * SHADOW_RADIUS); + + ShapeDrawable circle; + if (elevationSupported()) { + circle = new ShapeDrawable(new OvalShape()); + ViewCompat.setElevation(this, SHADOW_ELEVATION * density); + } else { + OvalShape oval = new OvalShadow(mShadowRadius, diameter); + circle = new ShapeDrawable(oval); + ViewCompat.setLayerType(this, ViewCompat.LAYER_TYPE_SOFTWARE, circle.getPaint()); + circle.getPaint().setShadowLayer(mShadowRadius, shadowXOffset, shadowYOffset, + KEY_SHADOW_COLOR); + final int padding = mShadowRadius; + // set padding so the inner image sits correctly within the shadow. + setPadding(padding, padding, padding, padding); + } + circle.getPaint().setColor(color); + setBackgroundDrawable(circle); + } + + private boolean elevationSupported() { + return android.os.Build.VERSION.SDK_INT >= 21; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (!elevationSupported()) { + setMeasuredDimension(getMeasuredWidth() + mShadowRadius*2, getMeasuredHeight() + + mShadowRadius*2); + } + } + + public void setAnimationListener(Animation.AnimationListener listener) { + mListener = listener; + } + + @Override + public void onAnimationStart() { + super.onAnimationStart(); + if (mListener != null) { + mListener.onAnimationStart(getAnimation()); + } + } + + @Override + public void onAnimationEnd() { + super.onAnimationEnd(); + if (mListener != null) { + mListener.onAnimationEnd(getAnimation()); + } + } + + /** + * Update the background color of the circle image view. + * + * @param colorRes Id of a color resource. + */ + public void setBackgroundColorRes(int colorRes) { + setBackgroundColor(getContext().getResources().getColor(colorRes)); + } + + @Override + public void setBackgroundColor(int color) { + if (getBackground() instanceof ShapeDrawable) { + ((ShapeDrawable) getBackground()).getPaint().setColor(color); + } + } + + private class OvalShadow extends OvalShape { + private RadialGradient mRadialGradient; + private Paint mShadowPaint; + private int mCircleDiameter; + + public OvalShadow(int shadowRadius, int circleDiameter) { + super(); + mShadowPaint = new Paint(); + mShadowRadius = shadowRadius; + mCircleDiameter = circleDiameter; + mRadialGradient = new RadialGradient(mCircleDiameter / 2, mCircleDiameter / 2, + mShadowRadius, new int[] { + FILL_SHADOW_COLOR, Color.TRANSPARENT + }, null, Shader.TileMode.CLAMP); + mShadowPaint.setShader(mRadialGradient); + } + + @Override + public void draw(Canvas canvas, Paint paint) { + final int viewWidth = CircleImageView.this.getWidth(); + final int viewHeight = CircleImageView.this.getHeight(); + canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2 + mShadowRadius), + mShadowPaint); + canvas.drawCircle(viewWidth / 2, viewHeight / 2, (mCircleDiameter / 2), paint); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ContentLoadingProgressBar.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ContentLoadingProgressBar.java new file mode 100644 index 00000000000..3335b9f9be1 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ContentLoadingProgressBar.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; +import android.widget.ProgressBar; + +/** + * ContentLoadingProgressBar implements a ProgressBar that waits a minimum time to be + * dismissed before showing. Once visible, the progress bar will be visible for + * a minimum amount of time to avoid "flashes" in the UI when an event could take + * a largely variable time to complete (from none, to a user perceivable amount) + */ +public class ContentLoadingProgressBar extends ProgressBar { + private static final int MIN_SHOW_TIME = 500; // ms + private static final int MIN_DELAY = 500; // ms + + private long mStartTime = -1; + + private boolean mPostedHide = false; + + private boolean mPostedShow = false; + + private boolean mDismissed = false; + + private final Runnable mDelayedHide = new Runnable() { + + @Override + public void run() { + mPostedHide = false; + mStartTime = -1; + setVisibility(View.GONE); + } + }; + + private final Runnable mDelayedShow = new Runnable() { + + @Override + public void run() { + mPostedShow = false; + if (!mDismissed) { + mStartTime = System.currentTimeMillis(); + setVisibility(View.VISIBLE); + } + } + }; + + public ContentLoadingProgressBar(Context context) { + this(context, null); + } + + public ContentLoadingProgressBar(Context context, AttributeSet attrs) { + super(context, attrs, 0); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + removeCallbacks(); + } + + @Override + public void onDetachedFromWindow() { + super.onDetachedFromWindow(); + removeCallbacks(); + } + + private void removeCallbacks() { + removeCallbacks(mDelayedHide); + removeCallbacks(mDelayedShow); + } + + /** + * Hide the progress view if it is visible. The progress view will not be + * hidden until it has been shown for at least a minimum show time. If the + * progress view was not yet visible, cancels showing the progress view. + */ + public void hide() { + mDismissed = true; + removeCallbacks(mDelayedShow); + long diff = System.currentTimeMillis() - mStartTime; + if (diff >= MIN_SHOW_TIME || mStartTime == -1) { + // The progress spinner has been shown long enough + // OR was not shown yet. If it wasn't shown yet, + // it will just never be shown. + setVisibility(View.GONE); + } else { + // The progress spinner is shown, but not long enough, + // so put a delayed message in to hide it when its been + // shown long enough. + if (!mPostedHide) { + postDelayed(mDelayedHide, MIN_SHOW_TIME - diff); + mPostedHide = true; + } + } + } + + /** + * Show the progress view after waiting for a minimum delay. If + * during that time, hide() is called, the view is never made visible. + */ + public void show() { + // Reset the start time. + mStartTime = -1; + mDismissed = false; + removeCallbacks(mDelayedHide); + if (!mPostedShow) { + postDelayed(mDelayedShow, MIN_DELAY); + mPostedShow = true; + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/CursorAdapter.java b/mobile/google-android-support-v4/src/android/support/v4/widget/CursorAdapter.java new file mode 100644 index 00000000000..96d9cfa25c0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/CursorAdapter.java @@ -0,0 +1,483 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.database.ContentObserver; +import android.database.Cursor; +import android.database.DataSetObserver; +import android.os.Handler; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.Filter; +import android.widget.FilterQueryProvider; +import android.widget.Filterable; + +/** + * Static library support version of the framework's {@link android.widget.CursorAdapter}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public abstract class CursorAdapter extends BaseAdapter implements Filterable, + CursorFilter.CursorFilterClient { + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected boolean mDataValid; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected boolean mAutoRequery; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected Cursor mCursor; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected Context mContext; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int mRowIDColumn; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected ChangeObserver mChangeObserver; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected DataSetObserver mDataSetObserver; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected CursorFilter mCursorFilter; + /** + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected FilterQueryProvider mFilterQueryProvider; + + /** + * If set the adapter will call requery() on the cursor whenever a content change + * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER}. + * + * @deprecated This option is discouraged, as it results in Cursor queries + * being performed on the application's UI thread and thus can cause poor + * responsiveness or even Application Not Responding errors. As an alternative, + * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. + */ + @Deprecated + public static final int FLAG_AUTO_REQUERY = 0x01; + + /** + * If set the adapter will register a content observer on the cursor and will call + * {@link #onContentChanged()} when a notification comes in. Be careful when + * using this flag: you will need to unset the current Cursor from the adapter + * to avoid leaks due to its registered observers. This flag is not needed + * when using a CursorAdapter with a + * {@link android.content.CursorLoader}. + */ + public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02; + + /** + * Constructor that always enables auto-requery. + * + * @deprecated This option is discouraged, as it results in Cursor queries + * being performed on the application's UI thread and thus can cause poor + * responsiveness or even Application Not Responding errors. As an alternative, + * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. + * + * @param c The cursor from which to get the data. + * @param context The context + */ + @Deprecated + public CursorAdapter(Context context, Cursor c) { + init(context, c, FLAG_AUTO_REQUERY); + } + + /** + * Constructor that allows control over auto-requery. It is recommended + * you not use this, but instead {@link #CursorAdapter(Context, Cursor, int)}. + * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER} + * will always be set. + * + * @param c The cursor from which to get the data. + * @param context The context + * @param autoRequery If true the adapter will call requery() on the + * cursor whenever it changes so the most recent + * data is always displayed. Using true here is discouraged. + */ + public CursorAdapter(Context context, Cursor c, boolean autoRequery) { + init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER); + } + + /** + * Recommended constructor. + * + * @param c The cursor from which to get the data. + * @param context The context + * @param flags Flags used to determine the behavior of the adapter; may + * be any combination of {@link #FLAG_AUTO_REQUERY} and + * {@link #FLAG_REGISTER_CONTENT_OBSERVER}. + */ + public CursorAdapter(Context context, Cursor c, int flags) { + init(context, c, flags); + } + + /** + * @deprecated Don't use this, use the normal constructor. This will + * be removed in the future. + */ + @Deprecated + protected void init(Context context, Cursor c, boolean autoRequery) { + init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER); + } + + void init(Context context, Cursor c, int flags) { + if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) { + flags |= FLAG_REGISTER_CONTENT_OBSERVER; + mAutoRequery = true; + } else { + mAutoRequery = false; + } + boolean cursorPresent = c != null; + mCursor = c; + mDataValid = cursorPresent; + mContext = context; + mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1; + if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) { + mChangeObserver = new ChangeObserver(); + mDataSetObserver = new MyDataSetObserver(); + } else { + mChangeObserver = null; + mDataSetObserver = null; + } + + if (cursorPresent) { + if (mChangeObserver != null) c.registerContentObserver(mChangeObserver); + if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver); + } + } + + /** + * Returns the cursor. + * @return the cursor. + */ + public Cursor getCursor() { + return mCursor; + } + + /** + * @see android.widget.ListAdapter#getCount() + */ + public int getCount() { + if (mDataValid && mCursor != null) { + return mCursor.getCount(); + } else { + return 0; + } + } + + /** + * @see android.widget.ListAdapter#getItem(int) + */ + public Object getItem(int position) { + if (mDataValid && mCursor != null) { + mCursor.moveToPosition(position); + return mCursor; + } else { + return null; + } + } + + /** + * @see android.widget.ListAdapter#getItemId(int) + */ + public long getItemId(int position) { + if (mDataValid && mCursor != null) { + if (mCursor.moveToPosition(position)) { + return mCursor.getLong(mRowIDColumn); + } else { + return 0; + } + } else { + return 0; + } + } + + @Override + public boolean hasStableIds() { + return true; + } + + /** + * @see android.widget.ListAdapter#getView(int, View, ViewGroup) + */ + public View getView(int position, View convertView, ViewGroup parent) { + if (!mDataValid) { + throw new IllegalStateException("this should only be called when the cursor is valid"); + } + if (!mCursor.moveToPosition(position)) { + throw new IllegalStateException("couldn't move cursor to position " + position); + } + View v; + if (convertView == null) { + v = newView(mContext, mCursor, parent); + } else { + v = convertView; + } + bindView(v, mContext, mCursor); + return v; + } + + @Override + public View getDropDownView(int position, View convertView, ViewGroup parent) { + if (mDataValid) { + mCursor.moveToPosition(position); + View v; + if (convertView == null) { + v = newDropDownView(mContext, mCursor, parent); + } else { + v = convertView; + } + bindView(v, mContext, mCursor); + return v; + } else { + return null; + } + } + + /** + * Makes a new view to hold the data pointed to by cursor. + * @param context Interface to application's global information + * @param cursor The cursor from which to get the data. The cursor is already + * moved to the correct position. + * @param parent The parent to which the new view is attached to + * @return the newly created view. + */ + public abstract View newView(Context context, Cursor cursor, ViewGroup parent); + + /** + * Makes a new drop down view to hold the data pointed to by cursor. + * @param context Interface to application's global information + * @param cursor The cursor from which to get the data. The cursor is already + * moved to the correct position. + * @param parent The parent to which the new view is attached to + * @return the newly created view. + */ + public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { + return newView(context, cursor, parent); + } + + /** + * Bind an existing view to the data pointed to by cursor + * @param view Existing view, returned earlier by newView + * @param context Interface to application's global information + * @param cursor The cursor from which to get the data. The cursor is already + * moved to the correct position. + */ + public abstract void bindView(View view, Context context, Cursor cursor); + + /** + * Change the underlying cursor to a new cursor. If there is an existing cursor it will be + * closed. + * + * @param cursor The new cursor to be used + */ + public void changeCursor(Cursor cursor) { + Cursor old = swapCursor(cursor); + if (old != null) { + old.close(); + } + } + + /** + * Swap in a new Cursor, returning the old Cursor. Unlike + * {@link #changeCursor(Cursor)}, the returned old Cursor is not + * closed. + * + * @param newCursor The new cursor to be used. + * @return Returns the previously set Cursor, or null if there wasa not one. + * If the given new Cursor is the same instance is the previously set + * Cursor, null is also returned. + */ + public Cursor swapCursor(Cursor newCursor) { + if (newCursor == mCursor) { + return null; + } + Cursor oldCursor = mCursor; + if (oldCursor != null) { + if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver); + if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver); + } + mCursor = newCursor; + if (newCursor != null) { + if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver); + if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver); + mRowIDColumn = newCursor.getColumnIndexOrThrow("_id"); + mDataValid = true; + // notify the observers about the new cursor + notifyDataSetChanged(); + } else { + mRowIDColumn = -1; + mDataValid = false; + // notify the observers about the lack of a data set + notifyDataSetInvalidated(); + } + return oldCursor; + } + + /** + *

    Converts the cursor into a CharSequence. Subclasses should override this + * method to convert their results. The default implementation returns an + * empty String for null values or the default String representation of + * the value.

    + * + * @param cursor the cursor to convert to a CharSequence + * @return a CharSequence representing the value + */ + public CharSequence convertToString(Cursor cursor) { + return cursor == null ? "" : cursor.toString(); + } + + /** + * Runs a query with the specified constraint. This query is requested + * by the filter attached to this adapter. + * + * The query is provided by a + * {@link android.widget.FilterQueryProvider}. + * If no provider is specified, the current cursor is not filtered and returned. + * + * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)} + * and the previous cursor is closed. + * + * This method is always executed on a background thread, not on the + * application's main thread (or UI thread.) + * + * Contract: when constraint is null or empty, the original results, + * prior to any filtering, must be returned. + * + * @param constraint the constraint with which the query must be filtered + * + * @return a Cursor representing the results of the new query + * + * @see #getFilter() + * @see #getFilterQueryProvider() + * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) + */ + public Cursor runQueryOnBackgroundThread(CharSequence constraint) { + if (mFilterQueryProvider != null) { + return mFilterQueryProvider.runQuery(constraint); + } + + return mCursor; + } + + public Filter getFilter() { + if (mCursorFilter == null) { + mCursorFilter = new CursorFilter(this); + } + return mCursorFilter; + } + + /** + * Returns the query filter provider used for filtering. When the + * provider is null, no filtering occurs. + * + * @return the current filter query provider or null if it does not exist + * + * @see #setFilterQueryProvider(android.widget.FilterQueryProvider) + * @see #runQueryOnBackgroundThread(CharSequence) + */ + public FilterQueryProvider getFilterQueryProvider() { + return mFilterQueryProvider; + } + + /** + * Sets the query filter provider used to filter the current Cursor. + * The provider's + * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)} + * method is invoked when filtering is requested by a client of + * this adapter. + * + * @param filterQueryProvider the filter query provider or null to remove it + * + * @see #getFilterQueryProvider() + * @see #runQueryOnBackgroundThread(CharSequence) + */ + public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) { + mFilterQueryProvider = filterQueryProvider; + } + + /** + * Called when the {@link ContentObserver} on the cursor receives a change notification. + * The default implementation provides the auto-requery logic, but may be overridden by + * sub classes. + * + * @see ContentObserver#onChange(boolean) + */ + protected void onContentChanged() { + if (mAutoRequery && mCursor != null && !mCursor.isClosed()) { + if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update"); + mDataValid = mCursor.requery(); + } + } + + private class ChangeObserver extends ContentObserver { + public ChangeObserver() { + super(new Handler()); + } + + @Override + public boolean deliverSelfNotifications() { + return true; + } + + @Override + public void onChange(boolean selfChange) { + onContentChanged(); + } + } + + private class MyDataSetObserver extends DataSetObserver { + @Override + public void onChanged() { + mDataValid = true; + notifyDataSetChanged(); + } + + @Override + public void onInvalidated() { + mDataValid = false; + notifyDataSetInvalidated(); + } + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/CursorFilter.java b/mobile/google-android-support-v4/src/android/support/v4/widget/CursorFilter.java new file mode 100644 index 00000000000..19fcead2436 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/CursorFilter.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.database.Cursor; +import android.widget.Filter; + +/** + * The CursorFilter delegates most of the work to the + * {@link android.widget.CursorAdapter}. Subclasses should override these + * delegate methods to run the queries and convert the results into String + * that can be used by auto-completion widgets. + */ +class CursorFilter extends Filter { + + CursorFilterClient mClient; + + interface CursorFilterClient { + CharSequence convertToString(Cursor cursor); + Cursor runQueryOnBackgroundThread(CharSequence constraint); + Cursor getCursor(); + void changeCursor(Cursor cursor); + } + + CursorFilter(CursorFilterClient client) { + mClient = client; + } + + @Override + public CharSequence convertResultToString(Object resultValue) { + return mClient.convertToString((Cursor) resultValue); + } + + @Override + protected FilterResults performFiltering(CharSequence constraint) { + Cursor cursor = mClient.runQueryOnBackgroundThread(constraint); + + FilterResults results = new FilterResults(); + if (cursor != null) { + results.count = cursor.getCount(); + results.values = cursor; + } else { + results.count = 0; + results.values = null; + } + return results; + } + + @Override + protected void publishResults(CharSequence constraint, FilterResults results) { + Cursor oldCursor = mClient.getCursor(); + + if (results.values != null && results.values != oldCursor) { + mClient.changeCursor((Cursor) results.values); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayout.java b/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayout.java new file mode 100644 index 00000000000..a8967a273c7 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayout.java @@ -0,0 +1,1958 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.SystemClock; +import android.support.annotation.DrawableRes; +import android.support.annotation.IntDef; +import android.support.annotation.Nullable; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.GravityCompat; +import android.support.v4.view.KeyEventCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewGroupCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.List; + +/** + * DrawerLayout acts as a top-level container for window content that allows for + * interactive "drawer" views to be pulled out from the edge of the window. + * + *

    Drawer positioning and layout is controlled using the android:layout_gravity + * attribute on child views corresponding to which side of the view you want the drawer + * to emerge from: left or right. (Or start/end on platform versions that support layout direction.) + *

    + * + *

    To use a DrawerLayout, position your primary content view as the first child with + * a width and height of match_parent. Add drawers as child views after the main + * content view and set the layout_gravity appropriately. Drawers commonly use + * match_parent for height with a fixed width.

    + * + *

    {@link DrawerListener} can be used to monitor the state and motion of drawer views. + * Avoid performing expensive operations such as layout during animation as it can cause + * stuttering; try to perform expensive operations during the {@link #STATE_IDLE} state. + * {@link SimpleDrawerListener} offers default/no-op implementations of each callback method.

    + * + *

    As per the Android Design + * guide, any drawers positioned to the left/start should + * always contain content for navigating around the application, whereas any drawers + * positioned to the right/end should always contain actions to take on the current content. + * This preserves the same navigation left, actions right structure present in the Action Bar + * and elsewhere.

    + * + *

    For more information about how to use DrawerLayout, read Creating a Navigation + * Drawer.

    + */ +public class DrawerLayout extends ViewGroup implements DrawerLayoutImpl { + private static final String TAG = "DrawerLayout"; + + @IntDef({STATE_IDLE, STATE_DRAGGING, STATE_SETTLING}) + @Retention(RetentionPolicy.SOURCE) + private @interface State {} + + /** + * Indicates that any drawers are in an idle, settled state. No animation is in progress. + */ + public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE; + + /** + * Indicates that a drawer is currently being dragged by the user. + */ + public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING; + + /** + * Indicates that a drawer is in the process of settling to a final position. + */ + public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING; + + /** @hide */ + @IntDef({LOCK_MODE_UNLOCKED, LOCK_MODE_LOCKED_CLOSED, LOCK_MODE_LOCKED_OPEN}) + @Retention(RetentionPolicy.SOURCE) + private @interface LockMode {} + + /** + * The drawer is unlocked. + */ + public static final int LOCK_MODE_UNLOCKED = 0; + + /** + * The drawer is locked closed. The user may not open it, though + * the app may open it programmatically. + */ + public static final int LOCK_MODE_LOCKED_CLOSED = 1; + + /** + * The drawer is locked open. The user may not close it, though the app + * may close it programmatically. + */ + public static final int LOCK_MODE_LOCKED_OPEN = 2; + + /** @hide */ + @IntDef({Gravity.LEFT, Gravity.RIGHT, GravityCompat.START, GravityCompat.END}) + @Retention(RetentionPolicy.SOURCE) + private @interface EdgeGravity {} + + + private static final int MIN_DRAWER_MARGIN = 64; // dp + + private static final int DEFAULT_SCRIM_COLOR = 0x99000000; + + /** + * Length of time to delay before peeking the drawer. + */ + private static final int PEEK_DELAY = 160; // ms + + /** + * Minimum velocity that will be detected as a fling + */ + private static final int MIN_FLING_VELOCITY = 400; // dips per second + + /** + * Experimental feature. + */ + private static final boolean ALLOW_EDGE_LOCK = false; + + private static final boolean CHILDREN_DISALLOW_INTERCEPT = true; + + private static final float TOUCH_SLOP_SENSITIVITY = 1.f; + + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.layout_gravity + }; + + /** Whether we can use NO_HIDE_DESCENDANTS accessibility importance. */ + private static final boolean CAN_HIDE_DESCENDANTS = Build.VERSION.SDK_INT >= 19; + + private final ChildAccessibilityDelegate mChildAccessibilityDelegate = + new ChildAccessibilityDelegate(); + + private int mMinDrawerMargin; + + private int mScrimColor = DEFAULT_SCRIM_COLOR; + private float mScrimOpacity; + private Paint mScrimPaint = new Paint(); + + private final ViewDragHelper mLeftDragger; + private final ViewDragHelper mRightDragger; + private final ViewDragCallback mLeftCallback; + private final ViewDragCallback mRightCallback; + private int mDrawerState; + private boolean mInLayout; + private boolean mFirstLayout = true; + private int mLockModeLeft; + private int mLockModeRight; + private boolean mDisallowInterceptRequested; + private boolean mChildrenCanceledTouch; + + private DrawerListener mListener; + + private float mInitialMotionX; + private float mInitialMotionY; + + private Drawable mShadowLeft; + private Drawable mShadowRight; + private Drawable mStatusBarBackground; + + private CharSequence mTitleLeft; + private CharSequence mTitleRight; + + private Object mLastInsets; + private boolean mDrawStatusBarBackground; + + /** + * Listener for monitoring events about drawers. + */ + public interface DrawerListener { + /** + * Called when a drawer's position changes. + * @param drawerView The child view that was moved + * @param slideOffset The new offset of this drawer within its range, from 0-1 + */ + public void onDrawerSlide(View drawerView, float slideOffset); + + /** + * Called when a drawer has settled in a completely open state. + * The drawer is interactive at this point. + * + * @param drawerView Drawer view that is now open + */ + public void onDrawerOpened(View drawerView); + + /** + * Called when a drawer has settled in a completely closed state. + * + * @param drawerView Drawer view that is now closed + */ + public void onDrawerClosed(View drawerView); + + /** + * Called when the drawer motion state changes. The new state will + * be one of {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. + * + * @param newState The new drawer motion state + */ + public void onDrawerStateChanged(@State int newState); + } + + /** + * Stub/no-op implementations of all methods of {@link DrawerListener}. + * Override this if you only care about a few of the available callback methods. + */ + public static abstract class SimpleDrawerListener implements DrawerListener { + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + } + + @Override + public void onDrawerOpened(View drawerView) { + } + + @Override + public void onDrawerClosed(View drawerView) { + } + + @Override + public void onDrawerStateChanged(int newState) { + } + } + + interface DrawerLayoutCompatImpl { + void configureApplyInsets(View drawerLayout); + void dispatchChildInsets(View child, Object insets, int drawerGravity); + void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity); + int getTopInset(Object lastInsets); + Drawable getDefaultStatusBarBackground(Context context); + } + + static class DrawerLayoutCompatImplBase implements DrawerLayoutCompatImpl { + public void configureApplyInsets(View drawerLayout) { + // This space for rent + } + + public void dispatchChildInsets(View child, Object insets, int drawerGravity) { + // This space for rent + } + + public void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity) { + // This space for rent + } + + public int getTopInset(Object insets) { + return 0; + } + + @Override + public Drawable getDefaultStatusBarBackground(Context context) { + return null; + } + } + + static class DrawerLayoutCompatImplApi21 implements DrawerLayoutCompatImpl { + public void configureApplyInsets(View drawerLayout) { + DrawerLayoutCompatApi21.configureApplyInsets(drawerLayout); + } + + public void dispatchChildInsets(View child, Object insets, int drawerGravity) { + DrawerLayoutCompatApi21.dispatchChildInsets(child, insets, drawerGravity); + } + + public void applyMarginInsets(MarginLayoutParams lp, Object insets, int drawerGravity) { + DrawerLayoutCompatApi21.applyMarginInsets(lp, insets, drawerGravity); + } + + public int getTopInset(Object insets) { + return DrawerLayoutCompatApi21.getTopInset(insets); + } + + @Override + public Drawable getDefaultStatusBarBackground(Context context) { + return DrawerLayoutCompatApi21.getDefaultStatusBarBackground(context); + } + } + + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 21) { + IMPL = new DrawerLayoutCompatImplApi21(); + } else { + IMPL = new DrawerLayoutCompatImplBase(); + } + } + + static final DrawerLayoutCompatImpl IMPL; + + public DrawerLayout(Context context) { + this(context, null); + } + + public DrawerLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public DrawerLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); + final float density = getResources().getDisplayMetrics().density; + mMinDrawerMargin = (int) (MIN_DRAWER_MARGIN * density + 0.5f); + final float minVel = MIN_FLING_VELOCITY * density; + + mLeftCallback = new ViewDragCallback(Gravity.LEFT); + mRightCallback = new ViewDragCallback(Gravity.RIGHT); + + mLeftDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mLeftCallback); + mLeftDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); + mLeftDragger.setMinVelocity(minVel); + mLeftCallback.setDragger(mLeftDragger); + + mRightDragger = ViewDragHelper.create(this, TOUCH_SLOP_SENSITIVITY, mRightCallback); + mRightDragger.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT); + mRightDragger.setMinVelocity(minVel); + mRightCallback.setDragger(mRightDragger); + + // So that we can catch the back button + setFocusableInTouchMode(true); + + ViewCompat.setImportantForAccessibility(this, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + + ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); + ViewGroupCompat.setMotionEventSplittingEnabled(this, false); + if (ViewCompat.getFitsSystemWindows(this)) { + IMPL.configureApplyInsets(this); + mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context); + } + } + + /** + * @hide Internal use only; called to apply window insets when configured + * with fitsSystemWindows="true" + */ + @Override + public void setChildInsets(Object insets, boolean draw) { + mLastInsets = insets; + mDrawStatusBarBackground = draw; + setWillNotDraw(!draw && getBackground() == null); + requestLayout(); + } + + /** + * Set a simple drawable used for the left or right shadow. + * The drawable provided must have a nonzero intrinsic width. + * + * @param shadowDrawable Shadow drawable to use at the edge of a drawer + * @param gravity Which drawer the shadow should apply to + */ + public void setDrawerShadow(Drawable shadowDrawable, @EdgeGravity int gravity) { + /* + * TODO Someone someday might want to set more complex drawables here. + * They're probably nuts, but we might want to consider registering callbacks, + * setting states, etc. properly. + */ + + final int absGravity = GravityCompat.getAbsoluteGravity(gravity, + ViewCompat.getLayoutDirection(this)); + if ((absGravity & Gravity.LEFT) == Gravity.LEFT) { + mShadowLeft = shadowDrawable; + invalidate(); + } + if ((absGravity & Gravity.RIGHT) == Gravity.RIGHT) { + mShadowRight = shadowDrawable; + invalidate(); + } + } + + /** + * Set a simple drawable used for the left or right shadow. + * The drawable provided must have a nonzero intrinsic width. + * + * @param resId Resource id of a shadow drawable to use at the edge of a drawer + * @param gravity Which drawer the shadow should apply to + */ + public void setDrawerShadow(@DrawableRes int resId, @EdgeGravity int gravity) { + setDrawerShadow(getResources().getDrawable(resId), gravity); + } + + /** + * Set a color to use for the scrim that obscures primary content while a drawer is open. + * + * @param color Color to use in 0xAARRGGBB format. + */ + public void setScrimColor(int color) { + mScrimColor = color; + invalidate(); + } + + /** + * Set a listener to be notified of drawer events. + * + * @param listener Listener to notify when drawer events occur + * @see DrawerListener + */ + public void setDrawerListener(DrawerListener listener) { + mListener = listener; + } + + /** + * Enable or disable interaction with all drawers. + * + *

    This allows the application to restrict the user's ability to open or close + * any drawer within this layout. DrawerLayout will still respond to calls to + * {@link #openDrawer(int)}, {@link #closeDrawer(int)} and friends if a drawer is locked.

    + * + *

    Locking drawers open or closed will implicitly open or close + * any drawers as appropriate.

    + * + * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, + * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. + */ + public void setDrawerLockMode(@LockMode int lockMode) { + setDrawerLockMode(lockMode, Gravity.LEFT); + setDrawerLockMode(lockMode, Gravity.RIGHT); + } + + /** + * Enable or disable interaction with the given drawer. + * + *

    This allows the application to restrict the user's ability to open or close + * the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer(int)}, + * {@link #closeDrawer(int)} and friends if a drawer is locked.

    + * + *

    Locking a drawer open or closed will implicitly open or close + * that drawer as appropriate.

    + * + * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, + * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. + * @param edgeGravity Gravity.LEFT, RIGHT, START or END. + * Expresses which drawer to change the mode for. + * + * @see #LOCK_MODE_UNLOCKED + * @see #LOCK_MODE_LOCKED_CLOSED + * @see #LOCK_MODE_LOCKED_OPEN + */ + public void setDrawerLockMode(@LockMode int lockMode, @EdgeGravity int edgeGravity) { + final int absGravity = GravityCompat.getAbsoluteGravity(edgeGravity, + ViewCompat.getLayoutDirection(this)); + if (absGravity == Gravity.LEFT) { + mLockModeLeft = lockMode; + } else if (absGravity == Gravity.RIGHT) { + mLockModeRight = lockMode; + } + if (lockMode != LOCK_MODE_UNLOCKED) { + // Cancel interaction in progress + final ViewDragHelper helper = absGravity == Gravity.LEFT ? mLeftDragger : mRightDragger; + helper.cancel(); + } + switch (lockMode) { + case LOCK_MODE_LOCKED_OPEN: + final View toOpen = findDrawerWithGravity(absGravity); + if (toOpen != null) { + openDrawer(toOpen); + } + break; + case LOCK_MODE_LOCKED_CLOSED: + final View toClose = findDrawerWithGravity(absGravity); + if (toClose != null) { + closeDrawer(toClose); + } + break; + // default: do nothing + } + } + + /** + * Enable or disable interaction with the given drawer. + * + *

    This allows the application to restrict the user's ability to open or close + * the given drawer. DrawerLayout will still respond to calls to {@link #openDrawer(int)}, + * {@link #closeDrawer(int)} and friends if a drawer is locked.

    + * + *

    Locking a drawer open or closed will implicitly open or close + * that drawer as appropriate.

    + * + * @param lockMode The new lock mode for the given drawer. One of {@link #LOCK_MODE_UNLOCKED}, + * {@link #LOCK_MODE_LOCKED_CLOSED} or {@link #LOCK_MODE_LOCKED_OPEN}. + * @param drawerView The drawer view to change the lock mode for + * + * @see #LOCK_MODE_UNLOCKED + * @see #LOCK_MODE_LOCKED_CLOSED + * @see #LOCK_MODE_LOCKED_OPEN + */ + public void setDrawerLockMode(@LockMode int lockMode, View drawerView) { + if (!isDrawerView(drawerView)) { + throw new IllegalArgumentException("View " + drawerView + " is not a " + + "drawer with appropriate layout_gravity"); + } + final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; + setDrawerLockMode(lockMode, gravity); + } + + /** + * Check the lock mode of the drawer with the given gravity. + * + * @param edgeGravity Gravity of the drawer to check + * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or + * {@link #LOCK_MODE_LOCKED_OPEN}. + */ + @LockMode + public int getDrawerLockMode(@EdgeGravity int edgeGravity) { + final int absGravity = GravityCompat.getAbsoluteGravity( + edgeGravity, ViewCompat.getLayoutDirection(this)); + if (absGravity == Gravity.LEFT) { + return mLockModeLeft; + } else if (absGravity == Gravity.RIGHT) { + return mLockModeRight; + } + return LOCK_MODE_UNLOCKED; + } + + /** + * Check the lock mode of the given drawer view. + * + * @param drawerView Drawer view to check lock mode + * @return one of {@link #LOCK_MODE_UNLOCKED}, {@link #LOCK_MODE_LOCKED_CLOSED} or + * {@link #LOCK_MODE_LOCKED_OPEN}. + */ + @LockMode + public int getDrawerLockMode(View drawerView) { + final int absGravity = getDrawerViewAbsoluteGravity(drawerView); + if (absGravity == Gravity.LEFT) { + return mLockModeLeft; + } else if (absGravity == Gravity.RIGHT) { + return mLockModeRight; + } + return LOCK_MODE_UNLOCKED; + } + + /** + * Sets the title of the drawer with the given gravity. + *

    + * When accessibility is turned on, this is the title that will be used to + * identify the drawer to the active accessibility service. + * + * @param edgeGravity Gravity.LEFT, RIGHT, START or END. Expresses which + * drawer to set the title for. + * @param title The title for the drawer. + */ + public void setDrawerTitle(@EdgeGravity int edgeGravity, CharSequence title) { + final int absGravity = GravityCompat.getAbsoluteGravity( + edgeGravity, ViewCompat.getLayoutDirection(this)); + if (absGravity == Gravity.LEFT) { + mTitleLeft = title; + } else if (absGravity == Gravity.RIGHT) { + mTitleRight = title; + } + } + + /** + * Returns the title of the drawer with the given gravity. + * + * @param edgeGravity Gravity.LEFT, RIGHT, START or END. Expresses which + * drawer to return the title for. + * @return The title of the drawer, or null if none set. + * @see #setDrawerTitle(int, CharSequence) + */ + @Nullable + public CharSequence getDrawerTitle(@EdgeGravity int edgeGravity) { + final int absGravity = GravityCompat.getAbsoluteGravity( + edgeGravity, ViewCompat.getLayoutDirection(this)); + if (absGravity == Gravity.LEFT) { + return mTitleLeft; + } else if (absGravity == Gravity.RIGHT) { + return mTitleRight; + } + return null; + } + + /** + * Resolve the shared state of all drawers from the component ViewDragHelpers. + * Should be called whenever a ViewDragHelper's state changes. + */ + void updateDrawerState(int forGravity, @State int activeState, View activeDrawer) { + final int leftState = mLeftDragger.getViewDragState(); + final int rightState = mRightDragger.getViewDragState(); + + final int state; + if (leftState == STATE_DRAGGING || rightState == STATE_DRAGGING) { + state = STATE_DRAGGING; + } else if (leftState == STATE_SETTLING || rightState == STATE_SETTLING) { + state = STATE_SETTLING; + } else { + state = STATE_IDLE; + } + + if (activeDrawer != null && activeState == STATE_IDLE) { + final LayoutParams lp = (LayoutParams) activeDrawer.getLayoutParams(); + if (lp.onScreen == 0) { + dispatchOnDrawerClosed(activeDrawer); + } else if (lp.onScreen == 1) { + dispatchOnDrawerOpened(activeDrawer); + } + } + + if (state != mDrawerState) { + mDrawerState = state; + + if (mListener != null) { + mListener.onDrawerStateChanged(state); + } + } + } + + void dispatchOnDrawerClosed(View drawerView) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + if (lp.knownOpen) { + lp.knownOpen = false; + if (mListener != null) { + mListener.onDrawerClosed(drawerView); + } + + updateChildrenImportantForAccessibility(drawerView, false); + + // Only send WINDOW_STATE_CHANGE if the host has window focus. This + // may change if support for multiple foreground windows (e.g. IME) + // improves. + if (hasWindowFocus()) { + final View rootView = getRootView(); + if (rootView != null) { + rootView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + } + } + } + + void dispatchOnDrawerOpened(View drawerView) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + if (!lp.knownOpen) { + lp.knownOpen = true; + if (mListener != null) { + mListener.onDrawerOpened(drawerView); + } + + updateChildrenImportantForAccessibility(drawerView, true); + + // Only send WINDOW_STATE_CHANGE if the host has window focus. + if (hasWindowFocus()) { + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + drawerView.requestFocus(); + } + } + + private void updateChildrenImportantForAccessibility(View drawerView, boolean isDrawerOpen) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (!isDrawerOpen && !isDrawerView(child) + || isDrawerOpen && child == drawerView) { + // Drawer is closed and this is a content view or this is an + // open drawer view, so it should be visible. + ViewCompat.setImportantForAccessibility(child, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } else { + ViewCompat.setImportantForAccessibility(child, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } + } + } + + void dispatchOnDrawerSlide(View drawerView, float slideOffset) { + if (mListener != null) { + mListener.onDrawerSlide(drawerView, slideOffset); + } + } + + void setDrawerViewOffset(View drawerView, float slideOffset) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + if (slideOffset == lp.onScreen) { + return; + } + + lp.onScreen = slideOffset; + dispatchOnDrawerSlide(drawerView, slideOffset); + } + + float getDrawerViewOffset(View drawerView) { + return ((LayoutParams) drawerView.getLayoutParams()).onScreen; + } + + /** + * @return the absolute gravity of the child drawerView, resolved according + * to the current layout direction + */ + int getDrawerViewAbsoluteGravity(View drawerView) { + final int gravity = ((LayoutParams) drawerView.getLayoutParams()).gravity; + return GravityCompat.getAbsoluteGravity(gravity, ViewCompat.getLayoutDirection(this)); + } + + boolean checkDrawerViewAbsoluteGravity(View drawerView, int checkFor) { + final int absGravity = getDrawerViewAbsoluteGravity(drawerView); + return (absGravity & checkFor) == checkFor; + } + + View findOpenDrawer() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (((LayoutParams) child.getLayoutParams()).knownOpen) { + return child; + } + } + return null; + } + + void moveDrawerToOffset(View drawerView, float slideOffset) { + final float oldOffset = getDrawerViewOffset(drawerView); + final int width = drawerView.getWidth(); + final int oldPos = (int) (width * oldOffset); + final int newPos = (int) (width * slideOffset); + final int dx = newPos - oldPos; + + drawerView.offsetLeftAndRight( + checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT) ? dx : -dx); + setDrawerViewOffset(drawerView, slideOffset); + } + + /** + * @param gravity the gravity of the child to return. If specified as a + * relative value, it will be resolved according to the current + * layout direction. + * @return the drawer with the specified gravity + */ + View findDrawerWithGravity(int gravity) { + final int absHorizGravity = GravityCompat.getAbsoluteGravity( + gravity, ViewCompat.getLayoutDirection(this)) & Gravity.HORIZONTAL_GRAVITY_MASK; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final int childAbsGravity = getDrawerViewAbsoluteGravity(child); + if ((childAbsGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == absHorizGravity) { + return child; + } + } + return null; + } + + /** + * Simple gravity to string - only supports LEFT and RIGHT for debugging output. + * + * @param gravity Absolute gravity value + * @return LEFT or RIGHT as appropriate, or a hex string + */ + static String gravityToString(@EdgeGravity int gravity) { + if ((gravity & Gravity.LEFT) == Gravity.LEFT) { + return "LEFT"; + } + if ((gravity & Gravity.RIGHT) == Gravity.RIGHT) { + return "RIGHT"; + } + return Integer.toHexString(gravity); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mFirstLayout = true; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY || heightMode != MeasureSpec.EXACTLY) { + if (isInEditMode()) { + // Don't crash the layout editor. Consume all of the space if specified + // or pick a magic number from thin air otherwise. + // TODO Better communication with tools of this bogus state. + // It will crash on a real device. + if (widthMode == MeasureSpec.AT_MOST) { + widthMode = MeasureSpec.EXACTLY; + } else if (widthMode == MeasureSpec.UNSPECIFIED) { + widthMode = MeasureSpec.EXACTLY; + widthSize = 300; + } + if (heightMode == MeasureSpec.AT_MOST) { + heightMode = MeasureSpec.EXACTLY; + } + else if (heightMode == MeasureSpec.UNSPECIFIED) { + heightMode = MeasureSpec.EXACTLY; + heightSize = 300; + } + } else { + throw new IllegalArgumentException( + "DrawerLayout must be measured with MeasureSpec.EXACTLY."); + } + } + + setMeasuredDimension(widthSize, heightSize); + + final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this); + final int layoutDirection = ViewCompat.getLayoutDirection(this); + + // Gravity value for each drawer we've seen. Only one of each permitted. + int foundDrawers = 0; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (applyInsets) { + final int cgrav = GravityCompat.getAbsoluteGravity(lp.gravity, layoutDirection); + if (ViewCompat.getFitsSystemWindows(child)) { + IMPL.dispatchChildInsets(child, mLastInsets, cgrav); + } else { + IMPL.applyMarginInsets(lp, mLastInsets, cgrav); + } + } + + if (isContentView(child)) { + // Content views get measured at exactly the layout's size. + final int contentWidthSpec = MeasureSpec.makeMeasureSpec( + widthSize - lp.leftMargin - lp.rightMargin, MeasureSpec.EXACTLY); + final int contentHeightSpec = MeasureSpec.makeMeasureSpec( + heightSize - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY); + child.measure(contentWidthSpec, contentHeightSpec); + } else if (isDrawerView(child)) { + final int childGravity = + getDrawerViewAbsoluteGravity(child) & Gravity.HORIZONTAL_GRAVITY_MASK; + if ((foundDrawers & childGravity) != 0) { + throw new IllegalStateException("Child drawer has absolute gravity " + + gravityToString(childGravity) + " but this " + TAG + " already has a " + + "drawer view along that edge"); + } + final int drawerWidthSpec = getChildMeasureSpec(widthMeasureSpec, + mMinDrawerMargin + lp.leftMargin + lp.rightMargin, + lp.width); + final int drawerHeightSpec = getChildMeasureSpec(heightMeasureSpec, + lp.topMargin + lp.bottomMargin, + lp.height); + child.measure(drawerWidthSpec, drawerHeightSpec); + } else { + throw new IllegalStateException("Child " + child + " at index " + i + + " does not have a valid layout_gravity - must be Gravity.LEFT, " + + "Gravity.RIGHT or Gravity.NO_GRAVITY"); + } + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mInLayout = true; + final int width = r - l; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (isContentView(child)) { + child.layout(lp.leftMargin, lp.topMargin, + lp.leftMargin + child.getMeasuredWidth(), + lp.topMargin + child.getMeasuredHeight()); + } else { // Drawer, if it wasn't onMeasure would have thrown an exception. + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + int childLeft; + + final float newOffset; + if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + childLeft = -childWidth + (int) (childWidth * lp.onScreen); + newOffset = (float) (childWidth + childLeft) / childWidth; + } else { // Right; onMeasure checked for us. + childLeft = width - (int) (childWidth * lp.onScreen); + newOffset = (float) (width - childLeft) / childWidth; + } + + final boolean changeOffset = newOffset != lp.onScreen; + + final int vgrav = lp.gravity & Gravity.VERTICAL_GRAVITY_MASK; + + switch (vgrav) { + default: + case Gravity.TOP: { + child.layout(childLeft, lp.topMargin, childLeft + childWidth, + lp.topMargin + childHeight); + break; + } + + case Gravity.BOTTOM: { + final int height = b - t; + child.layout(childLeft, + height - lp.bottomMargin - child.getMeasuredHeight(), + childLeft + childWidth, + height - lp.bottomMargin); + break; + } + + case Gravity.CENTER_VERTICAL: { + final int height = b - t; + int childTop = (height - childHeight) / 2; + + // Offset for margins. If things don't fit right because of + // bad measurement before, oh well. + if (childTop < lp.topMargin) { + childTop = lp.topMargin; + } else if (childTop + childHeight > height - lp.bottomMargin) { + childTop = height - lp.bottomMargin - childHeight; + } + child.layout(childLeft, childTop, childLeft + childWidth, + childTop + childHeight); + break; + } + } + + if (changeOffset) { + setDrawerViewOffset(child, newOffset); + } + + final int newVisibility = lp.onScreen > 0 ? VISIBLE : INVISIBLE; + if (child.getVisibility() != newVisibility) { + child.setVisibility(newVisibility); + } + } + } + mInLayout = false; + mFirstLayout = false; + } + + @Override + public void requestLayout() { + if (!mInLayout) { + super.requestLayout(); + } + } + + @Override + public void computeScroll() { + final int childCount = getChildCount(); + float scrimOpacity = 0; + for (int i = 0; i < childCount; i++) { + final float onscreen = ((LayoutParams) getChildAt(i).getLayoutParams()).onScreen; + scrimOpacity = Math.max(scrimOpacity, onscreen); + } + mScrimOpacity = scrimOpacity; + + // "|" used on purpose; both need to run. + if (mLeftDragger.continueSettling(true) | mRightDragger.continueSettling(true)) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private static boolean hasOpaqueBackground(View v) { + final Drawable bg = v.getBackground(); + if (bg != null) { + return bg.getOpacity() == PixelFormat.OPAQUE; + } + return false; + } + + /** + * Set a drawable to draw in the insets area for the status bar. + * Note that this will only be activated if this DrawerLayout fitsSystemWindows. + * + * @param bg Background drawable to draw behind the status bar + */ + public void setStatusBarBackground(Drawable bg) { + mStatusBarBackground = bg; + invalidate(); + } + + /** + * Gets the drawable used to draw in the insets area for the status bar. + * + * @return The status bar background drawable, or null if none set + */ + public Drawable getStatusBarBackgroundDrawable() { + return mStatusBarBackground; + } + + /** + * Set a drawable to draw in the insets area for the status bar. + * Note that this will only be activated if this DrawerLayout fitsSystemWindows. + * + * @param resId Resource id of a background drawable to draw behind the status bar + */ + public void setStatusBarBackground(int resId) { + mStatusBarBackground = resId != 0 ? ContextCompat.getDrawable(getContext(), resId) : null; + invalidate(); + } + + /** + * Set a drawable to draw in the insets area for the status bar. + * Note that this will only be activated if this DrawerLayout fitsSystemWindows. + * + * @param color Color to use as a background drawable to draw behind the status bar + * in 0xAARRGGBB format. + */ + public void setStatusBarBackgroundColor(int color) { + mStatusBarBackground = new ColorDrawable(color); + invalidate(); + } + + @Override + public void onDraw(Canvas c) { + super.onDraw(c); + if (mDrawStatusBarBackground && mStatusBarBackground != null) { + final int inset = IMPL.getTopInset(mLastInsets); + if (inset > 0) { + mStatusBarBackground.setBounds(0, 0, getWidth(), inset); + mStatusBarBackground.draw(c); + } + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + final int height = getHeight(); + final boolean drawingContent = isContentView(child); + int clipLeft = 0, clipRight = getWidth(); + + final int restoreCount = canvas.save(); + if (drawingContent) { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + if (v == child || v.getVisibility() != VISIBLE || + !hasOpaqueBackground(v) || !isDrawerView(v) || + v.getHeight() < height) { + continue; + } + + if (checkDrawerViewAbsoluteGravity(v, Gravity.LEFT)) { + final int vright = v.getRight(); + if (vright > clipLeft) clipLeft = vright; + } else { + final int vleft = v.getLeft(); + if (vleft < clipRight) clipRight = vleft; + } + } + canvas.clipRect(clipLeft, 0, clipRight, getHeight()); + } + final boolean result = super.drawChild(canvas, child, drawingTime); + canvas.restoreToCount(restoreCount); + + if (mScrimOpacity > 0 && drawingContent) { + final int baseAlpha = (mScrimColor & 0xff000000) >>> 24; + final int imag = (int) (baseAlpha * mScrimOpacity); + final int color = imag << 24 | (mScrimColor & 0xffffff); + mScrimPaint.setColor(color); + + canvas.drawRect(clipLeft, 0, clipRight, getHeight(), mScrimPaint); + } else if (mShadowLeft != null && checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + final int shadowWidth = mShadowLeft.getIntrinsicWidth(); + final int childRight = child.getRight(); + final int drawerPeekDistance = mLeftDragger.getEdgeSize(); + final float alpha = + Math.max(0, Math.min((float) childRight / drawerPeekDistance, 1.f)); + mShadowLeft.setBounds(childRight, child.getTop(), + childRight + shadowWidth, child.getBottom()); + mShadowLeft.setAlpha((int) (0xff * alpha)); + mShadowLeft.draw(canvas); + } else if (mShadowRight != null && checkDrawerViewAbsoluteGravity(child, Gravity.RIGHT)) { + final int shadowWidth = mShadowRight.getIntrinsicWidth(); + final int childLeft = child.getLeft(); + final int showing = getWidth() - childLeft; + final int drawerPeekDistance = mRightDragger.getEdgeSize(); + final float alpha = + Math.max(0, Math.min((float) showing / drawerPeekDistance, 1.f)); + mShadowRight.setBounds(childLeft - shadowWidth, child.getTop(), + childLeft, child.getBottom()); + mShadowRight.setAlpha((int) (0xff * alpha)); + mShadowRight.draw(canvas); + } + return result; + } + + boolean isContentView(View child) { + return ((LayoutParams) child.getLayoutParams()).gravity == Gravity.NO_GRAVITY; + } + + boolean isDrawerView(View child) { + final int gravity = ((LayoutParams) child.getLayoutParams()).gravity; + final int absGravity = GravityCompat.getAbsoluteGravity(gravity, + ViewCompat.getLayoutDirection(child)); + return (absGravity & (Gravity.LEFT | Gravity.RIGHT)) != 0; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + + // "|" used deliberately here; both methods should be invoked. + final boolean interceptForDrag = mLeftDragger.shouldInterceptTouchEvent(ev) | + mRightDragger.shouldInterceptTouchEvent(ev); + + boolean interceptForTap = false; + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + mInitialMotionX = x; + mInitialMotionY = y; + if (mScrimOpacity > 0) { + final View child = mLeftDragger.findTopChildUnder((int) x, (int) y); + if (child != null && isContentView(child)) { + interceptForTap = true; + } + } + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + break; + } + + case MotionEvent.ACTION_MOVE: { + // If we cross the touch slop, don't perform the delayed peek for an edge touch. + if (mLeftDragger.checkTouchSlop(ViewDragHelper.DIRECTION_ALL)) { + mLeftCallback.removeCallbacks(); + mRightCallback.removeCallbacks(); + } + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: { + closeDrawers(true); + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + } + } + + return interceptForDrag || interceptForTap || hasPeekingDrawer() || mChildrenCanceledTouch; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + mLeftDragger.processTouchEvent(ev); + mRightDragger.processTouchEvent(ev); + + final int action = ev.getAction(); + boolean wantTouchEvents = true; + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + mInitialMotionX = x; + mInitialMotionY = y; + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + break; + } + + case MotionEvent.ACTION_UP: { + final float x = ev.getX(); + final float y = ev.getY(); + boolean peekingOnly = true; + final View touchedView = mLeftDragger.findTopChildUnder((int) x, (int) y); + if (touchedView != null && isContentView(touchedView)) { + final float dx = x - mInitialMotionX; + final float dy = y - mInitialMotionY; + final int slop = mLeftDragger.getTouchSlop(); + if (dx * dx + dy * dy < slop * slop) { + // Taps close a dimmed open drawer but only if it isn't locked open. + final View openDrawer = findOpenDrawer(); + if (openDrawer != null) { + peekingOnly = getDrawerLockMode(openDrawer) == LOCK_MODE_LOCKED_OPEN; + } + } + } + closeDrawers(peekingOnly); + mDisallowInterceptRequested = false; + break; + } + + case MotionEvent.ACTION_CANCEL: { + closeDrawers(true); + mDisallowInterceptRequested = false; + mChildrenCanceledTouch = false; + break; + } + } + + return wantTouchEvents; + } + + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (CHILDREN_DISALLOW_INTERCEPT || + (!mLeftDragger.isEdgeTouched(ViewDragHelper.EDGE_LEFT) && + !mRightDragger.isEdgeTouched(ViewDragHelper.EDGE_RIGHT))) { + // If we have an edge touch we want to skip this and track it for later instead. + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + mDisallowInterceptRequested = disallowIntercept; + if (disallowIntercept) { + closeDrawers(true); + } + } + + /** + * Close all currently open drawer views by animating them out of view. + */ + public void closeDrawers() { + closeDrawers(false); + } + + void closeDrawers(boolean peekingOnly) { + boolean needsInvalidate = false; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (!isDrawerView(child) || (peekingOnly && !lp.isPeeking)) { + continue; + } + + final int childWidth = child.getWidth(); + + if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + needsInvalidate |= mLeftDragger.smoothSlideViewTo(child, + -childWidth, child.getTop()); + } else { + needsInvalidate |= mRightDragger.smoothSlideViewTo(child, + getWidth(), child.getTop()); + } + + lp.isPeeking = false; + } + + mLeftCallback.removeCallbacks(); + mRightCallback.removeCallbacks(); + + if (needsInvalidate) { + invalidate(); + } + } + + /** + * Open the specified drawer view by animating it into view. + * + * @param drawerView Drawer view to open + */ + public void openDrawer(View drawerView) { + if (!isDrawerView(drawerView)) { + throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); + } + + if (mFirstLayout) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + lp.onScreen = 1.f; + lp.knownOpen = true; + + updateChildrenImportantForAccessibility(drawerView, true); + } else { + if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { + mLeftDragger.smoothSlideViewTo(drawerView, 0, drawerView.getTop()); + } else { + mRightDragger.smoothSlideViewTo(drawerView, getWidth() - drawerView.getWidth(), + drawerView.getTop()); + } + } + invalidate(); + } + + /** + * Open the specified drawer by animating it out of view. + * + * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. + * GravityCompat.START or GravityCompat.END may also be used. + */ + public void openDrawer(@EdgeGravity int gravity) { + final View drawerView = findDrawerWithGravity(gravity); + if (drawerView == null) { + throw new IllegalArgumentException("No drawer view found with gravity " + + gravityToString(gravity)); + } + openDrawer(drawerView); + } + + /** + * Close the specified drawer view by animating it into view. + * + * @param drawerView Drawer view to close + */ + public void closeDrawer(View drawerView) { + if (!isDrawerView(drawerView)) { + throw new IllegalArgumentException("View " + drawerView + " is not a sliding drawer"); + } + + if (mFirstLayout) { + final LayoutParams lp = (LayoutParams) drawerView.getLayoutParams(); + lp.onScreen = 0.f; + lp.knownOpen = false; + } else { + if (checkDrawerViewAbsoluteGravity(drawerView, Gravity.LEFT)) { + mLeftDragger.smoothSlideViewTo(drawerView, -drawerView.getWidth(), + drawerView.getTop()); + } else { + mRightDragger.smoothSlideViewTo(drawerView, getWidth(), drawerView.getTop()); + } + } + invalidate(); + } + + /** + * Close the specified drawer by animating it out of view. + * + * @param gravity Gravity.LEFT to move the left drawer or Gravity.RIGHT for the right. + * GravityCompat.START or GravityCompat.END may also be used. + */ + public void closeDrawer(@EdgeGravity int gravity) { + final View drawerView = findDrawerWithGravity(gravity); + if (drawerView == null) { + throw new IllegalArgumentException("No drawer view found with gravity " + + gravityToString(gravity)); + } + closeDrawer(drawerView); + } + + /** + * Check if the given drawer view is currently in an open state. + * To be considered "open" the drawer must have settled into its fully + * visible state. To check for partial visibility use + * {@link #isDrawerVisible(android.view.View)}. + * + * @param drawer Drawer view to check + * @return true if the given drawer view is in an open state + * @see #isDrawerVisible(android.view.View) + */ + public boolean isDrawerOpen(View drawer) { + if (!isDrawerView(drawer)) { + throw new IllegalArgumentException("View " + drawer + " is not a drawer"); + } + return ((LayoutParams) drawer.getLayoutParams()).knownOpen; + } + + /** + * Check if the given drawer view is currently in an open state. + * To be considered "open" the drawer must have settled into its fully + * visible state. If there is no drawer with the given gravity this method + * will return false. + * + * @param drawerGravity Gravity of the drawer to check + * @return true if the given drawer view is in an open state + */ + public boolean isDrawerOpen(@EdgeGravity int drawerGravity) { + final View drawerView = findDrawerWithGravity(drawerGravity); + if (drawerView != null) { + return isDrawerOpen(drawerView); + } + return false; + } + + /** + * Check if a given drawer view is currently visible on-screen. The drawer + * may be only peeking onto the screen, fully extended, or anywhere inbetween. + * + * @param drawer Drawer view to check + * @return true if the given drawer is visible on-screen + * @see #isDrawerOpen(android.view.View) + */ + public boolean isDrawerVisible(View drawer) { + if (!isDrawerView(drawer)) { + throw new IllegalArgumentException("View " + drawer + " is not a drawer"); + } + return ((LayoutParams) drawer.getLayoutParams()).onScreen > 0; + } + + /** + * Check if a given drawer view is currently visible on-screen. The drawer + * may be only peeking onto the screen, fully extended, or anywhere in between. + * If there is no drawer with the given gravity this method will return false. + * + * @param drawerGravity Gravity of the drawer to check + * @return true if the given drawer is visible on-screen + */ + public boolean isDrawerVisible(@EdgeGravity int drawerGravity) { + final View drawerView = findDrawerWithGravity(drawerGravity); + if (drawerView != null) { + return isDrawerVisible(drawerView); + } + return false; + } + + private boolean hasPeekingDrawer() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams(); + if (lp.isPeeking) { + return true; + } + } + return false; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams + ? new LayoutParams((LayoutParams) p) + : p instanceof ViewGroup.MarginLayoutParams + ? new LayoutParams((MarginLayoutParams) p) + : new LayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + private boolean hasVisibleDrawer() { + return findVisibleDrawer() != null; + } + + private View findVisibleDrawer() { + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (isDrawerView(child) && isDrawerVisible(child)) { + return child; + } + } + return null; + } + + void cancelChildViewTouch() { + // Cancel child touches + if (!mChildrenCanceledTouch) { + final long now = SystemClock.uptimeMillis(); + final MotionEvent cancelEvent = MotionEvent.obtain(now, now, + MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + getChildAt(i).dispatchTouchEvent(cancelEvent); + } + cancelEvent.recycle(); + mChildrenCanceledTouch = true; + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK && hasVisibleDrawer()) { + KeyEventCompat.startTracking(event); + return true; + } + return super.onKeyDown(keyCode, event); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_BACK) { + final View visibleDrawer = findVisibleDrawer(); + if (visibleDrawer != null && getDrawerLockMode(visibleDrawer) == LOCK_MODE_UNLOCKED) { + closeDrawers(); + } + return visibleDrawer != null; + } + return super.onKeyUp(keyCode, event); + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + final SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (ss.openDrawerGravity != Gravity.NO_GRAVITY) { + final View toOpen = findDrawerWithGravity(ss.openDrawerGravity); + if (toOpen != null) { + openDrawer(toOpen); + } + } + + setDrawerLockMode(ss.lockModeLeft, Gravity.LEFT); + setDrawerLockMode(ss.lockModeRight, Gravity.RIGHT); + } + + @Override + protected Parcelable onSaveInstanceState() { + final Parcelable superState = super.onSaveInstanceState(); + final SavedState ss = new SavedState(superState); + + final View openDrawer = findOpenDrawer(); + if (openDrawer != null) { + ss.openDrawerGravity = ((LayoutParams) openDrawer.getLayoutParams()).gravity; + } + + ss.lockModeLeft = mLockModeLeft; + ss.lockModeRight = mLockModeRight; + + return ss; + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + super.addView(child, index, params); + + final View openDrawer = findOpenDrawer(); + if (openDrawer != null || isDrawerView(child)) { + // A drawer is already open or the new view is a drawer, so the + // new view should start out hidden. + ViewCompat.setImportantForAccessibility(child, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS); + } else { + // Otherwise this is a content view and no drawer is open, so the + // new view should start out visible. + ViewCompat.setImportantForAccessibility(child, + ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + } + + // We only need a delegate here if the framework doesn't understand + // NO_HIDE_DESCENDANTS importance. + if (!CAN_HIDE_DESCENDANTS) { + ViewCompat.setAccessibilityDelegate(child, mChildAccessibilityDelegate); + } + } + + private static boolean includeChildForAccessibility(View child) { + // If the child is not important for accessibility we make + // sure this hides the entire subtree rooted at it as the + // IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDATS is not + // supported on older platforms but we want to hide the entire + // content and not opened drawers if a drawer is opened. + return ViewCompat.getImportantForAccessibility(child) + != ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS + && ViewCompat.getImportantForAccessibility(child) + != ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO; + } + + /** + * State persisted across instances + */ + protected static class SavedState extends BaseSavedState { + int openDrawerGravity = Gravity.NO_GRAVITY; + int lockModeLeft = LOCK_MODE_UNLOCKED; + int lockModeRight = LOCK_MODE_UNLOCKED; + + public SavedState(Parcel in) { + super(in); + openDrawerGravity = in.readInt(); + } + + public SavedState(Parcelable superState) { + super(superState); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(openDrawerGravity); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel source) { + return new SavedState(source); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + private class ViewDragCallback extends ViewDragHelper.Callback { + private final int mAbsGravity; + private ViewDragHelper mDragger; + + private final Runnable mPeekRunnable = new Runnable() { + @Override public void run() { + peekDrawer(); + } + }; + + public ViewDragCallback(int gravity) { + mAbsGravity = gravity; + } + + public void setDragger(ViewDragHelper dragger) { + mDragger = dragger; + } + + public void removeCallbacks() { + DrawerLayout.this.removeCallbacks(mPeekRunnable); + } + + @Override + public boolean tryCaptureView(View child, int pointerId) { + // Only capture views where the gravity matches what we're looking for. + // This lets us use two ViewDragHelpers, one for each side drawer. + return isDrawerView(child) && checkDrawerViewAbsoluteGravity(child, mAbsGravity) + && getDrawerLockMode(child) == LOCK_MODE_UNLOCKED; + } + + @Override + public void onViewDragStateChanged(int state) { + updateDrawerState(mAbsGravity, state, mDragger.getCapturedView()); + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + float offset; + final int childWidth = changedView.getWidth(); + + // This reverses the positioning shown in onLayout. + if (checkDrawerViewAbsoluteGravity(changedView, Gravity.LEFT)) { + offset = (float) (childWidth + left) / childWidth; + } else { + final int width = getWidth(); + offset = (float) (width - left) / childWidth; + } + setDrawerViewOffset(changedView, offset); + changedView.setVisibility(offset == 0 ? INVISIBLE : VISIBLE); + invalidate(); + } + + @Override + public void onViewCaptured(View capturedChild, int activePointerId) { + final LayoutParams lp = (LayoutParams) capturedChild.getLayoutParams(); + lp.isPeeking = false; + + closeOtherDrawer(); + } + + private void closeOtherDrawer() { + final int otherGrav = mAbsGravity == Gravity.LEFT ? Gravity.RIGHT : Gravity.LEFT; + final View toClose = findDrawerWithGravity(otherGrav); + if (toClose != null) { + closeDrawer(toClose); + } + } + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + // Offset is how open the drawer is, therefore left/right values + // are reversed from one another. + final float offset = getDrawerViewOffset(releasedChild); + final int childWidth = releasedChild.getWidth(); + + int left; + if (checkDrawerViewAbsoluteGravity(releasedChild, Gravity.LEFT)) { + left = xvel > 0 || xvel == 0 && offset > 0.5f ? 0 : -childWidth; + } else { + final int width = getWidth(); + left = xvel < 0 || xvel == 0 && offset > 0.5f ? width - childWidth : width; + } + + mDragger.settleCapturedViewAt(left, releasedChild.getTop()); + invalidate(); + } + + @Override + public void onEdgeTouched(int edgeFlags, int pointerId) { + postDelayed(mPeekRunnable, PEEK_DELAY); + } + + private void peekDrawer() { + final View toCapture; + final int childLeft; + final int peekDistance = mDragger.getEdgeSize(); + final boolean leftEdge = mAbsGravity == Gravity.LEFT; + if (leftEdge) { + toCapture = findDrawerWithGravity(Gravity.LEFT); + childLeft = (toCapture != null ? -toCapture.getWidth() : 0) + peekDistance; + } else { + toCapture = findDrawerWithGravity(Gravity.RIGHT); + childLeft = getWidth() - peekDistance; + } + // Only peek if it would mean making the drawer more visible and the drawer isn't locked + if (toCapture != null && ((leftEdge && toCapture.getLeft() < childLeft) || + (!leftEdge && toCapture.getLeft() > childLeft)) && + getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { + final LayoutParams lp = (LayoutParams) toCapture.getLayoutParams(); + mDragger.smoothSlideViewTo(toCapture, childLeft, toCapture.getTop()); + lp.isPeeking = true; + invalidate(); + + closeOtherDrawer(); + + cancelChildViewTouch(); + } + } + + @Override + public boolean onEdgeLock(int edgeFlags) { + if (ALLOW_EDGE_LOCK) { + final View drawer = findDrawerWithGravity(mAbsGravity); + if (drawer != null && !isDrawerOpen(drawer)) { + closeDrawer(drawer); + } + return true; + } + return false; + } + + @Override + public void onEdgeDragStarted(int edgeFlags, int pointerId) { + final View toCapture; + if ((edgeFlags & ViewDragHelper.EDGE_LEFT) == ViewDragHelper.EDGE_LEFT) { + toCapture = findDrawerWithGravity(Gravity.LEFT); + } else { + toCapture = findDrawerWithGravity(Gravity.RIGHT); + } + + if (toCapture != null && getDrawerLockMode(toCapture) == LOCK_MODE_UNLOCKED) { + mDragger.captureChildView(toCapture, pointerId); + } + } + + @Override + public int getViewHorizontalDragRange(View child) { + return isDrawerView(child) ? child.getWidth() : 0; + } + + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + if (checkDrawerViewAbsoluteGravity(child, Gravity.LEFT)) { + return Math.max(-child.getWidth(), Math.min(left, 0)); + } else { + final int width = getWidth(); + return Math.max(width - child.getWidth(), Math.min(left, width)); + } + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + return child.getTop(); + } + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + + public int gravity = Gravity.NO_GRAVITY; + float onScreen; + boolean isPeeking; + boolean knownOpen; + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + this.gravity = a.getInt(0, Gravity.NO_GRAVITY); + a.recycle(); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(int width, int height, int gravity) { + this(width, height); + this.gravity = gravity; + } + + public LayoutParams(LayoutParams source) { + super(source); + this.gravity = source.gravity; + } + + public LayoutParams(ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(ViewGroup.MarginLayoutParams source) { + super(source); + } + } + + class AccessibilityDelegate extends AccessibilityDelegateCompat { + private final Rect mTmpRect = new Rect(); + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + if (CAN_HIDE_DESCENDANTS) { + super.onInitializeAccessibilityNodeInfo(host, info); + } else { + // Obtain a node for the host, then manually generate the list + // of children to only include non-obscured views. + final AccessibilityNodeInfoCompat superNode = + AccessibilityNodeInfoCompat.obtain(info); + super.onInitializeAccessibilityNodeInfo(host, superNode); + + info.setSource(host); + final ViewParent parent = ViewCompat.getParentForAccessibility(host); + if (parent instanceof View) { + info.setParent((View) parent); + } + copyNodeInfoNoChildren(info, superNode); + superNode.recycle(); + + addChildrenForAccessibility(info, (ViewGroup) host); + } + + info.setClassName(DrawerLayout.class.getName()); + + // This view reports itself as focusable so that it can intercept + // the back button, but we should prevent this view from reporting + // itself as focusable to accessibility services. + info.setFocusable(false); + info.setFocused(false); + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + + event.setClassName(DrawerLayout.class.getName()); + } + + @Override + public boolean dispatchPopulateAccessibilityEvent(View host, AccessibilityEvent event) { + // Special case to handle window state change events. As far as + // accessibility services are concerned, state changes from + // DrawerLayout invalidate the entire contents of the screen (like + // an Activity or Dialog) and they should announce the title of the + // new content. + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED) { + final List eventText = event.getText(); + final View visibleDrawer = findVisibleDrawer(); + if (visibleDrawer != null) { + final int edgeGravity = getDrawerViewAbsoluteGravity(visibleDrawer); + final CharSequence title = getDrawerTitle(edgeGravity); + if (title != null) { + eventText.add(title); + } + } + + return true; + } + + return super.dispatchPopulateAccessibilityEvent(host, event); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + if (CAN_HIDE_DESCENDANTS || includeChildForAccessibility(child)) { + return super.onRequestSendAccessibilityEvent(host, child, event); + } + return false; + } + + private void addChildrenForAccessibility(AccessibilityNodeInfoCompat info, ViewGroup v) { + final int childCount = v.getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = v.getChildAt(i); + if (includeChildForAccessibility(child)) { + info.addChild(child); + } + } + } + + /** + * This should really be in AccessibilityNodeInfoCompat, but there unfortunately + * seem to be a few elements that are not easily cloneable using the underlying API. + * Leave it private here as it's not general-purpose useful. + */ + private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, + AccessibilityNodeInfoCompat src) { + final Rect rect = mTmpRect; + + src.getBoundsInParent(rect); + dest.setBoundsInParent(rect); + + src.getBoundsInScreen(rect); + dest.setBoundsInScreen(rect); + + dest.setVisibleToUser(src.isVisibleToUser()); + dest.setPackageName(src.getPackageName()); + dest.setClassName(src.getClassName()); + dest.setContentDescription(src.getContentDescription()); + + dest.setEnabled(src.isEnabled()); + dest.setClickable(src.isClickable()); + dest.setFocusable(src.isFocusable()); + dest.setFocused(src.isFocused()); + dest.setAccessibilityFocused(src.isAccessibilityFocused()); + dest.setSelected(src.isSelected()); + dest.setLongClickable(src.isLongClickable()); + + dest.addAction(src.getActions()); + } + } + + final class ChildAccessibilityDelegate extends AccessibilityDelegateCompat { + @Override + public void onInitializeAccessibilityNodeInfo(View child, + AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(child, info); + + if (!includeChildForAccessibility(child)) { + // If we are ignoring the sub-tree rooted at the child, + // break the connection to the rest of the node tree. + // For details refer to includeChildForAccessibility. + info.setParent(null); + } + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutCompatApi21.java b/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutCompatApi21.java new file mode 100644 index 00000000000..07cc3faeaea --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutCompatApi21.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowInsets; + +/** + * Provides functionality for DrawerLayout unique to API 21 + */ +class DrawerLayoutCompatApi21 { + + private static final int[] THEME_ATTRS = { + android.R.attr.colorPrimaryDark + }; + + public static void configureApplyInsets(View drawerLayout) { + if (drawerLayout instanceof DrawerLayoutImpl) { + drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener()); + drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); + } + } + + public static void dispatchChildInsets(View child, Object insets, int gravity) { + WindowInsets wi = (WindowInsets) insets; + if (gravity == Gravity.LEFT) { + wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), + wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom()); + } else if (gravity == Gravity.RIGHT) { + wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), + wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom()); + } + child.dispatchApplyWindowInsets(wi); + } + + public static void applyMarginInsets(ViewGroup.MarginLayoutParams lp, Object insets, + int gravity) { + WindowInsets wi = (WindowInsets) insets; + if (gravity == Gravity.LEFT) { + wi = wi.replaceSystemWindowInsets(wi.getSystemWindowInsetLeft(), + wi.getSystemWindowInsetTop(), 0, wi.getSystemWindowInsetBottom()); + } else if (gravity == Gravity.RIGHT) { + wi = wi.replaceSystemWindowInsets(0, wi.getSystemWindowInsetTop(), + wi.getSystemWindowInsetRight(), wi.getSystemWindowInsetBottom()); + } + lp.leftMargin = wi.getSystemWindowInsetLeft(); + lp.topMargin = wi.getSystemWindowInsetTop(); + lp.rightMargin = wi.getSystemWindowInsetRight(); + lp.bottomMargin = wi.getSystemWindowInsetBottom(); + } + + public static int getTopInset(Object insets) { + return insets != null ? ((WindowInsets) insets).getSystemWindowInsetTop() : 0; + } + + public static Drawable getDefaultStatusBarBackground(Context context) { + final TypedArray a = context.obtainStyledAttributes(THEME_ATTRS); + try { + return a.getDrawable(0); + } finally { + a.recycle(); + } + } + + static class InsetsListener implements View.OnApplyWindowInsetsListener { + @Override + public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { + final DrawerLayoutImpl drawerLayout = (DrawerLayoutImpl) v; + drawerLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0); + return insets.consumeSystemWindowInsets(); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutImpl.java b/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutImpl.java new file mode 100644 index 00000000000..02545c19bf2 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/DrawerLayoutImpl.java @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.widget; + +/** + * Interface used to communicate from the v21-specific code for configuring a DrawerLayout + * to the DrawerLayout itself. + */ +interface DrawerLayoutImpl { + void setChildInsets(Object insets, boolean drawStatusBar); +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompat.java new file mode 100644 index 00000000000..55b1319f66c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompat.java @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.os.Build; + +/** + * Helper for accessing {@link android.widget.EdgeEffect} introduced after + * API level 4 in a backwards compatible fashion. + * + * This class is used to access {@link android.widget.EdgeEffect} on platform versions + * that support it. When running on older platforms it will result in no-ops. It should + * be used by views that wish to use the standard Android visual effects at the edges + * of scrolling containers. + */ +public class EdgeEffectCompat { + private Object mEdgeEffect; + + private static final EdgeEffectImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 21) { + IMPL = new EdgeEffectLollipopImpl(); // Lollipop + } else if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new EdgeEffectIcsImpl(); + } else { + IMPL = new BaseEdgeEffectImpl(); + } + } + + interface EdgeEffectImpl { + public Object newEdgeEffect(Context context); + public void setSize(Object edgeEffect, int width, int height); + public boolean isFinished(Object edgeEffect); + public void finish(Object edgeEffect); + public boolean onPull(Object edgeEffect, float deltaDistance); + public boolean onRelease(Object edgeEffect); + public boolean onAbsorb(Object edgeEffect, int velocity); + public boolean draw(Object edgeEffect, Canvas canvas); + public boolean onPull(Object edgeEffect, float deltaDistance, float displacement); + } + + /** + * Null implementation to use pre-ICS + */ + static class BaseEdgeEffectImpl implements EdgeEffectImpl { + public Object newEdgeEffect(Context context) { + return null; + } + + public void setSize(Object edgeEffect, int width, int height) { + } + + public boolean isFinished(Object edgeEffect) { + return true; + } + + public void finish(Object edgeEffect) { + } + + public boolean onPull(Object edgeEffect, float deltaDistance) { + return false; + } + + public boolean onRelease(Object edgeEffect) { + return false; + } + + public boolean onAbsorb(Object edgeEffect, int velocity) { + return false; + } + + public boolean draw(Object edgeEffect, Canvas canvas) { + return false; + } + + public boolean onPull(Object edgeEffect, float deltaDistance, float displacement) { + return false; + } + } + + static class EdgeEffectIcsImpl implements EdgeEffectImpl { + public Object newEdgeEffect(Context context) { + return EdgeEffectCompatIcs.newEdgeEffect(context); + } + + public void setSize(Object edgeEffect, int width, int height) { + EdgeEffectCompatIcs.setSize(edgeEffect, width, height); + } + + public boolean isFinished(Object edgeEffect) { + return EdgeEffectCompatIcs.isFinished(edgeEffect); + } + + public void finish(Object edgeEffect) { + EdgeEffectCompatIcs.finish(edgeEffect); + } + + public boolean onPull(Object edgeEffect, float deltaDistance) { + return EdgeEffectCompatIcs.onPull(edgeEffect, deltaDistance); + } + + public boolean onRelease(Object edgeEffect) { + return EdgeEffectCompatIcs.onRelease(edgeEffect); + } + + public boolean onAbsorb(Object edgeEffect, int velocity) { + return EdgeEffectCompatIcs.onAbsorb(edgeEffect, velocity); + } + + public boolean draw(Object edgeEffect, Canvas canvas) { + return EdgeEffectCompatIcs.draw(edgeEffect, canvas); + } + + public boolean onPull(Object edgeEffect, float deltaDistance, float displacement) { + return EdgeEffectCompatIcs.onPull(edgeEffect, deltaDistance); + } + } + + static class EdgeEffectLollipopImpl extends EdgeEffectIcsImpl { + public boolean onPull(Object edgeEffect, float deltaDistance, float displacement) { + return EdgeEffectCompatLollipop.onPull(edgeEffect, deltaDistance, displacement); + } + } + + /** + * Construct a new EdgeEffect themed using the given context. + * + *

    Note: On platform versions that do not support EdgeEffect, all operations + * on the newly constructed object will be mocked/no-ops.

    + * + * @param context Context to use for theming the effect + */ + public EdgeEffectCompat(Context context) { + mEdgeEffect = IMPL.newEdgeEffect(context); + } + + /** + * Set the size of this edge effect in pixels. + * + * @param width Effect width in pixels + * @param height Effect height in pixels + */ + public void setSize(int width, int height) { + IMPL.setSize(mEdgeEffect, width, height); + } + + /** + * Reports if this EdgeEffectCompat's animation is finished. If this method returns false + * after a call to {@link #draw(Canvas)} the host widget should schedule another + * drawing pass to continue the animation. + * + * @return true if animation is finished, false if drawing should continue on the next frame. + */ + public boolean isFinished() { + return IMPL.isFinished(mEdgeEffect); + } + + /** + * Immediately finish the current animation. + * After this call {@link #isFinished()} will return true. + */ + public void finish() { + IMPL.finish(mEdgeEffect); + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} if this method + * returns true and draw the results accordingly. + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + * @return true if the host view should call invalidate, false if it should not. + * @deprecated use {@link #onPull(float, float)} + */ + public boolean onPull(float deltaDistance) { + return IMPL.onPull(mEdgeEffect, deltaDistance); + } + + /** + * A view should call this when content is pulled away from an edge by the user. + * This will update the state of the current visual effect and its associated animation. + * The host view should always {@link android.view.View#invalidate()} if this method + * returns true and draw the results accordingly. + * + * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to + * 1.f (full length of the view) or negative values to express change + * back toward the edge reached to initiate the effect. + * @param displacement The displacement from the starting side of the effect of the point + * initiating the pull. In the case of touch this is the finger position. + * Values may be from 0-1. + * @return true if the host view should call invalidate, false if it should not. + */ + public boolean onPull(float deltaDistance, float displacement) { + return IMPL.onPull(mEdgeEffect, deltaDistance, displacement); + } + + /** + * Call when the object is released after being pulled. + * This will begin the "decay" phase of the effect. After calling this method + * the host view should {@link android.view.View#invalidate()} if this method + * returns true and thereby draw the results accordingly. + * + * @return true if the host view should invalidate, false if it should not. + */ + public boolean onRelease() { + return IMPL.onRelease(mEdgeEffect); + } + + /** + * Call when the effect absorbs an impact at the given velocity. + * Used when a fling reaches the scroll boundary. + * + *

    When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller}, + * the method getCurrVelocity will provide a reasonable approximation + * to use here.

    + * + * @param velocity Velocity at impact in pixels per second. + * @return true if the host view should invalidate, false if it should not. + */ + public boolean onAbsorb(int velocity) { + return IMPL.onAbsorb(mEdgeEffect, velocity); + } + + /** + * Draw into the provided canvas. Assumes that the canvas has been rotated + * accordingly and the size has been set. The effect will be drawn the full + * width of X=0 to X=width, beginning from Y=0 and extending to some factor < + * 1.f of height. + * + * @param canvas Canvas to draw into + * @return true if drawing should continue beyond this frame to continue the + * animation + */ + public boolean draw(Canvas canvas) { + return IMPL.draw(mEdgeEffect, canvas); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatIcs.java new file mode 100644 index 00000000000..c02eeb4804c --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatIcs.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.support.v4.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.widget.EdgeEffect; + +/** + * Stub implementation that contains a real EdgeEffect on ICS. + * + * This class is an implementation detail for EdgeEffectCompat + * and should not be used directly. + */ +class EdgeEffectCompatIcs { + public static Object newEdgeEffect(Context context) { + return new EdgeEffect(context); + } + + public static void setSize(Object edgeEffect, int width, int height) { + ((EdgeEffect) edgeEffect).setSize(width, height); + } + + public static boolean isFinished(Object edgeEffect) { + return ((EdgeEffect) edgeEffect).isFinished(); + } + + public static void finish(Object edgeEffect) { + ((EdgeEffect) edgeEffect).finish(); + } + + public static boolean onPull(Object edgeEffect, float deltaDistance) { + ((EdgeEffect) edgeEffect).onPull(deltaDistance); + return true; + } + + public static boolean onRelease(Object edgeEffect) { + EdgeEffect eff = (EdgeEffect) edgeEffect; + eff.onRelease(); + return eff.isFinished(); + } + + public static boolean onAbsorb(Object edgeEffect, int velocity) { + ((EdgeEffect) edgeEffect).onAbsorb(velocity); + return true; + } + + public static boolean draw(Object edgeEffect, Canvas canvas) { + return ((EdgeEffect) edgeEffect).draw(canvas); + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatLollipop.java b/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatLollipop.java new file mode 100644 index 00000000000..6ba379cf999 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/EdgeEffectCompatLollipop.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.widget; + +import android.widget.EdgeEffect; + +class EdgeEffectCompatLollipop { + public static boolean onPull(Object edgeEffect, float deltaDistance, float displacement) { + ((EdgeEffect) edgeEffect).onPull(deltaDistance, displacement); + return true; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ExploreByTouchHelper.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ExploreByTouchHelper.java new file mode 100644 index 00000000000..7adbc6f7ab4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ExploreByTouchHelper.java @@ -0,0 +1,725 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.graphics.Rect; +import android.os.Bundle; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.ViewParentCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityManagerCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityNodeProviderCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityManager; + +import java.util.LinkedList; +import java.util.List; + +/** + * ExploreByTouchHelper is a utility class for implementing accessibility + * support in custom {@link View}s that represent a collection of View-like + * logical items. It extends {@link AccessibilityNodeProviderCompat} and + * simplifies many aspects of providing information to accessibility services + * and managing accessibility focus. This class does not currently support + * hierarchies of logical items. + *

    + * This should be applied to the parent view using + * {@link ViewCompat#setAccessibilityDelegate}: + * + *

    + * mAccessHelper = ExploreByTouchHelper.create(someView, mAccessHelperCallback);
    + * ViewCompat.setAccessibilityDelegate(someView, mAccessHelper);
    + * 
    + */ +public abstract class ExploreByTouchHelper extends AccessibilityDelegateCompat { + /** Virtual node identifier value for invalid nodes. */ + public static final int INVALID_ID = Integer.MIN_VALUE; + + /** Default class name used for virtual views. */ + private static final String DEFAULT_CLASS_NAME = View.class.getName(); + + // Temporary, reusable data structures. + private final Rect mTempScreenRect = new Rect(); + private final Rect mTempParentRect = new Rect(); + private final Rect mTempVisibleRect = new Rect(); + private final int[] mTempGlobalRect = new int[2]; + + /** System accessibility manager, used to check state and send events. */ + private final AccessibilityManager mManager; + + /** View whose internal structure is exposed through this helper. */ + private final View mView; + + /** Node provider that handles creating nodes and performing actions. */ + private ExploreByTouchNodeProvider mNodeProvider; + + /** Virtual view id for the currently focused logical item. */ + private int mFocusedVirtualViewId = INVALID_ID; + + /** Virtual view id for the currently hovered logical item. */ + private int mHoveredVirtualViewId = INVALID_ID; + + /** + * Factory method to create a new {@link ExploreByTouchHelper}. + * + * @param forView View whose logical children are exposed by this helper. + */ + public ExploreByTouchHelper(View forView) { + if (forView == null) { + throw new IllegalArgumentException("View may not be null"); + } + + mView = forView; + final Context context = forView.getContext(); + mManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); + } + + /** + * Returns the {@link AccessibilityNodeProviderCompat} for this helper. + * + * @param host View whose logical children are exposed by this helper. + * @return The accessibility node provider for this helper. + */ + @Override + public AccessibilityNodeProviderCompat getAccessibilityNodeProvider(View host) { + if (mNodeProvider == null) { + mNodeProvider = new ExploreByTouchNodeProvider(); + } + return mNodeProvider; + } + + /** + * Dispatches hover {@link MotionEvent}s to the virtual view hierarchy when + * the Explore by Touch feature is enabled. + *

    + * This method should be called by overriding + * {@link View#dispatchHoverEvent}: + * + *

    @Override
    +     * public boolean dispatchHoverEvent(MotionEvent event) {
    +     *   if (mHelper.dispatchHoverEvent(this, event) {
    +     *     return true;
    +     *   }
    +     *   return super.dispatchHoverEvent(event);
    +     * }
    +     * 
    + * + * @param event The hover event to dispatch to the virtual view hierarchy. + * @return Whether the hover event was handled. + */ + public boolean dispatchHoverEvent(MotionEvent event) { + if (!mManager.isEnabled() + || !AccessibilityManagerCompat.isTouchExplorationEnabled(mManager)) { + return false; + } + + switch (event.getAction()) { + case MotionEventCompat.ACTION_HOVER_MOVE: + case MotionEventCompat.ACTION_HOVER_ENTER: + final int virtualViewId = getVirtualViewAt(event.getX(), event.getY()); + updateHoveredVirtualView(virtualViewId); + return (virtualViewId != INVALID_ID); + case MotionEventCompat.ACTION_HOVER_EXIT: + if (mFocusedVirtualViewId != INVALID_ID) { + updateHoveredVirtualView(INVALID_ID); + return true; + } + return false; + default: + return false; + } + } + + /** + * Populates an event of the specified type with information about an item + * and attempts to send it up through the view hierarchy. + *

    + * You should call this method after performing a user action that normally + * fires an accessibility event, such as clicking on an item. + * + *

    public void performItemClick(T item) {
    +     *   ...
    +     *   sendEventForVirtualViewId(item.id, AccessibilityEvent.TYPE_VIEW_CLICKED);
    +     * }
    +     * 
    + * + * @param virtualViewId The virtual view id for which to send an event. + * @param eventType The type of event to send. + * @return true if the event was sent successfully. + */ + public boolean sendEventForVirtualView(int virtualViewId, int eventType) { + if ((virtualViewId == INVALID_ID) || !mManager.isEnabled()) { + return false; + } + + final ViewParent parent = mView.getParent(); + if (parent == null) { + return false; + } + + final AccessibilityEvent event = createEvent(virtualViewId, eventType); + return ViewParentCompat.requestSendAccessibilityEvent(parent, mView, event); + } + + /** + * Notifies the accessibility framework that the properties of the parent + * view have changed. + *

    + * You must call this method after adding or removing items from the + * parent view. + */ + public void invalidateRoot() { + invalidateVirtualView(View.NO_ID); + } + + /** + * Notifies the accessibility framework that the properties of a particular + * item have changed. + *

    + * You must call this method after changing any of the properties set + * in {@link #onPopulateNodeForVirtualView}. + * + * @param virtualViewId The virtual view id to invalidate. + */ + public void invalidateVirtualView(int virtualViewId) { + sendEventForVirtualView( + virtualViewId, AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED); + } + + /** + * Returns the virtual view id for the currently focused item, + * + * @return A virtual view id, or {@link #INVALID_ID} if no item is + * currently focused. + */ + public int getFocusedVirtualView() { + return mFocusedVirtualViewId; + } + + /** + * Sets the currently hovered item, sending hover accessibility events as + * necessary to maintain the correct state. + * + * @param virtualViewId The virtual view id for the item currently being + * hovered, or {@link #INVALID_ID} if no item is hovered within + * the parent view. + */ + private void updateHoveredVirtualView(int virtualViewId) { + if (mHoveredVirtualViewId == virtualViewId) { + return; + } + + final int previousVirtualViewId = mHoveredVirtualViewId; + mHoveredVirtualViewId = virtualViewId; + + // Stay consistent with framework behavior by sending ENTER/EXIT pairs + // in reverse order. This is accurate as of API 18. + sendEventForVirtualView(virtualViewId, AccessibilityEventCompat.TYPE_VIEW_HOVER_ENTER); + sendEventForVirtualView( + previousVirtualViewId, AccessibilityEventCompat.TYPE_VIEW_HOVER_EXIT); + } + + /** + * Constructs and returns an {@link AccessibilityEvent} for the specified + * virtual view id, which includes the host view ({@link View#NO_ID}). + * + * @param virtualViewId The virtual view id for the item for which to + * construct an event. + * @param eventType The type of event to construct. + * @return An {@link AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEvent(int virtualViewId, int eventType) { + switch (virtualViewId) { + case View.NO_ID: + return createEventForHost(eventType); + default: + return createEventForChild(virtualViewId, eventType); + } + } + + /** + * Constructs and returns an {@link AccessibilityEvent} for the host node. + * + * @param eventType The type of event to construct. + * @return An {@link AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEventForHost(int eventType) { + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + ViewCompat.onInitializeAccessibilityEvent(mView, event); + return event; + } + + /** + * Constructs and returns an {@link AccessibilityEvent} populated with + * information about the specified item. + * + * @param virtualViewId The virtual view id for the item for which to + * construct an event. + * @param eventType The type of event to construct. + * @return An {@link AccessibilityEvent} populated with information about + * the specified item. + */ + private AccessibilityEvent createEventForChild(int virtualViewId, int eventType) { + final AccessibilityEvent event = AccessibilityEvent.obtain(eventType); + event.setEnabled(true); + event.setClassName(DEFAULT_CLASS_NAME); + + // Allow the client to populate the event. + onPopulateEventForVirtualView(virtualViewId, event); + + // Make sure the developer is following the rules. + if (event.getText().isEmpty() && (event.getContentDescription() == null)) { + throw new RuntimeException("Callbacks must add text or a content description in " + + "populateEventForVirtualViewId()"); + } + + // Don't allow the client to override these properties. + event.setPackageName(mView.getContext().getPackageName()); + + final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); + record.setSource(mView, virtualViewId); + + return event; + } + + /** + * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the + * specified virtual view id, which includes the host view + * ({@link View#NO_ID}). + * + * @param virtualViewId The virtual view id for the item for which to + * construct a node. + * @return An {@link AccessibilityNodeInfoCompat} populated with information + * about the specified item. + */ + private AccessibilityNodeInfoCompat createNode(int virtualViewId) { + switch (virtualViewId) { + case View.NO_ID: + return createNodeForHost(); + default: + return createNodeForChild(virtualViewId); + } + } + + /** + * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the + * host view populated with its virtual descendants. + * + * @return An {@link AccessibilityNodeInfoCompat} for the parent node. + */ + private AccessibilityNodeInfoCompat createNodeForHost() { + final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain(mView); + ViewCompat.onInitializeAccessibilityNodeInfo(mView, node); + + // Add the virtual descendants. + final LinkedList virtualViewIds = new LinkedList(); + getVisibleVirtualViews(virtualViewIds); + + for (Integer childVirtualViewId : virtualViewIds) { + node.addChild(mView, childVirtualViewId); + } + + return node; + } + + /** + * Constructs and returns an {@link AccessibilityNodeInfoCompat} for the + * specified item. Automatically manages accessibility focus actions. + *

    + * Allows the implementing class to specify most node properties, but + * overrides the following: + *

      + *
    • {@link AccessibilityNodeInfoCompat#setPackageName} + *
    • {@link AccessibilityNodeInfoCompat#setClassName} + *
    • {@link AccessibilityNodeInfoCompat#setParent(View)} + *
    • {@link AccessibilityNodeInfoCompat#setSource(View, int)} + *
    • {@link AccessibilityNodeInfoCompat#setVisibleToUser} + *
    • {@link AccessibilityNodeInfoCompat#setBoundsInScreen(Rect)} + *
    + *

    + * Uses the bounds of the parent view and the parent-relative bounding + * rectangle specified by + * {@link AccessibilityNodeInfoCompat#getBoundsInParent} to automatically + * update the following properties: + *

      + *
    • {@link AccessibilityNodeInfoCompat#setVisibleToUser} + *
    • {@link AccessibilityNodeInfoCompat#setBoundsInParent} + *
    + * + * @param virtualViewId The virtual view id for item for which to construct + * a node. + * @return An {@link AccessibilityNodeInfoCompat} for the specified item. + */ + private AccessibilityNodeInfoCompat createNodeForChild(int virtualViewId) { + final AccessibilityNodeInfoCompat node = AccessibilityNodeInfoCompat.obtain(); + + // Ensure the client has good defaults. + node.setEnabled(true); + node.setClassName(DEFAULT_CLASS_NAME); + + // Allow the client to populate the node. + onPopulateNodeForVirtualView(virtualViewId, node); + + // Make sure the developer is following the rules. + if ((node.getText() == null) && (node.getContentDescription() == null)) { + throw new RuntimeException("Callbacks must add text or a content description in " + + "populateNodeForVirtualViewId()"); + } + + node.getBoundsInParent(mTempParentRect); + if (mTempParentRect.isEmpty()) { + throw new RuntimeException("Callbacks must set parent bounds in " + + "populateNodeForVirtualViewId()"); + } + + final int actions = node.getActions(); + if ((actions & AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS) != 0) { + throw new RuntimeException("Callbacks must not add ACTION_ACCESSIBILITY_FOCUS in " + + "populateNodeForVirtualViewId()"); + } + if ((actions & AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS) != 0) { + throw new RuntimeException("Callbacks must not add ACTION_CLEAR_ACCESSIBILITY_FOCUS in " + + "populateNodeForVirtualViewId()"); + } + + // Don't allow the client to override these properties. + node.setPackageName(mView.getContext().getPackageName()); + node.setSource(mView, virtualViewId); + node.setParent(mView); + + // Manage internal accessibility focus state. + if (mFocusedVirtualViewId == virtualViewId) { + node.setAccessibilityFocused(true); + node.addAction(AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS); + } else { + node.setAccessibilityFocused(false); + node.addAction(AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS); + } + + // Set the visibility based on the parent bound. + if (intersectVisibleToUser(mTempParentRect)) { + node.setVisibleToUser(true); + node.setBoundsInParent(mTempParentRect); + } + + // Calculate screen-relative bound. + mView.getLocationOnScreen(mTempGlobalRect); + final int offsetX = mTempGlobalRect[0]; + final int offsetY = mTempGlobalRect[1]; + mTempScreenRect.set(mTempParentRect); + mTempScreenRect.offset(offsetX, offsetY); + node.setBoundsInScreen(mTempScreenRect); + + return node; + } + + private boolean performAction(int virtualViewId, int action, Bundle arguments) { + switch (virtualViewId) { + case View.NO_ID: + return performActionForHost(action, arguments); + default: + return performActionForChild(virtualViewId, action, arguments); + } + } + + private boolean performActionForHost(int action, Bundle arguments) { + return ViewCompat.performAccessibilityAction(mView, action, arguments); + } + + private boolean performActionForChild(int virtualViewId, int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: + case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return manageFocusForChild(virtualViewId, action, arguments); + default: + return onPerformActionForVirtualView(virtualViewId, action, arguments); + } + } + + private boolean manageFocusForChild(int virtualViewId, int action, Bundle arguments) { + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS: + return requestAccessibilityFocus(virtualViewId); + case AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS: + return clearAccessibilityFocus(virtualViewId); + default: + return false; + } + } + + /** + * Computes whether the specified {@link Rect} intersects with the visible + * portion of its parent {@link View}. Modifies {@code localRect} to contain + * only the visible portion. + * + * @param localRect A rectangle in local (parent) coordinates. + * @return Whether the specified {@link Rect} is visible on the screen. + */ + private boolean intersectVisibleToUser(Rect localRect) { + // Missing or empty bounds mean this view is not visible. + if ((localRect == null) || localRect.isEmpty()) { + return false; + } + + // Attached to invisible window means this view is not visible. + if (mView.getWindowVisibility() != View.VISIBLE) { + return false; + } + + // An invisible predecessor means that this view is not visible. + ViewParent viewParent = mView.getParent(); + while (viewParent instanceof View) { + final View view = (View) viewParent; + if ((ViewCompat.getAlpha(view) <= 0) || (view.getVisibility() != View.VISIBLE)) { + return false; + } + viewParent = view.getParent(); + } + + // A null parent implies the view is not visible. + if (viewParent == null) { + return false; + } + + // If no portion of the parent is visible, this view is not visible. + if (!mView.getLocalVisibleRect(mTempVisibleRect)) { + return false; + } + + // Check if the view intersects the visible portion of the parent. + return localRect.intersect(mTempVisibleRect); + } + + /** + * Returns whether this virtual view is accessibility focused. + * + * @return True if the view is accessibility focused. + */ + private boolean isAccessibilityFocused(int virtualViewId) { + return (mFocusedVirtualViewId == virtualViewId); + } + + /** + * Attempts to give accessibility focus to a virtual view. + *

    + * A virtual view will not actually take focus if + * {@link AccessibilityManager#isEnabled()} returns false, + * {@link AccessibilityManager#isTouchExplorationEnabled()} returns false, + * or the view already has accessibility focus. + * + * @param virtualViewId The id of the virtual view on which to place + * accessibility focus. + * @return Whether this virtual view actually took accessibility focus. + */ + private boolean requestAccessibilityFocus(int virtualViewId) { + if (!mManager.isEnabled() + || !AccessibilityManagerCompat.isTouchExplorationEnabled(mManager)) { + return false; + } + // TODO: Check virtual view visibility. + if (!isAccessibilityFocused(virtualViewId)) { + mFocusedVirtualViewId = virtualViewId; + // TODO: Only invalidate virtual view bounds. + mView.invalidate(); + sendEventForVirtualView(virtualViewId, + AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUSED); + return true; + } + return false; + } + + /** + * Attempts to clear accessibility focus from a virtual view. + * + * @param virtualViewId The id of the virtual view from which to clear + * accessibility focus. + * @return Whether this virtual view actually cleared accessibility focus. + */ + private boolean clearAccessibilityFocus(int virtualViewId) { + if (isAccessibilityFocused(virtualViewId)) { + mFocusedVirtualViewId = INVALID_ID; + mView.invalidate(); + sendEventForVirtualView(virtualViewId, + AccessibilityEventCompat.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED); + return true; + } + return false; + } + + /** + * Provides a mapping between view-relative coordinates and logical + * items. + * + * @param x The view-relative x coordinate + * @param y The view-relative y coordinate + * @return virtual view identifier for the logical item under + * coordinates (x,y) or {@link View#NO_ID} if there is no item at + * the given coordinates + */ + protected abstract int getVirtualViewAt(float x, float y); + + /** + * Populates a list with the view's visible items. The ordering of items + * within {@code virtualViewIds} specifies order of accessibility focus + * traversal. + * + * @param virtualViewIds The list to populate with visible items + */ + protected abstract void getVisibleVirtualViews(List virtualViewIds); + + /** + * Populates an {@link AccessibilityEvent} with information about the + * specified item. + *

    + * Implementations must populate the following required fields: + *

      + *
    • event text, see {@link AccessibilityEvent#getText} or + * {@link AccessibilityEvent#setContentDescription} + *
    + *

    + * The helper class automatically populates the following fields with + * default values, but implementations may optionally override them: + *

      + *
    • item class name, set to android.view.View, see + * {@link AccessibilityEvent#setClassName} + *
    + *

    + * The following required fields are automatically populated by the + * helper class and may not be overridden: + *

      + *
    • package name, set to the package of the host view's + * {@link Context}, see {@link AccessibilityEvent#setPackageName} + *
    • event source, set to the host view and virtual view identifier, + * see {@link AccessibilityRecordCompat#setSource(View, int)} + *
    + * + * @param virtualViewId The virtual view id for the item for which to + * populate the event + * @param event The event to populate + */ + protected abstract void onPopulateEventForVirtualView( + int virtualViewId, AccessibilityEvent event); + + /** + * Populates an {@link AccessibilityNodeInfoCompat} with information + * about the specified item. + *

    + * Implementations must populate the following required fields: + *

      + *
    • event text, see {@link AccessibilityNodeInfoCompat#setText} or + * {@link AccessibilityNodeInfoCompat#setContentDescription} + *
    • bounds in parent coordinates, see + * {@link AccessibilityNodeInfoCompat#setBoundsInParent} + *
    + *

    + * The helper class automatically populates the following fields with + * default values, but implementations may optionally override them: + *

      + *
    • enabled state, set to true, see + * {@link AccessibilityNodeInfoCompat#setEnabled} + *
    • item class name, identical to the class name set by + * {@link #onPopulateEventForVirtualView}, see + * {@link AccessibilityNodeInfoCompat#setClassName} + *
    + *

    + * The following required fields are automatically populated by the + * helper class and may not be overridden: + *

      + *
    • package name, identical to the package name set by + * {@link #onPopulateEventForVirtualView}, see + * {@link AccessibilityNodeInfoCompat#setPackageName} + *
    • node source, identical to the event source set in + * {@link #onPopulateEventForVirtualView}, see + * {@link AccessibilityNodeInfoCompat#setSource(View, int)} + *
    • parent view, set to the host view, see + * {@link AccessibilityNodeInfoCompat#setParent(View)} + *
    • visibility, computed based on parent-relative bounds, see + * {@link AccessibilityNodeInfoCompat#setVisibleToUser} + *
    • accessibility focus, computed based on internal helper state, see + * {@link AccessibilityNodeInfoCompat#setAccessibilityFocused} + *
    • bounds in screen coordinates, computed based on host view bounds, + * see {@link AccessibilityNodeInfoCompat#setBoundsInScreen} + *
    + *

    + * Additionally, the helper class automatically handles accessibility + * focus management by adding the appropriate + * {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS} or + * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS} + * action. Implementations must never manually add these actions. + *

    + * The helper class also automatically modifies parent- and + * screen-relative bounds to reflect the portion of the item visible + * within its parent. + * + * @param virtualViewId The virtual view identifier of the item for + * which to populate the node + * @param node The node to populate + */ + protected abstract void onPopulateNodeForVirtualView( + int virtualViewId, AccessibilityNodeInfoCompat node); + + /** + * Performs the specified accessibility action on the item associated + * with the virtual view identifier. See + * {@link AccessibilityNodeInfoCompat#performAction(int, Bundle)} for + * more information. + *

    + * Implementations must handle any actions added manually in + * {@link #onPopulateNodeForVirtualView}. + *

    + * The helper class automatically handles focus management resulting + * from {@link AccessibilityNodeInfoCompat#ACTION_ACCESSIBILITY_FOCUS} + * and + * {@link AccessibilityNodeInfoCompat#ACTION_CLEAR_ACCESSIBILITY_FOCUS} + * actions. + * + * @param virtualViewId The virtual view identifier of the item on which + * to perform the action + * @param action The accessibility action to perform + * @param arguments (Optional) A bundle with additional arguments, or + * null + * @return true if the action was performed + */ + protected abstract boolean onPerformActionForVirtualView( + int virtualViewId, int action, Bundle arguments); + + /** + * Exposes a virtual view hierarchy to the accessibility framework. Only + * used in API 16+. + */ + private class ExploreByTouchNodeProvider extends AccessibilityNodeProviderCompat { + @Override + public AccessibilityNodeInfoCompat createAccessibilityNodeInfo(int virtualViewId) { + return ExploreByTouchHelper.this.createNode(virtualViewId); + } + + @Override + public boolean performAction(int virtualViewId, int action, Bundle arguments) { + return ExploreByTouchHelper.this.performAction(virtualViewId, action, arguments); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompat.java new file mode 100644 index 00000000000..44222bcba83 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompat.java @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.view.View; +import android.view.View.OnTouchListener; + +/** + * Helper for accessing features in ListPopupWindow introduced after API level 4 + * in a backwards compatible fashion. + */ +public class ListPopupWindowCompat { + /** + * Interface for the full API. + */ + interface ListPopupWindowImpl { + public OnTouchListener createDragToOpenListener(Object listPopupWindow, View src); + } + + /** + * Interface implementation that doesn't use anything above v4 APIs. + */ + static class BaseListPopupWindowImpl implements ListPopupWindowImpl { + @Override + public OnTouchListener createDragToOpenListener(Object listPopupWindow, View src) { + return null; + } + } + + /** + * Interface implementation for devices with at least KitKat APIs. + */ + static class KitKatListPopupWindowImpl extends BaseListPopupWindowImpl { + @Override + public OnTouchListener createDragToOpenListener(Object listPopupWindow, View src) { + return ListPopupWindowCompatKitKat.createDragToOpenListener(listPopupWindow, src); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final ListPopupWindowImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 19) { + IMPL = new KitKatListPopupWindowImpl(); + } else { + IMPL = new BaseListPopupWindowImpl(); + } + } + + private ListPopupWindowCompat() { + // This class is not publicly instantiable. + } + + /** + * On API {@link android.os.Build.VERSION_CODES#KITKAT} and higher, returns + * an {@link OnTouchListener} that can be added to the source view to + * implement drag-to-open behavior. Generally, the source view should be the + * same view that was passed to ListPopupWindow.setAnchorView(View). + *

    + * When the listener is set on a view, touching that view and dragging + * outside of its bounds will open the popup window. Lifting will select the + * currently touched list item. + *

    + * Example usage: + * + *

    +     * ListPopupWindow myPopup = new ListPopupWindow(context);
    +     * myPopup.setAnchor(myAnchor);
    +     * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor);
    +     * myAnchor.setOnTouchListener(dragListener);
    +     * 
    + * + * @param listPopupWindow the ListPopupWindow against which to invoke the + * method + * @param src the view on which the resulting listener will be set + * @return a touch listener that controls drag-to-open behavior, or null on + * unsupported APIs + */ + public static OnTouchListener createDragToOpenListener(Object listPopupWindow, View src) { + return IMPL.createDragToOpenListener(listPopupWindow, src); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompatKitKat.java new file mode 100644 index 00000000000..7b8d8dddf81 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ListPopupWindowCompatKitKat.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.ListPopupWindow; + +/** + * Implementation of ListPopupWindow compatibility that can call KitKat APIs. + */ +class ListPopupWindowCompatKitKat { + public static OnTouchListener createDragToOpenListener(Object listPopupWindow, View src) { + return ((ListPopupWindow) listPopupWindow).createDragToOpenListener(src); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ListViewAutoScrollHelper.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ListViewAutoScrollHelper.java new file mode 100644 index 00000000000..b23ca23b2cc --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ListViewAutoScrollHelper.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.view.View; +import android.widget.ListView; + +/** + * An implementation of {@link AutoScrollHelper} that knows how to scroll + * through a {@link ListView}. + */ +public class ListViewAutoScrollHelper extends AutoScrollHelper { + private final ListView mTarget; + + public ListViewAutoScrollHelper(ListView target) { + super(target); + + mTarget = target; + } + + @Override + public void scrollTargetBy(int deltaX, int deltaY) { + final ListView target = mTarget; + final int firstPosition = target.getFirstVisiblePosition(); + if (firstPosition == ListView.INVALID_POSITION) { + return; + } + + final View firstView = target.getChildAt(0); + if (firstView == null) { + return; + } + + final int newTop = firstView.getTop() - deltaY; + target.setSelectionFromTop(firstPosition, newTop); + } + + @Override + public boolean canTargetScrollHorizontally(int direction) { + // List do not scroll horizontally. + return false; + } + + @Override + public boolean canTargetScrollVertically(int direction) { + final ListView target = mTarget; + final int itemCount = target.getCount(); + if (itemCount == 0) { + return false; + } + + final int childCount = target.getChildCount(); + final int firstPosition = target.getFirstVisiblePosition(); + final int lastPosition = firstPosition + childCount; + + if (direction > 0) { + // Are we already showing the entire last item? + if (lastPosition >= itemCount) { + final View lastView = target.getChildAt(childCount - 1); + if (lastView.getBottom() <= target.getHeight()) { + return false; + } + } + } else if (direction < 0) { + // Are we already showing the entire first item? + if (firstPosition <= 0) { + final View firstView = target.getChildAt(0); + if (firstView.getTop() >= 0) { + return false; + } + } + } else { + // The behavior for direction 0 is undefined and we can return + // whatever we want. + return false; + } + + return true; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/MaterialProgressDrawable.java b/mobile/google-android-support-v4/src/android/support/v4/widget/MaterialProgressDrawable.java new file mode 100644 index 00000000000..db46186e32d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/MaterialProgressDrawable.java @@ -0,0 +1,792 @@ +/* + * Copyright (C) 2014 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.Animation; +import android.view.animation.LinearInterpolator; +import android.view.animation.Transformation; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Path; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Animatable; +import android.support.annotation.IntDef; +import android.support.annotation.NonNull; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.util.DisplayMetrics; +import android.view.View; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; + +/** + * Fancy progress indicator for Material theme. + * + * @hide + */ +class MaterialProgressDrawable extends Drawable implements Animatable { + private static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator(); + private static final Interpolator MATERIAL_INTERPOLATOR = new FastOutSlowInInterpolator(); + + private static final float FULL_ROTATION = 1080.0f; + @Retention(RetentionPolicy.CLASS) + @IntDef({LARGE, DEFAULT}) + public @interface ProgressDrawableSize {} + // Maps to ProgressBar.Large style + static final int LARGE = 0; + // Maps to ProgressBar default style + static final int DEFAULT = 1; + + // Maps to ProgressBar default style + private static final int CIRCLE_DIAMETER = 40; + private static final float CENTER_RADIUS = 8.75f; //should add up to 10 when + stroke_width + private static final float STROKE_WIDTH = 2.5f; + + // Maps to ProgressBar.Large style + private static final int CIRCLE_DIAMETER_LARGE = 56; + private static final float CENTER_RADIUS_LARGE = 12.5f; + private static final float STROKE_WIDTH_LARGE = 3f; + + private final int[] COLORS = new int[] { + Color.BLACK + }; + + /** + * The value in the linear interpolator for animating the drawable at which + * the color transition should start + */ + private static final float COLOR_START_DELAY_OFFSET = 0.75f; + private static final float END_TRIM_START_DELAY_OFFSET = 0.5f; + private static final float START_TRIM_DURATION_OFFSET = 0.5f; + + /** The duration of a single progress spin in milliseconds. */ + private static final int ANIMATION_DURATION = 1332; + + /** The number of points in the progress "star". */ + private static final float NUM_POINTS = 5f; + /** The list of animators operating on this drawable. */ + private final ArrayList mAnimators = new ArrayList(); + + /** The indicator ring, used to manage animation state. */ + private final Ring mRing; + + /** Canvas rotation in degrees. */ + private float mRotation; + + /** Layout info for the arrowhead in dp */ + private static final int ARROW_WIDTH = 10; + private static final int ARROW_HEIGHT = 5; + private static final float ARROW_OFFSET_ANGLE = 5; + + /** Layout info for the arrowhead for the large spinner in dp */ + private static final int ARROW_WIDTH_LARGE = 12; + private static final int ARROW_HEIGHT_LARGE = 6; + private static final float MAX_PROGRESS_ARC = .8f; + + private Resources mResources; + private View mParent; + private Animation mAnimation; + private float mRotationCount; + private double mWidth; + private double mHeight; + boolean mFinishing; + + public MaterialProgressDrawable(Context context, View parent) { + mParent = parent; + mResources = context.getResources(); + + mRing = new Ring(mCallback); + mRing.setColors(COLORS); + + updateSizes(DEFAULT); + setupAnimators(); + } + + private void setSizeParameters(double progressCircleWidth, double progressCircleHeight, + double centerRadius, double strokeWidth, float arrowWidth, float arrowHeight) { + final Ring ring = mRing; + final DisplayMetrics metrics = mResources.getDisplayMetrics(); + final float screenDensity = metrics.density; + + mWidth = progressCircleWidth * screenDensity; + mHeight = progressCircleHeight * screenDensity; + ring.setStrokeWidth((float) strokeWidth * screenDensity); + ring.setCenterRadius(centerRadius * screenDensity); + ring.setColorIndex(0); + ring.setArrowDimensions(arrowWidth * screenDensity, arrowHeight * screenDensity); + ring.setInsets((int) mWidth, (int) mHeight); + } + + /** + * Set the overall size for the progress spinner. This updates the radius + * and stroke width of the ring. + * + * @param size One of {@link MaterialProgressDrawable.LARGE} or + * {@link MaterialProgressDrawable.DEFAULT} + */ + public void updateSizes(@ProgressDrawableSize int size) { + if (size == LARGE) { + setSizeParameters(CIRCLE_DIAMETER_LARGE, CIRCLE_DIAMETER_LARGE, CENTER_RADIUS_LARGE, + STROKE_WIDTH_LARGE, ARROW_WIDTH_LARGE, ARROW_HEIGHT_LARGE); + } else { + setSizeParameters(CIRCLE_DIAMETER, CIRCLE_DIAMETER, CENTER_RADIUS, STROKE_WIDTH, + ARROW_WIDTH, ARROW_HEIGHT); + } + } + + /** + * @param show Set to true to display the arrowhead on the progress spinner. + */ + public void showArrow(boolean show) { + mRing.setShowArrow(show); + } + + /** + * @param scale Set the scale of the arrowhead for the spinner. + */ + public void setArrowScale(float scale) { + mRing.setArrowScale(scale); + } + + /** + * Set the start and end trim for the progress spinner arc. + * + * @param startAngle start angle + * @param endAngle end angle + */ + public void setStartEndTrim(float startAngle, float endAngle) { + mRing.setStartTrim(startAngle); + mRing.setEndTrim(endAngle); + } + + /** + * Set the amount of rotation to apply to the progress spinner. + * + * @param rotation Rotation is from [0..1] + */ + public void setProgressRotation(float rotation) { + mRing.setRotation(rotation); + } + + /** + * Update the background color of the circle image view. + */ + public void setBackgroundColor(int color) { + mRing.setBackgroundColor(color); + } + + /** + * Set the colors used in the progress animation from color resources. + * The first color will also be the color of the bar that grows in response + * to a user swipe gesture. + * + * @param colors + */ + public void setColorSchemeColors(int... colors) { + mRing.setColors(colors); + mRing.setColorIndex(0); + } + + @Override + public int getIntrinsicHeight() { + return (int) mHeight; + } + + @Override + public int getIntrinsicWidth() { + return (int) mWidth; + } + + @Override + public void draw(Canvas c) { + final Rect bounds = getBounds(); + final int saveCount = c.save(); + c.rotate(mRotation, bounds.exactCenterX(), bounds.exactCenterY()); + mRing.draw(c, bounds); + c.restoreToCount(saveCount); + } + + @Override + public void setAlpha(int alpha) { + mRing.setAlpha(alpha); + } + + public int getAlpha() { + return mRing.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter colorFilter) { + mRing.setColorFilter(colorFilter); + } + + @SuppressWarnings("unused") + void setRotation(float rotation) { + mRotation = rotation; + invalidateSelf(); + } + + @SuppressWarnings("unused") + private float getRotation() { + return mRotation; + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public boolean isRunning() { + final ArrayList animators = mAnimators; + final int N = animators.size(); + for (int i = 0; i < N; i++) { + final Animation animator = animators.get(i); + if (animator.hasStarted() && !animator.hasEnded()) { + return true; + } + } + return false; + } + + @Override + public void start() { + mAnimation.reset(); + mRing.storeOriginals(); + // Already showing some part of the ring + if (mRing.getEndTrim() != mRing.getStartTrim()) { + mFinishing = true; + mAnimation.setDuration(ANIMATION_DURATION/2); + mParent.startAnimation(mAnimation); + } else { + mRing.setColorIndex(0); + mRing.resetOriginals(); + mAnimation.setDuration(ANIMATION_DURATION); + mParent.startAnimation(mAnimation); + } + } + + @Override + public void stop() { + mParent.clearAnimation(); + setRotation(0); + mRing.setShowArrow(false); + mRing.setColorIndex(0); + mRing.resetOriginals(); + } + + private float getMinProgressArc(Ring ring) { + return (float) Math.toRadians( + ring.getStrokeWidth() / (2 * Math.PI * ring.getCenterRadius())); + } + + // Adapted from ArgbEvaluator.java + private int evaluateColorChange(float fraction, int startValue, int endValue) { + int startInt = (Integer) startValue; + int startA = (startInt >> 24) & 0xff; + int startR = (startInt >> 16) & 0xff; + int startG = (startInt >> 8) & 0xff; + int startB = startInt & 0xff; + + int endInt = (Integer) endValue; + int endA = (endInt >> 24) & 0xff; + int endR = (endInt >> 16) & 0xff; + int endG = (endInt >> 8) & 0xff; + int endB = endInt & 0xff; + + return (int)((startA + (int)(fraction * (endA - startA))) << 24) | + (int)((startR + (int)(fraction * (endR - startR))) << 16) | + (int)((startG + (int)(fraction * (endG - startG))) << 8) | + (int)((startB + (int)(fraction * (endB - startB)))); + } + + /** + * Update the ring color if this is within the last 25% of the animation. + * The new ring color will be a translation from the starting ring color to + * the next color. + */ + private void updateRingColor(float interpolatedTime, Ring ring) { + if (interpolatedTime > COLOR_START_DELAY_OFFSET) { + // scale the interpolatedTime so that the full + // transformation from 0 - 1 takes place in the + // remaining time + ring.setColor(evaluateColorChange((interpolatedTime - COLOR_START_DELAY_OFFSET) + / (1.0f - COLOR_START_DELAY_OFFSET), ring.getStartingColor(), + ring.getNextColor())); + } + } + + private void applyFinishTranslation(float interpolatedTime, Ring ring) { + // shrink back down and complete a full rotation before + // starting other circles + // Rotation goes between [0..1]. + updateRingColor(interpolatedTime, ring); + float targetRotation = (float) (Math.floor(ring.getStartingRotation() / MAX_PROGRESS_ARC) + + 1f); + final float minProgressArc = getMinProgressArc(ring); + final float startTrim = ring.getStartingStartTrim() + + (ring.getStartingEndTrim() - minProgressArc - ring.getStartingStartTrim()) + * interpolatedTime; + ring.setStartTrim(startTrim); + ring.setEndTrim(ring.getStartingEndTrim()); + final float rotation = ring.getStartingRotation() + + ((targetRotation - ring.getStartingRotation()) * interpolatedTime); + ring.setRotation(rotation); + } + + private void setupAnimators() { + final Ring ring = mRing; + final Animation animation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + if (mFinishing) { + applyFinishTranslation(interpolatedTime, ring); + } else { + // The minProgressArc is calculated from 0 to create an + // angle that matches the stroke width. + final float minProgressArc = getMinProgressArc(ring); + final float startingEndTrim = ring.getStartingEndTrim(); + final float startingTrim = ring.getStartingStartTrim(); + final float startingRotation = ring.getStartingRotation(); + + updateRingColor(interpolatedTime, ring); + + // Moving the start trim only occurs in the first 50% of a + // single ring animation + if (interpolatedTime <= START_TRIM_DURATION_OFFSET) { + // scale the interpolatedTime so that the full + // transformation from 0 - 1 takes place in the + // remaining time + final float scaledTime = (interpolatedTime) + / (1.0f - START_TRIM_DURATION_OFFSET); + final float startTrim = startingTrim + + ((MAX_PROGRESS_ARC - minProgressArc) * MATERIAL_INTERPOLATOR + .getInterpolation(scaledTime)); + ring.setStartTrim(startTrim); + } + + // Moving the end trim starts after 50% of a single ring + // animation completes + if (interpolatedTime > END_TRIM_START_DELAY_OFFSET) { + // scale the interpolatedTime so that the full + // transformation from 0 - 1 takes place in the + // remaining time + final float minArc = MAX_PROGRESS_ARC - minProgressArc; + float scaledTime = (interpolatedTime - START_TRIM_DURATION_OFFSET) + / (1.0f - START_TRIM_DURATION_OFFSET); + final float endTrim = startingEndTrim + + (minArc * MATERIAL_INTERPOLATOR.getInterpolation(scaledTime)); + ring.setEndTrim(endTrim); + } + + final float rotation = startingRotation + (0.25f * interpolatedTime); + ring.setRotation(rotation); + + float groupRotation = ((FULL_ROTATION / NUM_POINTS) * interpolatedTime) + + (FULL_ROTATION * (mRotationCount / NUM_POINTS)); + setRotation(groupRotation); + } + } + }; + animation.setRepeatCount(Animation.INFINITE); + animation.setRepeatMode(Animation.RESTART); + animation.setInterpolator(LINEAR_INTERPOLATOR); + animation.setAnimationListener(new Animation.AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { + mRotationCount = 0; + } + + @Override + public void onAnimationEnd(Animation animation) { + // do nothing + } + + @Override + public void onAnimationRepeat(Animation animation) { + ring.storeOriginals(); + ring.goToNextColor(); + ring.setStartTrim(ring.getEndTrim()); + if (mFinishing) { + // finished closing the last ring from the swipe gesture; go + // into progress mode + mFinishing = false; + animation.setDuration(ANIMATION_DURATION); + ring.setShowArrow(false); + } else { + mRotationCount = (mRotationCount + 1) % (NUM_POINTS); + } + } + }); + mAnimation = animation; + } + + private final Callback mCallback = new Callback() { + @Override + public void invalidateDrawable(Drawable d) { + invalidateSelf(); + } + + @Override + public void scheduleDrawable(Drawable d, Runnable what, long when) { + scheduleSelf(what, when); + } + + @Override + public void unscheduleDrawable(Drawable d, Runnable what) { + unscheduleSelf(what); + } + }; + + private static class Ring { + private final RectF mTempBounds = new RectF(); + private final Paint mPaint = new Paint(); + private final Paint mArrowPaint = new Paint(); + + private final Callback mCallback; + + private float mStartTrim = 0.0f; + private float mEndTrim = 0.0f; + private float mRotation = 0.0f; + private float mStrokeWidth = 5.0f; + private float mStrokeInset = 2.5f; + + private int[] mColors; + // mColorIndex represents the offset into the available mColors that the + // progress circle should currently display. As the progress circle is + // animating, the mColorIndex moves by one to the next available color. + private int mColorIndex; + private float mStartingStartTrim; + private float mStartingEndTrim; + private float mStartingRotation; + private boolean mShowArrow; + private Path mArrow; + private float mArrowScale; + private double mRingCenterRadius; + private int mArrowWidth; + private int mArrowHeight; + private int mAlpha; + private final Paint mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + private int mBackgroundColor; + private int mCurrentColor; + + public Ring(Callback callback) { + mCallback = callback; + + mPaint.setStrokeCap(Paint.Cap.SQUARE); + mPaint.setAntiAlias(true); + mPaint.setStyle(Style.STROKE); + + mArrowPaint.setStyle(Paint.Style.FILL); + mArrowPaint.setAntiAlias(true); + } + + public void setBackgroundColor(int color) { + mBackgroundColor = color; + } + + /** + * Set the dimensions of the arrowhead. + * + * @param width Width of the hypotenuse of the arrow head + * @param height Height of the arrow point + */ + public void setArrowDimensions(float width, float height) { + mArrowWidth = (int) width; + mArrowHeight = (int) height; + } + + /** + * Draw the progress spinner + */ + public void draw(Canvas c, Rect bounds) { + final RectF arcBounds = mTempBounds; + arcBounds.set(bounds); + arcBounds.inset(mStrokeInset, mStrokeInset); + + final float startAngle = (mStartTrim + mRotation) * 360; + final float endAngle = (mEndTrim + mRotation) * 360; + float sweepAngle = endAngle - startAngle; + + mPaint.setColor(mCurrentColor); + c.drawArc(arcBounds, startAngle, sweepAngle, false, mPaint); + + drawTriangle(c, startAngle, sweepAngle, bounds); + + if (mAlpha < 255) { + mCirclePaint.setColor(mBackgroundColor); + mCirclePaint.setAlpha(255 - mAlpha); + c.drawCircle(bounds.exactCenterX(), bounds.exactCenterY(), bounds.width() / 2, + mCirclePaint); + } + } + + private void drawTriangle(Canvas c, float startAngle, float sweepAngle, Rect bounds) { + if (mShowArrow) { + if (mArrow == null) { + mArrow = new android.graphics.Path(); + mArrow.setFillType(android.graphics.Path.FillType.EVEN_ODD); + } else { + mArrow.reset(); + } + + // Adjust the position of the triangle so that it is inset as + // much as the arc, but also centered on the arc. + float inset = (int) mStrokeInset / 2 * mArrowScale; + float x = (float) (mRingCenterRadius * Math.cos(0) + bounds.exactCenterX()); + float y = (float) (mRingCenterRadius * Math.sin(0) + bounds.exactCenterY()); + + // Update the path each time. This works around an issue in SKIA + // where concatenating a rotation matrix to a scale matrix + // ignored a starting negative rotation. This appears to have + // been fixed as of API 21. + mArrow.moveTo(0, 0); + mArrow.lineTo(mArrowWidth * mArrowScale, 0); + mArrow.lineTo((mArrowWidth * mArrowScale / 2), (mArrowHeight + * mArrowScale)); + mArrow.offset(x - inset, y); + mArrow.close(); + // draw a triangle + mArrowPaint.setColor(mCurrentColor); + c.rotate(startAngle + sweepAngle - ARROW_OFFSET_ANGLE, bounds.exactCenterX(), + bounds.exactCenterY()); + c.drawPath(mArrow, mArrowPaint); + } + } + + /** + * Set the colors the progress spinner alternates between. + * + * @param colors Array of integers describing the colors. Must be non-null. + */ + public void setColors(@NonNull int[] colors) { + mColors = colors; + // if colors are reset, make sure to reset the color index as well + setColorIndex(0); + } + + /** + * Set the absolute color of the progress spinner. This is should only + * be used when animating between current and next color when the + * spinner is rotating. + * + * @param color int describing the color. + */ + public void setColor(int color) { + mCurrentColor = color; + } + + /** + * @param index Index into the color array of the color to display in + * the progress spinner. + */ + public void setColorIndex(int index) { + mColorIndex = index; + mCurrentColor = mColors[mColorIndex]; + } + + /** + * @return int describing the next color the progress spinner should use when drawing. + */ + public int getNextColor() { + return mColors[getNextColorIndex()]; + } + + private int getNextColorIndex() { + return (mColorIndex + 1) % (mColors.length); + } + + /** + * Proceed to the next available ring color. This will automatically + * wrap back to the beginning of colors. + */ + public void goToNextColor() { + setColorIndex(getNextColorIndex()); + } + + public void setColorFilter(ColorFilter filter) { + mPaint.setColorFilter(filter); + invalidateSelf(); + } + + /** + * @param alpha Set the alpha of the progress spinner and associated arrowhead. + */ + public void setAlpha(int alpha) { + mAlpha = alpha; + } + + /** + * @return Current alpha of the progress spinner and arrowhead. + */ + public int getAlpha() { + return mAlpha; + } + + /** + * @param strokeWidth Set the stroke width of the progress spinner in pixels. + */ + public void setStrokeWidth(float strokeWidth) { + mStrokeWidth = strokeWidth; + mPaint.setStrokeWidth(strokeWidth); + invalidateSelf(); + } + + @SuppressWarnings("unused") + public float getStrokeWidth() { + return mStrokeWidth; + } + + @SuppressWarnings("unused") + public void setStartTrim(float startTrim) { + mStartTrim = startTrim; + invalidateSelf(); + } + + @SuppressWarnings("unused") + public float getStartTrim() { + return mStartTrim; + } + + public float getStartingStartTrim() { + return mStartingStartTrim; + } + + public float getStartingEndTrim() { + return mStartingEndTrim; + } + + public int getStartingColor() { + return mColors[mColorIndex]; + } + + @SuppressWarnings("unused") + public void setEndTrim(float endTrim) { + mEndTrim = endTrim; + invalidateSelf(); + } + + @SuppressWarnings("unused") + public float getEndTrim() { + return mEndTrim; + } + + @SuppressWarnings("unused") + public void setRotation(float rotation) { + mRotation = rotation; + invalidateSelf(); + } + + @SuppressWarnings("unused") + public float getRotation() { + return mRotation; + } + + public void setInsets(int width, int height) { + final float minEdge = (float) Math.min(width, height); + float insets; + if (mRingCenterRadius <= 0 || minEdge < 0) { + insets = (float) Math.ceil(mStrokeWidth / 2.0f); + } else { + insets = (float) (minEdge / 2.0f - mRingCenterRadius); + } + mStrokeInset = insets; + } + + @SuppressWarnings("unused") + public float getInsets() { + return mStrokeInset; + } + + /** + * @param centerRadius Inner radius in px of the circle the progress + * spinner arc traces. + */ + public void setCenterRadius(double centerRadius) { + mRingCenterRadius = centerRadius; + } + + public double getCenterRadius() { + return mRingCenterRadius; + } + + /** + * @param show Set to true to show the arrow head on the progress spinner. + */ + public void setShowArrow(boolean show) { + if (mShowArrow != show) { + mShowArrow = show; + invalidateSelf(); + } + } + + /** + * @param scale Set the scale of the arrowhead for the spinner. + */ + public void setArrowScale(float scale) { + if (scale != mArrowScale) { + mArrowScale = scale; + invalidateSelf(); + } + } + + /** + * @return The amount the progress spinner is currently rotated, between [0..1]. + */ + public float getStartingRotation() { + return mStartingRotation; + } + + /** + * If the start / end trim are offset to begin with, store them so that + * animation starts from that offset. + */ + public void storeOriginals() { + mStartingStartTrim = mStartTrim; + mStartingEndTrim = mEndTrim; + mStartingRotation = mRotation; + } + + /** + * Reset the progress spinner to default rotation, start and end angles. + */ + public void resetOriginals() { + mStartingStartTrim = 0; + mStartingEndTrim = 0; + mStartingRotation = 0; + setStartTrim(0); + setEndTrim(0); + setRotation(0); + } + + private void invalidateSelf() { + mCallback.invalidateDrawable(null); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/NestedScrollView.java b/mobile/google-android-support-v4/src/android/support/v4/widget/NestedScrollView.java new file mode 100644 index 00000000000..eb2481491c6 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/NestedScrollView.java @@ -0,0 +1,1862 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.InputDeviceCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.NestedScrollingChild; +import android.support.v4.view.NestedScrollingChildHelper; +import android.support.v4.view.NestedScrollingParent; +import android.support.v4.view.NestedScrollingParentHelper; +import android.support.v4.view.VelocityTrackerCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityEventCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.support.v4.view.accessibility.AccessibilityRecordCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewDebug; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AnimationUtils; +import android.widget.FrameLayout; +import android.widget.ScrollView; + +import java.util.List; + +/** + * NestedScrollView is just like {@link android.widget.ScrollView}, but it supports acting + * as both a nested scrolling parent and child on both new and old versions of Android. + * Nested scrolling is enabled by default. + */ +public class NestedScrollView extends FrameLayout implements NestedScrollingParent, + NestedScrollingChild { + static final int ANIMATED_SCROLL_GAP = 250; + + static final float MAX_SCROLL_FACTOR = 0.5f; + + private static final String TAG = "NestedScrollView"; + + private long mLastScroll; + + private final Rect mTempRect = new Rect(); + private ScrollerCompat mScroller; + private EdgeEffectCompat mEdgeGlowTop; + private EdgeEffectCompat mEdgeGlowBottom; + + /** + * Position of the last motion event. + */ + private int mLastMotionY; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + private boolean mIsLaidOut = false; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + /** + * True if the user is currently dragging this ScrollView around. This is + * not the same as 'is being flinged', which can be checked by + * mScroller.isFinished() (flinging begins when the user lifts his finger). + */ + private boolean mIsBeingDragged = false; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + /** + * When set to true, the scroll view measure its child to make it fill the currently + * visible area. + */ + private boolean mFillViewport; + + /** + * Whether arrow scrolling is animated. + */ + private boolean mSmoothScrollingEnabled = true; + + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + + /** + * Used during scrolling to retrieve the new offset within the window. + */ + private final int[] mScrollOffset = new int[2]; + private final int[] mScrollConsumed = new int[2]; + private int mNestedYOffset; + + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + private SavedState mSavedState; + + private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); + + private static final int[] SCROLLVIEW_STYLEABLE = new int[] { + android.R.attr.fillViewport + }; + + private final NestedScrollingParentHelper mParentHelper; + private final NestedScrollingChildHelper mChildHelper; + + private float mVerticalScrollFactor; + + public NestedScrollView(Context context) { + this(context, null); + } + + public NestedScrollView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public NestedScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initScrollView(); + + final TypedArray a = context.obtainStyledAttributes( + attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0); + + setFillViewport(a.getBoolean(0, false)); + + a.recycle(); + + mParentHelper = new NestedScrollingParentHelper(this); + mChildHelper = new NestedScrollingChildHelper(this); + + // ...because why else would you be using this widget? + setNestedScrollingEnabled(true); + + ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); + } + + // NestedScrollingChild + + @Override + public void setNestedScrollingEnabled(boolean enabled) { + mChildHelper.setNestedScrollingEnabled(enabled); + } + + @Override + public boolean isNestedScrollingEnabled() { + return mChildHelper.isNestedScrollingEnabled(); + } + + @Override + public boolean startNestedScroll(int axes) { + return mChildHelper.startNestedScroll(axes); + } + + @Override + public void stopNestedScroll() { + mChildHelper.stopNestedScroll(); + } + + @Override + public boolean hasNestedScrollingParent() { + return mChildHelper.hasNestedScrollingParent(); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); + } + + // NestedScrollingParent + + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; + } + + @Override + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { + mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + } + + @Override + public void onStopNestedScroll(View target) { + stopNestedScroll(); + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed) { + final int oldScrollY = getScrollY(); + scrollBy(0, dyUnconsumed); + final int myConsumed = getScrollY() - oldScrollY; + final int myUnconsumed = dyUnconsumed - myConsumed; + dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null); + } + + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + // Do nothing + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (!consumed) { + flingWithNestedDispatch((int) velocityY); + return true; + } + return false; + } + + @Override + public boolean onNestedPreFling(View target, float velocityX, float velocityY) { + // Do nothing + return false; + } + + @Override + public int getNestedScrollAxes() { + return mParentHelper.getNestedScrollAxes(); + } + + // ScrollView import + + public boolean shouldDelayChildPressedState() { + return true; + } + + @Override + protected float getTopFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int scrollY = getScrollY(); + if (scrollY < length) { + return scrollY / (float) length; + } + + return 1.0f; + } + + @Override + protected float getBottomFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int bottomEdge = getHeight() - getPaddingBottom(); + final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (span < length) { + return span / (float) length; + } + + return 1.0f; + } + + /** + * @return The maximum amount this scroll view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmount() { + return (int) (MAX_SCROLL_FACTOR * getHeight()); + } + + private void initScrollView() { + mScroller = new ScrollerCompat(getContext(), null); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setWillNotDraw(false); + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + @Override + public void addView(View child) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child); + } + + @Override + public void addView(View child, int index) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, params); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index, params); + } + + /** + * @return Returns true this ScrollView can be scrolled + */ + private boolean canScroll() { + View child = getChildAt(0); + if (child != null) { + int childHeight = child.getHeight(); + return getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); + } + return false; + } + + /** + * Indicates whether this ScrollView's content is stretched to fill the viewport. + * + * @return True if the content fills the viewport, false otherwise. + * + * @attr ref android.R.styleable#ScrollView_fillViewport + */ + public boolean isFillViewport() { + return mFillViewport; + } + + /** + * Indicates this ScrollView whether it should stretch its content height to fill + * the viewport or not. + * + * @param fillViewport True to stretch the content's height to the viewport's + * boundaries, false otherwise. + * + * @attr ref android.R.styleable#ScrollView_fillViewport + */ + public void setFillViewport(boolean fillViewport) { + if (fillViewport != mFillViewport) { + mFillViewport = fillViewport; + requestLayout(); + } + } + + /** + * @return Whether arrow scrolling will animate its transition. + */ + public boolean isSmoothScrollingEnabled() { + return mSmoothScrollingEnabled; + } + + /** + * Set whether arrow scrolling will animate its transition. + * @param smoothScrollingEnabled whether arrow scrolling will animate its transition + */ + public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { + mSmoothScrollingEnabled = smoothScrollingEnabled; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (!mFillViewport) { + return; + } + + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == MeasureSpec.UNSPECIFIED) { + return; + } + + if (getChildCount() > 0) { + final View child = getChildAt(0); + int height = getMeasuredHeight(); + if (child.getMeasuredHeight() < height) { + final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeft() + getPaddingRight(), lp.width); + height -= getPaddingTop(); + height -= getPaddingBottom(); + int childHeightMeasureSpec = + MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + mTempRect.setEmpty(); + + if (!canScroll()) { + if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, + currentFocused, View.FOCUS_DOWN); + return nextFocused != null + && nextFocused != this + && nextFocused.requestFocus(View.FOCUS_DOWN); + } + return false; + } + + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_UP); + } else { + handled = fullScroll(View.FOCUS_UP); + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_DOWN); + } else { + handled = fullScroll(View.FOCUS_DOWN); + } + break; + case KeyEvent.KEYCODE_SPACE: + pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); + break; + } + } + + return handled; + } + + private boolean inChild(int x, int y) { + if (getChildCount() > 0) { + final int scrollY = getScrollY(); + final View child = getChildAt(0); + return !(y < child.getTop() - scrollY + || y >= child.getBottom() - scrollY + || x < child.getLeft() + || x >= child.getRight()); + } + return false; + } + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + private void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + recycleVelocityTracker(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { + return true; + } + + /* + * Don't try to intercept touch if we can't scroll anyway. + */ + if (getScrollY() == 0 && !ViewCompat.canScrollVertically(this, 1)) { + return false; + } + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId); + if (pointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + activePointerId + + " in onInterceptTouchEvent"); + break; + } + + final int y = (int) MotionEventCompat.getY(ev, pointerIndex); + final int yDiff = Math.abs(y - mLastMotionY); + if (yDiff > mTouchSlop + && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0) { + mIsBeingDragged = true; + mLastMotionY = y; + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + mNestedYOffset = 0; + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + final int y = (int) ev.getY(); + if (!inChild((int) ev.getX(), (int) y)) { + mIsBeingDragged = false; + recycleVelocityTracker(); + break; + } + + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionY = y; + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. + */ + mIsBeingDragged = !mScroller.isFinished(); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + /* Release the drag */ + mIsBeingDragged = false; + mActivePointerId = INVALID_POINTER; + recycleVelocityTracker(); + stopNestedScroll(); + break; + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + initVelocityTrackerIfNotExists(); + + MotionEvent vtev = MotionEvent.obtain(ev); + + final int actionMasked = MotionEventCompat.getActionMasked(ev); + + if (actionMasked == MotionEvent.ACTION_DOWN) { + mNestedYOffset = 0; + } + vtev.offsetLocation(0, mNestedYOffset); + + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: { + if (getChildCount() == 0) { + return false; + } + if ((mIsBeingDragged = !mScroller.isFinished())) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionY = (int) ev.getY(); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL); + break; + } + case MotionEvent.ACTION_MOVE: + final int activePointerIndex = MotionEventCompat.findPointerIndex(ev, + mActivePointerId); + if (activePointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); + break; + } + + final int y = (int) MotionEventCompat.getY(ev, activePointerIndex); + int deltaY = mLastMotionY - y; + if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) { + deltaY -= mScrollConsumed[1]; + vtev.offsetLocation(0, mScrollOffset[1]); + mNestedYOffset += mScrollOffset[1]; + } + if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + mIsBeingDragged = true; + if (deltaY > 0) { + deltaY -= mTouchSlop; + } else { + deltaY += mTouchSlop; + } + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + mLastMotionY = y - mScrollOffset[1]; + + final int oldY = getScrollY(); + final int range = getScrollRange(); + final int overscrollMode = ViewCompat.getOverScrollMode(this); + boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && + range > 0); + + // Calling overScrollByCompat will call onOverScrolled, which + // calls onScrollChanged if applicable. + if (overScrollByCompat(0, deltaY, 0, getScrollY(), 0, range, 0, + 0, true) && !hasNestedScrollingParent()) { + // Break our velocity if we hit a scroll barrier. + mVelocityTracker.clear(); + } + + final int scrolledDeltaY = getScrollY() - oldY; + final int unconsumedY = deltaY - scrolledDeltaY; + if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) { + mLastMotionY -= mScrollOffset[1]; + vtev.offsetLocation(0, mScrollOffset[1]); + mNestedYOffset += mScrollOffset[1]; + } else if (canOverscroll) { + ensureGlows(); + final int pulledToY = oldY + deltaY; + if (pulledToY < 0) { + mEdgeGlowTop.onPull((float) deltaY / getHeight(), + MotionEventCompat.getX(ev, activePointerIndex) / getWidth()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (pulledToY > range) { + mEdgeGlowBottom.onPull((float) deltaY / getHeight(), + 1.f - MotionEventCompat.getX(ev, activePointerIndex) + / getWidth()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + if (mEdgeGlowTop != null + && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + } + break; + case MotionEvent.ACTION_UP: + if (mIsBeingDragged) { + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocity = (int) VelocityTrackerCompat.getYVelocity(velocityTracker, + mActivePointerId); + + if ((Math.abs(initialVelocity) > mMinimumVelocity)) { + flingWithNestedDispatch(-initialVelocity); + } + + mActivePointerId = INVALID_POINTER; + endDrag(); + } + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged && getChildCount() > 0) { + mActivePointerId = INVALID_POINTER; + endDrag(); + } + break; + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mLastMotionY = (int) MotionEventCompat.getY(ev, index); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionY = (int) MotionEventCompat.getY(ev, + MotionEventCompat.findPointerIndex(ev, mActivePointerId)); + break; + } + + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(vtev); + } + vtev.recycle(); + return true; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = (ev.getAction() & MotionEventCompat.ACTION_POINTER_INDEX_MASK) >> + MotionEventCompat.ACTION_POINTER_INDEX_SHIFT; + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionY = (int) MotionEventCompat.getY(ev, newPointerIndex); + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + public boolean onGenericMotionEvent(MotionEvent event) { + if ((MotionEventCompat.getSource(event) & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEventCompat.ACTION_SCROLL: { + if (!mIsBeingDragged) { + final float vscroll = MotionEventCompat.getAxisValue(event, + MotionEventCompat.AXIS_VSCROLL); + if (vscroll != 0) { + final int delta = (int) (vscroll * getVerticalScrollFactorCompat()); + final int range = getScrollRange(); + int oldScrollY = getScrollY(); + int newScrollY = oldScrollY - delta; + if (newScrollY < 0) { + newScrollY = 0; + } else if (newScrollY > range) { + newScrollY = range; + } + if (newScrollY != oldScrollY) { + super.scrollTo(getScrollX(), newScrollY); + return true; + } + } + } + } + } + } + return false; + } + + private float getVerticalScrollFactorCompat() { + if (mVerticalScrollFactor == 0) { + TypedValue outValue = new TypedValue(); + final Context context = getContext(); + if (!context.getTheme().resolveAttribute( + android.R.attr.listPreferredItemHeight, outValue, true)) { + throw new IllegalStateException( + "Expected theme to define listPreferredItemHeight."); + } + mVerticalScrollFactor = outValue.getDimension( + context.getResources().getDisplayMetrics()); + } + return mVerticalScrollFactor; + } + + protected void onOverScrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + super.scrollTo(scrollX, scrollY); + } + + boolean overScrollByCompat(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverScrollX, int maxOverScrollY, + boolean isTouchEvent) { + final int overScrollMode = ViewCompat.getOverScrollMode(this); + final boolean canScrollHorizontal = + computeHorizontalScrollRange() > computeHorizontalScrollExtent(); + final boolean canScrollVertical = + computeVerticalScrollRange() > computeVerticalScrollExtent(); + final boolean overScrollHorizontal = overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); + final boolean overScrollVertical = overScrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overScrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); + + int newScrollX = scrollX + deltaX; + if (!overScrollHorizontal) { + maxOverScrollX = 0; + } + + int newScrollY = scrollY + deltaY; + if (!overScrollVertical) { + maxOverScrollY = 0; + } + + // Clamp values if at the limits and record + final int left = -maxOverScrollX; + final int right = maxOverScrollX + scrollRangeX; + final int top = -maxOverScrollY; + final int bottom = maxOverScrollY + scrollRangeY; + + boolean clampedX = false; + if (newScrollX > right) { + newScrollX = right; + clampedX = true; + } else if (newScrollX < left) { + newScrollX = left; + clampedX = true; + } + + boolean clampedY = false; + if (newScrollY > bottom) { + newScrollY = bottom; + clampedY = true; + } else if (newScrollY < top) { + newScrollY = top; + clampedY = true; + } + + onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); + + return clampedX || clampedY; + } + + private int getScrollRange() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop())); + } + return scrollRange; + } + + /** + *

    + * Finds the next focusable component that fits in the specified bounds. + *

    + * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found + * @param bottom the bottom offset of the bounds in which a focusable must + * be found + * @return the next focusable component in the bounds or null if none can + * be found + */ + private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { + + List focusables = getFocusables(View.FOCUS_FORWARD); + View focusCandidate = null; + + /* + * A fully contained focusable is one where its top is below the bound's + * top, and its bottom is above the bound's bottom. A partially + * contained focusable is one where some part of it is within the + * bounds, but it also has some part that is not within bounds. A fully contained + * focusable is preferred to a partially contained focusable. + */ + boolean foundFullyContainedFocusable = false; + + int count = focusables.size(); + for (int i = 0; i < count; i++) { + View view = focusables.get(i); + int viewTop = view.getTop(); + int viewBottom = view.getBottom(); + + if (top < viewBottom && viewTop < bottom) { + /* + * the focusable is in the target area, it is a candidate for + * focusing + */ + + final boolean viewIsFullyContained = (top < viewTop) && + (viewBottom < bottom); + + if (focusCandidate == null) { + /* No candidate, take this one */ + focusCandidate = view; + foundFullyContainedFocusable = viewIsFullyContained; + } else { + final boolean viewIsCloserToBoundary = + (topFocus && viewTop < focusCandidate.getTop()) || + (!topFocus && viewBottom > focusCandidate + .getBottom()); + + if (foundFullyContainedFocusable) { + if (viewIsFullyContained && viewIsCloserToBoundary) { + /* + * We're dealing with only fully contained views, so + * it has to be closer to the boundary to beat our + * candidate + */ + focusCandidate = view; + } + } else { + if (viewIsFullyContained) { + /* Any fully contained view beats a partially contained view */ + focusCandidate = view; + foundFullyContainedFocusable = true; + } else if (viewIsCloserToBoundary) { + /* + * Partially contained view beats another partially + * contained view if it's closer + */ + focusCandidate = view; + } + } + } + } + } + + return focusCandidate; + } + + /** + *

    Handles scrolling in response to a "page up/down" shortcut press. This + * method will scroll the view by one page up or down and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

    + * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go one page up or + * {@link android.view.View#FOCUS_DOWN} to go one page down + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean pageScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + if (down) { + mTempRect.top = getScrollY() + height; + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + if (mTempRect.top + height > view.getBottom()) { + mTempRect.top = view.getBottom() - height; + } + } + } else { + mTempRect.top = getScrollY() - height; + if (mTempRect.top < 0) { + mTempRect.top = 0; + } + } + mTempRect.bottom = mTempRect.top + height; + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + *

    Handles scrolling in response to a "home/end" shortcut press. This + * method will scroll the view to the top or bottom and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

    + * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go the top of the view or + * {@link android.view.View#FOCUS_DOWN} to go the bottom + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean fullScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + mTempRect.top = 0; + mTempRect.bottom = height; + + if (down) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.bottom = view.getBottom() + getPaddingBottom(); + mTempRect.top = mTempRect.bottom - height; + } + } + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + *

    Scrolls the view to make the area defined by top and + * bottom visible. This method attempts to give the focus + * to a component visible in this area. If no component can be focused in + * the new visible area, the focus is reclaimed by this ScrollView.

    + * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go upward, {@link android.view.View#FOCUS_DOWN} to downward + * @param top the top offset of the new area to be made visible + * @param bottom the bottom offset of the new area to be made visible + * @return true if the key event is consumed by this method, false otherwise + */ + private boolean scrollAndFocus(int direction, int top, int bottom) { + boolean handled = true; + + int height = getHeight(); + int containerTop = getScrollY(); + int containerBottom = containerTop + height; + boolean up = direction == View.FOCUS_UP; + + View newFocused = findFocusableViewInBounds(up, top, bottom); + if (newFocused == null) { + newFocused = this; + } + + if (top >= containerTop && bottom <= containerBottom) { + handled = false; + } else { + int delta = up ? (top - containerTop) : (bottom - containerBottom); + doScrollY(delta); + } + + if (newFocused != findFocus()) newFocused.requestFocus(direction); + + return handled; + } + + /** + * Handle scrolling in response to an up or down arrow click. + * + * @param direction The direction corresponding to the arrow key that was + * pressed + * @return True if we consumed the event, false otherwise + */ + public boolean arrowScroll(int direction) { + + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); + + final int maxJump = getMaxScrollAmount(); + + if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScrollY(scrollDelta); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + if (getChildCount() > 0) { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight() - getPaddingBottom(); + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) { + return false; + } + doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta); + } + + if (currentFocused != null && currentFocused.isFocused() + && isOffScreen(currentFocused)) { + // previously focused item still has focus and is off screen, give + // it up (take it back to ourselves) + // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are + // sure to + // get it) + final int descendantFocusability = getDescendantFocusability(); // save + setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); + requestFocus(); + setDescendantFocusability(descendantFocusability); // restore + } + return true; + } + + /** + * @return whether the descendant of this scroll view is scrolled off + * screen. + */ + private boolean isOffScreen(View descendant) { + return !isWithinDeltaOfScreen(descendant, 0, getHeight()); + } + + /** + * @return whether the descendant of this scroll view is within delta + * pixels of being on the screen. + */ + private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) { + descendant.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(descendant, mTempRect); + + return (mTempRect.bottom + delta) >= getScrollY() + && (mTempRect.top - delta) <= (getScrollY() + height); + } + + /** + * Smooth scroll by a Y delta + * + * @param delta the number of pixels to scroll by on the Y axis + */ + private void doScrollY(int delta) { + if (delta != 0) { + if (mSmoothScrollingEnabled) { + smoothScrollBy(0, delta); + } else { + scrollBy(0, delta); + } + } + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param dx the number of pixels to scroll by on the X axis + * @param dy the number of pixels to scroll by on the Y axis + */ + public final void smoothScrollBy(int dx, int dy) { + if (getChildCount() == 0) { + // Nothing to do. + return; + } + long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; + if (duration > ANIMATED_SCROLL_GAP) { + final int height = getHeight() - getPaddingBottom() - getPaddingTop(); + final int bottom = getChildAt(0).getHeight(); + final int maxY = Math.max(0, bottom - height); + final int scrollY = getScrollY(); + dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; + + mScroller.startScroll(getScrollX(), scrollY, 0, dy); + ViewCompat.postInvalidateOnAnimation(this); + } else { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + scrollBy(dx, dy); + } + mLastScroll = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Like {@link #scrollTo}, but scroll smoothly instead of immediately. + * + * @param x the position where to scroll on the X axis + * @param y the position where to scroll on the Y axis + */ + public final void smoothScrollTo(int x, int y) { + smoothScrollBy(x - getScrollX(), y - getScrollY()); + } + + /** + *

    The scroll range of a scroll view is the overall height of all of its + * children.

    + */ + @Override + protected int computeVerticalScrollRange() { + final int count = getChildCount(); + final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop(); + if (count == 0) { + return contentHeight; + } + + int scrollRange = getChildAt(0).getBottom(); + final int scrollY = getScrollY(); + final int overscrollBottom = Math.max(0, scrollRange - contentHeight); + if (scrollY < 0) { + scrollRange -= scrollY; + } else if (scrollY > overscrollBottom) { + scrollRange += scrollY - overscrollBottom; + } + + return scrollRange; + } + + @Override + protected int computeVerticalScrollOffset() { + return Math.max(0, super.computeVerticalScrollOffset()); + } + + @Override + protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, getPaddingLeft() + + getPaddingRight(), lp.width); + + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, + getPaddingLeft() + getPaddingRight() + lp.leftMargin + lp.rightMargin + + widthUsed, lp.width); + final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + int oldX = getScrollX(); + int oldY = getScrollY(); + int x = mScroller.getCurrX(); + int y = mScroller.getCurrY(); + + if (oldX != x || oldY != y) { + final int range = getScrollRange(); + final int overscrollMode = ViewCompat.getOverScrollMode(this); + final boolean canOverscroll = overscrollMode == ViewCompat.OVER_SCROLL_ALWAYS || + (overscrollMode == ViewCompat.OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0); + + overScrollByCompat(x - oldX, y - oldY, oldX, oldY, 0, range, + 0, 0, false); + + if (canOverscroll) { + ensureGlows(); + if (y <= 0 && oldY > 0) { + mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (y >= range && oldY < range) { + mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); + } + } + } + } + } + + /** + * Scrolls the view to the given child. + * + * @param child the View to scroll to + */ + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + + if (scrollDelta != 0) { + scrollBy(0, scrollDelta); + } + } + + /** + * If rect is off screen, scroll just enough to get it (or at least the + * first screen size chunk of it) on screen. + * + * @param rect The rectangle. + * @param immediate True to scroll immediately without animation + * @return true if scrolling was performed + */ + private boolean scrollToChildRect(Rect rect, boolean immediate) { + final int delta = computeScrollDeltaToGetChildRectOnScreen(rect); + final boolean scroll = delta != 0; + if (scroll) { + if (immediate) { + scrollBy(0, delta); + } else { + smoothScrollBy(0, delta); + } + } + return scroll; + } + + /** + * Compute the amount to scroll in the Y direction in order to get + * a rectangle completely on the screen (or, if taller than the screen, + * at least the first screen size chunk of it). + * + * @param rect The rect. + * @return The scroll delta. + */ + protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { + if (getChildCount() == 0) return 0; + + int height = getHeight(); + int screenTop = getScrollY(); + int screenBottom = screenTop + height; + + int fadingEdge = getVerticalFadingEdgeLength(); + + // leave room for top fading edge as long as rect isn't at very top + if (rect.top > 0) { + screenTop += fadingEdge; + } + + // leave room for bottom fading edge as long as rect isn't at very bottom + if (rect.bottom < getChildAt(0).getHeight()) { + screenBottom -= fadingEdge; + } + + int scrollYDelta = 0; + + if (rect.bottom > screenBottom && rect.top > screenTop) { + // need to move down to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + + if (rect.height() > height) { + // just enough to get screen size chunk on + scrollYDelta += (rect.top - screenTop); + } else { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - screenBottom); + } + + // make sure we aren't scrolling beyond the end of our content + int bottom = getChildAt(0).getBottom(); + int distanceToBottom = bottom - screenBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + + } else if (rect.top < screenTop && rect.bottom < screenBottom) { + // need to move up to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.height() > height) { + // screen size chunk + scrollYDelta -= (screenBottom - rect.bottom); + } else { + // entire rect at top + scrollYDelta -= (screenTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our content + scrollYDelta = Math.max(scrollYDelta, -getScrollY()); + } + return scrollYDelta; + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mIsLayoutDirty) { + scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); + } + + + /** + * When looking for focus in children of a scroll view, need to be a little + * more careful not to give focus to something that is scrolled off screen. + * + * This is more expensive than the default {@link android.view.ViewGroup} + * implementation, otherwise this behavior might have been made the default. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + + // convert from forward / backward notation to up / down / left / right + // (ugh). + if (direction == View.FOCUS_FORWARD) { + direction = View.FOCUS_DOWN; + } else if (direction == View.FOCUS_BACKWARD) { + direction = View.FOCUS_UP; + } + + final View nextFocus = previouslyFocusedRect == null ? + FocusFinder.getInstance().findNextFocus(this, null, direction) : + FocusFinder.getInstance().findNextFocusFromRect(this, + previouslyFocusedRect, direction); + + if (nextFocus == null) { + return false; + } + + if (isOffScreen(nextFocus)) { + return false; + } + + return nextFocus.requestFocus(direction, previouslyFocusedRect); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, + boolean immediate) { + // offset into coordinate space of this scroll view + rectangle.offset(child.getLeft() - child.getScrollX(), + child.getTop() - child.getScrollY()); + + return scrollToChildRect(rectangle, immediate); + } + + @Override + public void requestLayout() { + mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mIsLayoutDirty = false; + // Give a child focus if it needs it + if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { + scrollToChild(mChildToScrollTo); + } + mChildToScrollTo = null; + + if (!mIsLaidOut) { + if (mSavedState != null) { + scrollTo(getScrollX(), mSavedState.scrollPosition); + mSavedState = null; + } // mScrollY default value is "0" + + final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; + final int scrollRange = Math.max(0, + childHeight - (b - t - getPaddingBottom() - getPaddingTop())); + + // Don't forget to clamp + if (getScrollY() > scrollRange) { + scrollTo(getScrollX(), scrollRange); + } else if (getScrollY() < 0) { + scrollTo(getScrollX(), 0); + } + } + + // Calling this with the present values causes it to re-claim them + scrollTo(getScrollX(), getScrollY()); + mIsLaidOut = true; + } + + @Override + public void onAttachedToWindow() { + mIsLaidOut = false; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + View currentFocused = findFocus(); + if (null == currentFocused || this == currentFocused) + return; + + // If the currently-focused view was visible on the screen when the + // screen was at the old height, then scroll the screen to make that + // view visible with the new screen height. + if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) { + currentFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocused, mTempRect); + int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScrollY(scrollDelta); + } + } + + /** + * Return true if child is a descendant of parent, (or equal to the parent). + */ + private static boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + /** + * Fling the scroll view + * + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/cursor is moving down the screen, + * which means we want to scroll towards the top. + */ + public void fling(int velocityY) { + if (getChildCount() > 0) { + int height = getHeight() - getPaddingBottom() - getPaddingTop(); + int bottom = getChildAt(0).getHeight(); + + mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0, + Math.max(0, bottom - height), 0, height/2); + + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private void flingWithNestedDispatch(int velocityY) { + final int scrollY = getScrollY(); + final boolean canFling = (scrollY > 0 || velocityY > 0) && + (scrollY < getScrollRange() || velocityY < 0); + if (!dispatchNestedPreFling(0, velocityY)) { + dispatchNestedFling(0, velocityY, canFling); + if (canFling) { + fling(velocityY); + } + } + } + + private void endDrag() { + mIsBeingDragged = false; + + recycleVelocityTracker(); + + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + } + } + + /** + * {@inheritDoc} + * + *

    This version also clamps the scrolling to the bounds of our child. + */ + @Override + public void scrollTo(int x, int y) { + // we rely on the fact the View.scrollBy calls scrollTo. + if (getChildCount() > 0) { + View child = getChildAt(0); + x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); + y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); + if (x != getScrollX() || y != getScrollY()) { + super.scrollTo(x, y); + } + } + } + + private void ensureGlows() { + if (ViewCompat.getOverScrollMode(this) != ViewCompat.OVER_SCROLL_NEVER) { + if (mEdgeGlowTop == null) { + Context context = getContext(); + mEdgeGlowTop = new EdgeEffectCompat(context); + mEdgeGlowBottom = new EdgeEffectCompat(context); + } + } else { + mEdgeGlowTop = null; + mEdgeGlowBottom = null; + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mEdgeGlowTop != null) { + final int scrollY = getScrollY(); + if (!mEdgeGlowTop.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + + canvas.translate(getPaddingLeft(), Math.min(0, scrollY)); + mEdgeGlowTop.setSize(width, getHeight()); + if (mEdgeGlowTop.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowBottom.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + final int height = getHeight(); + + canvas.translate(-width + getPaddingLeft(), + Math.max(getScrollRange(), scrollY) + height); + canvas.rotate(180, width, 0); + mEdgeGlowBottom.setSize(width, height); + if (mEdgeGlowBottom.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + } + } + + private static int clamp(int n, int my, int child) { + if (my >= child || n < 0) { + /* my >= child is this case: + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * + * n < 0 is this case: + * |------ me ------| + * |-------- child --------| + * |-- mScrollX --| + */ + return 0; + } + if ((my+n) > child) { + /* this case: + * |------ me ------| + * |------ child ------| + * |-- mScrollX --| + */ + return child-my; + } + return n; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mSavedState = ss; + requestLayout(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPosition = getScrollY(); + return ss; + } + + static class SavedState extends BaseSavedState { + public int scrollPosition; + + SavedState(Parcelable superState) { + super(superState); + } + + public SavedState(Parcel source) { + super(source); + scrollPosition = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(scrollPosition); + } + + @Override + public String toString() { + return "HorizontalScrollView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " scrollPosition=" + scrollPosition + "}"; + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + static class AccessibilityDelegate extends AccessibilityDelegateCompat { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle arguments) { + if (super.performAccessibilityAction(host, action, arguments)) { + return true; + } + final NestedScrollView nsvHost = (NestedScrollView) host; + if (!nsvHost.isEnabled()) { + return false; + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { + final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() + - nsvHost.getPaddingTop(); + final int targetScrollY = Math.min(nsvHost.getScrollY() + viewportHeight, + nsvHost.getScrollRange()); + if (targetScrollY != nsvHost.getScrollY()) { + nsvHost.smoothScrollTo(0, targetScrollY); + return true; + } + } + return false; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { + final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() + - nsvHost.getPaddingTop(); + final int targetScrollY = Math.max(nsvHost.getScrollY() - viewportHeight, 0); + if (targetScrollY != nsvHost.getScrollY()) { + nsvHost.smoothScrollTo(0, targetScrollY); + return true; + } + } + return false; + } + return false; + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final NestedScrollView nsvHost = (NestedScrollView) host; + info.setClassName(ScrollView.class.getName()); + if (nsvHost.isEnabled()) { + final int scrollRange = nsvHost.getScrollRange(); + if (scrollRange > 0) { + info.setScrollable(true); + if (nsvHost.getScrollY() > 0) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } + if (nsvHost.getScrollY() < scrollRange) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } + } + } + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + final NestedScrollView nsvHost = (NestedScrollView) host; + event.setClassName(ScrollView.class.getName()); + final AccessibilityRecordCompat record = AccessibilityEventCompat.asRecord(event); + final boolean scrollable = nsvHost.getScrollRange() > 0; + record.setScrollable(scrollable); + record.setScrollX(nsvHost.getScrollX()); + record.setScrollY(nsvHost.getScrollY()); + record.setMaxScrollX(nsvHost.getScrollX()); + record.setMaxScrollY(nsvHost.getScrollRange()); + } + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompat.java new file mode 100644 index 00000000000..db0e2c98495 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompat.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.view.View.OnTouchListener; + +/** + * Helper for accessing features in PopupMenu introduced after API level 4 in a + * backwards compatible fashion. + */ +public class PopupMenuCompat { + /** + * Interface for the full API. + */ + interface PopupMenuImpl { + public OnTouchListener getDragToOpenListener(Object popupMenu); + } + + /** + * Interface implementation that doesn't use anything above v4 APIs. + */ + static class BasePopupMenuImpl implements PopupMenuImpl { + @Override + public OnTouchListener getDragToOpenListener(Object popupMenu) { + return null; + } + } + + /** + * Interface implementation for devices with at least KitKat APIs. + */ + static class KitKatPopupMenuImpl extends BasePopupMenuImpl { + @Override + public OnTouchListener getDragToOpenListener(Object popupMenu) { + return PopupMenuCompatKitKat.getDragToOpenListener(popupMenu); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final PopupMenuImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 19) { + IMPL = new KitKatPopupMenuImpl(); + } else { + IMPL = new BasePopupMenuImpl(); + } + } + + private PopupMenuCompat() { + // This class is not publicly instantiable. + } + + /** + * On API {@link android.os.Build.VERSION_CODES#KITKAT} and higher, returns + * an {@link OnTouchListener} that can be added to the anchor view to + * implement drag-to-open behavior. + *

    + * When the listener is set on a view, touching that view and dragging + * outside of its bounds will open the popup window. Lifting will select the + * currently touched list item. + *

    + * Example usage: + *

    +     * PopupMenu myPopup = new PopupMenu(context, myAnchor);
    +     * myAnchor.setOnTouchListener(myPopup.getDragToOpenListener());
    +     * 
    + * + * @param popupMenu the PopupMenu against which to invoke the method + * @return a touch listener that controls drag-to-open behavior, or null on + * unsupported APIs + */ + public static OnTouchListener getDragToOpenListener(Object popupMenu) { + return IMPL.getDragToOpenListener(popupMenu); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompatKitKat.java new file mode 100644 index 00000000000..45587981a82 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupMenuCompatKitKat.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.view.View.OnTouchListener; +import android.widget.PopupMenu; + +/** + * Implementation of PopupMenu compatibility that can call KitKat APIs. + */ +class PopupMenuCompatKitKat { + public static OnTouchListener getDragToOpenListener(Object popupMenu) { + return ((PopupMenu) popupMenu).getDragToOpenListener(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompat.java new file mode 100644 index 00000000000..be96a739a51 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompat.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.PopupWindow; + +/** + * Helper for accessing features in PopupWindow introduced after API level 4 + * in a backwards compatible fashion. + */ +public class PopupWindowCompat { + /** + * Interface for the full API. + */ + interface PopupWindowImpl { + public void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff, + int gravity); + } + + /** + * Interface implementation that doesn't use anything above v4 APIs. + */ + static class BasePopupWindowImpl implements PopupWindowImpl { + @Override + public void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff, + int gravity) { + popup.showAsDropDown(anchor, xoff, yoff); + } + } + + /** + * Interface implementation for devices with at least KitKat APIs. + */ + static class KitKatPopupWindowImpl extends BasePopupWindowImpl { + @Override + public void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff, + int gravity) { + PopupWindowCompatKitKat.showAsDropDown(popup, anchor, xoff, yoff, gravity); + } + } + + /** + * Select the correct implementation to use for the current platform. + */ + static final PopupWindowImpl IMPL; + static { + final int version = android.os.Build.VERSION.SDK_INT; + if (version >= 19) { + IMPL = new KitKatPopupWindowImpl(); + } else { + IMPL = new BasePopupWindowImpl(); + } + } + + private PopupWindowCompat() { + // This class is not publicly instantiable. + } + + /** + *

    Display the content view in a popup window anchored to the bottom-left + * corner of the anchor view offset by the specified x and y coordinates. + * If there is not enough room on screen to show + * the popup in its entirety, this method tries to find a parent scroll + * view to scroll. If no parent scroll view can be scrolled, the bottom-left + * corner of the popup is pinned at the top left corner of the anchor view.

    + *

    If the view later scrolls to move anchor to a different + * location, the popup will be moved correspondingly.

    + * + * @param popup the PopupWindow to show + * @param anchor the view on which to pin the popup window + * @param xoff A horizontal offset from the anchor in pixels + * @param yoff A vertical offset from the anchor in pixels + * @param gravity Alignment of the popup relative to the anchor + */ + public static void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff, + int gravity) { + IMPL.showAsDropDown(popup, anchor, xoff, yoff, gravity); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompatKitKat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompatKitKat.java new file mode 100644 index 00000000000..4333f4a37da --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/PopupWindowCompatKitKat.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.view.View; +import android.view.View.OnTouchListener; +import android.widget.ListPopupWindow; +import android.widget.PopupWindow; + +/** + * Implementation of PopupWindow compatibility that can call KitKat APIs. + */ +class PopupWindowCompatKitKat { + public static void showAsDropDown(PopupWindow popup, View anchor, int xoff, int yoff, + int gravity) { + popup.showAsDropDown(anchor, xoff, yoff, gravity); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ResourceCursorAdapter.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ResourceCursorAdapter.java new file mode 100644 index 00000000000..c854cf55086 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ResourceCursorAdapter.java @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.view.LayoutInflater; + +/** + * Static library support version of the framework's {@link android.widget.ResourceCursorAdapter}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public abstract class ResourceCursorAdapter extends CursorAdapter { + private int mLayout; + + private int mDropDownLayout; + + private LayoutInflater mInflater; + + /** + * Constructor the enables auto-requery. + * + * @deprecated This option is discouraged, as it results in Cursor queries + * being performed on the application's UI thread and thus can cause poor + * responsiveness or even Application Not Responding errors. As an alternative, + * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. + * + * @param context The context where the ListView associated with this adapter is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + */ + @Deprecated + public ResourceCursorAdapter(Context context, int layout, Cursor c) { + super(context, c); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Constructor with default behavior as per + * {@link CursorAdapter#CursorAdapter(Context, Cursor, boolean)}; it is recommended + * you not use this, but instead {@link #ResourceCursorAdapter(Context, int, Cursor, int)}. + * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER} + * will always be set. + * + * @param context The context where the ListView associated with this adapter is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + * @param c The cursor from which to get the data. + * @param autoRequery If true the adapter will call requery() on the + * cursor whenever it changes so the most recent + * data is always displayed. Using true here is discouraged. + */ + public ResourceCursorAdapter(Context context, int layout, Cursor c, boolean autoRequery) { + super(context, c, autoRequery); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Standard constructor. + * + * @param context The context where the ListView associated with this adapter is running + * @param layout Resource identifier of a layout file that defines the views + * for this list item. Unless you override them later, this will + * define both the item views and the drop down views. + * @param c The cursor from which to get the data. + * @param flags Flags used to determine the behavior of the adapter, + * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}. + */ + public ResourceCursorAdapter(Context context, int layout, Cursor c, int flags) { + super(context, c, flags); + mLayout = mDropDownLayout = layout; + mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + } + + /** + * Inflates view(s) from the specified XML file. + * + * @see android.widget.CursorAdapter#newView(android.content.Context, + * android.database.Cursor, ViewGroup) + */ + @Override + public View newView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(mLayout, parent, false); + } + + @Override + public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) { + return mInflater.inflate(mDropDownLayout, parent, false); + } + + /** + *

    Sets the layout resource of the item views.

    + * + * @param layout the layout resources used to create item views + */ + public void setViewResource(int layout) { + mLayout = layout; + } + + /** + *

    Sets the layout resource of the drop down views.

    + * + * @param dropDownLayout the layout resources used to create drop down views + */ + public void setDropDownViewResource(int dropDownLayout) { + mDropDownLayout = dropDownLayout; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompat.java new file mode 100644 index 00000000000..afbf89704e0 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompat.java @@ -0,0 +1,484 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.os.Build; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; +import android.widget.Scroller; + +/** + * Provides access to new {@link android.widget.Scroller Scroller} APIs when available. + * + *

    This class provides a platform version-independent mechanism for obeying the + * current device's preferred scroll physics and fling behavior. It offers a subset of + * the APIs from Scroller or OverScroller.

    + */ +public class ScrollerCompat { + private static final String TAG = "ScrollerCompat"; + + Object mScroller; + ScrollerCompatImpl mImpl; + + interface ScrollerCompatImpl { + Object createScroller(Context context, Interpolator interpolator); + boolean isFinished(Object scroller); + int getCurrX(Object scroller); + int getCurrY(Object scroller); + float getCurrVelocity(Object scroller); + boolean computeScrollOffset(Object scroller); + void startScroll(Object scroller, int startX, int startY, int dx, int dy); + void startScroll(Object scroller, int startX, int startY, int dx, int dy, int duration); + void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY); + void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY, int overX, int overY); + void abortAnimation(Object scroller); + void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, int overX); + void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY); + boolean isOverScrolled(Object scroller); + int getFinalX(Object scroller); + int getFinalY(Object scroller); + } + + static final int CHASE_FRAME_TIME = 16; // ms per target frame + + static class ScrollerCompatImplBase implements ScrollerCompatImpl { + @Override + public Object createScroller(Context context, Interpolator interpolator) { + return interpolator != null ? + new Scroller(context, interpolator) : new Scroller(context); + } + + @Override + public boolean isFinished(Object scroller) { + return ((Scroller) scroller).isFinished(); + } + + @Override + public int getCurrX(Object scroller) { + return ((Scroller) scroller).getCurrX(); + } + + @Override + public int getCurrY(Object scroller) { + return ((Scroller) scroller).getCurrY(); + } + + @Override + public float getCurrVelocity(Object scroller) { + return 0; + } + + @Override + public boolean computeScrollOffset(Object scroller) { + final Scroller s = (Scroller) scroller; + return s.computeScrollOffset(); + } + + @Override + public void startScroll(Object scroller, int startX, int startY, int dx, int dy) { + ((Scroller) scroller).startScroll(startX, startY, dx, dy); + } + + @Override + public void startScroll(Object scroller, int startX, int startY, int dx, int dy, + int duration) { + ((Scroller) scroller).startScroll(startX, startY, dx, dy, duration); + } + + @Override + public void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY) { + ((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY); + } + + @Override + public void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY, int overX, int overY) { + ((Scroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY); + } + + @Override + public void abortAnimation(Object scroller) { + ((Scroller) scroller).abortAnimation(); + } + + @Override + public void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, + int overX) { + // No-op + } + + @Override + public void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) { + // No-op + } + + @Override + public boolean isOverScrolled(Object scroller) { + // Always false + return false; + } + + @Override + public int getFinalX(Object scroller) { + return ((Scroller) scroller).getFinalX(); + } + + @Override + public int getFinalY(Object scroller) { + return ((Scroller) scroller).getFinalY(); + } + } + + static class ScrollerCompatImplGingerbread implements ScrollerCompatImpl { + @Override + public Object createScroller(Context context, Interpolator interpolator) { + return ScrollerCompatGingerbread.createScroller(context, interpolator); + } + + @Override + public boolean isFinished(Object scroller) { + return ScrollerCompatGingerbread.isFinished(scroller); + } + + @Override + public int getCurrX(Object scroller) { + return ScrollerCompatGingerbread.getCurrX(scroller); + } + + @Override + public int getCurrY(Object scroller) { + return ScrollerCompatGingerbread.getCurrY(scroller); + } + + @Override + public float getCurrVelocity(Object scroller) { + return 0; + } + + @Override + public boolean computeScrollOffset(Object scroller) { + return ScrollerCompatGingerbread.computeScrollOffset(scroller); + } + + @Override + public void startScroll(Object scroller, int startX, int startY, int dx, int dy) { + ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy); + } + + @Override + public void startScroll(Object scroller, int startX, int startY, int dx, int dy, + int duration) { + ScrollerCompatGingerbread.startScroll(scroller, startX, startY, dx, dy, duration); + } + + @Override + public void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY) { + ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY, + minX, maxX, minY, maxY); + } + + @Override + public void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY, int overX, int overY) { + ScrollerCompatGingerbread.fling(scroller, startX, startY, velX, velY, + minX, maxX, minY, maxY, overX, overY); + } + + @Override + public void abortAnimation(Object scroller) { + ScrollerCompatGingerbread.abortAnimation(scroller); + } + + @Override + public void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, + int overX) { + ScrollerCompatGingerbread.notifyHorizontalEdgeReached(scroller, startX, finalX, overX); + } + + @Override + public void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) { + ScrollerCompatGingerbread.notifyVerticalEdgeReached(scroller, startY, finalY, overY); + } + + @Override + public boolean isOverScrolled(Object scroller) { + return ScrollerCompatGingerbread.isOverScrolled(scroller); + } + + @Override + public int getFinalX(Object scroller) { + return ScrollerCompatGingerbread.getFinalX(scroller); + } + + @Override + public int getFinalY(Object scroller) { + return ScrollerCompatGingerbread.getFinalY(scroller); + } + } + + static class ScrollerCompatImplIcs extends ScrollerCompatImplGingerbread { + @Override + public float getCurrVelocity(Object scroller) { + return ScrollerCompatIcs.getCurrVelocity(scroller); + } + } + + public static ScrollerCompat create(Context context) { + return create(context, null); + } + + public static ScrollerCompat create(Context context, Interpolator interpolator) { + return new ScrollerCompat(context, interpolator); + } + + ScrollerCompat(Context context, Interpolator interpolator) { + this(Build.VERSION.SDK_INT, context, interpolator); + + } + + /** + * Private constructer where API version can be provided. + * Useful for unit testing. + */ + private ScrollerCompat(int apiVersion, Context context, Interpolator interpolator) { + if (apiVersion >= 14) { // ICS + mImpl = new ScrollerCompatImplIcs(); + } else if (apiVersion>= 9) { // Gingerbread + mImpl = new ScrollerCompatImplGingerbread(); + } else { + mImpl = new ScrollerCompatImplBase(); + } + mScroller = mImpl.createScroller(context, interpolator); + } + + /** + * Returns whether the scroller has finished scrolling. + * + * @return True if the scroller has finished scrolling, false otherwise. + */ + public boolean isFinished() { + return mImpl.isFinished(mScroller); + } + + /** + * Returns the current X offset in the scroll. + * + * @return The new X offset as an absolute distance from the origin. + */ + public int getCurrX() { + return mImpl.getCurrX(mScroller); + } + + /** + * Returns the current Y offset in the scroll. + * + * @return The new Y offset as an absolute distance from the origin. + */ + public int getCurrY() { + return mImpl.getCurrY(mScroller); + } + + /** + * @return The final X position for the scroll in progress, if known. + */ + public int getFinalX() { + return mImpl.getFinalX(mScroller); + } + + /** + * @return The final Y position for the scroll in progress, if known. + */ + public int getFinalY() { + return mImpl.getFinalY(mScroller); + } + + /** + * Returns the current velocity on platform versions that support it. + * + *

    The device must support at least API level 14 (Ice Cream Sandwich). + * On older platform versions this method will return 0. This method should + * only be used as input for nonessential visual effects such as {@link EdgeEffectCompat}.

    + * + * @return The original velocity less the deceleration. Result may be + * negative. + */ + public float getCurrVelocity() { + return mImpl.getCurrVelocity(mScroller); + } + + /** + * Call this when you want to know the new location. If it returns true, + * the animation is not yet finished. loc will be altered to provide the + * new location. + */ + public boolean computeScrollOffset() { + return mImpl.computeScrollOffset(mScroller); + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * The scroll will use the default value of 250 milliseconds for the + * duration. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + */ + public void startScroll(int startX, int startY, int dx, int dy) { + mImpl.startScroll(mScroller, startX, startY, dx, dy); + } + + /** + * Start scrolling by providing a starting point and the distance to travel. + * + * @param startX Starting horizontal scroll offset in pixels. Positive + * numbers will scroll the content to the left. + * @param startY Starting vertical scroll offset in pixels. Positive numbers + * will scroll the content up. + * @param dx Horizontal distance to travel. Positive numbers will scroll the + * content to the left. + * @param dy Vertical distance to travel. Positive numbers will scroll the + * content up. + * @param duration Duration of the scroll in milliseconds. + */ + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + mImpl.startScroll(mScroller, startX, startY, dx, dy, duration); + } + + /** + * Start scrolling based on a fling gesture. The distance travelled will + * depend on the initial velocity of the fling. + * + * @param startX Starting point of the scroll (X) + * @param startY Starting point of the scroll (Y) + * @param velocityX Initial velocity of the fling (X) measured in pixels per + * second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per + * second + * @param minX Minimum X value. The scroller will not scroll past this + * point. + * @param maxX Maximum X value. The scroller will not scroll past this + * point. + * @param minY Minimum Y value. The scroller will not scroll past this + * point. + * @param maxY Maximum Y value. The scroller will not scroll past this + * point. + */ + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY) { + mImpl.fling(mScroller, startX, startY, velocityX, velocityY, minX, maxX, minY, maxY); + } + + /** + * Start scrolling based on a fling gesture. The distance travelled will + * depend on the initial velocity of the fling. + * + * @param startX Starting point of the scroll (X) + * @param startY Starting point of the scroll (Y) + * @param velocityX Initial velocity of the fling (X) measured in pixels per + * second. + * @param velocityY Initial velocity of the fling (Y) measured in pixels per + * second + * @param minX Minimum X value. The scroller will not scroll past this + * point. + * @param maxX Maximum X value. The scroller will not scroll past this + * point. + * @param minY Minimum Y value. The scroller will not scroll past this + * point. + * @param maxY Maximum Y value. The scroller will not scroll past this + * point. + * @param overX Overfling range. If > 0, horizontal overfling in either + * direction will be possible. + * @param overY Overfling range. If > 0, vertical overfling in either + * direction will be possible. + */ + public void fling(int startX, int startY, int velocityX, int velocityY, + int minX, int maxX, int minY, int maxY, int overX, int overY) { + mImpl.fling(mScroller, startX, startY, velocityX, velocityY, + minX, maxX, minY, maxY, overX, overY); + } + + /** + * Stops the animation. Aborting the animation causes the scroller to move to the final x and y + * position. + */ + public void abortAnimation() { + mImpl.abortAnimation(mScroller); + } + + + /** + * Notify the scroller that we've reached a horizontal boundary. + * Normally the information to handle this will already be known + * when the animation is started, such as in a call to one of the + * fling functions. However there are cases where this cannot be known + * in advance. This function will transition the current motion and + * animate from startX to finalX as appropriate. + * + * @param startX Starting/current X position + * @param finalX Desired final X position + * @param overX Magnitude of overscroll allowed. This should be the maximum + * desired distance from finalX. Absolute value - must be positive. + */ + public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) { + mImpl.notifyHorizontalEdgeReached(mScroller, startX, finalX, overX); + } + + /** + * Notify the scroller that we've reached a vertical boundary. + * Normally the information to handle this will already be known + * when the animation is started, such as in a call to one of the + * fling functions. However there are cases where this cannot be known + * in advance. This function will animate a parabolic motion from + * startY to finalY. + * + * @param startY Starting/current Y position + * @param finalY Desired final Y position + * @param overY Magnitude of overscroll allowed. This should be the maximum + * desired distance from finalY. Absolute value - must be positive. + */ + public void notifyVerticalEdgeReached(int startY, int finalY, int overY) { + mImpl.notifyVerticalEdgeReached(mScroller, startY, finalY, overY); + } + + /** + * Returns whether the current Scroller is currently returning to a valid position. + * Valid bounds were provided by the + * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method. + * + * One should check this value before calling + * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress + * to restore a valid position will then be stopped. The caller has to take into account + * the fact that the started scroll will start from an overscrolled position. + * + * @return true when the current position is overscrolled and in the process of + * interpolating back to a valid value. + */ + public boolean isOverScrolled() { + return mImpl.isOverScrolled(mScroller); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatGingerbread.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatGingerbread.java new file mode 100644 index 00000000000..429e864b087 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatGingerbread.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.widget; + +import android.content.Context; +import android.view.animation.Interpolator; +import android.widget.OverScroller; + +class ScrollerCompatGingerbread { + public static Object createScroller(Context context, Interpolator interpolator) { + return interpolator != null ? + new OverScroller(context, interpolator) : new OverScroller(context); + } + + public static boolean isFinished(Object scroller) { + return ((OverScroller) scroller).isFinished(); + } + + public static int getCurrX(Object scroller) { + return ((OverScroller) scroller).getCurrX(); + } + + public static int getCurrY(Object scroller) { + return ((OverScroller) scroller).getCurrY(); + } + + public static boolean computeScrollOffset(Object scroller) { + return ((OverScroller) scroller).computeScrollOffset(); + } + + public static void startScroll(Object scroller, int startX, int startY, int dx, int dy) { + ((OverScroller) scroller).startScroll(startX, startY, dx, dy); + } + + public static void startScroll(Object scroller, int startX, int startY, int dx, int dy, + int duration) { + ((OverScroller) scroller).startScroll(startX, startY, dx, dy, duration); + } + + public static void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY) { + ((OverScroller) scroller).fling(startX, startY, velX, velY, minX, maxX, minY, maxY); + } + + public static void fling(Object scroller, int startX, int startY, int velX, int velY, + int minX, int maxX, int minY, int maxY, int overX, int overY) { + ((OverScroller) scroller).fling(startX, startY, velX, velY, + minX, maxX, minY, maxY, overX, overY); + } + + public static void abortAnimation(Object scroller) { + ((OverScroller) scroller).abortAnimation(); + } + + public static void notifyHorizontalEdgeReached(Object scroller, int startX, int finalX, + int overX) { + ((OverScroller) scroller).notifyHorizontalEdgeReached(startX, finalX, overX); + } + + public static void notifyVerticalEdgeReached(Object scroller, int startY, int finalY, int overY) { + ((OverScroller) scroller).notifyVerticalEdgeReached(startY, finalY, overY); + } + + public static boolean isOverScrolled(Object scroller) { + return ((OverScroller) scroller).isOverScrolled(); + } + + public static int getFinalX(Object scroller) { + return ((OverScroller) scroller).getFinalX(); + } + + public static int getFinalY(Object scroller) { + return ((OverScroller) scroller).getFinalY(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatIcs.java new file mode 100644 index 00000000000..76b9a2b595d --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ScrollerCompatIcs.java @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.widget.OverScroller; + +/** + * ICS API access for ScrollerCompat + */ +class ScrollerCompatIcs { + public static float getCurrVelocity(Object scroller) { + return ((OverScroller) scroller).getCurrVelocity(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompat.java new file mode 100644 index 00000000000..c51ecf27b1b --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompat.java @@ -0,0 +1,519 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.app.SearchManager; +import android.content.ComponentName; +import android.content.Context; +import android.os.Build; +import android.view.View; +import android.widget.TextView; + +/** + * Helper for accessing features in {@link android.widget.SearchView} + * introduced after API level 4 in a backwards compatible fashion. + */ +public class SearchViewCompat { + + interface SearchViewCompatImpl { + View newSearchView(Context context); + void setSearchableInfo(View searchView, ComponentName searchableComponent); + void setImeOptions(View searchView, int imeOptions); + void setInputType(View searchView, int inputType); + Object newOnQueryTextListener(OnQueryTextListenerCompat listener); + void setOnQueryTextListener(Object searchView, Object listener); + Object newOnCloseListener(OnCloseListenerCompat listener); + void setOnCloseListener(Object searchView, Object listener); + CharSequence getQuery(View searchView); + void setQuery(View searchView, CharSequence query, boolean submit); + void setQueryHint(View searchView, CharSequence hint); + void setIconified(View searchView, boolean iconify); + boolean isIconified(View searchView); + void setSubmitButtonEnabled(View searchView, boolean enabled); + boolean isSubmitButtonEnabled(View searchView); + void setQueryRefinementEnabled(View searchView, boolean enable); + boolean isQueryRefinementEnabled(View searchView); + void setMaxWidth(View searchView, int maxpixels); + } + + static class SearchViewCompatStubImpl implements SearchViewCompatImpl { + + @Override + public View newSearchView(Context context) { + return null; + } + + @Override + public void setSearchableInfo(View searchView, ComponentName searchableComponent) { + } + + @Override + public void setImeOptions(View searchView, int imeOptions) { + } + + @Override + public void setInputType(View searchView, int inputType) { + } + + @Override + public Object newOnQueryTextListener(OnQueryTextListenerCompat listener) { + return null; + } + + @Override + public void setOnQueryTextListener(Object searchView, Object listener) { + } + + @Override + public Object newOnCloseListener(OnCloseListenerCompat listener) { + return null; + } + + @Override + public void setOnCloseListener(Object searchView, Object listener) { + } + + @Override + public CharSequence getQuery(View searchView) { + return null; + } + + @Override + public void setQuery(View searchView, CharSequence query, boolean submit) { + } + + @Override + public void setQueryHint(View searchView, CharSequence hint) { + } + + @Override + public void setIconified(View searchView, boolean iconify) { + } + + @Override + public boolean isIconified(View searchView) { + return true; + } + + @Override + public void setSubmitButtonEnabled(View searchView, boolean enabled) { + } + + @Override + public boolean isSubmitButtonEnabled(View searchView) { + return false; + } + + @Override + public void setQueryRefinementEnabled(View searchView, boolean enable) { + } + + @Override + public boolean isQueryRefinementEnabled(View searchView) { + return false; + } + + @Override + public void setMaxWidth(View searchView, int maxpixels) { + } + } + + static class SearchViewCompatHoneycombImpl extends SearchViewCompatStubImpl { + + @Override + public View newSearchView(Context context) { + return SearchViewCompatHoneycomb.newSearchView(context); + } + + @Override + public void setSearchableInfo(View searchView, ComponentName searchableComponent) { + SearchViewCompatHoneycomb.setSearchableInfo(searchView, searchableComponent); + } + + @Override + public Object newOnQueryTextListener(final OnQueryTextListenerCompat listener) { + return SearchViewCompatHoneycomb.newOnQueryTextListener( + new SearchViewCompatHoneycomb.OnQueryTextListenerCompatBridge() { + @Override + public boolean onQueryTextSubmit(String query) { + return listener.onQueryTextSubmit(query); + } + @Override + public boolean onQueryTextChange(String newText) { + return listener.onQueryTextChange(newText); + } + }); + } + + @Override + public void setOnQueryTextListener(Object searchView, Object listener) { + SearchViewCompatHoneycomb.setOnQueryTextListener(searchView, listener); + } + + @Override + public Object newOnCloseListener(final OnCloseListenerCompat listener) { + return SearchViewCompatHoneycomb.newOnCloseListener( + new SearchViewCompatHoneycomb.OnCloseListenerCompatBridge() { + @Override + public boolean onClose() { + return listener.onClose(); + } + }); + } + + @Override + public void setOnCloseListener(Object searchView, Object listener) { + SearchViewCompatHoneycomb.setOnCloseListener(searchView, listener); + } + + @Override + public CharSequence getQuery(View searchView) { + return SearchViewCompatHoneycomb.getQuery(searchView); + } + + @Override + public void setQuery(View searchView, CharSequence query, boolean submit) { + SearchViewCompatHoneycomb.setQuery(searchView, query, submit); + } + + @Override + public void setQueryHint(View searchView, CharSequence hint) { + SearchViewCompatHoneycomb.setQueryHint(searchView, hint); + } + + @Override + public void setIconified(View searchView, boolean iconify) { + SearchViewCompatHoneycomb.setIconified(searchView, iconify); + } + + @Override + public boolean isIconified(View searchView) { + return SearchViewCompatHoneycomb.isIconified(searchView); + } + + @Override + public void setSubmitButtonEnabled(View searchView, boolean enabled) { + SearchViewCompatHoneycomb.setSubmitButtonEnabled(searchView, enabled); + } + + @Override + public boolean isSubmitButtonEnabled(View searchView) { + return SearchViewCompatHoneycomb.isSubmitButtonEnabled(searchView); + } + + @Override + public void setQueryRefinementEnabled(View searchView, boolean enable) { + SearchViewCompatHoneycomb.setQueryRefinementEnabled(searchView, enable); + } + + @Override + public boolean isQueryRefinementEnabled(View searchView) { + return SearchViewCompatHoneycomb.isQueryRefinementEnabled(searchView); + } + + @Override + public void setMaxWidth(View searchView, int maxpixels) { + SearchViewCompatHoneycomb.setMaxWidth(searchView, maxpixels); + } + } + + static class SearchViewCompatIcsImpl extends SearchViewCompatHoneycombImpl { + + @Override + public View newSearchView(Context context) { + return SearchViewCompatIcs.newSearchView(context); + } + + @Override + public void setImeOptions(View searchView, int imeOptions) { + SearchViewCompatIcs.setImeOptions(searchView, imeOptions); + } + + @Override + public void setInputType(View searchView, int inputType) { + SearchViewCompatIcs.setInputType(searchView, inputType); + } + } + + private static final SearchViewCompatImpl IMPL; + + static { + if (Build.VERSION.SDK_INT >= 14) { // ICS + IMPL = new SearchViewCompatIcsImpl(); + } else if (Build.VERSION.SDK_INT >= 11) { // Honeycomb + IMPL = new SearchViewCompatHoneycombImpl(); + } else { + IMPL = new SearchViewCompatStubImpl(); + } + } + + private SearchViewCompat(Context context) { + /* Hide constructor */ + } + + /** + * Creates a new SearchView. + * + * @param context The Context the view is running in. + * @return A SearchView instance if the class is present on the current + * platform, null otherwise. + */ + public static View newSearchView(Context context) { + return IMPL.newSearchView(context); + } + + /** + * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used + * to display labels, hints, suggestions, create intents for launching search results screens + * and controlling other affordances such as a voice button. + * + * @param searchView The SearchView to operate on. + * @param searchableComponent The application component whose + * {@link android.app.SearchableInfo} should be loaded and applied to + * the SearchView. + */ + public static void setSearchableInfo(View searchView, ComponentName searchableComponent) { + IMPL.setSearchableInfo(searchView, searchableComponent); + } + + /** + * Sets the IME options on the query text field. This is a no-op if + * called on pre-{@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} + * platforms. + * + * @see TextView#setImeOptions(int) + * @param searchView The SearchView to operate on. + * @param imeOptions the options to set on the query text field + */ + public static void setImeOptions(View searchView, int imeOptions) { + IMPL.setImeOptions(searchView, imeOptions); + } + + /** + * Sets the input type on the query text field. This is a no-op if + * called on pre-{@link android.os.Build.VERSION_CODES#ICE_CREAM_SANDWICH} + * platforms. + * + * @see TextView#setInputType(int) + * @param searchView The SearchView to operate on. + * @param inputType the input type to set on the query text field + */ + public static void setInputType(View searchView, int inputType) { + IMPL.setInputType(searchView, inputType); + } + + /** + * Sets a listener for user actions within the SearchView. + * + * @param searchView The SearchView in which to register the listener. + * @param listener the listener object that receives callbacks when the user performs + * actions in the SearchView such as clicking on buttons or typing a query. + */ + public static void setOnQueryTextListener(View searchView, OnQueryTextListenerCompat listener) { + IMPL.setOnQueryTextListener(searchView, listener.mListener); + } + + /** + * Callbacks for changes to the query text. + */ + public static abstract class OnQueryTextListenerCompat { + final Object mListener; + + public OnQueryTextListenerCompat() { + mListener = IMPL.newOnQueryTextListener(this); + } + + /** + * Called when the user submits the query. This could be due to a key press on the + * keyboard or due to pressing a submit button. + * The listener can override the standard behavior by returning true + * to indicate that it has handled the submit request. Otherwise return false to + * let the SearchView handle the submission by launching any associated intent. + * + * @param query the query text that is to be submitted + * + * @return true if the query has been handled by the listener, false to let the + * SearchView perform the default action. + */ + public boolean onQueryTextSubmit(String query) { + return false; + } + + /** + * Called when the query text is changed by the user. + * + * @param newText the new content of the query text field. + * + * @return false if the SearchView should perform the default action of showing any + * suggestions if available, true if the action was handled by the listener. + */ + public boolean onQueryTextChange(String newText) { + return false; + } + } + + /** + * Sets a listener to inform when the user closes the SearchView. + * + * @param searchView The SearchView in which to register the listener. + * @param listener the listener to call when the user closes the SearchView. + */ + public static void setOnCloseListener(View searchView, OnCloseListenerCompat listener) { + IMPL.setOnCloseListener(searchView, listener.mListener); + } + + /** + * Callback for closing the query UI. + */ + public static abstract class OnCloseListenerCompat { + final Object mListener; + + public OnCloseListenerCompat() { + mListener = IMPL.newOnCloseListener(this); + } + + /** + * The user is attempting to close the SearchView. + * + * @return true if the listener wants to override the default behavior of clearing the + * text field and dismissing it, false otherwise. + */ + public boolean onClose() { + return false; + } + } + + /** + * Returns the query string currently in the text field. + * + * @param searchView The SearchView to operate on. + * + * @return the query string + */ + public static CharSequence getQuery(View searchView) { + return IMPL.getQuery(searchView); + } + + /** + * Sets a query string in the text field and optionally submits the query as well. + * + * @param searchView The SearchView to operate on. + * @param query the query string. This replaces any query text already present in the + * text field. + * @param submit whether to submit the query right now or only update the contents of + * text field. + */ + public static void setQuery(View searchView, CharSequence query, boolean submit) { + IMPL.setQuery(searchView, query, submit); + } + + /** + * Sets the hint text to display in the query text field. This overrides any hint specified + * in the SearchableInfo. + * + * @param searchView The SearchView to operate on. + * @param hint the hint text to display + */ + public static void setQueryHint(View searchView, CharSequence hint) { + IMPL.setQueryHint(searchView, hint); + } + + /** + * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is + * a temporary state and does not override the default iconified state set by + * setIconifiedByDefault(boolean). If the default state is iconified, then + * a false here will only be valid until the user closes the field. And if the default + * state is expanded, then a true here will only clear the text field and not close it. + * + * @param searchView The SearchView to operate on. + * @param iconify a true value will collapse the SearchView to an icon, while a false will + * expand it. + */ + public static void setIconified(View searchView, boolean iconify) { + IMPL.setIconified(searchView, iconify); + } + + /** + * Returns the current iconified state of the SearchView. + * + * @param searchView The SearchView to operate on. + * @return true if the SearchView is currently iconified, false if the search field is + * fully visible. + */ + public static boolean isIconified(View searchView) { + return IMPL.isIconified(searchView); + } + + /** + * Enables showing a submit button when the query is non-empty. In cases where the SearchView + * is being used to filter the contents of the current activity and doesn't launch a separate + * results activity, then the submit button should be disabled. + * + * @param searchView The SearchView to operate on. + * @param enabled true to show a submit button for submitting queries, false if a submit + * button is not required. + */ + public static void setSubmitButtonEnabled(View searchView, boolean enabled) { + IMPL.setSubmitButtonEnabled(searchView, enabled); + } + + /** + * Returns whether the submit button is enabled when necessary or never displayed. + * + * @param searchView The SearchView to operate on. + * @return whether the submit button is enabled automatically when necessary + */ + public static boolean isSubmitButtonEnabled(View searchView) { + return IMPL.isSubmitButtonEnabled(searchView); + } + + /** + * Specifies if a query refinement button should be displayed alongside each suggestion + * or if it should depend on the flags set in the individual items retrieved from the + * suggestions provider. Clicking on the query refinement button will replace the text + * in the query text field with the text from the suggestion. This flag only takes effect + * if a SearchableInfo has been specified with {@link #setSearchableInfo(View, ComponentName)} + * and not when using a custom adapter. + * + * @param searchView The SearchView to operate on. + * @param enable true if all items should have a query refinement button, false if only + * those items that have a query refinement flag set should have the button. + * + * @see SearchManager#SUGGEST_COLUMN_FLAGS + * @see SearchManager#FLAG_QUERY_REFINEMENT + */ + public static void setQueryRefinementEnabled(View searchView, boolean enable) { + IMPL.setQueryRefinementEnabled(searchView, enable); + } + + /** + * Returns whether query refinement is enabled for all items or only specific ones. + * @param searchView The SearchView to operate on. + * @return true if enabled for all items, false otherwise. + */ + public static boolean isQueryRefinementEnabled(View searchView) { + return IMPL.isQueryRefinementEnabled(searchView); + } + + /** + * Makes the view at most this many pixels wide + * @param searchView The SearchView to operate on. + */ + public static void setMaxWidth(View searchView, int maxpixels) { + IMPL.setMaxWidth(searchView, maxpixels); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatHoneycomb.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatHoneycomb.java new file mode 100644 index 00000000000..eeadfabb332 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatHoneycomb.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.app.SearchManager; +import android.content.ComponentName; +import android.content.Context; +import android.view.View; +import android.widget.SearchView; +import android.widget.SearchView.OnCloseListener; +import android.widget.SearchView.OnQueryTextListener; + +/** + * Implementation of SearchView compatibility that can call Honeycomb APIs. + */ +class SearchViewCompatHoneycomb { + + interface OnQueryTextListenerCompatBridge { + public boolean onQueryTextSubmit(String query); + public boolean onQueryTextChange(String newText); + } + + interface OnCloseListenerCompatBridge { + public boolean onClose(); + } + + public static View newSearchView(Context context) { + return new SearchView(context); + } + + public static void setSearchableInfo(View searchView, ComponentName searchableComponent) { + SearchView sv = ((SearchView) searchView); + SearchManager searchManager = (SearchManager) + sv.getContext().getSystemService(Context.SEARCH_SERVICE); + sv.setSearchableInfo(searchManager.getSearchableInfo(searchableComponent)); + } + + public static Object newOnQueryTextListener(final OnQueryTextListenerCompatBridge listener) { + return new OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + return listener.onQueryTextSubmit(query); + } + + @Override + public boolean onQueryTextChange(String newText) { + return listener.onQueryTextChange(newText); + } + }; + } + + public static void setOnQueryTextListener(Object searchView, Object listener) { + ((SearchView) searchView).setOnQueryTextListener((OnQueryTextListener) listener); + } + + public static Object newOnCloseListener(final OnCloseListenerCompatBridge listener) { + return new OnCloseListener() { + @Override + public boolean onClose() { + return listener.onClose(); + } + }; + } + + public static void setOnCloseListener(Object searchView, Object listener) { + ((SearchView) searchView).setOnCloseListener((OnCloseListener) listener); + } + + public static CharSequence getQuery(View searchView) { + return ((SearchView) searchView).getQuery(); + } + + public static void setQuery(View searchView, CharSequence query, boolean submit) { + ((SearchView) searchView).setQuery(query, submit); + } + + public static void setQueryHint(View searchView, CharSequence hint) { + ((SearchView) searchView).setQueryHint(hint); + } + + public static void setIconified(View searchView, boolean iconify) { + ((SearchView) searchView).setIconified(iconify); + } + + public static boolean isIconified(View searchView) { + return ((SearchView) searchView).isIconified(); + } + + public static void setSubmitButtonEnabled(View searchView, boolean enabled) { + ((SearchView) searchView).setSubmitButtonEnabled(enabled); + } + + public static boolean isSubmitButtonEnabled(View searchView) { + return ((SearchView) searchView).isSubmitButtonEnabled(); + } + + public static void setQueryRefinementEnabled(View searchView, boolean enable) { + ((SearchView) searchView).setQueryRefinementEnabled(enable); + } + + public static boolean isQueryRefinementEnabled(View searchView) { + return ((SearchView) searchView).isQueryRefinementEnabled(); + } + + public static void setMaxWidth(View searchView, int maxpixels) { + ((SearchView) searchView).setMaxWidth(maxpixels); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatIcs.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatIcs.java new file mode 100644 index 00000000000..b8d719cb8e4 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SearchViewCompatIcs.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.view.View; +import android.widget.SearchView; + +/** + * Implementation of SearchView compatibility that can call ICS APIs. + */ +class SearchViewCompatIcs { + + public static class MySearchView extends SearchView { + public MySearchView(Context context) { + super(context); + } + + // The normal SearchView doesn't clear its search text when + // collapsed, so we will do this for it. + @Override + public void onActionViewCollapsed() { + setQuery("", false); + super.onActionViewCollapsed(); + } + } + + public static View newSearchView(Context context) { + return new MySearchView(context); + } + + public static void setImeOptions(View searchView, int imeOptions) { + ((SearchView) searchView).setImeOptions(imeOptions); + } + + public static void setInputType(View searchView, int inputType) { + ((SearchView) searchView).setInputType(inputType); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SimpleCursorAdapter.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SimpleCursorAdapter.java new file mode 100644 index 00000000000..ca71e9ecc57 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SimpleCursorAdapter.java @@ -0,0 +1,398 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +/** + * Static library support version of the framework's {@link android.widget.SimpleCursorAdapter}. + * Used to write apps that run on platforms prior to Android 3.0. When running + * on Android 3.0 or above, this implementation is still used; it does not try + * to switch to the framework's implementation. See the framework SDK + * documentation for a class overview. + */ +public class SimpleCursorAdapter extends ResourceCursorAdapter { + /** + * A list of columns containing the data to bind to the UI. + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int[] mFrom; + /** + * A list of View ids representing the views to which the data must be bound. + * This field should be made private, so it is hidden from the SDK. + * {@hide} + */ + protected int[] mTo; + + private int mStringConversionColumn = -1; + private CursorToStringConverter mCursorToStringConverter; + private ViewBinder mViewBinder; + + String[] mOriginalFrom; + + /** + * Constructor the enables auto-requery. + * + * @deprecated This option is discouraged, as it results in Cursor queries + * being performed on the application's UI thread and thus can cause poor + * responsiveness or even Application Not Responding errors. As an alternative, + * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}. + */ + @Deprecated + public SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) { + super(context, layout, c); + mTo = to; + mOriginalFrom = from; + findColumns(from); + } + + /** + * Standard constructor. + * + * @param context The context where the ListView associated with this + * SimpleListItemFactory is running + * @param layout resource identifier of a layout file that defines the views + * for this list item. The layout file should include at least + * those named views defined in "to" + * @param c The database cursor. Can be null if the cursor is not available yet. + * @param from A list of column names representing the data to bind to the UI. Can be null + * if the cursor is not available yet. + * @param to The views that should display column in the "from" parameter. + * These should all be TextViews. The first N views in this list + * are given the values of the first N columns in the from + * parameter. Can be null if the cursor is not available yet. + * @param flags Flags used to determine the behavior of the adapter, + * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}. + */ + public SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, + int[] to, int flags) { + super(context, layout, c, flags); + mTo = to; + mOriginalFrom = from; + findColumns(from); + } + + /** + * Binds all of the field names passed into the "to" parameter of the + * constructor with their corresponding cursor columns as specified in the + * "from" parameter. + * + * Binding occurs in two phases. First, if a + * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available, + * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)} + * is invoked. If the returned value is true, binding has occured. If the + * returned value is false and the view to bind is a TextView, + * {@link #setViewText(TextView, String)} is invoked. If the returned value is + * false and the view to bind is an ImageView, + * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate + * binding can be found, an {@link IllegalStateException} is thrown. + * + * @throws IllegalStateException if binding cannot occur + * + * @see android.widget.CursorAdapter#bindView(android.view.View, + * android.content.Context, android.database.Cursor) + * @see #getViewBinder() + * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder) + * @see #setViewImage(ImageView, String) + * @see #setViewText(TextView, String) + */ + @Override + public void bindView(View view, Context context, Cursor cursor) { + final ViewBinder binder = mViewBinder; + final int count = mTo.length; + final int[] from = mFrom; + final int[] to = mTo; + + for (int i = 0; i < count; i++) { + final View v = view.findViewById(to[i]); + if (v != null) { + boolean bound = false; + if (binder != null) { + bound = binder.setViewValue(v, cursor, from[i]); + } + + if (!bound) { + String text = cursor.getString(from[i]); + if (text == null) { + text = ""; + } + + if (v instanceof TextView) { + setViewText((TextView) v, text); + } else if (v instanceof ImageView) { + setViewImage((ImageView) v, text); + } else { + throw new IllegalStateException(v.getClass().getName() + " is not a " + + " view that can be bounds by this SimpleCursorAdapter"); + } + } + } + } + } + + /** + * Returns the {@link ViewBinder} used to bind data to views. + * + * @return a ViewBinder or null if the binder does not exist + * + * @see #bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder) + */ + public ViewBinder getViewBinder() { + return mViewBinder; + } + + /** + * Sets the binder used to bind data to views. + * + * @param viewBinder the binder used to bind data to views, can be null to + * remove the existing binder + * + * @see #bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see #getViewBinder() + */ + public void setViewBinder(ViewBinder viewBinder) { + mViewBinder = viewBinder; + } + + /** + * Called by bindView() to set the image for an ImageView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to an ImageView. + * + * By default, the value will be treated as an image resource. If the + * value cannot be used as an image resource, the value is used as an + * image Uri. + * + * Intended to be overridden by Adapters that need to filter strings + * retrieved from the database. + * + * @param v ImageView to receive an image + * @param value the value retrieved from the cursor + */ + public void setViewImage(ImageView v, String value) { + try { + v.setImageResource(Integer.parseInt(value)); + } catch (NumberFormatException nfe) { + v.setImageURI(Uri.parse(value)); + } + } + + /** + * Called by bindView() to set the text for a TextView but only if + * there is no existing ViewBinder or if the existing ViewBinder cannot + * handle binding to an TextView. + * + * Intended to be overridden by Adapters that need to filter strings + * retrieved from the database. + * + * @param v TextView to receive text + * @param text the text to be set for the TextView + */ + public void setViewText(TextView v, String text) { + v.setText(text); + } + + /** + * Return the index of the column used to get a String representation + * of the Cursor. + * + * @return a valid index in the current Cursor or -1 + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + * @see #setStringConversionColumn(int) + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getCursorToStringConverter() + */ + public int getStringConversionColumn() { + return mStringConversionColumn; + } + + /** + * Defines the index of the column in the Cursor used to get a String + * representation of that Cursor. The column is used to convert the + * Cursor to a String only when the current CursorToStringConverter + * is null. + * + * @param stringConversionColumn a valid index in the current Cursor or -1 to use the default + * conversion mechanism + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + * @see #getStringConversionColumn() + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getCursorToStringConverter() + */ + public void setStringConversionColumn(int stringConversionColumn) { + mStringConversionColumn = stringConversionColumn; + } + + /** + * Returns the converter used to convert the filtering Cursor + * into a String. + * + * @return null if the converter does not exist or an instance of + * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} + * + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getStringConversionColumn() + * @see #setStringConversionColumn(int) + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public CursorToStringConverter getCursorToStringConverter() { + return mCursorToStringConverter; + } + + /** + * Sets the converter used to convert the filtering Cursor + * into a String. + * + * @param cursorToStringConverter the Cursor to String converter, or + * null to remove the converter + * + * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) + * @see #getStringConversionColumn() + * @see #setStringConversionColumn(int) + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) { + mCursorToStringConverter = cursorToStringConverter; + } + + /** + * Returns a CharSequence representation of the specified Cursor as defined + * by the current CursorToStringConverter. If no CursorToStringConverter + * has been set, the String conversion column is used instead. If the + * conversion column is -1, the returned String is empty if the cursor + * is null or Cursor.toString(). + * + * @param cursor the Cursor to convert to a CharSequence + * + * @return a non-null CharSequence representing the cursor + */ + @Override + public CharSequence convertToString(Cursor cursor) { + if (mCursorToStringConverter != null) { + return mCursorToStringConverter.convertToString(cursor); + } else if (mStringConversionColumn > -1) { + return cursor.getString(mStringConversionColumn); + } + + return super.convertToString(cursor); + } + + /** + * Create a map from an array of strings to an array of column-id integers in mCursor. + * If mCursor is null, the array will be discarded. + * + * @param from the Strings naming the columns of interest + */ + private void findColumns(String[] from) { + if (mCursor != null) { + int i; + int count = from.length; + if (mFrom == null || mFrom.length != count) { + mFrom = new int[count]; + } + for (i = 0; i < count; i++) { + mFrom[i] = mCursor.getColumnIndexOrThrow(from[i]); + } + } else { + mFrom = null; + } + } + + @Override + public Cursor swapCursor(Cursor c) { + Cursor res = super.swapCursor(c); + // rescan columns in case cursor layout is different + findColumns(mOriginalFrom); + return res; + } + + /** + * Change the cursor and change the column-to-view mappings at the same time. + * + * @param c The database cursor. Can be null if the cursor is not available yet. + * @param from A list of column names representing the data to bind to the UI. Can be null + * if the cursor is not available yet. + * @param to The views that should display column in the "from" parameter. + * These should all be TextViews. The first N views in this list + * are given the values of the first N columns in the from + * parameter. Can be null if the cursor is not available yet. + */ + public void changeCursorAndColumns(Cursor c, String[] from, int[] to) { + mOriginalFrom = from; + mTo = to; + super.changeCursor(c); + findColumns(mOriginalFrom); + } + + /** + * This class can be used by external clients of SimpleCursorAdapter + * to bind values fom the Cursor to views. + * + * You should use this class to bind values from the Cursor to views + * that are not directly supported by SimpleCursorAdapter or to + * change the way binding occurs for views supported by + * SimpleCursorAdapter. + * + * @see SimpleCursorAdapter#bindView(android.view.View, android.content.Context, android.database.Cursor) + * @see SimpleCursorAdapter#setViewImage(ImageView, String) + * @see SimpleCursorAdapter#setViewText(TextView, String) + */ + public static interface ViewBinder { + /** + * Binds the Cursor column defined by the specified index to the specified view. + * + * When binding is handled by this ViewBinder, this method must return true. + * If this method returns false, SimpleCursorAdapter will attempts to handle + * the binding on its own. + * + * @param view the view to bind the data to + * @param cursor the cursor to get the data from + * @param columnIndex the column at which the data can be found in the cursor + * + * @return true if the data was bound to the view, false otherwise + */ + boolean setViewValue(View view, Cursor cursor, int columnIndex); + } + + /** + * This class can be used by external clients of SimpleCursorAdapter + * to define how the Cursor should be converted to a String. + * + * @see android.widget.CursorAdapter#convertToString(android.database.Cursor) + */ + public static interface CursorToStringConverter { + /** + * Returns a CharSequence representing the specified Cursor. + * + * @param cursor the cursor for which a CharSequence representation + * is requested + * + * @return a non-null CharSequence representing the cursor + */ + CharSequence convertToString(Cursor cursor); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SlidingPaneLayout.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SlidingPaneLayout.java new file mode 100644 index 00000000000..391ba999556 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SlidingPaneLayout.java @@ -0,0 +1,1644 @@ +/* + * Copyright (C) 2012 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.DrawableRes; +import android.support.v4.view.AccessibilityDelegateCompat; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; + +/** + * SlidingPaneLayout provides a horizontal, multi-pane layout for use at the top level + * of a UI. A left (or first) pane is treated as a content list or browser, subordinate to a + * primary detail view for displaying content. + * + *

    Child views may overlap if their combined width exceeds the available width + * in the SlidingPaneLayout. When this occurs the user may slide the topmost view out of the way + * by dragging it, or by navigating in the direction of the overlapped view using a keyboard. + * If the content of the dragged child view is itself horizontally scrollable, the user may + * grab it by the very edge.

    + * + *

    Thanks to this sliding behavior, SlidingPaneLayout may be suitable for creating layouts + * that can smoothly adapt across many different screen sizes, expanding out fully on larger + * screens and collapsing on smaller screens.

    + * + *

    SlidingPaneLayout is distinct from a navigation drawer as described in the design + * guide and should not be used in the same scenarios. SlidingPaneLayout should be thought + * of only as a way to allow a two-pane layout normally used on larger screens to adapt to smaller + * screens in a natural way. The interaction patterns expressed by SlidingPaneLayout imply + * a physicality and direct information hierarchy between panes that does not necessarily exist + * in a scenario where a navigation drawer should be used instead.

    + * + *

    Appropriate uses of SlidingPaneLayout include pairings of panes such as a contact list and + * subordinate interactions with those contacts, or an email thread list with the content pane + * displaying the contents of the selected thread. Inappropriate uses of SlidingPaneLayout include + * switching between disparate functions of your app, such as jumping from a social stream view + * to a view of your personal profile - cases such as this should use the navigation drawer + * pattern instead. ({@link DrawerLayout DrawerLayout} implements this pattern.)

    + * + *

    Like {@link android.widget.LinearLayout LinearLayout}, SlidingPaneLayout supports + * the use of the layout parameter layout_weight on child views to determine + * how to divide leftover space after measurement is complete. It is only relevant for width. + * When views do not overlap weight behaves as it does in a LinearLayout.

    + * + *

    When views do overlap, weight on a slideable pane indicates that the pane should be + * sized to fill all available space in the closed state. Weight on a pane that becomes covered + * indicates that the pane should be sized to fill all available space except a small minimum strip + * that the user may use to grab the slideable view and pull it back over into a closed state.

    + */ +public class SlidingPaneLayout extends ViewGroup { + private static final String TAG = "SlidingPaneLayout"; + + /** + * Default size of the overhang for a pane in the open state. + * At least this much of a sliding pane will remain visible. + * This indicates that there is more content available and provides + * a "physical" edge to grab to pull it closed. + */ + private static final int DEFAULT_OVERHANG_SIZE = 32; // dp; + + /** + * If no fade color is given by default it will fade to 80% gray. + */ + private static final int DEFAULT_FADE_COLOR = 0xcccccccc; + + /** + * The fade color used for the sliding panel. 0 = no fading. + */ + private int mSliderFadeColor = DEFAULT_FADE_COLOR; + + /** + * Minimum velocity that will be detected as a fling + */ + private static final int MIN_FLING_VELOCITY = 400; // dips per second + + /** + * The fade color used for the panel covered by the slider. 0 = no fading. + */ + private int mCoveredFadeColor; + + /** + * Drawable used to draw the shadow between panes by default. + */ + private Drawable mShadowDrawableLeft; + + /** + * Drawable used to draw the shadow between panes to support RTL (right to left language). + */ + private Drawable mShadowDrawableRight; + + /** + * The size of the overhang in pixels. + * This is the minimum section of the sliding panel that will + * be visible in the open state to allow for a closing drag. + */ + private final int mOverhangSize; + + /** + * True if a panel can slide with the current measurements + */ + private boolean mCanSlide; + + /** + * The child view that can slide, if any. + */ + private View mSlideableView; + + /** + * How far the panel is offset from its closed position. + * range [0, 1] where 0 = closed, 1 = open. + */ + private float mSlideOffset; + + /** + * How far the non-sliding panel is parallaxed from its usual position when open. + * range [0, 1] + */ + private float mParallaxOffset; + + /** + * How far in pixels the slideable panel may move. + */ + private int mSlideRange; + + /** + * A panel view is locked into internal scrolling or another condition that + * is preventing a drag. + */ + private boolean mIsUnableToDrag; + + /** + * Distance in pixels to parallax the fixed pane by when fully closed + */ + private int mParallaxBy; + + private float mInitialMotionX; + private float mInitialMotionY; + + private PanelSlideListener mPanelSlideListener; + + private final ViewDragHelper mDragHelper; + + /** + * Stores whether or not the pane was open the last time it was slideable. + * If open/close operations are invoked this state is modified. Used by + * instance state save/restore. + */ + private boolean mPreservedOpenState; + private boolean mFirstLayout = true; + + private final Rect mTmpRect = new Rect(); + + private final ArrayList mPostedRunnables = + new ArrayList(); + + static final SlidingPanelLayoutImpl IMPL; + + static { + final int deviceVersion = Build.VERSION.SDK_INT; + if (deviceVersion >= 17) { + IMPL = new SlidingPanelLayoutImplJBMR1(); + } else if (deviceVersion >= 16) { + IMPL = new SlidingPanelLayoutImplJB(); + } else { + IMPL = new SlidingPanelLayoutImplBase(); + } + } + + /** + * Listener for monitoring events about sliding panes. + */ + public interface PanelSlideListener { + /** + * Called when a sliding pane's position changes. + * @param panel The child view that was moved + * @param slideOffset The new offset of this sliding pane within its range, from 0-1 + */ + public void onPanelSlide(View panel, float slideOffset); + /** + * Called when a sliding pane becomes slid completely open. The pane may or may not + * be interactive at this point depending on how much of the pane is visible. + * @param panel The child view that was slid to an open position, revealing other panes + */ + public void onPanelOpened(View panel); + + /** + * Called when a sliding pane becomes slid completely closed. The pane is now guaranteed + * to be interactive. It may now obscure other views in the layout. + * @param panel The child view that was slid to a closed position + */ + public void onPanelClosed(View panel); + } + + /** + * No-op stubs for {@link PanelSlideListener}. If you only want to implement a subset + * of the listener methods you can extend this instead of implement the full interface. + */ + public static class SimplePanelSlideListener implements PanelSlideListener { + @Override + public void onPanelSlide(View panel, float slideOffset) { + } + @Override + public void onPanelOpened(View panel) { + } + @Override + public void onPanelClosed(View panel) { + } + } + + public SlidingPaneLayout(Context context) { + this(context, null); + } + + public SlidingPaneLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public SlidingPaneLayout(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + final float density = context.getResources().getDisplayMetrics().density; + mOverhangSize = (int) (DEFAULT_OVERHANG_SIZE * density + 0.5f); + + final ViewConfiguration viewConfig = ViewConfiguration.get(context); + + setWillNotDraw(false); + + ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegate()); + ViewCompat.setImportantForAccessibility(this, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + + mDragHelper = ViewDragHelper.create(this, 0.5f, new DragHelperCallback()); + mDragHelper.setMinVelocity(MIN_FLING_VELOCITY * density); + } + + /** + * Set a distance to parallax the lower pane by when the upper pane is in its + * fully closed state. The lower pane will scroll between this position and + * its fully open state. + * + * @param parallaxBy Distance to parallax by in pixels + */ + public void setParallaxDistance(int parallaxBy) { + mParallaxBy = parallaxBy; + requestLayout(); + } + + /** + * @return The distance the lower pane will parallax by when the upper pane is fully closed. + * + * @see #setParallaxDistance(int) + */ + public int getParallaxDistance() { + return mParallaxBy; + } + + /** + * Set the color used to fade the sliding pane out when it is slid most of the way offscreen. + * + * @param color An ARGB-packed color value + */ + public void setSliderFadeColor(int color) { + mSliderFadeColor = color; + } + + /** + * @return The ARGB-packed color value used to fade the sliding pane + */ + public int getSliderFadeColor() { + return mSliderFadeColor; + } + + /** + * Set the color used to fade the pane covered by the sliding pane out when the pane + * will become fully covered in the closed state. + * + * @param color An ARGB-packed color value + */ + public void setCoveredFadeColor(int color) { + mCoveredFadeColor = color; + } + + /** + * @return The ARGB-packed color value used to fade the fixed pane + */ + public int getCoveredFadeColor() { + return mCoveredFadeColor; + } + + public void setPanelSlideListener(PanelSlideListener listener) { + mPanelSlideListener = listener; + } + + void dispatchOnPanelSlide(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelSlide(panel, mSlideOffset); + } + } + + void dispatchOnPanelOpened(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelOpened(panel); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void dispatchOnPanelClosed(View panel) { + if (mPanelSlideListener != null) { + mPanelSlideListener.onPanelClosed(panel); + } + sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); + } + + void updateObscuredViewsVisibility(View panel) { + final boolean isLayoutRtl = isLayoutRtlSupport(); + final int startBound = isLayoutRtl ? (getWidth() - getPaddingRight()) : + getPaddingLeft(); + final int endBound = isLayoutRtl ? getPaddingLeft() : + (getWidth() - getPaddingRight()); + final int topBound = getPaddingTop(); + final int bottomBound = getHeight() - getPaddingBottom(); + final int left; + final int right; + final int top; + final int bottom; + if (panel != null && viewIsOpaque(panel)) { + left = panel.getLeft(); + right = panel.getRight(); + top = panel.getTop(); + bottom = panel.getBottom(); + } else { + left = right = top = bottom = 0; + } + + for (int i = 0, childCount = getChildCount(); i < childCount; i++) { + final View child = getChildAt(i); + + if (child == panel) { + // There are still more children above the panel but they won't be affected. + break; + } + + final int clampedChildLeft = Math.max((isLayoutRtl ? endBound : + startBound), child.getLeft()); + final int clampedChildTop = Math.max(topBound, child.getTop()); + final int clampedChildRight = Math.min((isLayoutRtl ? startBound : + endBound), child.getRight()); + final int clampedChildBottom = Math.min(bottomBound, child.getBottom()); + final int vis; + if (clampedChildLeft >= left && clampedChildTop >= top && + clampedChildRight <= right && clampedChildBottom <= bottom) { + vis = INVISIBLE; + } else { + vis = VISIBLE; + } + child.setVisibility(vis); + } + } + + void setAllChildrenVisible() { + for (int i = 0, childCount = getChildCount(); i < childCount; i++) { + final View child = getChildAt(i); + if (child.getVisibility() == INVISIBLE) { + child.setVisibility(VISIBLE); + } + } + } + + private static boolean viewIsOpaque(View v) { + if (ViewCompat.isOpaque(v)) return true; + + // View#isOpaque didn't take all valid opaque scrollbar modes into account + // before API 18 (JB-MR2). On newer devices rely solely on isOpaque above and return false + // here. On older devices, check the view's background drawable directly as a fallback. + if (Build.VERSION.SDK_INT >= 18) return false; + + final Drawable bg = v.getBackground(); + if (bg != null) { + return bg.getOpacity() == PixelFormat.OPAQUE; + } + return false; + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + mFirstLayout = true; + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + mFirstLayout = true; + + for (int i = 0, count = mPostedRunnables.size(); i < count; i++) { + final DisableLayerRunnable dlr = mPostedRunnables.get(i); + dlr.run(); + } + mPostedRunnables.clear(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int widthSize = MeasureSpec.getSize(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + int heightSize = MeasureSpec.getSize(heightMeasureSpec); + + if (widthMode != MeasureSpec.EXACTLY) { + if (isInEditMode()) { + // Don't crash the layout editor. Consume all of the space if specified + // or pick a magic number from thin air otherwise. + // TODO Better communication with tools of this bogus state. + // It will crash on a real device. + if (widthMode == MeasureSpec.AT_MOST) { + widthMode = MeasureSpec.EXACTLY; + } else if (widthMode == MeasureSpec.UNSPECIFIED) { + widthMode = MeasureSpec.EXACTLY; + widthSize = 300; + } + } else { + throw new IllegalStateException("Width must have an exact value or MATCH_PARENT"); + } + } else if (heightMode == MeasureSpec.UNSPECIFIED) { + if (isInEditMode()) { + // Don't crash the layout editor. Pick a magic number from thin air instead. + // TODO Better communication with tools of this bogus state. + // It will crash on a real device. + if (heightMode == MeasureSpec.UNSPECIFIED) { + heightMode = MeasureSpec.AT_MOST; + heightSize = 300; + } + } else { + throw new IllegalStateException("Height must not be UNSPECIFIED"); + } + } + + int layoutHeight = 0; + int maxLayoutHeight = -1; + switch (heightMode) { + case MeasureSpec.EXACTLY: + layoutHeight = maxLayoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); + break; + case MeasureSpec.AT_MOST: + maxLayoutHeight = heightSize - getPaddingTop() - getPaddingBottom(); + break; + } + + float weightSum = 0; + boolean canSlide = false; + final int widthAvailable = widthSize - getPaddingLeft() - getPaddingRight(); + int widthRemaining = widthAvailable; + final int childCount = getChildCount(); + + if (childCount > 2) { + Log.e(TAG, "onMeasure: More than two child views are not supported."); + } + + // We'll find the current one below. + mSlideableView = null; + + // First pass. Measure based on child LayoutParams width/height. + // Weight will incur a second pass. + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (child.getVisibility() == GONE) { + lp.dimWhenOffset = false; + continue; + } + + if (lp.weight > 0) { + weightSum += lp.weight; + + // If we have no width, weight is the only contributor to the final size. + // Measure this view on the weight pass only. + if (lp.width == 0) continue; + } + + int childWidthSpec; + final int horizontalMargin = lp.leftMargin + lp.rightMargin; + if (lp.width == LayoutParams.WRAP_CONTENT) { + childWidthSpec = MeasureSpec.makeMeasureSpec(widthAvailable - horizontalMargin, + MeasureSpec.AT_MOST); + } else if (lp.width == LayoutParams.FILL_PARENT) { + childWidthSpec = MeasureSpec.makeMeasureSpec(widthAvailable - horizontalMargin, + MeasureSpec.EXACTLY); + } else { + childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY); + } + + int childHeightSpec; + if (lp.height == LayoutParams.WRAP_CONTENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, MeasureSpec.AT_MOST); + } else if (lp.height == LayoutParams.FILL_PARENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY); + } + + child.measure(childWidthSpec, childHeightSpec); + final int childWidth = child.getMeasuredWidth(); + final int childHeight = child.getMeasuredHeight(); + + if (heightMode == MeasureSpec.AT_MOST && childHeight > layoutHeight) { + layoutHeight = Math.min(childHeight, maxLayoutHeight); + } + + widthRemaining -= childWidth; + canSlide |= lp.slideable = widthRemaining < 0; + if (lp.slideable) { + mSlideableView = child; + } + } + + // Resolve weight and make sure non-sliding panels are smaller than the full screen. + if (canSlide || weightSum > 0) { + final int fixedPanelWidthLimit = widthAvailable - mOverhangSize; + + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + if (child.getVisibility() == GONE) { + continue; + } + + final boolean skippedFirstPass = lp.width == 0 && lp.weight > 0; + final int measuredWidth = skippedFirstPass ? 0 : child.getMeasuredWidth(); + if (canSlide && child != mSlideableView) { + if (lp.width < 0 && (measuredWidth > fixedPanelWidthLimit || lp.weight > 0)) { + // Fixed panels in a sliding configuration should + // be clamped to the fixed panel limit. + final int childHeightSpec; + if (skippedFirstPass) { + // Do initial height measurement if we skipped measuring this view + // the first time around. + if (lp.height == LayoutParams.WRAP_CONTENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, + MeasureSpec.AT_MOST); + } else if (lp.height == LayoutParams.FILL_PARENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, + MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, + MeasureSpec.EXACTLY); + } + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec( + child.getMeasuredHeight(), MeasureSpec.EXACTLY); + } + final int childWidthSpec = MeasureSpec.makeMeasureSpec( + fixedPanelWidthLimit, MeasureSpec.EXACTLY); + child.measure(childWidthSpec, childHeightSpec); + } + } else if (lp.weight > 0) { + int childHeightSpec; + if (lp.width == 0) { + // This was skipped the first time; figure out a real height spec. + if (lp.height == LayoutParams.WRAP_CONTENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, + MeasureSpec.AT_MOST); + } else if (lp.height == LayoutParams.FILL_PARENT) { + childHeightSpec = MeasureSpec.makeMeasureSpec(maxLayoutHeight, + MeasureSpec.EXACTLY); + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, + MeasureSpec.EXACTLY); + } + } else { + childHeightSpec = MeasureSpec.makeMeasureSpec( + child.getMeasuredHeight(), MeasureSpec.EXACTLY); + } + + if (canSlide) { + // Consume available space + final int horizontalMargin = lp.leftMargin + lp.rightMargin; + final int newWidth = widthAvailable - horizontalMargin; + final int childWidthSpec = MeasureSpec.makeMeasureSpec( + newWidth, MeasureSpec.EXACTLY); + if (measuredWidth != newWidth) { + child.measure(childWidthSpec, childHeightSpec); + } + } else { + // Distribute the extra width proportionally similar to LinearLayout + final int widthToDistribute = Math.max(0, widthRemaining); + final int addedWidth = (int) (lp.weight * widthToDistribute / weightSum); + final int childWidthSpec = MeasureSpec.makeMeasureSpec( + measuredWidth + addedWidth, MeasureSpec.EXACTLY); + child.measure(childWidthSpec, childHeightSpec); + } + } + } + } + + final int measuredWidth = widthSize; + final int measuredHeight = layoutHeight + getPaddingTop() + getPaddingBottom(); + + setMeasuredDimension(measuredWidth, measuredHeight); + mCanSlide = canSlide; + + if (mDragHelper.getViewDragState() != ViewDragHelper.STATE_IDLE && !canSlide) { + // Cancel scrolling in progress, it's no longer relevant. + mDragHelper.abort(); + } + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + final boolean isLayoutRtl = isLayoutRtlSupport(); + if (isLayoutRtl) { + mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_RIGHT); + } else { + mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT); + } + final int width = r - l; + final int paddingStart = isLayoutRtl ? getPaddingRight() : getPaddingLeft(); + final int paddingEnd = isLayoutRtl ? getPaddingLeft() : getPaddingRight(); + final int paddingTop = getPaddingTop(); + + final int childCount = getChildCount(); + int xStart = paddingStart; + int nextXStart = xStart; + + if (mFirstLayout) { + mSlideOffset = mCanSlide && mPreservedOpenState ? 1.f : 0.f; + } + + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + + if (child.getVisibility() == GONE) { + continue; + } + + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + + final int childWidth = child.getMeasuredWidth(); + int offset = 0; + + if (lp.slideable) { + final int margin = lp.leftMargin + lp.rightMargin; + final int range = Math.min(nextXStart, + width - paddingEnd - mOverhangSize) - xStart - margin; + mSlideRange = range; + final int lpMargin = isLayoutRtl ? lp.rightMargin : lp.leftMargin; + lp.dimWhenOffset = xStart + lpMargin + range + childWidth / 2 > + width - paddingEnd; + final int pos = (int) (range * mSlideOffset); + xStart += pos + lpMargin; + mSlideOffset = (float) pos / mSlideRange; + } else if (mCanSlide && mParallaxBy != 0) { + offset = (int) ((1 - mSlideOffset) * mParallaxBy); + xStart = nextXStart; + } else { + xStart = nextXStart; + } + + final int childRight; + final int childLeft; + if (isLayoutRtl) { + childRight = width - xStart + offset; + childLeft = childRight - childWidth; + } else { + childLeft = xStart - offset; + childRight = childLeft + childWidth; + } + + final int childTop = paddingTop; + final int childBottom = childTop + child.getMeasuredHeight(); + child.layout(childLeft, paddingTop, childRight, childBottom); + + nextXStart += child.getWidth(); + } + + if (mFirstLayout) { + if (mCanSlide) { + if (mParallaxBy != 0) { + parallaxOtherViews(mSlideOffset); + } + if (((LayoutParams) mSlideableView.getLayoutParams()).dimWhenOffset) { + dimChildView(mSlideableView, mSlideOffset, mSliderFadeColor); + } + } else { + // Reset the dim level of all children; it's irrelevant when nothing moves. + for (int i = 0; i < childCount; i++) { + dimChildView(getChildAt(i), 0, mSliderFadeColor); + } + } + updateObscuredViewsVisibility(mSlideableView); + } + + mFirstLayout = false; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + // Recalculate sliding panes and their details + if (w != oldw) { + mFirstLayout = true; + } + } + + @Override + public void requestChildFocus(View child, View focused) { + super.requestChildFocus(child, focused); + if (!isInTouchMode() && !mCanSlide) { + mPreservedOpenState = child == mSlideableView; + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + + // Preserve the open state based on the last view that was touched. + if (!mCanSlide && action == MotionEvent.ACTION_DOWN && getChildCount() > 1) { + // After the first things will be slideable. + final View secondChild = getChildAt(1); + if (secondChild != null) { + mPreservedOpenState = !mDragHelper.isViewUnder(secondChild, + (int) ev.getX(), (int) ev.getY()); + } + } + + if (!mCanSlide || (mIsUnableToDrag && action != MotionEvent.ACTION_DOWN)) { + mDragHelper.cancel(); + return super.onInterceptTouchEvent(ev); + } + + if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) { + mDragHelper.cancel(); + return false; + } + + boolean interceptTap = false; + + switch (action) { + case MotionEvent.ACTION_DOWN: { + mIsUnableToDrag = false; + final float x = ev.getX(); + final float y = ev.getY(); + mInitialMotionX = x; + mInitialMotionY = y; + + if (mDragHelper.isViewUnder(mSlideableView, (int) x, (int) y) && + isDimmed(mSlideableView)) { + interceptTap = true; + } + break; + } + + case MotionEvent.ACTION_MOVE: { + final float x = ev.getX(); + final float y = ev.getY(); + final float adx = Math.abs(x - mInitialMotionX); + final float ady = Math.abs(y - mInitialMotionY); + final int slop = mDragHelper.getTouchSlop(); + if (adx > slop && ady > adx) { + mDragHelper.cancel(); + mIsUnableToDrag = true; + return false; + } + } + } + + final boolean interceptForDrag = mDragHelper.shouldInterceptTouchEvent(ev); + + return interceptForDrag || interceptTap; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (!mCanSlide) { + return super.onTouchEvent(ev); + } + + mDragHelper.processTouchEvent(ev); + + final int action = ev.getAction(); + boolean wantTouchEvents = true; + + switch (action & MotionEventCompat.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + mInitialMotionX = x; + mInitialMotionY = y; + break; + } + + case MotionEvent.ACTION_UP: { + if (isDimmed(mSlideableView)) { + final float x = ev.getX(); + final float y = ev.getY(); + final float dx = x - mInitialMotionX; + final float dy = y - mInitialMotionY; + final int slop = mDragHelper.getTouchSlop(); + if (dx * dx + dy * dy < slop * slop && + mDragHelper.isViewUnder(mSlideableView, (int) x, (int) y)) { + // Taps close a dimmed open pane. + closePane(mSlideableView, 0); + break; + } + } + break; + } + } + + return wantTouchEvents; + } + + private boolean closePane(View pane, int initialVelocity) { + if (mFirstLayout || smoothSlideTo(0.f, initialVelocity)) { + mPreservedOpenState = false; + return true; + } + return false; + } + + private boolean openPane(View pane, int initialVelocity) { + if (mFirstLayout || smoothSlideTo(1.f, initialVelocity)) { + mPreservedOpenState = true; + return true; + } + return false; + } + + /** + * @deprecated Renamed to {@link #openPane()} - this method is going away soon! + */ + @Deprecated + public void smoothSlideOpen() { + openPane(); + } + + /** + * Open the sliding pane if it is currently slideable. If first layout + * has already completed this will animate. + * + * @return true if the pane was slideable and is now open/in the process of opening + */ + public boolean openPane() { + return openPane(mSlideableView, 0); + } + + /** + * @deprecated Renamed to {@link #closePane()} - this method is going away soon! + */ + @Deprecated + public void smoothSlideClosed() { + closePane(); + } + + /** + * Close the sliding pane if it is currently slideable. If first layout + * has already completed this will animate. + * + * @return true if the pane was slideable and is now closed/in the process of closing + */ + public boolean closePane() { + return closePane(mSlideableView, 0); + } + + /** + * Check if the layout is completely open. It can be open either because the slider + * itself is open revealing the left pane, or if all content fits without sliding. + * + * @return true if sliding panels are completely open + */ + public boolean isOpen() { + return !mCanSlide || mSlideOffset == 1; + } + + /** + * @return true if content in this layout can be slid open and closed + * @deprecated Renamed to {@link #isSlideable()} - this method is going away soon! + */ + @Deprecated + public boolean canSlide() { + return mCanSlide; + } + + /** + * Check if the content in this layout cannot fully fit side by side and therefore + * the content pane can be slid back and forth. + * + * @return true if content in this layout can be slid open and closed + */ + public boolean isSlideable() { + return mCanSlide; + } + + private void onPanelDragged(int newLeft) { + if (mSlideableView == null) { + // This can happen if we're aborting motion during layout because everything now fits. + mSlideOffset = 0; + return; + } + final boolean isLayoutRtl = isLayoutRtlSupport(); + final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); + + int childWidth = mSlideableView.getWidth(); + final int newStart = isLayoutRtl ? getWidth() - newLeft - childWidth : newLeft; + + final int paddingStart = isLayoutRtl ? getPaddingRight() : getPaddingLeft(); + final int lpMargin = isLayoutRtl ? lp.rightMargin : lp.leftMargin; + final int startBound = paddingStart + lpMargin; + + mSlideOffset = (float) (newStart - startBound) / mSlideRange; + + if (mParallaxBy != 0) { + parallaxOtherViews(mSlideOffset); + } + + if (lp.dimWhenOffset) { + dimChildView(mSlideableView, mSlideOffset, mSliderFadeColor); + } + dispatchOnPanelSlide(mSlideableView); + } + + private void dimChildView(View v, float mag, int fadeColor) { + final LayoutParams lp = (LayoutParams) v.getLayoutParams(); + + if (mag > 0 && fadeColor != 0) { + final int baseAlpha = (fadeColor & 0xff000000) >>> 24; + int imag = (int) (baseAlpha * mag); + int color = imag << 24 | (fadeColor & 0xffffff); + if (lp.dimPaint == null) { + lp.dimPaint = new Paint(); + } + lp.dimPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_OVER)); + if (ViewCompat.getLayerType(v) != ViewCompat.LAYER_TYPE_HARDWARE) { + ViewCompat.setLayerType(v, ViewCompat.LAYER_TYPE_HARDWARE, lp.dimPaint); + } + invalidateChildRegion(v); + } else if (ViewCompat.getLayerType(v) != ViewCompat.LAYER_TYPE_NONE) { + if (lp.dimPaint != null) { + lp.dimPaint.setColorFilter(null); + } + final DisableLayerRunnable dlr = new DisableLayerRunnable(v); + mPostedRunnables.add(dlr); + ViewCompat.postOnAnimation(this, dlr); + } + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + boolean result; + final int save = canvas.save(Canvas.CLIP_SAVE_FLAG); + + if (mCanSlide && !lp.slideable && mSlideableView != null) { + // Clip against the slider; no sense drawing what will immediately be covered. + canvas.getClipBounds(mTmpRect); + if (isLayoutRtlSupport()) { + mTmpRect.left = Math.max(mTmpRect.left, mSlideableView.getRight()); + } else { + mTmpRect.right = Math.min(mTmpRect.right, mSlideableView.getLeft()); + } + canvas.clipRect(mTmpRect); + } + + if (Build.VERSION.SDK_INT >= 11) { // HC + result = super.drawChild(canvas, child, drawingTime); + } else { + if (lp.dimWhenOffset && mSlideOffset > 0) { + if (!child.isDrawingCacheEnabled()) { + child.setDrawingCacheEnabled(true); + } + final Bitmap cache = child.getDrawingCache(); + if (cache != null) { + canvas.drawBitmap(cache, child.getLeft(), child.getTop(), lp.dimPaint); + result = false; + } else { + Log.e(TAG, "drawChild: child view " + child + " returned null drawing cache"); + result = super.drawChild(canvas, child, drawingTime); + } + } else { + if (child.isDrawingCacheEnabled()) { + child.setDrawingCacheEnabled(false); + } + result = super.drawChild(canvas, child, drawingTime); + } + } + + canvas.restoreToCount(save); + + return result; + } + + private void invalidateChildRegion(View v) { + IMPL.invalidateChildRegion(this, v); + } + + /** + * Smoothly animate mDraggingPane to the target X position within its range. + * + * @param slideOffset position to animate to + * @param velocity initial velocity in case of fling, or 0. + */ + boolean smoothSlideTo(float slideOffset, int velocity) { + if (!mCanSlide) { + // Nothing to do. + return false; + } + + final boolean isLayoutRtl = isLayoutRtlSupport(); + final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); + + int x; + if (isLayoutRtl) { + int startBound = getPaddingRight() + lp.rightMargin; + int childWidth = mSlideableView.getWidth(); + x = (int) (getWidth() - (startBound + slideOffset * mSlideRange + childWidth)); + } else { + int startBound = getPaddingLeft() + lp.leftMargin; + x = (int) (startBound + slideOffset * mSlideRange); + } + + if (mDragHelper.smoothSlideViewTo(mSlideableView, x, mSlideableView.getTop())) { + setAllChildrenVisible(); + ViewCompat.postInvalidateOnAnimation(this); + return true; + } + return false; + } + + @Override + public void computeScroll() { + if (mDragHelper.continueSettling(true)) { + if (!mCanSlide) { + mDragHelper.abort(); + return; + } + + ViewCompat.postInvalidateOnAnimation(this); + } + } + + /** + * @deprecated Renamed to {@link #setShadowDrawableLeft(Drawable d)} to support LTR (left to + * right language) and {@link #setShadowDrawableRight(Drawable d)} to support RTL (right to left + * language) during opening/closing. + * + * @param d drawable to use as a shadow + */ + @Deprecated + public void setShadowDrawable(Drawable d) { + setShadowDrawableLeft(d); + } + + /** + * Set a drawable to use as a shadow cast by the right pane onto the left pane + * during opening/closing. + * + * @param d drawable to use as a shadow + */ + public void setShadowDrawableLeft(Drawable d) { + mShadowDrawableLeft = d; + } + + /** + * Set a drawable to use as a shadow cast by the left pane onto the right pane + * during opening/closing to support right to left language. + * + * @param d drawable to use as a shadow + */ + public void setShadowDrawableRight(Drawable d) { + mShadowDrawableRight = d; + } + + /** + * Set a drawable to use as a shadow cast by the right pane onto the left pane + * during opening/closing. + * + * @param resId Resource ID of a drawable to use + */ + @Deprecated + public void setShadowResource(@DrawableRes int resId) { + setShadowDrawable(getResources().getDrawable(resId)); + } + + /** + * Set a drawable to use as a shadow cast by the right pane onto the left pane + * during opening/closing. + * + * @param resId Resource ID of a drawable to use + */ + public void setShadowResourceLeft(int resId) { + setShadowDrawableLeft(getResources().getDrawable(resId)); + } + + /** + * Set a drawable to use as a shadow cast by the left pane onto the right pane + * during opening/closing to support right to left language. + * + * @param resId Resource ID of a drawable to use + */ + public void setShadowResourceRight(int resId) { + setShadowDrawableRight(getResources().getDrawable(resId)); + } + + + @Override + public void draw(Canvas c) { + super.draw(c); + final boolean isLayoutRtl = isLayoutRtlSupport(); + Drawable shadowDrawable; + if (isLayoutRtl) { + shadowDrawable = mShadowDrawableRight; + } else { + shadowDrawable = mShadowDrawableLeft; + } + + final View shadowView = getChildCount() > 1 ? getChildAt(1) : null; + if (shadowView == null || shadowDrawable == null) { + // No need to draw a shadow if we don't have one. + return; + } + + final int top = shadowView.getTop(); + final int bottom = shadowView.getBottom(); + + final int shadowWidth = shadowDrawable.getIntrinsicWidth(); + final int left; + final int right; + if (isLayoutRtlSupport()) { + left = shadowView.getRight(); + right = left + shadowWidth; + } else { + right = shadowView.getLeft(); + left = right - shadowWidth; + } + + shadowDrawable.setBounds(left, top, right, bottom); + shadowDrawable.draw(c); + } + + private void parallaxOtherViews(float slideOffset) { + final boolean isLayoutRtl = isLayoutRtlSupport(); + final LayoutParams slideLp = (LayoutParams) mSlideableView.getLayoutParams(); + final boolean dimViews = slideLp.dimWhenOffset && + (isLayoutRtl ? slideLp.rightMargin : slideLp.leftMargin) <= 0; + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View v = getChildAt(i); + if (v == mSlideableView) continue; + + final int oldOffset = (int) ((1 - mParallaxOffset) * mParallaxBy); + mParallaxOffset = slideOffset; + final int newOffset = (int) ((1 - slideOffset) * mParallaxBy); + final int dx = oldOffset - newOffset; + + v.offsetLeftAndRight(isLayoutRtl ? -dx : dx); + + if (dimViews) { + dimChildView(v, isLayoutRtl ? mParallaxOffset - 1 : + 1 - mParallaxOffset, mCoveredFadeColor); + } + } + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && + y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && + canScroll(child, true, dx, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && ViewCompat.canScrollHorizontally(v, (isLayoutRtlSupport() ? dx : -dx)); + } + + boolean isDimmed(View child) { + if (child == null) { + return false; + } + final LayoutParams lp = (LayoutParams) child.getLayoutParams(); + return mCanSlide && lp.dimWhenOffset && mSlideOffset > 0; + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LayoutParams(); + } + + @Override + protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof MarginLayoutParams + ? new LayoutParams((MarginLayoutParams) p) + : new LayoutParams(p); + } + + @Override + protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { + return p instanceof LayoutParams && super.checkLayoutParams(p); + } + + @Override + public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) { + return new LayoutParams(getContext(), attrs); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + + SavedState ss = new SavedState(superState); + ss.isOpen = isSlideable() ? isOpen() : mPreservedOpenState; + + return ss; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + + if (ss.isOpen) { + openPane(); + } else { + closePane(); + } + mPreservedOpenState = ss.isOpen; + } + + private class DragHelperCallback extends ViewDragHelper.Callback { + + @Override + public boolean tryCaptureView(View child, int pointerId) { + if (mIsUnableToDrag) { + return false; + } + + return ((LayoutParams) child.getLayoutParams()).slideable; + } + + @Override + public void onViewDragStateChanged(int state) { + if (mDragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) { + if (mSlideOffset == 0) { + updateObscuredViewsVisibility(mSlideableView); + dispatchOnPanelClosed(mSlideableView); + mPreservedOpenState = false; + } else { + dispatchOnPanelOpened(mSlideableView); + mPreservedOpenState = true; + } + } + } + + @Override + public void onViewCaptured(View capturedChild, int activePointerId) { + // Make all child views visible in preparation for sliding things around + setAllChildrenVisible(); + } + + @Override + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) { + onPanelDragged(left); + invalidate(); + } + + @Override + public void onViewReleased(View releasedChild, float xvel, float yvel) { + final LayoutParams lp = (LayoutParams) releasedChild.getLayoutParams(); + + int left; + if (isLayoutRtlSupport()) { + int startToRight = getPaddingRight() + lp.rightMargin; + if (xvel < 0 || (xvel == 0 && mSlideOffset > 0.5f)) { + startToRight += mSlideRange; + } + int childWidth = mSlideableView.getWidth(); + left = getWidth() - startToRight - childWidth; + } else { + left = getPaddingLeft() + lp.leftMargin; + if (xvel > 0 || (xvel == 0 && mSlideOffset > 0.5f)) { + left += mSlideRange; + } + } + mDragHelper.settleCapturedViewAt(left, releasedChild.getTop()); + invalidate(); + } + + @Override + public int getViewHorizontalDragRange(View child) { + return mSlideRange; + } + + @Override + public int clampViewPositionHorizontal(View child, int left, int dx) { + final LayoutParams lp = (LayoutParams) mSlideableView.getLayoutParams(); + + final int newLeft; + if (isLayoutRtlSupport()) { + int startBound = getWidth() - + (getPaddingRight() + lp.rightMargin + mSlideableView.getWidth()); + int endBound = startBound - mSlideRange; + newLeft = Math.max(Math.min(left, startBound), endBound); + } else { + int startBound = getPaddingLeft() + lp.leftMargin; + int endBound = startBound + mSlideRange; + newLeft = Math.min(Math.max(left, startBound), endBound); + } + return newLeft; + } + + @Override + public int clampViewPositionVertical(View child, int top, int dy) { + // Make sure we never move views vertically. + // This could happen if the child has less height than its parent. + return child.getTop(); + } + + @Override + public void onEdgeDragStarted(int edgeFlags, int pointerId) { + mDragHelper.captureChildView(mSlideableView, pointerId); + } + } + + public static class LayoutParams extends ViewGroup.MarginLayoutParams { + private static final int[] ATTRS = new int[] { + android.R.attr.layout_weight + }; + + /** + * The weighted proportion of how much of the leftover space + * this child should consume after measurement. + */ + public float weight = 0; + + /** + * True if this pane is the slideable pane in the layout. + */ + boolean slideable; + + /** + * True if this view should be drawn dimmed + * when it's been offset from its default position. + */ + boolean dimWhenOffset; + + Paint dimPaint; + + public LayoutParams() { + super(FILL_PARENT, FILL_PARENT); + } + + public LayoutParams(int width, int height) { + super(width, height); + } + + public LayoutParams(android.view.ViewGroup.LayoutParams source) { + super(source); + } + + public LayoutParams(MarginLayoutParams source) { + super(source); + } + + public LayoutParams(LayoutParams source) { + super(source); + this.weight = source.weight; + } + + public LayoutParams(Context c, AttributeSet attrs) { + super(c, attrs); + + final TypedArray a = c.obtainStyledAttributes(attrs, ATTRS); + this.weight = a.getFloat(0, 0); + a.recycle(); + } + + } + + static class SavedState extends BaseSavedState { + boolean isOpen; + + SavedState(Parcelable superState) { + super(superState); + } + + private SavedState(Parcel in) { + super(in); + isOpen = in.readInt() != 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + super.writeToParcel(out, flags); + out.writeInt(isOpen ? 1 : 0); + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + interface SlidingPanelLayoutImpl { + void invalidateChildRegion(SlidingPaneLayout parent, View child); + } + + static class SlidingPanelLayoutImplBase implements SlidingPanelLayoutImpl { + public void invalidateChildRegion(SlidingPaneLayout parent, View child) { + ViewCompat.postInvalidateOnAnimation(parent, child.getLeft(), child.getTop(), + child.getRight(), child.getBottom()); + } + } + + static class SlidingPanelLayoutImplJB extends SlidingPanelLayoutImplBase { + /* + * Private API hacks! Nasty! Bad! + * + * In Jellybean, some optimizations in the hardware UI renderer + * prevent a changed Paint on a View using a hardware layer from having + * the intended effect. This twiddles some internal bits on the view to force + * it to recreate the display list. + */ + private Method mGetDisplayList; + private Field mRecreateDisplayList; + + SlidingPanelLayoutImplJB() { + try { + mGetDisplayList = View.class.getDeclaredMethod("getDisplayList", (Class[]) null); + } catch (NoSuchMethodException e) { + Log.e(TAG, "Couldn't fetch getDisplayList method; dimming won't work right.", e); + } + try { + mRecreateDisplayList = View.class.getDeclaredField("mRecreateDisplayList"); + mRecreateDisplayList.setAccessible(true); + } catch (NoSuchFieldException e) { + Log.e(TAG, "Couldn't fetch mRecreateDisplayList field; dimming will be slow.", e); + } + } + + @Override + public void invalidateChildRegion(SlidingPaneLayout parent, View child) { + if (mGetDisplayList != null && mRecreateDisplayList != null) { + try { + mRecreateDisplayList.setBoolean(child, true); + mGetDisplayList.invoke(child, (Object[]) null); + } catch (Exception e) { + Log.e(TAG, "Error refreshing display list state", e); + } + } else { + // Slow path. REALLY slow path. Let's hope we don't get here. + child.invalidate(); + return; + } + super.invalidateChildRegion(parent, child); + } + } + + static class SlidingPanelLayoutImplJBMR1 extends SlidingPanelLayoutImplBase { + @Override + public void invalidateChildRegion(SlidingPaneLayout parent, View child) { + ViewCompat.setLayerPaint(child, ((LayoutParams) child.getLayoutParams()).dimPaint); + } + } + + class AccessibilityDelegate extends AccessibilityDelegateCompat { + private final Rect mTmpRect = new Rect(); + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + final AccessibilityNodeInfoCompat superNode = AccessibilityNodeInfoCompat.obtain(info); + super.onInitializeAccessibilityNodeInfo(host, superNode); + copyNodeInfoNoChildren(info, superNode); + superNode.recycle(); + + info.setClassName(SlidingPaneLayout.class.getName()); + info.setSource(host); + + final ViewParent parent = ViewCompat.getParentForAccessibility(host); + if (parent instanceof View) { + info.setParent((View) parent); + } + + // This is a best-approximation of addChildrenForAccessibility() + // that accounts for filtering. + final int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + final View child = getChildAt(i); + if (!filter(child) && (child.getVisibility() == View.VISIBLE)) { + // Force importance to "yes" since we can't read the value. + ViewCompat.setImportantForAccessibility( + child, ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES); + info.addChild(child); + } + } + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + + event.setClassName(SlidingPaneLayout.class.getName()); + } + + @Override + public boolean onRequestSendAccessibilityEvent(ViewGroup host, View child, + AccessibilityEvent event) { + if (!filter(child)) { + return super.onRequestSendAccessibilityEvent(host, child, event); + } + return false; + } + + public boolean filter(View child) { + return isDimmed(child); + } + + /** + * This should really be in AccessibilityNodeInfoCompat, but there unfortunately + * seem to be a few elements that are not easily cloneable using the underlying API. + * Leave it private here as it's not general-purpose useful. + */ + private void copyNodeInfoNoChildren(AccessibilityNodeInfoCompat dest, + AccessibilityNodeInfoCompat src) { + final Rect rect = mTmpRect; + + src.getBoundsInParent(rect); + dest.setBoundsInParent(rect); + + src.getBoundsInScreen(rect); + dest.setBoundsInScreen(rect); + + dest.setVisibleToUser(src.isVisibleToUser()); + dest.setPackageName(src.getPackageName()); + dest.setClassName(src.getClassName()); + dest.setContentDescription(src.getContentDescription()); + + dest.setEnabled(src.isEnabled()); + dest.setClickable(src.isClickable()); + dest.setFocusable(src.isFocusable()); + dest.setFocused(src.isFocused()); + dest.setAccessibilityFocused(src.isAccessibilityFocused()); + dest.setSelected(src.isSelected()); + dest.setLongClickable(src.isLongClickable()); + + dest.addAction(src.getActions()); + + dest.setMovementGranularities(src.getMovementGranularities()); + } + } + + private class DisableLayerRunnable implements Runnable { + final View mChildView; + + DisableLayerRunnable(View childView) { + mChildView = childView; + } + + @Override + public void run() { + if (mChildView.getParent() == SlidingPaneLayout.this) { + ViewCompat.setLayerType(mChildView, ViewCompat.LAYER_TYPE_NONE, null); + invalidateChildRegion(mChildView); + } + mPostedRunnables.remove(this); + } + } + + private boolean isLayoutRtlSupport() { + return ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL; + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/Space.java b/mobile/google-android-support-v4/src/android/support/v4/widget/Space.java new file mode 100644 index 00000000000..48574799881 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/Space.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.util.AttributeSet; +import android.view.View; + +/** + * Space is a lightweight {@link View} subclass that may be used to create gaps between components + * in general purpose layouts. + */ +public class Space extends View { + + public Space(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + if (getVisibility() == VISIBLE) { + setVisibility(INVISIBLE); + } + } + + public Space(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public Space(Context context) { + this(context, null); + } + + /** + * Draw nothing. + * + * @param canvas an unused parameter. + */ + @Override + public void draw(Canvas canvas) { + } + + /** + * Compare to: {@link View#getDefaultSize(int, int)} + * If mode is AT_MOST, return the child size instead of the parent size + * (unless it is too big). + */ + private static int getDefaultSize2(int size, int measureSpec) { + int result = size; + int specMode = MeasureSpec.getMode(measureSpec); + int specSize = MeasureSpec.getSize(measureSpec); + + switch (specMode) { + case MeasureSpec.UNSPECIFIED: + result = size; + break; + case MeasureSpec.AT_MOST: + result = Math.min(size, specSize); + break; + case MeasureSpec.EXACTLY: + result = specSize; + break; + } + return result; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension( + getDefaultSize2(getSuggestedMinimumWidth(), widthMeasureSpec), + getDefaultSize2(getSuggestedMinimumHeight(), heightMeasureSpec)); + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SwipeProgressBar.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SwipeProgressBar.java new file mode 100644 index 00000000000..2314a5ef2d9 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SwipeProgressBar.java @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.support.v4.view.ViewCompat; +import android.support.v4.view.animation.FastOutSlowInInterpolator; +import android.view.View; +import android.view.animation.AnimationUtils; +import android.view.animation.Interpolator; + + +/** + * Custom progress bar that shows a cycle of colors as widening circles that + * overdraw each other. When finished, the bar is cleared from the inside out as + * the main cycle continues. Before running, this can also indicate how close + * the user is to triggering something (e.g. how far they need to pull down to + * trigger a refresh). + */ +final class SwipeProgressBar { + + // Default progress animation colors are grays. + private final static int COLOR1 = 0xB3000000; + private final static int COLOR2 = 0x80000000; + private final static int COLOR3 = 0x4d000000; + private final static int COLOR4 = 0x1a000000; + + // The duration of the animation cycle. + private static final int ANIMATION_DURATION_MS = 2000; + + // The duration of the animation to clear the bar. + private static final int FINISH_ANIMATION_DURATION_MS = 1000; + + // Interpolator for varying the speed of the animation. + private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator(); + + private final Paint mPaint = new Paint(); + private final RectF mClipRect = new RectF(); + private float mTriggerPercentage; + private long mStartTime; + private long mFinishTime; + private boolean mRunning; + + // Colors used when rendering the animation, + private int mColor1; + private int mColor2; + private int mColor3; + private int mColor4; + private View mParent; + + private Rect mBounds = new Rect(); + + public SwipeProgressBar(View parent) { + mParent = parent; + mColor1 = COLOR1; + mColor2 = COLOR2; + mColor3 = COLOR3; + mColor4 = COLOR4; + } + + /** + * Set the four colors used in the progress animation. The first color will + * also be the color of the bar that grows in response to a user swipe + * gesture. + * + * @param color1 Integer representation of a color. + * @param color2 Integer representation of a color. + * @param color3 Integer representation of a color. + * @param color4 Integer representation of a color. + */ + void setColorScheme(int color1, int color2, int color3, int color4) { + mColor1 = color1; + mColor2 = color2; + mColor3 = color3; + mColor4 = color4; + } + + /** + * Update the progress the user has made toward triggering the swipe + * gesture. and use this value to update the percentage of the trigger that + * is shown. + */ + void setTriggerPercentage(float triggerPercentage) { + mTriggerPercentage = triggerPercentage; + mStartTime = 0; + ViewCompat.postInvalidateOnAnimation( + mParent, mBounds.left, mBounds.top, mBounds.right, mBounds.bottom); + } + + /** + * Start showing the progress animation. + */ + void start() { + if (!mRunning) { + mTriggerPercentage = 0; + mStartTime = AnimationUtils.currentAnimationTimeMillis(); + mRunning = true; + mParent.postInvalidate(); + } + } + + /** + * Stop showing the progress animation. + */ + void stop() { + if (mRunning) { + mTriggerPercentage = 0; + mFinishTime = AnimationUtils.currentAnimationTimeMillis(); + mRunning = false; + mParent.postInvalidate(); + } + } + + /** + * @return Return whether the progress animation is currently running. + */ + boolean isRunning() { + return mRunning || mFinishTime > 0; + } + + void draw(Canvas canvas) { + final int width = mBounds.width(); + final int height = mBounds.height(); + final int cx = width / 2; + final int cy = height / 2; + boolean drawTriggerWhileFinishing = false; + int restoreCount = canvas.save(); + canvas.clipRect(mBounds); + + if (mRunning || (mFinishTime > 0)) { + long now = AnimationUtils.currentAnimationTimeMillis(); + long elapsed = (now - mStartTime) % ANIMATION_DURATION_MS; + long iterations = (now - mStartTime) / ANIMATION_DURATION_MS; + float rawProgress = (elapsed / (ANIMATION_DURATION_MS / 100f)); + + // If we're not running anymore, that means we're running through + // the finish animation. + if (!mRunning) { + // If the finish animation is done, don't draw anything, and + // don't repost. + if ((now - mFinishTime) >= FINISH_ANIMATION_DURATION_MS) { + mFinishTime = 0; + return; + } + + // Otherwise, use a 0 opacity alpha layer to clear the animation + // from the inside out. This layer will prevent the circles from + // drawing within its bounds. + long finishElapsed = (now - mFinishTime) % FINISH_ANIMATION_DURATION_MS; + float finishProgress = (finishElapsed / (FINISH_ANIMATION_DURATION_MS / 100f)); + float pct = (finishProgress / 100f); + // Radius of the circle is half of the screen. + float clearRadius = width / 2 * INTERPOLATOR.getInterpolation(pct); + mClipRect.set(cx - clearRadius, 0, cx + clearRadius, height); + canvas.saveLayerAlpha(mClipRect, 0, 0); + // Only draw the trigger if there is a space in the center of + // this refreshing view that needs to be filled in by the + // trigger. If the progress view is just still animating, let it + // continue animating. + drawTriggerWhileFinishing = true; + } + + // First fill in with the last color that would have finished drawing. + if (iterations == 0) { + canvas.drawColor(mColor1); + } else { + if (rawProgress >= 0 && rawProgress < 25) { + canvas.drawColor(mColor4); + } else if (rawProgress >= 25 && rawProgress < 50) { + canvas.drawColor(mColor1); + } else if (rawProgress >= 50 && rawProgress < 75) { + canvas.drawColor(mColor2); + } else { + canvas.drawColor(mColor3); + } + } + + // Then draw up to 4 overlapping concentric circles of varying radii, based on how far + // along we are in the cycle. + // progress 0-50 draw mColor2 + // progress 25-75 draw mColor3 + // progress 50-100 draw mColor4 + // progress 75 (wrap to 25) draw mColor1 + if ((rawProgress >= 0 && rawProgress <= 25)) { + float pct = (((rawProgress + 25) * 2) / 100f); + drawCircle(canvas, cx, cy, mColor1, pct); + } + if (rawProgress >= 0 && rawProgress <= 50) { + float pct = ((rawProgress * 2) / 100f); + drawCircle(canvas, cx, cy, mColor2, pct); + } + if (rawProgress >= 25 && rawProgress <= 75) { + float pct = (((rawProgress - 25) * 2) / 100f); + drawCircle(canvas, cx, cy, mColor3, pct); + } + if (rawProgress >= 50 && rawProgress <= 100) { + float pct = (((rawProgress - 50) * 2) / 100f); + drawCircle(canvas, cx, cy, mColor4, pct); + } + if ((rawProgress >= 75 && rawProgress <= 100)) { + float pct = (((rawProgress - 75) * 2) / 100f); + drawCircle(canvas, cx, cy, mColor1, pct); + } + if (mTriggerPercentage > 0 && drawTriggerWhileFinishing) { + // There is some portion of trigger to draw. Restore the canvas, + // then draw the trigger. Otherwise, the trigger does not appear + // until after the bar has finished animating and appears to + // just jump in at a larger width than expected. + canvas.restoreToCount(restoreCount); + restoreCount = canvas.save(); + canvas.clipRect(mBounds); + drawTrigger(canvas, cx, cy); + } + // Keep running until we finish out the last cycle. + ViewCompat.postInvalidateOnAnimation( + mParent, mBounds.left, mBounds.top, mBounds.right, mBounds.bottom); + } else { + // Otherwise if we're in the middle of a trigger, draw that. + if (mTriggerPercentage > 0 && mTriggerPercentage <= 1.0) { + drawTrigger(canvas, cx, cy); + } + } + canvas.restoreToCount(restoreCount); + } + + private void drawTrigger(Canvas canvas, int cx, int cy) { + mPaint.setColor(mColor1); + canvas.drawCircle(cx, cy, cx * mTriggerPercentage, mPaint); + } + + /** + * Draws a circle centered in the view. + * + * @param canvas the canvas to draw on + * @param cx the center x coordinate + * @param cy the center y coordinate + * @param color the color to draw + * @param pct the percentage of the view that the circle should cover + */ + private void drawCircle(Canvas canvas, float cx, float cy, int color, float pct) { + mPaint.setColor(color); + canvas.save(); + canvas.translate(cx, cy); + float radiusScale = INTERPOLATOR.getInterpolation(pct); + canvas.scale(radiusScale, radiusScale); + canvas.drawCircle(0, 0, cx, mPaint); + canvas.restore(); + } + + /** + * Set the drawing bounds of this SwipeProgressBar. + */ + void setBounds(int left, int top, int right, int bottom) { + mBounds.left = left; + mBounds.top = top; + mBounds.right = right; + mBounds.bottom = bottom; + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/SwipeRefreshLayout.java b/mobile/google-android-support-v4/src/android/support/v4/widget/SwipeRefreshLayout.java new file mode 100644 index 00000000000..55f2ce2b1bc --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/SwipeRefreshLayout.java @@ -0,0 +1,956 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.ScrollingView; +import android.support.v4.view.ViewCompat; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.Animation.AnimationListener; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Transformation; +import android.widget.AbsListView; + +/** + * The SwipeRefreshLayout should be used whenever the user can refresh the + * contents of a view via a vertical swipe gesture. The activity that + * instantiates this view should add an OnRefreshListener to be notified + * whenever the swipe to refresh gesture is completed. The SwipeRefreshLayout + * will notify the listener each and every time the gesture is completed again; + * the listener is responsible for correctly determining when to actually + * initiate a refresh of its content. If the listener determines there should + * not be a refresh, it must call setRefreshing(false) to cancel any visual + * indication of a refresh. If an activity wishes to show just the progress + * animation, it should call setRefreshing(true). To disable the gesture and + * progress animation, call setEnabled(false) on the view. + *

    + * This layout should be made the parent of the view that will be refreshed as a + * result of the gesture and can only support one direct child. This view will + * also be made the target of the gesture and will be forced to match both the + * width and the height supplied in this layout. The SwipeRefreshLayout does not + * provide accessibility events; instead, a menu item must be provided to allow + * refresh of the content wherever this gesture is used. + *

    + */ +public class SwipeRefreshLayout extends ViewGroup { + // Maps to ProgressBar.Large style + public static final int LARGE = MaterialProgressDrawable.LARGE; + // Maps to ProgressBar default style + public static final int DEFAULT = MaterialProgressDrawable.DEFAULT; + + private static final String LOG_TAG = SwipeRefreshLayout.class.getSimpleName(); + + private static final int MAX_ALPHA = 255; + private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA); + + private static final int CIRCLE_DIAMETER = 40; + private static final int CIRCLE_DIAMETER_LARGE = 56; + + private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; + private static final int INVALID_POINTER = -1; + private static final float DRAG_RATE = .5f; + + // Max amount of circle that can be filled by progress during swipe gesture, + // where 1.0 is a full circle + private static final float MAX_PROGRESS_ANGLE = .8f; + + private static final int SCALE_DOWN_DURATION = 150; + + private static final int ALPHA_ANIMATION_DURATION = 300; + + private static final int ANIMATE_TO_TRIGGER_DURATION = 200; + + private static final int ANIMATE_TO_START_DURATION = 200; + + // Default background for the progress spinner + private static final int CIRCLE_BG_LIGHT = 0xFFFAFAFA; + // Default offset in dips from the top of the view to where the progress spinner should stop + private static final int DEFAULT_CIRCLE_TARGET = 64; + + private View mTarget; // the target of the gesture + private OnRefreshListener mListener; + private boolean mRefreshing = false; + private int mTouchSlop; + private float mTotalDragDistance = -1; + private int mMediumAnimationDuration; + private int mCurrentTargetOffsetTop; + // Whether or not the starting offset has been determined. + private boolean mOriginalOffsetCalculated = false; + + private float mInitialMotionY; + private float mInitialDownY; + private boolean mIsBeingDragged; + private int mActivePointerId = INVALID_POINTER; + // Whether this item is scaled up rather than clipped + private boolean mScale; + + // Target is returning to its start offset because it was cancelled or a + // refresh was triggered. + private boolean mReturningToStart; + private final DecelerateInterpolator mDecelerateInterpolator; + private static final int[] LAYOUT_ATTRS = new int[] { + android.R.attr.enabled + }; + + private CircleImageView mCircleView; + private int mCircleViewIndex = -1; + + protected int mFrom; + + private float mStartingScale; + + protected int mOriginalOffsetTop; + + private MaterialProgressDrawable mProgress; + + private Animation mScaleAnimation; + + private Animation mScaleDownAnimation; + + private Animation mAlphaStartAnimation; + + private Animation mAlphaMaxAnimation; + + private Animation mScaleDownToStartAnimation; + + private float mSpinnerFinalOffset; + + private boolean mNotify; + + private int mCircleWidth; + + private int mCircleHeight; + + // Whether the client has set a custom starting position; + private boolean mUsingCustomStart; + + private Animation.AnimationListener mRefreshListener = new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + if (mRefreshing) { + // Make sure the progress view is fully visible + mProgress.setAlpha(MAX_ALPHA); + mProgress.start(); + if (mNotify) { + if (mListener != null) { + mListener.onRefresh(); + } + } + } else { + mProgress.stop(); + mCircleView.setVisibility(View.GONE); + setColorViewAlpha(MAX_ALPHA); + // Return the circle to its start position + if (mScale) { + setAnimationProgress(0 /* animation complete and view is hidden */); + } else { + setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop, + true /* requires update */); + } + } + mCurrentTargetOffsetTop = mCircleView.getTop(); + } + }; + + private void setColorViewAlpha(int targetAlpha) { + mCircleView.getBackground().setAlpha(targetAlpha); + mProgress.setAlpha(targetAlpha); + } + + /** + * The refresh indicator starting and resting position is always positioned + * near the top of the refreshing content. This position is a consistent + * location, but can be adjusted in either direction based on whether or not + * there is a toolbar or actionbar present. + * + * @param scale Set to true if there is no view at a higher z-order than + * where the progress spinner is set to appear. + * @param start The offset in pixels from the top of this view at which the + * progress spinner should appear. + * @param end The offset in pixels from the top of this view at which the + * progress spinner should come to rest after a successful swipe + * gesture. + */ + public void setProgressViewOffset(boolean scale, int start, int end) { + mScale = scale; + mCircleView.setVisibility(View.GONE); + mOriginalOffsetTop = mCurrentTargetOffsetTop = start; + mSpinnerFinalOffset = end; + mUsingCustomStart = true; + mCircleView.invalidate(); + } + + /** + * The refresh indicator resting position is always positioned near the top + * of the refreshing content. This position is a consistent location, but + * can be adjusted in either direction based on whether or not there is a + * toolbar or actionbar present. + * + * @param scale Set to true if there is no view at a higher z-order than + * where the progress spinner is set to appear. + * @param end The offset in pixels from the top of this view at which the + * progress spinner should come to rest after a successful swipe + * gesture. + */ + public void setProgressViewEndTarget(boolean scale, int end) { + mSpinnerFinalOffset = end; + mScale = scale; + mCircleView.invalidate(); + } + + /** + * One of DEFAULT, or LARGE. + */ + public void setSize(int size) { + if (size != MaterialProgressDrawable.LARGE && size != MaterialProgressDrawable.DEFAULT) { + return; + } + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + if (size == MaterialProgressDrawable.LARGE) { + mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER_LARGE * metrics.density); + } else { + mCircleHeight = mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); + } + // force the bounds of the progress circle inside the circle view to + // update by setting it to null before updating its size and then + // re-setting it + mCircleView.setImageDrawable(null); + mProgress.updateSizes(size); + mCircleView.setImageDrawable(mProgress); + } + + /** + * Simple constructor to use when creating a SwipeRefreshLayout from code. + * + * @param context + */ + public SwipeRefreshLayout(Context context) { + this(context, null); + } + + /** + * Constructor that is called when inflating SwipeRefreshLayout from XML. + * + * @param context + * @param attrs + */ + public SwipeRefreshLayout(Context context, AttributeSet attrs) { + super(context, attrs); + + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + mMediumAnimationDuration = getResources().getInteger( + android.R.integer.config_mediumAnimTime); + + setWillNotDraw(false); + mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + setEnabled(a.getBoolean(0, true)); + a.recycle(); + + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + mCircleWidth = (int) (CIRCLE_DIAMETER * metrics.density); + mCircleHeight = (int) (CIRCLE_DIAMETER * metrics.density); + + createProgressView(); + ViewCompat.setChildrenDrawingOrderEnabled(this, true); + // the absolute offset has to take into account that the circle starts at an offset + mSpinnerFinalOffset = DEFAULT_CIRCLE_TARGET * metrics.density; + mTotalDragDistance = mSpinnerFinalOffset; + } + + protected int getChildDrawingOrder(int childCount, int i) { + if (mCircleViewIndex < 0) { + return i; + } else if (i == childCount - 1) { + // Draw the selected child last + return mCircleViewIndex; + } else if (i >= mCircleViewIndex) { + // Move the children after the selected child earlier one + return i + 1; + } else { + // Keep the children before the selected child the same + return i; + } + } + + private void createProgressView() { + mCircleView = new CircleImageView(getContext(), CIRCLE_BG_LIGHT, CIRCLE_DIAMETER/2); + mProgress = new MaterialProgressDrawable(getContext(), this); + mProgress.setBackgroundColor(CIRCLE_BG_LIGHT); + mCircleView.setImageDrawable(mProgress); + mCircleView.setVisibility(View.GONE); + addView(mCircleView); + } + + /** + * Set the listener to be notified when a refresh is triggered via the swipe + * gesture. + */ + public void setOnRefreshListener(OnRefreshListener listener) { + mListener = listener; + } + + /** + * Pre API 11, alpha is used to make the progress circle appear instead of scale. + */ + private boolean isAlphaUsedForScale() { + return android.os.Build.VERSION.SDK_INT < 11; + } + + /** + * Notify the widget that refresh state has changed. Do not call this when + * refresh is triggered by a swipe gesture. + * + * @param refreshing Whether or not the view should show refresh progress. + */ + public void setRefreshing(boolean refreshing) { + if (refreshing && mRefreshing != refreshing) { + // scale and show + mRefreshing = refreshing; + int endTarget = 0; + if (!mUsingCustomStart) { + endTarget = (int) (mSpinnerFinalOffset + mOriginalOffsetTop); + } else { + endTarget = (int) mSpinnerFinalOffset; + } + setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop, + true /* requires update */); + mNotify = false; + startScaleUpAnimation(mRefreshListener); + } else { + setRefreshing(refreshing, false /* notify */); + } + } + + private void startScaleUpAnimation(AnimationListener listener) { + mCircleView.setVisibility(View.VISIBLE); + if (android.os.Build.VERSION.SDK_INT >= 11) { + // Pre API 11, alpha is used in place of scale up to show the + // progress circle appearing. + // Don't adjust the alpha during appearance otherwise. + mProgress.setAlpha(MAX_ALPHA); + } + mScaleAnimation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + setAnimationProgress(interpolatedTime); + } + }; + mScaleAnimation.setDuration(mMediumAnimationDuration); + if (listener != null) { + mCircleView.setAnimationListener(listener); + } + mCircleView.clearAnimation(); + mCircleView.startAnimation(mScaleAnimation); + } + + /** + * Pre API 11, this does an alpha animation. + * @param progress + */ + private void setAnimationProgress(float progress) { + if (isAlphaUsedForScale()) { + setColorViewAlpha((int) (progress * MAX_ALPHA)); + } else { + ViewCompat.setScaleX(mCircleView, progress); + ViewCompat.setScaleY(mCircleView, progress); + } + } + + private void setRefreshing(boolean refreshing, final boolean notify) { + if (mRefreshing != refreshing) { + mNotify = notify; + ensureTarget(); + mRefreshing = refreshing; + if (mRefreshing) { + animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); + } else { + startScaleDownAnimation(mRefreshListener); + } + } + } + + private void startScaleDownAnimation(Animation.AnimationListener listener) { + mScaleDownAnimation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + setAnimationProgress(1 - interpolatedTime); + } + }; + mScaleDownAnimation.setDuration(SCALE_DOWN_DURATION); + mCircleView.setAnimationListener(listener); + mCircleView.clearAnimation(); + mCircleView.startAnimation(mScaleDownAnimation); + } + + private void startProgressAlphaStartAnimation() { + mAlphaStartAnimation = startAlphaAnimation(mProgress.getAlpha(), STARTING_PROGRESS_ALPHA); + } + + private void startProgressAlphaMaxAnimation() { + mAlphaMaxAnimation = startAlphaAnimation(mProgress.getAlpha(), MAX_ALPHA); + } + + private Animation startAlphaAnimation(final int startingAlpha, final int endingAlpha) { + // Pre API 11, alpha is used in place of scale. Don't also use it to + // show the trigger point. + if (mScale && isAlphaUsedForScale()) { + return null; + } + Animation alpha = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + mProgress + .setAlpha((int) (startingAlpha+ ((endingAlpha - startingAlpha) + * interpolatedTime))); + } + }; + alpha.setDuration(ALPHA_ANIMATION_DURATION); + // Clear out the previous animation listeners. + mCircleView.setAnimationListener(null); + mCircleView.clearAnimation(); + mCircleView.startAnimation(alpha); + return alpha; + } + + /** + * @deprecated Use {@link #setProgressBackgroundColorSchemeResource(int)} + */ + @Deprecated + public void setProgressBackgroundColor(int colorRes) { + setProgressBackgroundColorSchemeResource(colorRes); + } + + /** + * Set the background color of the progress spinner disc. + * + * @param colorRes Resource id of the color. + */ + public void setProgressBackgroundColorSchemeResource(int colorRes) { + setProgressBackgroundColorSchemeColor(getResources().getColor(colorRes)); + } + + /** + * Set the background color of the progress spinner disc. + * + * @param color + */ + public void setProgressBackgroundColorSchemeColor(int color) { + mCircleView.setBackgroundColor(color); + mProgress.setBackgroundColor(color); + } + + /** + * @deprecated Use {@link #setColorSchemeResources(int...)} + */ + @Deprecated + public void setColorScheme(int... colors) { + setColorSchemeResources(colors); + } + + /** + * Set the color resources used in the progress animation from color resources. + * The first color will also be the color of the bar that grows in response + * to a user swipe gesture. + * + * @param colorResIds + */ + public void setColorSchemeResources(int... colorResIds) { + final Resources res = getResources(); + int[] colorRes = new int[colorResIds.length]; + for (int i = 0; i < colorResIds.length; i++) { + colorRes[i] = res.getColor(colorResIds[i]); + } + setColorSchemeColors(colorRes); + } + + /** + * Set the colors used in the progress animation. The first + * color will also be the color of the bar that grows in response to a user + * swipe gesture. + * + * @param colors + */ + public void setColorSchemeColors(int... colors) { + ensureTarget(); + mProgress.setColorSchemeColors(colors); + } + + /** + * @return Whether the SwipeRefreshWidget is actively showing refresh + * progress. + */ + public boolean isRefreshing() { + return mRefreshing; + } + + private void ensureTarget() { + // Don't bother getting the parent height if the parent hasn't been laid + // out yet. + if (mTarget == null) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (!child.equals(mCircleView)) { + mTarget = child; + break; + } + } + } + } + + /** + * Set the distance to trigger a sync in dips + * + * @param distance + */ + public void setDistanceToTriggerSync(int distance) { + mTotalDragDistance = distance; + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final int width = getMeasuredWidth(); + final int height = getMeasuredHeight(); + if (getChildCount() == 0) { + return; + } + if (mTarget == null) { + ensureTarget(); + } + if (mTarget == null) { + return; + } + final View child = mTarget; + final int childLeft = getPaddingLeft(); + final int childTop = getPaddingTop(); + final int childWidth = width - getPaddingLeft() - getPaddingRight(); + final int childHeight = height - getPaddingTop() - getPaddingBottom(); + child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); + int circleWidth = mCircleView.getMeasuredWidth(); + int circleHeight = mCircleView.getMeasuredHeight(); + mCircleView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, + (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (mTarget == null) { + ensureTarget(); + } + if (mTarget == null) { + return; + } + mTarget.measure(MeasureSpec.makeMeasureSpec( + getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( + getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); + mCircleView.measure(MeasureSpec.makeMeasureSpec(mCircleWidth, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(mCircleHeight, MeasureSpec.EXACTLY)); + if (!mUsingCustomStart && !mOriginalOffsetCalculated) { + mOriginalOffsetCalculated = true; + mCurrentTargetOffsetTop = mOriginalOffsetTop = -mCircleView.getMeasuredHeight(); + } + mCircleViewIndex = -1; + // Get the index of the circleview. + for (int index = 0; index < getChildCount(); index++) { + if (getChildAt(index) == mCircleView) { + mCircleViewIndex = index; + break; + } + } + } + + /** + * Get the diameter of the progress circle that is displayed as part of the + * swipe to refresh layout. This is not valid until a measure pass has + * completed. + * + * @return Diameter in pixels of the progress circle view. + */ + public int getProgressCircleDiameter() { + return mCircleView != null ?mCircleView.getMeasuredHeight() : 0; + } + + /** + * @return Whether it is possible for the child view of this layout to + * scroll up. Override this if the child view is a custom view. + */ + public boolean canChildScrollUp() { + if (android.os.Build.VERSION.SDK_INT < 14) { + if (mTarget instanceof AbsListView) { + final AbsListView absListView = (AbsListView) mTarget; + return absListView.getChildCount() > 0 + && (absListView.getFirstVisiblePosition() > 0 || absListView.getChildAt(0) + .getTop() < absListView.getPaddingTop()); + } else { + return ViewCompat.canScrollVertically(mTarget, -1) || mTarget.getScrollY() > 0; + } + } else { + return ViewCompat.canScrollVertically(mTarget, -1); + } + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + ensureTarget(); + + final int action = MotionEventCompat.getActionMasked(ev); + + if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { + mReturningToStart = false; + } + + if (!isEnabled() || mReturningToStart || canChildScrollUp() || mRefreshing) { + // Fail fast if we're not in a state where a swipe is possible + return false; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCircleView.getTop(), true); + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mIsBeingDragged = false; + final float initialDownY = getMotionEventY(ev, mActivePointerId); + if (initialDownY == -1) { + return false; + } + mInitialDownY = initialDownY; + break; + + case MotionEvent.ACTION_MOVE: + if (mActivePointerId == INVALID_POINTER) { + Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); + return false; + } + + final float y = getMotionEventY(ev, mActivePointerId); + if (y == -1) { + return false; + } + final float yDiff = y - mInitialDownY; + if (yDiff > mTouchSlop && !mIsBeingDragged) { + mInitialMotionY = mInitialDownY + mTouchSlop; + mIsBeingDragged = true; + mProgress.setAlpha(STARTING_PROGRESS_ALPHA); + } + break; + + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mIsBeingDragged = false; + mActivePointerId = INVALID_POINTER; + break; + } + + return mIsBeingDragged; + } + + private float getMotionEventY(MotionEvent ev, int activePointerId) { + final int index = MotionEventCompat.findPointerIndex(ev, activePointerId); + if (index < 0) { + return -1; + } + return MotionEventCompat.getY(ev, index); + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean b) { + // Nope. + } + + private boolean isAnimationRunning(Animation animation) { + return animation != null && animation.hasStarted() && !animation.hasEnded(); + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + + if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { + mReturningToStart = false; + } + + if (!isEnabled() || mReturningToStart || canChildScrollUp()) { + // Fail fast if we're not in a state where a swipe is possible + return false; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = MotionEventCompat.getPointerId(ev, 0); + mIsBeingDragged = false; + break; + + case MotionEvent.ACTION_MOVE: { + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + if (pointerIndex < 0) { + Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); + return false; + } + + final float y = MotionEventCompat.getY(ev, pointerIndex); + final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; + if (mIsBeingDragged) { + mProgress.showArrow(true); + float originalDragPercent = overscrollTop / mTotalDragDistance; + if (originalDragPercent < 0) { + return false; + } + float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); + float adjustedPercent = (float) Math.max(dragPercent - .4, 0) * 5 / 3; + float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; + float slingshotDist = mUsingCustomStart ? mSpinnerFinalOffset + - mOriginalOffsetTop : mSpinnerFinalOffset; + float tensionSlingshotPercent = Math.max(0, + Math.min(extraOS, slingshotDist * 2) / slingshotDist); + float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( + (tensionSlingshotPercent / 4), 2)) * 2f; + float extraMove = (slingshotDist) * tensionPercent * 2; + + int targetY = mOriginalOffsetTop + + (int) ((slingshotDist * dragPercent) + extraMove); + // where 1.0f is a full circle + if (mCircleView.getVisibility() != View.VISIBLE) { + mCircleView.setVisibility(View.VISIBLE); + } + if (!mScale) { + ViewCompat.setScaleX(mCircleView, 1f); + ViewCompat.setScaleY(mCircleView, 1f); + } + if (overscrollTop < mTotalDragDistance) { + if (mScale) { + setAnimationProgress(overscrollTop / mTotalDragDistance); + } + if (mProgress.getAlpha() > STARTING_PROGRESS_ALPHA + && !isAnimationRunning(mAlphaStartAnimation)) { + // Animate the alpha + startProgressAlphaStartAnimation(); + } + float strokeStart = adjustedPercent * .8f; + mProgress.setStartEndTrim(0f, Math.min(MAX_PROGRESS_ANGLE, strokeStart)); + mProgress.setArrowScale(Math.min(1f, adjustedPercent)); + } else { + if (mProgress.getAlpha() < MAX_ALPHA + && !isAnimationRunning(mAlphaMaxAnimation)) { + // Animate the alpha + startProgressAlphaMaxAnimation(); + } + } + float rotation = (-0.25f + .4f * adjustedPercent + tensionPercent * 2) * .5f; + mProgress.setProgressRotation(rotation); + setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop, + true /* requires update */); + } + break; + } + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int index = MotionEventCompat.getActionIndex(ev); + mActivePointerId = MotionEventCompat.getPointerId(ev, index); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + if (mActivePointerId == INVALID_POINTER) { + if (action == MotionEvent.ACTION_UP) { + Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); + } + return false; + } + final int pointerIndex = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float y = MotionEventCompat.getY(ev, pointerIndex); + final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; + mIsBeingDragged = false; + if (overscrollTop > mTotalDragDistance) { + setRefreshing(true, true /* notify */); + } else { + // cancel refresh + mRefreshing = false; + mProgress.setStartEndTrim(0f, 0f); + Animation.AnimationListener listener = null; + if (!mScale) { + listener = new Animation.AnimationListener() { + + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + if (!mScale) { + startScaleDownAnimation(null); + } + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + }; + } + animateOffsetToStartPosition(mCurrentTargetOffsetTop, listener); + mProgress.showArrow(false); + } + mActivePointerId = INVALID_POINTER; + return false; + } + } + + return true; + } + + private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { + mFrom = from; + mAnimateToCorrectPosition.reset(); + mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); + mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); + if (listener != null) { + mCircleView.setAnimationListener(listener); + } + mCircleView.clearAnimation(); + mCircleView.startAnimation(mAnimateToCorrectPosition); + } + + private void animateOffsetToStartPosition(int from, AnimationListener listener) { + if (mScale) { + // Scale the item back down + startScaleDownReturnToStartAnimation(from, listener); + } else { + mFrom = from; + mAnimateToStartPosition.reset(); + mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); + mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); + if (listener != null) { + mCircleView.setAnimationListener(listener); + } + mCircleView.clearAnimation(); + mCircleView.startAnimation(mAnimateToStartPosition); + } + } + + private final Animation mAnimateToCorrectPosition = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + int targetTop = 0; + int endTarget = 0; + if (!mUsingCustomStart) { + endTarget = (int) (mSpinnerFinalOffset - Math.abs(mOriginalOffsetTop)); + } else { + endTarget = (int) mSpinnerFinalOffset; + } + targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); + int offset = targetTop - mCircleView.getTop(); + setTargetOffsetTopAndBottom(offset, false /* requires update */); + mProgress.setArrowScale(1 - interpolatedTime); + } + }; + + private void moveToStart(float interpolatedTime) { + int targetTop = 0; + targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); + int offset = targetTop - mCircleView.getTop(); + setTargetOffsetTopAndBottom(offset, false /* requires update */); + } + + private final Animation mAnimateToStartPosition = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + moveToStart(interpolatedTime); + } + }; + + private void startScaleDownReturnToStartAnimation(int from, + Animation.AnimationListener listener) { + mFrom = from; + if (isAlphaUsedForScale()) { + mStartingScale = mProgress.getAlpha(); + } else { + mStartingScale = ViewCompat.getScaleX(mCircleView); + } + mScaleDownToStartAnimation = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + float targetScale = (mStartingScale + (-mStartingScale * interpolatedTime)); + setAnimationProgress(targetScale); + moveToStart(interpolatedTime); + } + }; + mScaleDownToStartAnimation.setDuration(SCALE_DOWN_DURATION); + if (listener != null) { + mCircleView.setAnimationListener(listener); + } + mCircleView.clearAnimation(); + mCircleView.startAnimation(mScaleDownToStartAnimation); + } + + private void setTargetOffsetTopAndBottom(int offset, boolean requiresUpdate) { + mCircleView.bringToFront(); + mCircleView.offsetTopAndBottom(offset); + mCurrentTargetOffsetTop = mCircleView.getTop(); + if (requiresUpdate && android.os.Build.VERSION.SDK_INT < 11) { + invalidate(); + } + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = MotionEventCompat.getActionIndex(ev); + final int pointerId = MotionEventCompat.getPointerId(ev, pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = MotionEventCompat.getPointerId(ev, newPointerIndex); + } + } + + /** + * Classes that wish to be notified when the swipe gesture correctly + * triggers a refresh should implement this interface. + */ + public interface OnRefreshListener { + public void onRefresh(); + } +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompat.java b/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompat.java new file mode 100644 index 00000000000..c31196aeac1 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompat.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.widget.TextView; + +/** + * Helper for accessing features in {@link TextView} introduced after API level + * 4 in a backwards compatible fashion. + */ +public class TextViewCompat { + + // Hide constructor + private TextViewCompat() { + } + + interface TextViewCompatImpl { + + public void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom); + + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom); + + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom); + + } + + static class BaseTextViewCompatImpl implements TextViewCompatImpl { + + @Override + public void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + textView.setCompoundDrawables(start, top, end, bottom); + } + + @Override + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); + } + + @Override + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom) { + textView.setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom); + } + + } + + static class JbMr1TextViewCompatImpl extends BaseTextViewCompatImpl { + + @Override + public void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + TextViewCompatJbMr1.setCompoundDrawablesRelative(textView, start, top, end, bottom); + } + + @Override + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + TextViewCompatJbMr1.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, + start, top, end, bottom); + } + + @Override + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom) { + TextViewCompatJbMr1.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, + start, top, end, bottom); + } + + } + + static class JbMr2TextViewCompatImpl extends JbMr1TextViewCompatImpl { + + @Override + public void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + TextViewCompatJbMr2.setCompoundDrawablesRelative(textView, start, top, end, bottom); + } + + @Override + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + TextViewCompatJbMr2 + .setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, + bottom); + } + + @Override + public void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom) { + TextViewCompatJbMr2.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, + start, top, end, bottom); + } + } + + static final TextViewCompatImpl IMPL; + + static { + final int version = Build.VERSION.SDK_INT; + if (version >= 18) { + IMPL = new JbMr2TextViewCompatImpl(); + } else if (version >= 17) { + IMPL = new JbMr1TextViewCompatImpl(); + } else { + IMPL = new BaseTextViewCompatImpl(); + } + } + + /** + * Sets the Drawables (if any) to appear to the start of, above, to the end + * of, and below the text. Use {@code null} if you do not want a Drawable + * there. The Drawables must already have had {@link Drawable#setBounds} + * called. + *

    + * Calling this method will overwrite any Drawables previously set using + * {@link TextView#setCompoundDrawables} or related methods. + * + * @param textView The TextView against which to invoke the method. + * @attr ref android.R.styleable#TextView_drawableStart + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableEnd + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public static void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + IMPL.setCompoundDrawablesRelative(textView, start, top, end, bottom); + } + + /** + * Sets the Drawables (if any) to appear to the start of, above, to the end + * of, and below the text. Use {@code null} if you do not want a Drawable + * there. The Drawables' bounds will be set to their intrinsic bounds. + *

    + * Calling this method will overwrite any Drawables previously set using + * {@link TextView#setCompoundDrawables} or related methods. + * + * @param textView The TextView against which to invoke the method. + * @attr ref android.R.styleable#TextView_drawableStart + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableEnd + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + IMPL.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, bottom); + } + + /** + * Sets the Drawables (if any) to appear to the start of, above, to the end + * of, and below the text. Use 0 if you do not want a Drawable there. The + * Drawables' bounds will be set to their intrinsic bounds. + *

    + * Calling this method will overwrite any Drawables previously set using + * {@link TextView#setCompoundDrawables} or related methods. + * + * @param textView The TextView against which to invoke the method. + * @param start Resource identifier of the start Drawable. + * @param top Resource identifier of the top Drawable. + * @param end Resource identifier of the end Drawable. + * @param bottom Resource identifier of the bottom Drawable. + * @attr ref android.R.styleable#TextView_drawableStart + * @attr ref android.R.styleable#TextView_drawableTop + * @attr ref android.R.styleable#TextView_drawableEnd + * @attr ref android.R.styleable#TextView_drawableBottom + */ + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom) { + IMPL.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, start, top, end, bottom); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr1.java b/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr1.java new file mode 100644 index 00000000000..7c073c65e12 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr1.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.view.View; +import android.widget.TextView; + +class TextViewCompatJbMr1 { + + public static void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + textView.setCompoundDrawables(rtl ? end : start, top, rtl ? start : end, bottom); + } + + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top, rtl ? start : end, + bottom); + } + + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom) { + boolean rtl = textView.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + textView.setCompoundDrawablesWithIntrinsicBounds(rtl ? end : start, top, rtl ? start : end, + bottom); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr2.java b/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr2.java new file mode 100644 index 00000000000..ec9fe613bff --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/TextViewCompatJbMr2.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.support.v4.widget; + +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.widget.TextView; + +class TextViewCompatJbMr2 { + + public static void setCompoundDrawablesRelative(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + textView.setCompoundDrawablesRelative(start, top, end, bottom); + } + + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + @Nullable Drawable start, @Nullable Drawable top, @Nullable Drawable end, + @Nullable Drawable bottom) { + textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); + } + + public static void setCompoundDrawablesRelativeWithIntrinsicBounds(@NonNull TextView textView, + int start, int top, int end, int bottom) { + textView.setCompoundDrawablesRelativeWithIntrinsicBounds(start, top, end, bottom); + } + +} diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/ViewDragHelper.java b/mobile/google-android-support-v4/src/android/support/v4/widget/ViewDragHelper.java new file mode 100644 index 00000000000..3f4e571276f --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/ViewDragHelper.java @@ -0,0 +1,1482 @@ +/* + * Copyright (C) 2013 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.support.v4.widget; + +import android.content.Context; +import android.support.v4.view.MotionEventCompat; +import android.support.v4.view.VelocityTrackerCompat; +import android.support.v4.view.ViewCompat; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.Interpolator; + +import java.util.Arrays; + +/** + * ViewDragHelper is a utility class for writing custom ViewGroups. It offers a number + * of useful operations and state tracking for allowing a user to drag and reposition + * views within their parent ViewGroup. + */ +public class ViewDragHelper { + private static final String TAG = "ViewDragHelper"; + + /** + * A null/invalid pointer ID. + */ + public static final int INVALID_POINTER = -1; + + /** + * A view is not currently being dragged or animating as a result of a fling/snap. + */ + public static final int STATE_IDLE = 0; + + /** + * A view is currently being dragged. The position is currently changing as a result + * of user input or simulated user input. + */ + public static final int STATE_DRAGGING = 1; + + /** + * A view is currently settling into place as a result of a fling or + * predefined non-interactive motion. + */ + public static final int STATE_SETTLING = 2; + + /** + * Edge flag indicating that the left edge should be affected. + */ + public static final int EDGE_LEFT = 1 << 0; + + /** + * Edge flag indicating that the right edge should be affected. + */ + public static final int EDGE_RIGHT = 1 << 1; + + /** + * Edge flag indicating that the top edge should be affected. + */ + public static final int EDGE_TOP = 1 << 2; + + /** + * Edge flag indicating that the bottom edge should be affected. + */ + public static final int EDGE_BOTTOM = 1 << 3; + + /** + * Edge flag set indicating all edges should be affected. + */ + public static final int EDGE_ALL = EDGE_LEFT | EDGE_TOP | EDGE_RIGHT | EDGE_BOTTOM; + + /** + * Indicates that a check should occur along the horizontal axis + */ + public static final int DIRECTION_HORIZONTAL = 1 << 0; + + /** + * Indicates that a check should occur along the vertical axis + */ + public static final int DIRECTION_VERTICAL = 1 << 1; + + /** + * Indicates that a check should occur along all axes + */ + public static final int DIRECTION_ALL = DIRECTION_HORIZONTAL | DIRECTION_VERTICAL; + + private static final int EDGE_SIZE = 20; // dp + + private static final int BASE_SETTLE_DURATION = 256; // ms + private static final int MAX_SETTLE_DURATION = 600; // ms + + // Current drag state; idle, dragging or settling + private int mDragState; + + // Distance to travel before a drag may begin + private int mTouchSlop; + + // Last known position/pointer tracking + private int mActivePointerId = INVALID_POINTER; + private float[] mInitialMotionX; + private float[] mInitialMotionY; + private float[] mLastMotionX; + private float[] mLastMotionY; + private int[] mInitialEdgesTouched; + private int[] mEdgeDragsInProgress; + private int[] mEdgeDragsLocked; + private int mPointersDown; + + private VelocityTracker mVelocityTracker; + private float mMaxVelocity; + private float mMinVelocity; + + private int mEdgeSize; + private int mTrackingEdges; + + private ScrollerCompat mScroller; + + private final Callback mCallback; + + private View mCapturedView; + private boolean mReleaseInProgress; + + private final ViewGroup mParentView; + + /** + * A Callback is used as a communication channel with the ViewDragHelper back to the + * parent view using it. on*methods are invoked on siginficant events and several + * accessor methods are expected to provide the ViewDragHelper with more information + * about the state of the parent view upon request. The callback also makes decisions + * governing the range and draggability of child views. + */ + public static abstract class Callback { + /** + * Called when the drag state changes. See the STATE_* constants + * for more information. + * + * @param state The new drag state + * + * @see #STATE_IDLE + * @see #STATE_DRAGGING + * @see #STATE_SETTLING + */ + public void onViewDragStateChanged(int state) {} + + /** + * Called when the captured view's position changes as the result of a drag or settle. + * + * @param changedView View whose position changed + * @param left New X coordinate of the left edge of the view + * @param top New Y coordinate of the top edge of the view + * @param dx Change in X position from the last call + * @param dy Change in Y position from the last call + */ + public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {} + + /** + * Called when a child view is captured for dragging or settling. The ID of the pointer + * currently dragging the captured view is supplied. If activePointerId is + * identified as {@link #INVALID_POINTER} the capture is programmatic instead of + * pointer-initiated. + * + * @param capturedChild Child view that was captured + * @param activePointerId Pointer id tracking the child capture + */ + public void onViewCaptured(View capturedChild, int activePointerId) {} + + /** + * Called when the child view is no longer being actively dragged. + * The fling velocity is also supplied, if relevant. The velocity values may + * be clamped to system minimums or maximums. + * + *

    Calling code may decide to fling or otherwise release the view to let it + * settle into place. It should do so using {@link #settleCapturedViewAt(int, int)} + * or {@link #flingCapturedView(int, int, int, int)}. If the Callback invokes + * one of these methods, the ViewDragHelper will enter {@link #STATE_SETTLING} + * and the view capture will not fully end until it comes to a complete stop. + * If neither of these methods is invoked before onViewReleased returns, + * the view will stop in place and the ViewDragHelper will return to + * {@link #STATE_IDLE}.

    + * + * @param releasedChild The captured child view now being released + * @param xvel X velocity of the pointer as it left the screen in pixels per second. + * @param yvel Y velocity of the pointer as it left the screen in pixels per second. + */ + public void onViewReleased(View releasedChild, float xvel, float yvel) {} + + /** + * Called when one of the subscribed edges in the parent view has been touched + * by the user while no child view is currently captured. + * + * @param edgeFlags A combination of edge flags describing the edge(s) currently touched + * @param pointerId ID of the pointer touching the described edge(s) + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void onEdgeTouched(int edgeFlags, int pointerId) {} + + /** + * Called when the given edge may become locked. This can happen if an edge drag + * was preliminarily rejected before beginning, but after {@link #onEdgeTouched(int, int)} + * was called. This method should return true to lock this edge or false to leave it + * unlocked. The default behavior is to leave edges unlocked. + * + * @param edgeFlags A combination of edge flags describing the edge(s) locked + * @return true to lock the edge, false to leave it unlocked + */ + public boolean onEdgeLock(int edgeFlags) { + return false; + } + + /** + * Called when the user has started a deliberate drag away from one + * of the subscribed edges in the parent view while no child view is currently captured. + * + * @param edgeFlags A combination of edge flags describing the edge(s) dragged + * @param pointerId ID of the pointer touching the described edge(s) + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void onEdgeDragStarted(int edgeFlags, int pointerId) {} + + /** + * Called to determine the Z-order of child views. + * + * @param index the ordered position to query for + * @return index of the view that should be ordered at position index + */ + public int getOrderedChildIndex(int index) { + return index; + } + + /** + * Return the magnitude of a draggable child view's horizontal range of motion in pixels. + * This method should return 0 for views that cannot move horizontally. + * + * @param child Child view to check + * @return range of horizontal motion in pixels + */ + public int getViewHorizontalDragRange(View child) { + return 0; + } + + /** + * Return the magnitude of a draggable child view's vertical range of motion in pixels. + * This method should return 0 for views that cannot move vertically. + * + * @param child Child view to check + * @return range of vertical motion in pixels + */ + public int getViewVerticalDragRange(View child) { + return 0; + } + + /** + * Called when the user's input indicates that they want to capture the given child view + * with the pointer indicated by pointerId. The callback should return true if the user + * is permitted to drag the given view with the indicated pointer. + * + *

    ViewDragHelper may call this method multiple times for the same view even if + * the view is already captured; this indicates that a new pointer is trying to take + * control of the view.

    + * + *

    If this method returns true, a call to {@link #onViewCaptured(android.view.View, int)} + * will follow if the capture is successful.

    + * + * @param child Child the user is attempting to capture + * @param pointerId ID of the pointer attempting the capture + * @return true if capture should be allowed, false otherwise + */ + public abstract boolean tryCaptureView(View child, int pointerId); + + /** + * Restrict the motion of the dragged child view along the horizontal axis. + * The default implementation does not allow horizontal motion; the extending + * class must override this method and provide the desired clamping. + * + * + * @param child Child view being dragged + * @param left Attempted motion along the X axis + * @param dx Proposed change in position for left + * @return The new clamped position for left + */ + public int clampViewPositionHorizontal(View child, int left, int dx) { + return 0; + } + + /** + * Restrict the motion of the dragged child view along the vertical axis. + * The default implementation does not allow vertical motion; the extending + * class must override this method and provide the desired clamping. + * + * + * @param child Child view being dragged + * @param top Attempted motion along the Y axis + * @param dy Proposed change in position for top + * @return The new clamped position for top + */ + public int clampViewPositionVertical(View child, int top, int dy) { + return 0; + } + } + + /** + * Interpolator defining the animation curve for mScroller + */ + private static final Interpolator sInterpolator = new Interpolator() { + public float getInterpolation(float t) { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + } + }; + + private final Runnable mSetIdleRunnable = new Runnable() { + public void run() { + setDragState(STATE_IDLE); + } + }; + + /** + * Factory method to create a new ViewDragHelper. + * + * @param forParent Parent view to monitor + * @param cb Callback to provide information and receive events + * @return a new ViewDragHelper instance + */ + public static ViewDragHelper create(ViewGroup forParent, Callback cb) { + return new ViewDragHelper(forParent.getContext(), forParent, cb); + } + + /** + * Factory method to create a new ViewDragHelper. + * + * @param forParent Parent view to monitor + * @param sensitivity Multiplier for how sensitive the helper should be about detecting + * the start of a drag. Larger values are more sensitive. 1.0f is normal. + * @param cb Callback to provide information and receive events + * @return a new ViewDragHelper instance + */ + public static ViewDragHelper create(ViewGroup forParent, float sensitivity, Callback cb) { + final ViewDragHelper helper = create(forParent, cb); + helper.mTouchSlop = (int) (helper.mTouchSlop * (1 / sensitivity)); + return helper; + } + + /** + * Apps should use ViewDragHelper.create() to get a new instance. + * This will allow VDH to use internal compatibility implementations for different + * platform versions. + * + * @param context Context to initialize config-dependent params from + * @param forParent Parent view to monitor + */ + private ViewDragHelper(Context context, ViewGroup forParent, Callback cb) { + if (forParent == null) { + throw new IllegalArgumentException("Parent view may not be null"); + } + if (cb == null) { + throw new IllegalArgumentException("Callback may not be null"); + } + + mParentView = forParent; + mCallback = cb; + + final ViewConfiguration vc = ViewConfiguration.get(context); + final float density = context.getResources().getDisplayMetrics().density; + mEdgeSize = (int) (EDGE_SIZE * density + 0.5f); + + mTouchSlop = vc.getScaledTouchSlop(); + mMaxVelocity = vc.getScaledMaximumFlingVelocity(); + mMinVelocity = vc.getScaledMinimumFlingVelocity(); + mScroller = ScrollerCompat.create(context, sInterpolator); + } + + /** + * Set the minimum velocity that will be detected as having a magnitude greater than zero + * in pixels per second. Callback methods accepting a velocity will be clamped appropriately. + * + * @param minVel Minimum velocity to detect + */ + public void setMinVelocity(float minVel) { + mMinVelocity = minVel; + } + + /** + * Return the currently configured minimum velocity. Any flings with a magnitude less + * than this value in pixels per second. Callback methods accepting a velocity will receive + * zero as a velocity value if the real detected velocity was below this threshold. + * + * @return the minimum velocity that will be detected + */ + public float getMinVelocity() { + return mMinVelocity; + } + + /** + * Retrieve the current drag state of this helper. This will return one of + * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}. + * @return The current drag state + */ + public int getViewDragState() { + return mDragState; + } + + /** + * Enable edge tracking for the selected edges of the parent view. + * The callback's {@link Callback#onEdgeTouched(int, int)} and + * {@link Callback#onEdgeDragStarted(int, int)} methods will only be invoked + * for edges for which edge tracking has been enabled. + * + * @param edgeFlags Combination of edge flags describing the edges to watch + * @see #EDGE_LEFT + * @see #EDGE_TOP + * @see #EDGE_RIGHT + * @see #EDGE_BOTTOM + */ + public void setEdgeTrackingEnabled(int edgeFlags) { + mTrackingEdges = edgeFlags; + } + + /** + * Return the size of an edge. This is the range in pixels along the edges of this view + * that will actively detect edge touches or drags if edge tracking is enabled. + * + * @return The size of an edge in pixels + * @see #setEdgeTrackingEnabled(int) + */ + public int getEdgeSize() { + return mEdgeSize; + } + + /** + * Capture a specific child view for dragging within the parent. The callback will be notified + * but {@link Callback#tryCaptureView(android.view.View, int)} will not be asked permission to + * capture this view. + * + * @param childView Child view to capture + * @param activePointerId ID of the pointer that is dragging the captured child view + */ + public void captureChildView(View childView, int activePointerId) { + if (childView.getParent() != mParentView) { + throw new IllegalArgumentException("captureChildView: parameter must be a descendant " + + "of the ViewDragHelper's tracked parent view (" + mParentView + ")"); + } + + mCapturedView = childView; + mActivePointerId = activePointerId; + mCallback.onViewCaptured(childView, activePointerId); + setDragState(STATE_DRAGGING); + } + + /** + * @return The currently captured view, or null if no view has been captured. + */ + public View getCapturedView() { + return mCapturedView; + } + + /** + * @return The ID of the pointer currently dragging the captured view, + * or {@link #INVALID_POINTER}. + */ + public int getActivePointerId() { + return mActivePointerId; + } + + /** + * @return The minimum distance in pixels that the user must travel to initiate a drag + */ + public int getTouchSlop() { + return mTouchSlop; + } + + /** + * The result of a call to this method is equivalent to + * {@link #processTouchEvent(android.view.MotionEvent)} receiving an ACTION_CANCEL event. + */ + public void cancel() { + mActivePointerId = INVALID_POINTER; + clearMotionHistory(); + + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + /** + * {@link #cancel()}, but also abort all motion in progress and snap to the end of any + * animation. + */ + public void abort() { + cancel(); + if (mDragState == STATE_SETTLING) { + final int oldX = mScroller.getCurrX(); + final int oldY = mScroller.getCurrY(); + mScroller.abortAnimation(); + final int newX = mScroller.getCurrX(); + final int newY = mScroller.getCurrY(); + mCallback.onViewPositionChanged(mCapturedView, newX, newY, newX - oldX, newY - oldY); + } + setDragState(STATE_IDLE); + } + + /** + * Animate the view child to the given (left, top) position. + * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} + * on each subsequent frame to continue the motion until it returns false. If this method + * returns false there is no further work to do to complete the movement. + * + *

    This operation does not count as a capture event, though {@link #getCapturedView()} + * will still report the sliding view while the slide is in progress.

    + * + * @param child Child view to capture and animate + * @param finalLeft Final left position of child + * @param finalTop Final top position of child + * @return true if animation should continue through {@link #continueSettling(boolean)} calls + */ + public boolean smoothSlideViewTo(View child, int finalLeft, int finalTop) { + mCapturedView = child; + mActivePointerId = INVALID_POINTER; + + boolean continueSliding = forceSettleCapturedViewAt(finalLeft, finalTop, 0, 0); + if (!continueSliding && mDragState == STATE_IDLE && mCapturedView != null) { + // If we're in an IDLE state to begin with and aren't moving anywhere, we + // end up having a non-null capturedView with an IDLE dragState + mCapturedView = null; + } + + return continueSliding; + } + + /** + * Settle the captured view at the given (left, top) position. + * The appropriate velocity from prior motion will be taken into account. + * If this method returns true, the caller should invoke {@link #continueSettling(boolean)} + * on each subsequent frame to continue the motion until it returns false. If this method + * returns false there is no further work to do to complete the movement. + * + * @param finalLeft Settled left edge position for the captured view + * @param finalTop Settled top edge position for the captured view + * @return true if animation should continue through {@link #continueSettling(boolean)} calls + */ + public boolean settleCapturedViewAt(int finalLeft, int finalTop) { + if (!mReleaseInProgress) { + throw new IllegalStateException("Cannot settleCapturedViewAt outside of a call to " + + "Callback#onViewReleased"); + } + + return forceSettleCapturedViewAt(finalLeft, finalTop, + (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), + (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId)); + } + + /** + * Settle the captured view at the given (left, top) position. + * + * @param finalLeft Target left position for the captured view + * @param finalTop Target top position for the captured view + * @param xvel Horizontal velocity + * @param yvel Vertical velocity + * @return true if animation should continue through {@link #continueSettling(boolean)} calls + */ + private boolean forceSettleCapturedViewAt(int finalLeft, int finalTop, int xvel, int yvel) { + final int startLeft = mCapturedView.getLeft(); + final int startTop = mCapturedView.getTop(); + final int dx = finalLeft - startLeft; + final int dy = finalTop - startTop; + + if (dx == 0 && dy == 0) { + // Nothing to do. Send callbacks, be done. + mScroller.abortAnimation(); + setDragState(STATE_IDLE); + return false; + } + + final int duration = computeSettleDuration(mCapturedView, dx, dy, xvel, yvel); + mScroller.startScroll(startLeft, startTop, dx, dy, duration); + + setDragState(STATE_SETTLING); + return true; + } + + private int computeSettleDuration(View child, int dx, int dy, int xvel, int yvel) { + xvel = clampMag(xvel, (int) mMinVelocity, (int) mMaxVelocity); + yvel = clampMag(yvel, (int) mMinVelocity, (int) mMaxVelocity); + final int absDx = Math.abs(dx); + final int absDy = Math.abs(dy); + final int absXVel = Math.abs(xvel); + final int absYVel = Math.abs(yvel); + final int addedVel = absXVel + absYVel; + final int addedDistance = absDx + absDy; + + final float xweight = xvel != 0 ? (float) absXVel / addedVel : + (float) absDx / addedDistance; + final float yweight = yvel != 0 ? (float) absYVel / addedVel : + (float) absDy / addedDistance; + + int xduration = computeAxisDuration(dx, xvel, mCallback.getViewHorizontalDragRange(child)); + int yduration = computeAxisDuration(dy, yvel, mCallback.getViewVerticalDragRange(child)); + + return (int) (xduration * xweight + yduration * yweight); + } + + private int computeAxisDuration(int delta, int velocity, int motionRange) { + if (delta == 0) { + return 0; + } + + final int width = mParentView.getWidth(); + final int halfWidth = width / 2; + final float distanceRatio = Math.min(1f, (float) Math.abs(delta) / width); + final float distance = halfWidth + halfWidth * + distanceInfluenceForSnapDuration(distanceRatio); + + int duration; + velocity = Math.abs(velocity); + if (velocity > 0) { + duration = 4 * Math.round(1000 * Math.abs(distance / velocity)); + } else { + final float range = (float) Math.abs(delta) / motionRange; + duration = (int) ((range + 1) * BASE_SETTLE_DURATION); + } + return Math.min(duration, MAX_SETTLE_DURATION); + } + + /** + * Clamp the magnitude of value for absMin and absMax. + * If the value is below the minimum, it will be clamped to zero. + * If the value is above the maximum, it will be clamped to the maximum. + * + * @param value Value to clamp + * @param absMin Absolute value of the minimum significant value to return + * @param absMax Absolute value of the maximum value to return + * @return The clamped value with the same sign as value + */ + private int clampMag(int value, int absMin, int absMax) { + final int absValue = Math.abs(value); + if (absValue < absMin) return 0; + if (absValue > absMax) return value > 0 ? absMax : -absMax; + return value; + } + + /** + * Clamp the magnitude of value for absMin and absMax. + * If the value is below the minimum, it will be clamped to zero. + * If the value is above the maximum, it will be clamped to the maximum. + * + * @param value Value to clamp + * @param absMin Absolute value of the minimum significant value to return + * @param absMax Absolute value of the maximum value to return + * @return The clamped value with the same sign as value + */ + private float clampMag(float value, float absMin, float absMax) { + final float absValue = Math.abs(value); + if (absValue < absMin) return 0; + if (absValue > absMax) return value > 0 ? absMax : -absMax; + return value; + } + + private float distanceInfluenceForSnapDuration(float f) { + f -= 0.5f; // center the values about 0. + f *= 0.3f * Math.PI / 2.0f; + return (float) Math.sin(f); + } + + /** + * Settle the captured view based on standard free-moving fling behavior. + * The caller should invoke {@link #continueSettling(boolean)} on each subsequent frame + * to continue the motion until it returns false. + * + * @param minLeft Minimum X position for the view's left edge + * @param minTop Minimum Y position for the view's top edge + * @param maxLeft Maximum X position for the view's left edge + * @param maxTop Maximum Y position for the view's top edge + */ + public void flingCapturedView(int minLeft, int minTop, int maxLeft, int maxTop) { + if (!mReleaseInProgress) { + throw new IllegalStateException("Cannot flingCapturedView outside of a call to " + + "Callback#onViewReleased"); + } + + mScroller.fling(mCapturedView.getLeft(), mCapturedView.getTop(), + (int) VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), + (int) VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), + minLeft, maxLeft, minTop, maxTop); + + setDragState(STATE_SETTLING); + } + + /** + * Move the captured settling view by the appropriate amount for the current time. + * If continueSettling returns true, the caller should call it again + * on the next frame to continue. + * + * @param deferCallbacks true if state callbacks should be deferred via posted message. + * Set this to true if you are calling this method from + * {@link android.view.View#computeScroll()} or similar methods + * invoked as part of layout or drawing. + * @return true if settle is still in progress + */ + public boolean continueSettling(boolean deferCallbacks) { + if (mDragState == STATE_SETTLING) { + boolean keepGoing = mScroller.computeScrollOffset(); + final int x = mScroller.getCurrX(); + final int y = mScroller.getCurrY(); + final int dx = x - mCapturedView.getLeft(); + final int dy = y - mCapturedView.getTop(); + + if (dx != 0) { + mCapturedView.offsetLeftAndRight(dx); + } + if (dy != 0) { + mCapturedView.offsetTopAndBottom(dy); + } + + if (dx != 0 || dy != 0) { + mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy); + } + + if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) { + // Close enough. The interpolator/scroller might think we're still moving + // but the user sure doesn't. + mScroller.abortAnimation(); + keepGoing = false; + } + + if (!keepGoing) { + if (deferCallbacks) { + mParentView.post(mSetIdleRunnable); + } else { + setDragState(STATE_IDLE); + } + } + } + + return mDragState == STATE_SETTLING; + } + + /** + * Like all callback events this must happen on the UI thread, but release + * involves some extra semantics. During a release (mReleaseInProgress) + * is the only time it is valid to call {@link #settleCapturedViewAt(int, int)} + * or {@link #flingCapturedView(int, int, int, int)}. + */ + private void dispatchViewReleased(float xvel, float yvel) { + mReleaseInProgress = true; + mCallback.onViewReleased(mCapturedView, xvel, yvel); + mReleaseInProgress = false; + + if (mDragState == STATE_DRAGGING) { + // onViewReleased didn't call a method that would have changed this. Go idle. + setDragState(STATE_IDLE); + } + } + + private void clearMotionHistory() { + if (mInitialMotionX == null) { + return; + } + Arrays.fill(mInitialMotionX, 0); + Arrays.fill(mInitialMotionY, 0); + Arrays.fill(mLastMotionX, 0); + Arrays.fill(mLastMotionY, 0); + Arrays.fill(mInitialEdgesTouched, 0); + Arrays.fill(mEdgeDragsInProgress, 0); + Arrays.fill(mEdgeDragsLocked, 0); + mPointersDown = 0; + } + + private void clearMotionHistory(int pointerId) { + if (mInitialMotionX == null) { + return; + } + mInitialMotionX[pointerId] = 0; + mInitialMotionY[pointerId] = 0; + mLastMotionX[pointerId] = 0; + mLastMotionY[pointerId] = 0; + mInitialEdgesTouched[pointerId] = 0; + mEdgeDragsInProgress[pointerId] = 0; + mEdgeDragsLocked[pointerId] = 0; + mPointersDown &= ~(1 << pointerId); + } + + private void ensureMotionHistorySizeForId(int pointerId) { + if (mInitialMotionX == null || mInitialMotionX.length <= pointerId) { + float[] imx = new float[pointerId + 1]; + float[] imy = new float[pointerId + 1]; + float[] lmx = new float[pointerId + 1]; + float[] lmy = new float[pointerId + 1]; + int[] iit = new int[pointerId + 1]; + int[] edip = new int[pointerId + 1]; + int[] edl = new int[pointerId + 1]; + + if (mInitialMotionX != null) { + System.arraycopy(mInitialMotionX, 0, imx, 0, mInitialMotionX.length); + System.arraycopy(mInitialMotionY, 0, imy, 0, mInitialMotionY.length); + System.arraycopy(mLastMotionX, 0, lmx, 0, mLastMotionX.length); + System.arraycopy(mLastMotionY, 0, lmy, 0, mLastMotionY.length); + System.arraycopy(mInitialEdgesTouched, 0, iit, 0, mInitialEdgesTouched.length); + System.arraycopy(mEdgeDragsInProgress, 0, edip, 0, mEdgeDragsInProgress.length); + System.arraycopy(mEdgeDragsLocked, 0, edl, 0, mEdgeDragsLocked.length); + } + + mInitialMotionX = imx; + mInitialMotionY = imy; + mLastMotionX = lmx; + mLastMotionY = lmy; + mInitialEdgesTouched = iit; + mEdgeDragsInProgress = edip; + mEdgeDragsLocked = edl; + } + } + + private void saveInitialMotion(float x, float y, int pointerId) { + ensureMotionHistorySizeForId(pointerId); + mInitialMotionX[pointerId] = mLastMotionX[pointerId] = x; + mInitialMotionY[pointerId] = mLastMotionY[pointerId] = y; + mInitialEdgesTouched[pointerId] = getEdgesTouched((int) x, (int) y); + mPointersDown |= 1 << pointerId; + } + + private void saveLastMotion(MotionEvent ev) { + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = MotionEventCompat.getPointerId(ev, i); + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + mLastMotionX[pointerId] = x; + mLastMotionY[pointerId] = y; + } + } + + /** + * Check if the given pointer ID represents a pointer that is currently down (to the best + * of the ViewDragHelper's knowledge). + * + *

    The state used to report this information is populated by the methods + * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or + * {@link #processTouchEvent(android.view.MotionEvent)}. If one of these methods has not + * been called for all relevant MotionEvents to track, the information reported + * by this method may be stale or incorrect.

    + * + * @param pointerId pointer ID to check; corresponds to IDs provided by MotionEvent + * @return true if the pointer with the given ID is still down + */ + public boolean isPointerDown(int pointerId) { + return (mPointersDown & 1 << pointerId) != 0; + } + + void setDragState(int state) { + mParentView.removeCallbacks(mSetIdleRunnable); + if (mDragState != state) { + mDragState = state; + mCallback.onViewDragStateChanged(state); + if (mDragState == STATE_IDLE) { + mCapturedView = null; + } + } + } + + /** + * Attempt to capture the view with the given pointer ID. The callback will be involved. + * This will put us into the "dragging" state. If we've already captured this view with + * this pointer this method will immediately return true without consulting the callback. + * + * @param toCapture View to capture + * @param pointerId Pointer to capture with + * @return true if capture was successful + */ + boolean tryCaptureViewForDrag(View toCapture, int pointerId) { + if (toCapture == mCapturedView && mActivePointerId == pointerId) { + // Already done! + return true; + } + if (toCapture != null && mCallback.tryCaptureView(toCapture, pointerId)) { + mActivePointerId = pointerId; + captureChildView(toCapture, pointerId); + return true; + } + return false; + } + + /** + * Tests scrollability within child views of v given a delta of dx. + * + * @param v View to test for horizontal scrollability + * @param checkV Whether the view v passed should itself be checked for scrollability (true), + * or just its children (false). + * @param dx Delta scrolled in pixels along the X axis + * @param dy Delta scrolled in pixels along the Y axis + * @param x X coordinate of the active touch point + * @param y Y coordinate of the active touch point + * @return true if child views of v can be scrolled by delta of dx. + */ + protected boolean canScroll(View v, boolean checkV, int dx, int dy, int x, int y) { + if (v instanceof ViewGroup) { + final ViewGroup group = (ViewGroup) v; + final int scrollX = v.getScrollX(); + final int scrollY = v.getScrollY(); + final int count = group.getChildCount(); + // Count backwards - let topmost views consume scroll distance first. + for (int i = count - 1; i >= 0; i--) { + // TODO: Add versioned support here for transformed views. + // This will not work for transformed views in Honeycomb+ + final View child = group.getChildAt(i); + if (x + scrollX >= child.getLeft() && x + scrollX < child.getRight() && + y + scrollY >= child.getTop() && y + scrollY < child.getBottom() && + canScroll(child, true, dx, dy, x + scrollX - child.getLeft(), + y + scrollY - child.getTop())) { + return true; + } + } + } + + return checkV && (ViewCompat.canScrollHorizontally(v, -dx) || + ViewCompat.canScrollVertically(v, -dy)); + } + + /** + * Check if this event as provided to the parent view's onInterceptTouchEvent should + * cause the parent to intercept the touch event stream. + * + * @param ev MotionEvent provided to onInterceptTouchEvent + * @return true if the parent view should return true from onInterceptTouchEvent + */ + public boolean shouldInterceptTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + final int actionIndex = MotionEventCompat.getActionIndex(ev); + + if (action == MotionEvent.ACTION_DOWN) { + // Reset things for a new event stream, just in case we didn't get + // the whole previous stream. + cancel(); + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + final int pointerId = MotionEventCompat.getPointerId(ev, 0); + saveInitialMotion(x, y, pointerId); + + final View toCapture = findTopChildUnder((int) x, (int) y); + + // Catch a settling view if possible. + if (toCapture == mCapturedView && mDragState == STATE_SETTLING) { + tryCaptureViewForDrag(toCapture, pointerId); + } + + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + break; + } + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + final float x = MotionEventCompat.getX(ev, actionIndex); + final float y = MotionEventCompat.getY(ev, actionIndex); + + saveInitialMotion(x, y, pointerId); + + // A ViewDragHelper can only manipulate one view at a time. + if (mDragState == STATE_IDLE) { + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + } else if (mDragState == STATE_SETTLING) { + // Catch a settling view if possible. + final View toCapture = findTopChildUnder((int) x, (int) y); + if (toCapture == mCapturedView) { + tryCaptureViewForDrag(toCapture, pointerId); + } + } + break; + } + + case MotionEvent.ACTION_MOVE: { + if (mInitialMotionX == null || mInitialMotionY == null) break; + + // First to cross a touch slop over a draggable view wins. Also report edge drags. + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = MotionEventCompat.getPointerId(ev, i); + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + final float dx = x - mInitialMotionX[pointerId]; + final float dy = y - mInitialMotionY[pointerId]; + + final View toCapture = findTopChildUnder((int) x, (int) y); + final boolean pastSlop = toCapture != null && checkTouchSlop(toCapture, dx, dy); + if (pastSlop) { + // check the callback's + // getView[Horizontal|Vertical]DragRange methods to know + // if you can move at all along an axis, then see if it + // would clamp to the same value. If you can't move at + // all in every dimension with a nonzero range, bail. + final int oldLeft = toCapture.getLeft(); + final int targetLeft = oldLeft + (int) dx; + final int newLeft = mCallback.clampViewPositionHorizontal(toCapture, + targetLeft, (int) dx); + final int oldTop = toCapture.getTop(); + final int targetTop = oldTop + (int) dy; + final int newTop = mCallback.clampViewPositionVertical(toCapture, targetTop, + (int) dy); + final int horizontalDragRange = mCallback.getViewHorizontalDragRange( + toCapture); + final int verticalDragRange = mCallback.getViewVerticalDragRange(toCapture); + if ((horizontalDragRange == 0 || horizontalDragRange > 0 + && newLeft == oldLeft) && (verticalDragRange == 0 + || verticalDragRange > 0 && newTop == oldTop)) { + break; + } + } + reportNewEdgeDrags(dx, dy, pointerId); + if (mDragState == STATE_DRAGGING) { + // Callback might have started an edge drag + break; + } + + if (pastSlop && tryCaptureViewForDrag(toCapture, pointerId)) { + break; + } + } + saveLastMotion(ev); + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + clearMotionHistory(pointerId); + break; + } + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: { + cancel(); + break; + } + } + + return mDragState == STATE_DRAGGING; + } + + /** + * Process a touch event received by the parent view. This method will dispatch callback events + * as needed before returning. The parent view's onTouchEvent implementation should call this. + * + * @param ev The touch event received by the parent view + */ + public void processTouchEvent(MotionEvent ev) { + final int action = MotionEventCompat.getActionMasked(ev); + final int actionIndex = MotionEventCompat.getActionIndex(ev); + + if (action == MotionEvent.ACTION_DOWN) { + // Reset things for a new event stream, just in case we didn't get + // the whole previous stream. + cancel(); + } + + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + mVelocityTracker.addMovement(ev); + + switch (action) { + case MotionEvent.ACTION_DOWN: { + final float x = ev.getX(); + final float y = ev.getY(); + final int pointerId = MotionEventCompat.getPointerId(ev, 0); + final View toCapture = findTopChildUnder((int) x, (int) y); + + saveInitialMotion(x, y, pointerId); + + // Since the parent is already directly processing this touch event, + // there is no reason to delay for a slop before dragging. + // Start immediately if possible. + tryCaptureViewForDrag(toCapture, pointerId); + + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + break; + } + + case MotionEventCompat.ACTION_POINTER_DOWN: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + final float x = MotionEventCompat.getX(ev, actionIndex); + final float y = MotionEventCompat.getY(ev, actionIndex); + + saveInitialMotion(x, y, pointerId); + + // A ViewDragHelper can only manipulate one view at a time. + if (mDragState == STATE_IDLE) { + // If we're idle we can do anything! Treat it like a normal down event. + + final View toCapture = findTopChildUnder((int) x, (int) y); + tryCaptureViewForDrag(toCapture, pointerId); + + final int edgesTouched = mInitialEdgesTouched[pointerId]; + if ((edgesTouched & mTrackingEdges) != 0) { + mCallback.onEdgeTouched(edgesTouched & mTrackingEdges, pointerId); + } + } else if (isCapturedViewUnder((int) x, (int) y)) { + // We're still tracking a captured view. If the same view is under this + // point, we'll swap to controlling it with this pointer instead. + // (This will still work if we're "catching" a settling view.) + + tryCaptureViewForDrag(mCapturedView, pointerId); + } + break; + } + + case MotionEvent.ACTION_MOVE: { + if (mDragState == STATE_DRAGGING) { + final int index = MotionEventCompat.findPointerIndex(ev, mActivePointerId); + final float x = MotionEventCompat.getX(ev, index); + final float y = MotionEventCompat.getY(ev, index); + final int idx = (int) (x - mLastMotionX[mActivePointerId]); + final int idy = (int) (y - mLastMotionY[mActivePointerId]); + + dragTo(mCapturedView.getLeft() + idx, mCapturedView.getTop() + idy, idx, idy); + + saveLastMotion(ev); + } else { + // Check to see if any pointer is now over a draggable view. + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int pointerId = MotionEventCompat.getPointerId(ev, i); + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + final float dx = x - mInitialMotionX[pointerId]; + final float dy = y - mInitialMotionY[pointerId]; + + reportNewEdgeDrags(dx, dy, pointerId); + if (mDragState == STATE_DRAGGING) { + // Callback might have started an edge drag. + break; + } + + final View toCapture = findTopChildUnder((int) x, (int) y); + if (checkTouchSlop(toCapture, dx, dy) && + tryCaptureViewForDrag(toCapture, pointerId)) { + break; + } + } + saveLastMotion(ev); + } + break; + } + + case MotionEventCompat.ACTION_POINTER_UP: { + final int pointerId = MotionEventCompat.getPointerId(ev, actionIndex); + if (mDragState == STATE_DRAGGING && pointerId == mActivePointerId) { + // Try to find another pointer that's still holding on to the captured view. + int newActivePointer = INVALID_POINTER; + final int pointerCount = MotionEventCompat.getPointerCount(ev); + for (int i = 0; i < pointerCount; i++) { + final int id = MotionEventCompat.getPointerId(ev, i); + if (id == mActivePointerId) { + // This one's going away, skip. + continue; + } + + final float x = MotionEventCompat.getX(ev, i); + final float y = MotionEventCompat.getY(ev, i); + if (findTopChildUnder((int) x, (int) y) == mCapturedView && + tryCaptureViewForDrag(mCapturedView, id)) { + newActivePointer = mActivePointerId; + break; + } + } + + if (newActivePointer == INVALID_POINTER) { + // We didn't find another pointer still touching the view, release it. + releaseViewForPointerUp(); + } + } + clearMotionHistory(pointerId); + break; + } + + case MotionEvent.ACTION_UP: { + if (mDragState == STATE_DRAGGING) { + releaseViewForPointerUp(); + } + cancel(); + break; + } + + case MotionEvent.ACTION_CANCEL: { + if (mDragState == STATE_DRAGGING) { + dispatchViewReleased(0, 0); + } + cancel(); + break; + } + } + } + + private void reportNewEdgeDrags(float dx, float dy, int pointerId) { + int dragsStarted = 0; + if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_LEFT)) { + dragsStarted |= EDGE_LEFT; + } + if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_TOP)) { + dragsStarted |= EDGE_TOP; + } + if (checkNewEdgeDrag(dx, dy, pointerId, EDGE_RIGHT)) { + dragsStarted |= EDGE_RIGHT; + } + if (checkNewEdgeDrag(dy, dx, pointerId, EDGE_BOTTOM)) { + dragsStarted |= EDGE_BOTTOM; + } + + if (dragsStarted != 0) { + mEdgeDragsInProgress[pointerId] |= dragsStarted; + mCallback.onEdgeDragStarted(dragsStarted, pointerId); + } + } + + private boolean checkNewEdgeDrag(float delta, float odelta, int pointerId, int edge) { + final float absDelta = Math.abs(delta); + final float absODelta = Math.abs(odelta); + + if ((mInitialEdgesTouched[pointerId] & edge) != edge || (mTrackingEdges & edge) == 0 || + (mEdgeDragsLocked[pointerId] & edge) == edge || + (mEdgeDragsInProgress[pointerId] & edge) == edge || + (absDelta <= mTouchSlop && absODelta <= mTouchSlop)) { + return false; + } + if (absDelta < absODelta * 0.5f && mCallback.onEdgeLock(edge)) { + mEdgeDragsLocked[pointerId] |= edge; + return false; + } + return (mEdgeDragsInProgress[pointerId] & edge) == 0 && absDelta > mTouchSlop; + } + + /** + * Check if we've crossed a reasonable touch slop for the given child view. + * If the child cannot be dragged along the horizontal or vertical axis, motion + * along that axis will not count toward the slop check. + * + * @param child Child to check + * @param dx Motion since initial position along X axis + * @param dy Motion since initial position along Y axis + * @return true if the touch slop has been crossed + */ + private boolean checkTouchSlop(View child, float dx, float dy) { + if (child == null) { + return false; + } + final boolean checkHorizontal = mCallback.getViewHorizontalDragRange(child) > 0; + final boolean checkVertical = mCallback.getViewVerticalDragRange(child) > 0; + + if (checkHorizontal && checkVertical) { + return dx * dx + dy * dy > mTouchSlop * mTouchSlop; + } else if (checkHorizontal) { + return Math.abs(dx) > mTouchSlop; + } else if (checkVertical) { + return Math.abs(dy) > mTouchSlop; + } + return false; + } + + /** + * Check if any pointer tracked in the current gesture has crossed + * the required slop threshold. + * + *

    This depends on internal state populated by + * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or + * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on + * the results of this method after all currently available touch data + * has been provided to one of these two methods.

    + * + * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, + * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL} + * @return true if the slop threshold has been crossed, false otherwise + */ + public boolean checkTouchSlop(int directions) { + final int count = mInitialMotionX.length; + for (int i = 0; i < count; i++) { + if (checkTouchSlop(directions, i)) { + return true; + } + } + return false; + } + + /** + * Check if the specified pointer tracked in the current gesture has crossed + * the required slop threshold. + * + *

    This depends on internal state populated by + * {@link #shouldInterceptTouchEvent(android.view.MotionEvent)} or + * {@link #processTouchEvent(android.view.MotionEvent)}. You should only rely on + * the results of this method after all currently available touch data + * has been provided to one of these two methods.

    + * + * @param directions Combination of direction flags, see {@link #DIRECTION_HORIZONTAL}, + * {@link #DIRECTION_VERTICAL}, {@link #DIRECTION_ALL} + * @param pointerId ID of the pointer to slop check as specified by MotionEvent + * @return true if the slop threshold has been crossed, false otherwise + */ + public boolean checkTouchSlop(int directions, int pointerId) { + if (!isPointerDown(pointerId)) { + return false; + } + + final boolean checkHorizontal = (directions & DIRECTION_HORIZONTAL) == DIRECTION_HORIZONTAL; + final boolean checkVertical = (directions & DIRECTION_VERTICAL) == DIRECTION_VERTICAL; + + final float dx = mLastMotionX[pointerId] - mInitialMotionX[pointerId]; + final float dy = mLastMotionY[pointerId] - mInitialMotionY[pointerId]; + + if (checkHorizontal && checkVertical) { + return dx * dx + dy * dy > mTouchSlop * mTouchSlop; + } else if (checkHorizontal) { + return Math.abs(dx) > mTouchSlop; + } else if (checkVertical) { + return Math.abs(dy) > mTouchSlop; + } + return false; + } + + /** + * Check if any of the edges specified were initially touched in the currently active gesture. + * If there is no currently active gesture this method will return false. + * + * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, + * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and + * {@link #EDGE_ALL} + * @return true if any of the edges specified were initially touched in the current gesture + */ + public boolean isEdgeTouched(int edges) { + final int count = mInitialEdgesTouched.length; + for (int i = 0; i < count; i++) { + if (isEdgeTouched(edges, i)) { + return true; + } + } + return false; + } + + /** + * Check if any of the edges specified were initially touched by the pointer with + * the specified ID. If there is no currently active gesture or if there is no pointer with + * the given ID currently down this method will return false. + * + * @param edges Edges to check for an initial edge touch. See {@link #EDGE_LEFT}, + * {@link #EDGE_TOP}, {@link #EDGE_RIGHT}, {@link #EDGE_BOTTOM} and + * {@link #EDGE_ALL} + * @return true if any of the edges specified were initially touched in the current gesture + */ + public boolean isEdgeTouched(int edges, int pointerId) { + return isPointerDown(pointerId) && (mInitialEdgesTouched[pointerId] & edges) != 0; + } + + private void releaseViewForPointerUp() { + mVelocityTracker.computeCurrentVelocity(1000, mMaxVelocity); + final float xvel = clampMag( + VelocityTrackerCompat.getXVelocity(mVelocityTracker, mActivePointerId), + mMinVelocity, mMaxVelocity); + final float yvel = clampMag( + VelocityTrackerCompat.getYVelocity(mVelocityTracker, mActivePointerId), + mMinVelocity, mMaxVelocity); + dispatchViewReleased(xvel, yvel); + } + + private void dragTo(int left, int top, int dx, int dy) { + int clampedX = left; + int clampedY = top; + final int oldLeft = mCapturedView.getLeft(); + final int oldTop = mCapturedView.getTop(); + if (dx != 0) { + clampedX = mCallback.clampViewPositionHorizontal(mCapturedView, left, dx); + mCapturedView.offsetLeftAndRight(clampedX - oldLeft); + } + if (dy != 0) { + clampedY = mCallback.clampViewPositionVertical(mCapturedView, top, dy); + mCapturedView.offsetTopAndBottom(clampedY - oldTop); + } + + if (dx != 0 || dy != 0) { + final int clampedDx = clampedX - oldLeft; + final int clampedDy = clampedY - oldTop; + mCallback.onViewPositionChanged(mCapturedView, clampedX, clampedY, + clampedDx, clampedDy); + } + } + + /** + * Determine if the currently captured view is under the given point in the + * parent view's coordinate system. If there is no captured view this method + * will return false. + * + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return true if the captured view is under the given point, false otherwise + */ + public boolean isCapturedViewUnder(int x, int y) { + return isViewUnder(mCapturedView, x, y); + } + + /** + * Determine if the supplied view is under the given point in the + * parent view's coordinate system. + * + * @param view Child view of the parent to hit test + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return true if the supplied view is under the given point, false otherwise + */ + public boolean isViewUnder(View view, int x, int y) { + if (view == null) { + return false; + } + return x >= view.getLeft() && + x < view.getRight() && + y >= view.getTop() && + y < view.getBottom(); + } + + /** + * Find the topmost child under the given point within the parent view's coordinate system. + * The child order is determined using {@link Callback#getOrderedChildIndex(int)}. + * + * @param x X position to test in the parent's coordinate system + * @param y Y position to test in the parent's coordinate system + * @return The topmost child view under (x, y) or null if none found. + */ + public View findTopChildUnder(int x, int y) { + final int childCount = mParentView.getChildCount(); + for (int i = childCount - 1; i >= 0; i--) { + final View child = mParentView.getChildAt(mCallback.getOrderedChildIndex(i)); + if (x >= child.getLeft() && x < child.getRight() && + y >= child.getTop() && y < child.getBottom()) { + return child; + } + } + return null; + } + + private int getEdgesTouched(int x, int y) { + int result = 0; + + if (x < mParentView.getLeft() + mEdgeSize) result |= EDGE_LEFT; + if (y < mParentView.getTop() + mEdgeSize) result |= EDGE_TOP; + if (x > mParentView.getRight() - mEdgeSize) result |= EDGE_RIGHT; + if (y > mParentView.getBottom() - mEdgeSize) result |= EDGE_BOTTOM; + + return result; + } +} \ No newline at end of file diff --git a/mobile/google-android-support-v4/src/android/support/v4/widget/package.html b/mobile/google-android-support-v4/src/android/support/v4/widget/package.html new file mode 100644 index 00000000000..e2c636d3b05 --- /dev/null +++ b/mobile/google-android-support-v4/src/android/support/v4/widget/package.html @@ -0,0 +1,8 @@ + + +Support android.widget classes to assist with development of applications for +android API level 4 or later. This includes a complete modern implementation +of {@link android.support.v4.widget.CursorAdapter} and related classes, which +is needed for use with {@link android.support.v4.content.CursorLoader}. + + diff --git a/mobile/google-android-support-v7-appcompat_lib/.classpath b/mobile/google-android-support-v7-appcompat_lib/.classpath index 7bc01d9a9c6..34488603ca2 100644 --- a/mobile/google-android-support-v7-appcompat_lib/.classpath +++ b/mobile/google-android-support-v7-appcompat_lib/.classpath @@ -1,9 +1,11 @@ - - - - - - - - - + + + + + + + + + + + diff --git a/mobile/google-android-support-v7-appcompat_lib/AndroidManifest.xml b/mobile/google-android-support-v7-appcompat_lib/AndroidManifest.xml index dac4cb27ed9..58122b1dcff 100644 --- a/mobile/google-android-support-v7-appcompat_lib/AndroidManifest.xml +++ b/mobile/google-android-support-v7-appcompat_lib/AndroidManifest.xml @@ -1,5 +1,6 @@ - - + package="android.support.v7.appcompat" > + + + - + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v13.jar b/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v13.jar deleted file mode 100644 index 0d364e7452a..00000000000 Binary files a/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v13.jar and /dev/null differ diff --git a/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v4.jar b/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v4.jar deleted file mode 100644 index bd450c73404..00000000000 Binary files a/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v4.jar and /dev/null differ diff --git a/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v7-appcompat.jar b/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v7-appcompat.jar deleted file mode 100644 index e13b16c73fd..00000000000 Binary files a/mobile/google-android-support-v7-appcompat_lib/libs/android-support-v7-appcompat.jar and /dev/null differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_in.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_in.xml old mode 100644 new mode 100755 index da7ee295c99..35e4aa5c79a --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_in.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_in.xml @@ -17,4 +17,4 @@ \ No newline at end of file + android:duration="@android:integer/config_mediumAnimTime" /> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_out.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_out.xml old mode 100644 new mode 100755 index c81b39a9b13..c71dc128375 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_out.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_fade_out.xml @@ -17,4 +17,4 @@ \ No newline at end of file + android:duration="@android:integer/config_mediumAnimTime" /> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_grow_fade_in_from_bottom.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_grow_fade_in_from_bottom.xml old mode 100644 new mode 100755 index 79d02d44ca6..c9cb7d3116e --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_grow_fade_in_from_bottom.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_grow_fade_in_from_bottom.xml @@ -27,4 +27,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_enter.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_enter.xml old mode 100644 new mode 100755 index 91664da17ee..c7b18b2112a --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_enter.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_enter.xml @@ -18,4 +18,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_exit.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_exit.xml old mode 100644 new mode 100755 index db7e8073a84..535a0924b58 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_exit.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_popup_exit.xml @@ -18,4 +18,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_shrink_fade_out_from_bottom.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_shrink_fade_out_from_bottom.xml old mode 100644 new mode 100755 index 9a23cd2025a..8d28106bd30 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_shrink_fade_out_from_bottom.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_shrink_fade_out_from_bottom.xml @@ -24,4 +24,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_bottom.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_bottom.xml old mode 100644 new mode 100755 index 1afa8febc52..7d819d57a37 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_bottom.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_bottom.xml @@ -17,3 +17,4 @@ android:interpolator="@android:anim/decelerate_interpolator" android:fromYDelta="50%p" android:toYDelta="0" android:duration="@android:integer/config_mediumAnimTime"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_top.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_top.xml old mode 100644 new mode 100755 index ab824f2e4ac..7dd98809ee6 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_top.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_in_top.xml @@ -16,4 +16,4 @@ \ No newline at end of file + android:duration="@android:integer/config_mediumAnimTime"/> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_bottom.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_bottom.xml old mode 100644 new mode 100755 index b309fe89c64..e11c80c7293 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_bottom.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_bottom.xml @@ -16,4 +16,4 @@ \ No newline at end of file + android:duration="@android:integer/config_mediumAnimTime"/> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_top.xml b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_top.xml old mode 100644 new mode 100755 index e84d1c7fb6e..9fa9e4fb003 --- a/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_top.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/anim/abc_slide_out_top.xml @@ -16,4 +16,4 @@ \ No newline at end of file + android:duration="@android:integer/config_mediumAnimTime"/> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_dark.xml old mode 100644 new mode 100755 index e0160766e08..61e47c20698 --- a/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_dark.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_light.xml old mode 100644 new mode 100755 index 290faf1a0e0..926f2bd8a33 --- a/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color-v11/abc_background_cache_hint_selector_material_light.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_dark.xml old mode 100644 new mode 100755 index 9a7af53d4ec..e5c143ab491 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_dark.xml @@ -17,3 +17,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_light.xml old mode 100644 new mode 100755 index aa53f3577e8..697c8ba807e --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_background_cache_hint_selector_material_light.xml @@ -17,3 +17,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_dark.xml old mode 100644 new mode 100755 index 724c2557dad..2d22cdfccb7 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_dark.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_light.xml old mode 100644 new mode 100755 index 7395e680c65..7f4bd596dfe --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_disable_only_material_light.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_dark.xml old mode 100644 new mode 100755 index 7d66d02d637..a82fbea64c2 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_dark.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_light.xml old mode 100644 new mode 100755 index 105b643ddb4..169b3cdb588 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_primary_text_material_light.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_search_url_text.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_search_url_text.xml old mode 100644 new mode 100755 index 0631d5d4ca1..9827356a769 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_search_url_text.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_search_url_text.xml @@ -18,4 +18,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_dark.xml old mode 100644 new mode 100755 index 6399b1d028f..5566c2a9eb4 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_dark.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_light.xml old mode 100644 new mode 100755 index 87c015a4cd6..e96a7df570a --- a/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/abc_secondary_text_material_light.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_dark.xml old mode 100644 new mode 100755 index 6153382c7c5..132aef8a1c2 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_dark.xml @@ -17,4 +17,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_light.xml old mode 100644 new mode 100755 index 94d71148213..8e95351d602 --- a/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/color/switch_thumb_material_light.xml @@ -17,4 +17,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ab_share_pack_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ab_share_pack_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_check_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_check_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_check_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_check_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_radio_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_radio_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_radio_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_radio_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_rating_star_off_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_rating_star_off_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_rating_star_on_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_rating_star_on_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00001.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00001.9.png old mode 100644 new mode 100755 index 705e0d9b78c..88f1767fa90 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00001.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00001.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00012.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00012.9.png old mode 100644 new mode 100755 index b69271067ba..d5e1a001134 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00012.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_btn_switch_to_on_mtrl_00012.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_cab_background_top_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_cab_background_top_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_clear_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_clear_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_commit_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_commit_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_go_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_go_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_paste_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_paste_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_selectall_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_selectall_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_share_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_menu_share_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_voice_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_ic_voice_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_divider_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_divider_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_focused_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_focused_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_longpressed_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_longpressed_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_pressed_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_pressed_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_pressed_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_pressed_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_selector_disabled_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_selector_disabled_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_selector_disabled_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_list_selector_disabled_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_menu_hardkey_panel_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_menu_hardkey_panel_mtrl_mult.9.png old mode 100644 new mode 100755 index a6b3696e484..2cf413c5c96 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_menu_hardkey_panel_mtrl_mult.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_menu_hardkey_panel_mtrl_mult.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_popup_background_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_popup_background_mtrl_mult.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_switch_track_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_switch_track_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_tab_indicator_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_tab_indicator_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_text_cursor_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_text_cursor_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_search_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_search_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_search_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-hdpi/abc_textfield_search_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-hdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-mdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xhdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxhdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-ldrtl-xxxhdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ab_share_pack_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ab_share_pack_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_check_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_check_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_check_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_check_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_radio_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_radio_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_radio_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_radio_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_rating_star_off_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_rating_star_off_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_rating_star_on_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_rating_star_on_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00001.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00001.9.png old mode 100644 new mode 100755 index b2191dad9f4..d890a62a040 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00001.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00001.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00012.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00012.9.png old mode 100644 new mode 100755 index 2a94e6e4ce2..0620439460d Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00012.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_btn_switch_to_on_mtrl_00012.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_cab_background_top_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_cab_background_top_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_clear_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_clear_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_commit_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_commit_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_go_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_go_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_paste_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_paste_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_selectall_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_selectall_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_share_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_menu_share_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_voice_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_ic_voice_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_divider_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_divider_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_focused_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_focused_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_longpressed_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_longpressed_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_pressed_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_pressed_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_pressed_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_pressed_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_selector_disabled_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_selector_disabled_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_selector_disabled_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_list_selector_disabled_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_menu_hardkey_panel_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_menu_hardkey_panel_mtrl_mult.9.png old mode 100644 new mode 100755 index 91b0cb8d01f..fe0ec49abce Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_menu_hardkey_panel_mtrl_mult.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_menu_hardkey_panel_mtrl_mult.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_popup_background_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_popup_background_mtrl_mult.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_switch_track_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_switch_track_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_tab_indicator_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_tab_indicator_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_text_cursor_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_text_cursor_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_search_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_search_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_search_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-mdpi/abc_textfield_search_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00001.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00001.9.png deleted file mode 100644 index 530ca456daa..00000000000 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00001.9.png and /dev/null differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00012.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00012.9.png deleted file mode 100644 index b527495ee61..00000000000 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-tvdpi/abc_btn_switch_to_on_mtrl_00012.9.png and /dev/null differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ab_share_pack_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ab_share_pack_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_check_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_check_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_check_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_check_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_radio_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_radio_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_radio_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_radio_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_rating_star_off_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_rating_star_off_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_rating_star_on_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_rating_star_on_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00001.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00001.9.png old mode 100644 new mode 100755 index 0d2274389f3..a854864d7df Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00001.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00001.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00012.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00012.9.png old mode 100644 new mode 100755 index 17fc083dff1..726b1dc59e3 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00012.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_btn_switch_to_on_mtrl_00012.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_cab_background_top_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_cab_background_top_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_clear_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_clear_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_commit_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_commit_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_go_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_go_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_paste_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_paste_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_selectall_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_selectall_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_share_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_menu_share_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_voice_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_ic_voice_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_divider_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_divider_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_focused_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_focused_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_longpressed_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_longpressed_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_pressed_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_pressed_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_pressed_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_pressed_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_selector_disabled_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_selector_disabled_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_selector_disabled_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_list_selector_disabled_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png old mode 100644 new mode 100755 index c67a5ddcd17..09d10225d8a Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_popup_background_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_popup_background_mtrl_mult.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_switch_track_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_switch_track_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_tab_indicator_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_tab_indicator_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_text_cursor_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_text_cursor_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_search_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_search_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_search_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xhdpi/abc_textfield_search_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ab_share_pack_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ab_share_pack_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_check_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_check_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_check_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_check_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_radio_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_radio_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_radio_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_radio_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_rating_star_off_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_rating_star_off_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_rating_star_on_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_rating_star_on_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png old mode 100644 new mode 100755 index 133de5258bd..f026a41c853 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png old mode 100644 new mode 100755 index ff3b59630cc..c8b9f68939f Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_cab_background_top_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_cab_background_top_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_clear_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_clear_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_commit_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_commit_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_go_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_go_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_paste_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_paste_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_selectall_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_selectall_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_share_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_menu_share_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_voice_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_ic_voice_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_divider_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_divider_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_focused_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_focused_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_longpressed_holo.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_longpressed_holo.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_pressed_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_pressed_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_pressed_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_pressed_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_selector_disabled_holo_dark.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_selector_disabled_holo_dark.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_selector_disabled_holo_light.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_list_selector_disabled_holo_light.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png old mode 100644 new mode 100755 index 2ab970d5168..f7f306db29b Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_menu_hardkey_panel_mtrl_mult.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_popup_background_mtrl_mult.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_popup_background_mtrl_mult.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_switch_track_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_switch_track_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_tab_indicator_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_tab_indicator_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_text_cursor_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_text_cursor_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_search_activated_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_search_activated_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_search_default_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxhdpi/abc_textfield_search_default_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_check_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_check_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_check_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_check_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_radio_to_on_mtrl_000.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_radio_to_on_mtrl_000.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_radio_to_on_mtrl_015.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_radio_to_on_mtrl_015.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png old mode 100644 new mode 100755 index e075ab8ddae..6fd5bfef8e8 Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00001.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png old mode 100644 new mode 100755 index 25274ee8a1f..99e68cc0e7f Binary files a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png and b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_btn_switch_to_on_mtrl_00012.9.png differ diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_ab_back_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_ab_back_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_clear_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_clear_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_copy_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_cut_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_cut_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_moreoverflow_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_paste_mtrl_am_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_paste_mtrl_am_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_selectall_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_selectall_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_share_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_menu_share_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_voice_search_api_mtrl_alpha.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_ic_voice_search_api_mtrl_alpha.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_spinner_mtrl_am_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_spinner_mtrl_am_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_switch_track_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_switch_track_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_tab_indicator_mtrl_alpha.9.png b/mobile/google-android-support-v7-appcompat_lib/res/drawable-xxxhdpi/abc_tab_indicator_mtrl_alpha.9.png old mode 100644 new mode 100755 diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_borderless_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_borderless_material.xml old mode 100644 new mode 100755 index f3894600ba0..012e5243633 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_borderless_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_borderless_material.xml @@ -20,3 +20,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_check_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_check_material.xml old mode 100644 new mode 100755 index 4934a929d6e..68f606bd7f3 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_check_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_check_material.xml @@ -17,4 +17,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_default_mtrl_shape.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_default_mtrl_shape.xml old mode 100644 new mode 100755 index c50d4b10f00..95d33823e52 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_default_mtrl_shape.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_default_mtrl_shape.xml @@ -30,3 +30,4 @@ android:bottom="@dimen/abc_button_padding_vertical_material" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_radio_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_radio_material.xml old mode 100644 new mode 100755 index 6e9f9cf3741..3444b5c557b --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_radio_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_btn_radio_material.xml @@ -17,4 +17,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_internal_bg.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_internal_bg.xml old mode 100644 new mode 100755 index 9faf60ac616..52655a5ac3b --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_internal_bg.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_internal_bg.xml @@ -20,4 +20,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_top_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_top_material.xml old mode 100644 new mode 100755 index f20add7e4b8..acacfd9e898 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_top_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_cab_background_top_material.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_dark.xml old mode 100644 new mode 100755 index 41c4a6f842d..f98179fe369 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_dark.xml @@ -23,4 +23,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_light.xml old mode 100644 new mode 100755 index 248b13af946..5411635caf4 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_dialog_material_background_light.xml @@ -23,4 +23,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_edit_text_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_edit_text_material.xml old mode 100644 new mode 100755 index 46c4e912003..ab9c90f4d9b --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_edit_text_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_edit_text_material.xml @@ -27,3 +27,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_dark.xml old mode 100644 new mode 100755 index 72162c222ee..db7baccc0b2 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_dark.xml @@ -24,3 +24,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_light.xml old mode 100644 new mode 100755 index 1c180b2ee48..c38248fcdc2 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_item_background_holo_light.xml @@ -24,3 +24,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_dark.xml old mode 100644 new mode 100755 index 0add58c86ac..bb98c2f3125 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_dark.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_light.xml old mode 100644 new mode 100755 index 0c1d3e67821..4e95440383b --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_background_transition_holo_light.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_dark.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_dark.xml old mode 100644 new mode 100755 index 1fb5fc4516d..f23ed0abe89 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_dark.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_dark.xml @@ -25,3 +25,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_light.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_light.xml old mode 100644 new mode 100755 index 8d24047229b..329d880a9b4 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_light.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_list_selector_holo_light.xml @@ -26,3 +26,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_ratingbar_full_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_ratingbar_full_material.xml old mode 100644 new mode 100755 index 535e2da25e5..964686ac9dc --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_ratingbar_full_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_ratingbar_full_material.xml @@ -22,3 +22,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_spinner_textfield_background_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_spinner_textfield_background_material.xml old mode 100644 new mode 100755 index d0f46a80974..315afd2d15c --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_spinner_textfield_background_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_spinner_textfield_background_material.xml @@ -33,4 +33,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_switch_thumb_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_switch_thumb_material.xml old mode 100644 new mode 100755 index ee96ec2e7ab..170e9588ce9 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_switch_thumb_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_switch_thumb_material.xml @@ -17,4 +17,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_tab_indicator_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_tab_indicator_material.xml old mode 100644 new mode 100755 index 1a8de1b69b5..17aacb204de --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_tab_indicator_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_tab_indicator_material.xml @@ -18,3 +18,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_textfield_search_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_textfield_search_material.xml old mode 100644 new mode 100755 index 08873966e43..503fe9e4fa7 --- a/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_textfield_search_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/drawable/abc_textfield_search_material.xml @@ -20,3 +20,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout-v11/abc_screen_content_include.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout-v11/abc_screen_content_include.xml deleted file mode 100644 index 757be1c1df7..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout-v11/abc_screen_content_include.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_title_item.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_title_item.xml old mode 100644 new mode 100755 index 194afb74cb8..c6c180c4d16 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_title_item.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_title_item.xml @@ -32,3 +32,4 @@ android:ellipsize="end" android:visibility="gone" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_up_container.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_up_container.xml old mode 100644 new mode 100755 index f46550a553e..ca4712a5e8c --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_up_container.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_up_container.xml @@ -21,3 +21,4 @@ android:gravity="center_vertical" android:enabled="false"> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_view_list_nav_layout.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_view_list_nav_layout.xml old mode 100644 new mode 100755 index 5c105ab551c..7ce06a015cf --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_view_list_nav_layout.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_bar_view_list_nav_layout.xml @@ -20,4 +20,4 @@ android:layout_width="fill_parent" android:layout_height="fill_parent" style="?attr/actionBarTabBarStyle"> - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_item_layout.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_item_layout.xml old mode 100644 new mode 100755 index 150ea50dc3c..73482e2aac7 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_item_layout.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_item_layout.xml @@ -28,3 +28,4 @@ android:textAppearance="?attr/actionMenuTextAppearance" android:textColor="?attr/actionMenuTextColor" style="?attr/actionButtonStyle"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_layout.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_layout.xml old mode 100644 new mode 100755 index 4918d2fba96..e1a9e0db20c --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_layout.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_menu_layout.xml @@ -22,3 +22,4 @@ app:divider="?attr/actionBarDivider" app:dividerPadding="12dip" android:gravity="center_vertical"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_bar.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_bar.xml old mode 100644 new mode 100755 index 34b56a1b09b..ffb6a77ec93 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_bar.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_bar.xml @@ -23,3 +23,4 @@ android:visibility="gone" android:theme="?attr/actionBarTheme" style="?attr/actionModeStyle"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_close_item_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_close_item_material.xml old mode 100644 new mode 100755 index dfc4debec48..c52ce7d4ee6 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_close_item_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_action_mode_close_item_material.xml @@ -23,4 +23,4 @@ android:src="?attr/actionModeCloseDrawable" style="?attr/actionModeCloseButtonStyle" android:layout_width="wrap_content" - android:layout_height="match_parent" /> \ No newline at end of file + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view.xml old mode 100644 new mode 100755 index 85fd94e37f1..df8895d0ecc --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view.xml @@ -69,3 +69,4 @@
    + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view_list_item.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view_list_item.xml old mode 100644 new mode 100755 index 887427d8093..29c798bd8b4 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view_list_item.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_activity_chooser_view_list_item.xml @@ -49,4 +49,4 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_alert_dialog_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_alert_dialog_material.xml old mode 100644 new mode 100755 index 86d59f76e24..a763dbdbf0f --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_alert_dialog_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_alert_dialog_material.xml @@ -32,7 +32,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" - android:gravity="center_vertical|start" + android:gravity="center_vertical" android:paddingLeft="?attr/dialogPreferredPadding" android:paddingRight="?attr/dialogPreferredPadding" android:paddingTop="@dimen/abc_dialog_padding_top_material"> @@ -41,9 +41,9 @@ android:id="@android:id/icon" android:layout_width="32dip" android:layout_height="32dip" - android:layout_marginEnd="8dip" android:scaleType="fitCenter" - android:src="@null"/> + android:src="@null" + style="@style/RtlOverlay.Widget.AppCompat.DialogTitle.Icon"/> + android:textAlignment="viewStart" /> @@ -145,4 +145,4 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"/> - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_dialog_title_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_dialog_title_material.xml old mode 100644 new mode 100755 index 068b9e907c1..472faacbb66 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_dialog_title_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_dialog_title_material.xml @@ -44,4 +44,4 @@ enabled. android:layout_height="wrap_content" android:layout_weight="1"/> - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_expanded_menu_layout.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_expanded_menu_layout.xml old mode 100644 new mode 100755 index 371151f2ec8..449be093ac5 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_expanded_menu_layout.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_expanded_menu_layout.xml @@ -19,3 +19,4 @@ android:id="@+id/expanded_menu" android:layout_width="?attr/panelMenuListWidth" android:layout_height="wrap_content" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_checkbox.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_checkbox.xml old mode 100644 new mode 100755 index d9c3f068114..f1b0a0708e5 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_checkbox.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_checkbox.xml @@ -24,3 +24,4 @@ android:duplicateParentState="true"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_icon.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_icon.xml old mode 100644 new mode 100755 index acd005a13b9..e4613d5b469 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_icon.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_icon.xml @@ -26,3 +26,4 @@ android:scaleType="centerInside" android:duplicateParentState="true"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_layout.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_layout.xml old mode 100644 new mode 100755 index 1cee43e7048..e31c0b9e2e5 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_layout.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_layout.xml @@ -58,3 +58,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_radio.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_radio.xml old mode 100644 new mode 100755 index 0ca8d7a2a5d..7bc5206980e --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_radio.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_list_menu_item_radio.xml @@ -22,3 +22,4 @@ android:focusable="false" android:clickable="false" android:duplicateParentState="true"/> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_popup_menu_item_layout.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_popup_menu_item_layout.xml old mode 100644 new mode 100755 index 76820e0789a..0d10365961d --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_popup_menu_item_layout.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_popup_menu_item_layout.xml @@ -59,3 +59,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_content_include.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_content_include.xml old mode 100644 new mode 100755 index 6e21f2e339f..7a6d272579e --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_content_include.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_content_include.xml @@ -24,3 +24,4 @@ android:foreground="?android:attr/windowContentOverlay" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple.xml old mode 100644 new mode 100755 index 426851c20aa..f96f64ffd8b --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple.xml @@ -32,3 +32,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple_overlay_action_mode.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple_overlay_action_mode.xml old mode 100644 new mode 100755 index ac399c1d7c7..468fe145b68 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple_overlay_action_mode.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_simple_overlay_action_mode.xml @@ -36,4 +36,4 @@ enabled. android:layout_width="match_parent" android:layout_height="wrap_content" /> - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_toolbar.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_toolbar.xml old mode 100644 new mode 100755 index 572ec3463b3..2ab548b6a2d --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_toolbar.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_screen_toolbar.xml @@ -51,3 +51,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_dropdown_item_icons_2line.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_dropdown_item_icons_2line.xml old mode 100644 new mode 100755 index 7407498a55e..dfc0d7ebb07 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_dropdown_item_icons_2line.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_dropdown_item_icons_2line.xml @@ -83,3 +83,4 @@ android:layout_above="@android:id/text2" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_view.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_view.xml old mode 100644 new mode 100755 index ff9361dcaf7..00cfd331bd0 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_view.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_search_view.xml @@ -139,3 +139,4 @@ + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_select_dialog_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_select_dialog_material.xml old mode 100644 new mode 100755 index 12bcbf1f3c2..a4011d84999 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_select_dialog_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_select_dialog_material.xml @@ -32,4 +32,4 @@ android:paddingTop="@dimen/abc_dialog_list_padding_vertical_material" android:paddingBottom="@dimen/abc_dialog_list_padding_vertical_material" android:clipToPadding="false" - style="@style/Widget.AppCompat.ListView" /> \ No newline at end of file + style="@style/Widget.AppCompat.ListView" /> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_simple_dropdown_hint.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_simple_dropdown_hint.xml old mode 100644 new mode 100755 index 8326b5ccfa1..c76ab753dd2 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_simple_dropdown_hint.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/abc_simple_dropdown_hint.xml @@ -20,4 +20,4 @@ android:singleLine="true" android:layout_margin="3dip" android:layout_width="match_parent" - android:layout_height="wrap_content" /> \ No newline at end of file + android:layout_height="wrap_content" /> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_media_action.xml old mode 100644 new mode 100755 similarity index 53% rename from mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens.xml rename to mobile/google-android-support-v7-appcompat_lib/res/layout/notification_media_action.xml index f0b6892a9f5..fa4b17192c6 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_media_action.xml @@ -14,8 +14,12 @@ limitations under the License. --> - - - 32dp - - \ No newline at end of file + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_media_cancel_action.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_media_cancel_action.xml new file mode 100755 index 00000000000..2ea0e6c7f81 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_media_cancel_action.xml @@ -0,0 +1,28 @@ + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_big_media.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_big_media.xml new file mode 100755 index 00000000000..2d953210850 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_big_media.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_big_media_narrow.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_big_media_narrow.xml new file mode 100755 index 00000000000..e1e4ff53af2 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_big_media_narrow.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_lines.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_lines.xml new file mode 100755 index 00000000000..7cdaaf96c9e --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_lines.xml @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_media.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_media.xml new file mode 100755 index 00000000000..0c53d24e307 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_media.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_part_chronometer.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_part_chronometer.xml new file mode 100755 index 00000000000..b76205cd605 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_part_chronometer.xml @@ -0,0 +1,29 @@ + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_part_time.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_part_time.xml new file mode 100755 index 00000000000..e5d3864ebd9 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/notification_template_part_time.xml @@ -0,0 +1,29 @@ + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_item_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_item_material.xml old mode 100644 new mode 100755 index 677b178d6db..91f62f8a777 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_item_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_item_material.xml @@ -31,3 +31,4 @@ android:paddingLeft="?attr/listPreferredItemPaddingLeft" android:paddingRight="?attr/listPreferredItemPaddingRight" android:ellipsize="marquee" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_multichoice_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_multichoice_material.xml old mode 100644 new mode 100755 index 8f75bd34bd8..76d2c37bdb1 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_multichoice_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_multichoice_material.xml @@ -27,3 +27,4 @@ android:paddingRight="?attr/dialogPreferredPadding" android:checkMark="?android:attr/listChoiceIndicatorMultiple" android:ellipsize="marquee" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_singlechoice_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_singlechoice_material.xml old mode 100644 new mode 100755 index 3c2264cb8fb..eeccbc2d7a6 --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_singlechoice_material.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/select_dialog_singlechoice_material.xml @@ -27,3 +27,4 @@ android:paddingRight="?attr/dialogPreferredPadding" android:checkMark="?android:attr/listChoiceIndicatorSingle" android:ellipsize="marquee" /> + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/layout/support_simple_spinner_dropdown_item.xml b/mobile/google-android-support-v7-appcompat_lib/res/layout/support_simple_spinner_dropdown_item.xml old mode 100644 new mode 100755 index d2f177ac8cb..7c177889cff --- a/mobile/google-android-support-v7-appcompat_lib/res/layout/support_simple_spinner_dropdown_item.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/layout/support_simple_spinner_dropdown_item.xml @@ -22,4 +22,4 @@ android:singleLine="true" android:layout_width="match_parent" android:layout_height="?attr/dropdownListPreferredItemHeight" - android:ellipsize="marquee"/> \ No newline at end of file + android:ellipsize="marquee"/> \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-af/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-af/strings.xml deleted file mode 100644 index f7348c46b65..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-af/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Klaar" - "Navigeer tuis" - "Navigeer op" - "Nog opsies" - "Soek" - "Soeknavraag" - "Vee navraag uit" - "Dien navraag in" - "Stemsoektog" - "Kies \'n program" - "Sien alles" - "Deel met %s" - "Deel met" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-af/values-af.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-af/values-af.xml new file mode 100755 index 00000000000..8050f0de0e9 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-af/values-af.xml @@ -0,0 +1,18 @@ + + + + + "Navigeer tuis" + "Navigeer op" + "Nog opsies" + "Klaar" + "Sien alles" + "Kies \'n program" + "Vee navraag uit" + "Soeknavraag" + "Soek" + "Dien navraag in" + "Stemsoektog" + "Deel met" + "Deel met %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-am/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-am/strings.xml deleted file mode 100644 index e849b31861f..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-am/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "ተከናውኗል" - "ወደ መነሻ ይዳስሱ" - "ወደ ላይ ይዳስሱ" - "ተጨማሪ አማራጮች" - "ፍለጋ" - "የፍለጋ ጥያቄ" - "መጠይቅ አጽዳ" - "መጠይቅ ያስረክቡ" - "የድምፅ ፍለጋ" - "መተግበሪያ ይምረጡ" - "ሁሉንም ይመልከቱ" - "ከ%s ጋር ያጋሩ" - "ከሚከተለው ጋር ያጋሩ" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-am/values-am.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-am/values-am.xml new file mode 100755 index 00000000000..bceeaf6ce97 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-am/values-am.xml @@ -0,0 +1,18 @@ + + + + + "ወደ መነሻ ይዳስሱ" + "ወደ ላይ ይዳስሱ" + "ተጨማሪ አማራጮች" + "ተከናውኗል" + "ሁሉንም ይመልከቱ" + "መተግበሪያ ይምረጡ" + "መጠይቅ አጽዳ" + "የፍለጋ ጥያቄ" + "ፍለጋ" + "መጠይቅ ያስረክቡ" + "የድምፅ ፍለጋ" + "ከሚከተለው ጋር ያጋሩ" + "ከ%s ጋር ያጋሩ" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ar/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ar/strings.xml deleted file mode 100644 index 93dde04224b..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-ar/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "تم" - "التنقل إلى الشاشة الرئيسية" - "التنقل إلى أعلى" - "خيارات إضافية" - "بحث" - "طلب البحث" - "محو طلب البحث" - "إرسال طلب البحث" - "البحث الصوتي" - "اختيار تطبيق" - "عرض الكل" - "‏مشاركة مع %s" - "مشاركة مع" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ar/values-ar.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ar/values-ar.xml new file mode 100755 index 00000000000..495be021ae8 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-ar/values-ar.xml @@ -0,0 +1,18 @@ + + + + + "التنقل إلى الشاشة الرئيسية" + "التنقل إلى أعلى" + "خيارات إضافية" + "تم" + "عرض الكل" + "اختيار تطبيق" + "محو طلب البحث" + "طلب البحث" + "بحث" + "إرسال طلب البحث" + "البحث الصوتي" + "مشاركة مع" + "‏مشاركة مع %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-bg/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-bg/strings.xml deleted file mode 100644 index de3bde8f418..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-bg/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Готово" - "Придвижване към „Начало“" - "Придвижване нагоре" - "Още опции" - "Търсене" - "Заявка за търсене" - "Изчистване на заявката" - "Изпращане на заявката" - "Гласово търсене" - "Изберете приложение" - "Вижте всички" - "Споделяне със: %s" - "Споделяне със:" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-bg/values-bg.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-bg/values-bg.xml new file mode 100755 index 00000000000..39a0b8ce00d --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-bg/values-bg.xml @@ -0,0 +1,18 @@ + + + + + "Придвижване към „Начало“" + "Придвижване нагоре" + "Още опции" + "Готово" + "Вижте всички" + "Изберете приложение" + "Изчистване на заявката" + "Заявка за търсене" + "Търсене" + "Изпращане на заявката" + "Гласово търсене" + "Споделяне със:" + "Споделяне със: %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/strings.xml deleted file mode 100644 index 393240f9397..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "সম্পন্ন হয়েছে" - "হোম এ নেভিগেট করুন" - "উপরের দিকে নেভিগেট করুন" - "আরো বিকল্প" - "অনুসন্ধান করুন" - "ক্যোয়ারী অনুসন্ধান করুন" - "ক্যোয়ারী সাফ করুন" - "ক্যোয়ারী জমা দিন" - "ভয়েস অনুসন্ধান" - "একটি অ্যাপ্লিকেশান চয়ন করুন" - "সবগুলো দেখুন" - "%s এর সাথে ভাগ করুন" - "এর সাথে ভাগ করুন" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/values-bn-rBD.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/values-bn-rBD.xml new file mode 100755 index 00000000000..078dd47220f --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-bn-rBD/values-bn-rBD.xml @@ -0,0 +1,18 @@ + + + + + "হোম এ নেভিগেট করুন" + "উপরের দিকে নেভিগেট করুন" + "আরো বিকল্প" + "সম্পন্ন হয়েছে" + "সবগুলো দেখুন" + "একটি অ্যাপ্লিকেশান চয়ন করুন" + "ক্যোয়ারী সাফ করুন" + "ক্যোয়ারী অনুসন্ধান করুন" + "অনুসন্ধান করুন" + "ক্যোয়ারী জমা দিন" + "ভয়েস অনুসন্ধান" + "এর সাথে ভাগ করুন" + "%s এর সাথে ভাগ করুন" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ca/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ca/strings.xml deleted file mode 100644 index bfd4cb07a09..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-ca/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Fet" - "Navega a la pàgina d\'inici" - "Navega cap a dalt" - "Més opcions" - "Cerca" - "Consulta de cerca" - "Esborra la consulta" - "Envia la consulta" - "Cerca per veu" - "Selecciona una aplicació" - "Mostra\'ls tots" - "Comparteix amb %s" - "Comparteix amb" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ca/values-ca.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ca/values-ca.xml new file mode 100755 index 00000000000..16e784bd6eb --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-ca/values-ca.xml @@ -0,0 +1,18 @@ + + + + + "Navega a la pàgina d\'inici" + "Navega cap a dalt" + "Més opcions" + "Fet" + "Mostra\'ls tots" + "Selecciona una aplicació" + "Esborra la consulta" + "Consulta de cerca" + "Cerca" + "Envia la consulta" + "Cerca per veu" + "Comparteix amb" + "Comparteix amb %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-cs/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-cs/strings.xml deleted file mode 100644 index 1465fdcd90d..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-cs/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Hotovo" - "Přejít na plochu" - "Přejít nahoru" - "Více možností" - "Hledat" - "Vyhledávací dotaz" - "Smazat dotaz" - "Odeslat dotaz" - "Hlasové vyhledávání" - "Vybrat aplikaci" - "Zobrazit vše" - "Sdílet pomocí %s" - "Sdílet pomocí" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-cs/values-cs.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-cs/values-cs.xml new file mode 100755 index 00000000000..ef5477c726e --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-cs/values-cs.xml @@ -0,0 +1,18 @@ + + + + + "Přejít na plochu" + "Přejít nahoru" + "Více možností" + "Hotovo" + "Zobrazit vše" + "Vybrat aplikaci" + "Smazat dotaz" + "Vyhledávací dotaz" + "Hledat" + "Odeslat dotaz" + "Hlasové vyhledávání" + "Sdílet pomocí" + "Sdílet pomocí %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-da/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-da/strings.xml deleted file mode 100644 index b178513e30e..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-da/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Luk" - "Naviger hjem" - "Naviger op" - "Flere muligheder" - "Søg" - "Søgeforespørgsel" - "Ryd forespørgslen" - "Indsend forespørgslen" - "Stemmesøgning" - "Vælg en app" - "Se alle" - "Del med %s" - "Del med" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-da/values-da.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-da/values-da.xml new file mode 100755 index 00000000000..3d9628d5228 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-da/values-da.xml @@ -0,0 +1,18 @@ + + + + + "Naviger hjem" + "Naviger op" + "Flere muligheder" + "Luk" + "Se alle" + "Vælg en app" + "Ryd forespørgslen" + "Søgeforespørgsel" + "Søg" + "Indsend forespørgslen" + "Stemmesøgning" + "Del med" + "Del med %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-de/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-de/strings.xml deleted file mode 100644 index 6da4b71340b..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-de/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Fertig" - "Zur Startseite" - "Nach oben" - "Weitere Optionen" - "Suchen" - "Suchanfrage" - "Suchanfrage löschen" - "Suchanfrage senden" - "Sprachsuche" - "App auswählen" - "Alle ansehen" - "Freigeben für %s" - "Freigeben für" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-de/values-de.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-de/values-de.xml new file mode 100755 index 00000000000..33867f270ae --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-de/values-de.xml @@ -0,0 +1,18 @@ + + + + + "Zur Startseite" + "Nach oben" + "Weitere Optionen" + "Fertig" + "Alle ansehen" + "App auswählen" + "Suchanfrage löschen" + "Suchanfrage" + "Suchen" + "Suchanfrage senden" + "Sprachsuche" + "Freigeben für" + "Freigeben für %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-el/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-el/strings.xml deleted file mode 100644 index 4c0e286f8af..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-el/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Τέλος" - "Πλοήγηση στην αρχική σελίδα" - "Πλοήγηση προς τα επάνω" - "Περισσότερες επιλογές" - "Αναζήτηση" - "Ερώτημα αναζήτησης" - "Διαγραφή ερωτήματος" - "Υποβολή ερωτήματος" - "Φωνητική αναζήτηση" - "Επιλέξτε κάποια εφαρμογή" - "Προβολή όλων" - "Κοινή χρήση με %s" - "Κοινή χρήση με" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-el/values-el.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-el/values-el.xml new file mode 100755 index 00000000000..24b8e5185b4 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-el/values-el.xml @@ -0,0 +1,18 @@ + + + + + "Πλοήγηση στην αρχική σελίδα" + "Πλοήγηση προς τα επάνω" + "Περισσότερες επιλογές" + "Τέλος" + "Προβολή όλων" + "Επιλέξτε κάποια εφαρμογή" + "Διαγραφή ερωτήματος" + "Ερώτημα αναζήτησης" + "Αναζήτηση" + "Υποβολή ερωτήματος" + "Φωνητική αναζήτηση" + "Κοινή χρήση με" + "Κοινή χρήση με %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/strings.xml deleted file mode 100644 index 3ec0b0e9eed..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Finished" - "Navigate home" - "Navigate up" - "More options" - "Search" - "Search query" - "Clear query" - "Submit query" - "Voice search" - "Choose an app" - "See all" - "Share with %s" - "Share with" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/values-en-rGB.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/values-en-rGB.xml new file mode 100755 index 00000000000..cd0bcbe46a3 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-en-rGB/values-en-rGB.xml @@ -0,0 +1,18 @@ + + + + + "Navigate home" + "Navigate up" + "More options" + "Finished" + "See all" + "Choose an app" + "Clear query" + "Search query" + "Search" + "Submit query" + "Voice search" + "Share with" + "Share with %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/strings.xml deleted file mode 100644 index 3ec0b0e9eed..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Finished" - "Navigate home" - "Navigate up" - "More options" - "Search" - "Search query" - "Clear query" - "Submit query" - "Voice search" - "Choose an app" - "See all" - "Share with %s" - "Share with" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/values-en-rIN.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/values-en-rIN.xml new file mode 100755 index 00000000000..ab5fea65965 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-en-rIN/values-en-rIN.xml @@ -0,0 +1,18 @@ + + + + + "Navigate home" + "Navigate up" + "More options" + "Finished" + "See all" + "Choose an app" + "Clear query" + "Search query" + "Search" + "Submit query" + "Voice search" + "Share with" + "Share with %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/strings.xml deleted file mode 100644 index 6ab7942b95a..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Listo" - "Navegar a la página principal" - "Navegar hacia arriba" - "Más opciones" - "Búsqueda" - "Consulta de búsqueda" - "Eliminar la consulta" - "Enviar consulta" - "Búsqueda por voz" - "Elige una aplicación." - "Ver todo" - "Compartir con %s" - "Compartir con" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/values-es-rUS.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/values-es-rUS.xml new file mode 100755 index 00000000000..df9949f0b5a --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-es-rUS/values-es-rUS.xml @@ -0,0 +1,18 @@ + + + + + "Navegar a la página principal" + "Navegar hacia arriba" + "Más opciones" + "Listo" + "Ver todo" + "Elige una aplicación." + "Eliminar la consulta" + "Consulta de búsqueda" + "Búsqueda" + "Enviar consulta" + "Búsqueda por voz" + "Compartir con" + "Compartir con %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-es/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-es/strings.xml deleted file mode 100644 index ed15b35c447..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-es/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Listo" - "Ir a la pantalla de inicio" - "Desplazarse hacia arriba" - "Más opciones" - "Buscar" - "Consulta" - "Borrar consulta" - "Enviar consulta" - "Búsqueda por voz" - "Seleccionar una aplicación" - "Ver todo" - "Compartir con %s" - "Compartir con" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-es/values-es.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-es/values-es.xml new file mode 100755 index 00000000000..329ddece05a --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-es/values-es.xml @@ -0,0 +1,18 @@ + + + + + "Ir a la pantalla de inicio" + "Desplazarse hacia arriba" + "Más opciones" + "Listo" + "Ver todo" + "Seleccionar una aplicación" + "Borrar consulta" + "Consulta" + "Buscar" + "Enviar consulta" + "Búsqueda por voz" + "Compartir con" + "Compartir con %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/strings.xml deleted file mode 100644 index 2ae925d407e..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Valmis" - "Navigeerimine avaekraanile" - "Navigeerimine üles" - "Rohkem valikuid" - "Otsing" - "Otsingupäring" - "Päringu tühistamine" - "Päringu esitamine" - "Häälotsing" - "Valige rakendus" - "Kuva kõik" - "Jagamine kasutajaga %s" - "Jagamine:" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/values-et-rEE.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/values-et-rEE.xml new file mode 100755 index 00000000000..2caae5866c7 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-et-rEE/values-et-rEE.xml @@ -0,0 +1,18 @@ + + + + + "Navigeerimine avaekraanile" + "Navigeerimine üles" + "Rohkem valikuid" + "Valmis" + "Kuva kõik" + "Valige rakendus" + "Päringu tühistamine" + "Otsingupäring" + "Otsing" + "Päringu esitamine" + "Häälotsing" + "Jagamine:" + "Jagamine kasutajaga %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/strings.xml deleted file mode 100644 index ee6ac4d2fad..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Eginda" - "Joan orri nagusira" - "Joan gora" - "Aukera gehiago" - "Bilatu" - "Bilaketa-kontsulta" - "Garbitu kontsulta" - "Bidali kontsulta" - "Ahots bidezko bilaketa" - "Aukeratu aplikazio bat" - "Ikusi guztiak" - "Partekatu %s erabiltzailearekin" - "Partekatu hauekin" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/values-eu-rES.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/values-eu-rES.xml new file mode 100755 index 00000000000..e09765764e6 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-eu-rES/values-eu-rES.xml @@ -0,0 +1,18 @@ + + + + + "Joan orri nagusira" + "Joan gora" + "Aukera gehiago" + "Eginda" + "Ikusi guztiak" + "Aukeratu aplikazio bat" + "Garbitu kontsulta" + "Bilaketa-kontsulta" + "Bilatu" + "Bidali kontsulta" + "Ahots bidezko bilaketa" + "Partekatu hauekin" + "Partekatu %s erabiltzailearekin" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fa/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fa/strings.xml deleted file mode 100644 index 8e10e92b1ba..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-fa/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "انجام شد" - "پیمایش به صفحه اصلی" - "پیمایش به بالا" - "گزینه‌های بیشتر" - "جستجو" - "عبارت جستجو" - "پاک کردن عبارت جستجو" - "ارسال عبارت جستجو" - "جستجوی شفاهی" - "انتخاب برنامه" - "مشاهده همه" - "‏اشتراک‌گذاری با %s" - "اشتراک‌گذاری با" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fa/values-fa.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fa/values-fa.xml new file mode 100755 index 00000000000..1bd735485f0 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-fa/values-fa.xml @@ -0,0 +1,18 @@ + + + + + "پیمایش به صفحه اصلی" + "پیمایش به بالا" + "گزینه‌های بیشتر" + "انجام شد" + "مشاهده همه" + "انتخاب برنامه" + "پاک کردن عبارت جستجو" + "عبارت جستجو" + "جستجو" + "ارسال عبارت جستجو" + "جستجوی شفاهی" + "اشتراک‌گذاری با" + "‏اشتراک‌گذاری با %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fi/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fi/strings.xml deleted file mode 100644 index 6755ceaac2e..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-fi/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Valmis" - "Siirry etusivulle" - "Siirry ylös" - "Lisää" - "Haku" - "Hakulauseke" - "Tyhjennä kysely" - "Lähetä kysely" - "Puhehaku" - "Valitse sovellus" - "Näytä kaikki" - "Jakaminen: %s" - "Jakaminen:" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fi/values-fi.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fi/values-fi.xml new file mode 100755 index 00000000000..28ecbb9c335 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-fi/values-fi.xml @@ -0,0 +1,18 @@ + + + + + "Siirry etusivulle" + "Siirry ylös" + "Lisää" + "Valmis" + "Näytä kaikki" + "Valitse sovellus" + "Tyhjennä kysely" + "Hakulauseke" + "Haku" + "Lähetä kysely" + "Puhehaku" + "Jakaminen:" + "Jakaminen: %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/strings.xml deleted file mode 100644 index 417705a74fc..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Terminé" - "Revenir à l\'accueil" - "Revenir en haut de la page" - "Plus d\'options" - "Rechercher" - "Requête de recherche" - "Effacer la requête" - "Envoyer la requête" - "Recherche vocale" - "Sélectionnez une application" - "Voir toutes les chaînes" - "Partager avec %s" - "Partager avec" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/values-fr-rCA.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/values-fr-rCA.xml new file mode 100755 index 00000000000..703a48a9249 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-fr-rCA/values-fr-rCA.xml @@ -0,0 +1,18 @@ + + + + + "Revenir à l\'accueil" + "Revenir en haut de la page" + "Plus d\'options" + "Terminé" + "Voir toutes les chaînes" + "Sélectionnez une application" + "Effacer la requête" + "Requête de recherche" + "Rechercher" + "Envoyer la requête" + "Recherche vocale" + "Partager avec" + "Partager avec %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fr/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fr/strings.xml deleted file mode 100644 index 27b8f389ac4..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-fr/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "OK" - "Revenir à l\'accueil" - "Revenir en haut de la page" - "Plus d\'options" - "Rechercher" - "Requête de recherche" - "Effacer la requête" - "Envoyer la requête" - "Recherche vocale" - "Sélectionner une application" - "Tout afficher" - "Partager avec %s" - "Partager avec" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-fr/values-fr.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-fr/values-fr.xml new file mode 100755 index 00000000000..ee73b76ceb7 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-fr/values-fr.xml @@ -0,0 +1,18 @@ + + + + + "Revenir à l\'accueil" + "Revenir en haut de la page" + "Plus d\'options" + "OK" + "Tout afficher" + "Sélectionner une application" + "Effacer la requête" + "Requête de recherche" + "Rechercher" + "Envoyer la requête" + "Recherche vocale" + "Partager avec" + "Partager avec %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/strings.xml deleted file mode 100644 index 1d0d50de37e..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Feito" - "Ir á páxina de inicio" - "Desprazarse cara arriba" - "Máis opcións" - "Buscar" - "Consulta de busca" - "Borrar consulta" - "Enviar consulta" - "Busca de voz" - "Escoller unha aplicación" - "Ver todas" - "Compartir con %s" - "Compartir con" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/values-gl-rES.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/values-gl-rES.xml new file mode 100755 index 00000000000..bd0a3ff757f --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-gl-rES/values-gl-rES.xml @@ -0,0 +1,18 @@ + + + + + "Ir á páxina de inicio" + "Desprazarse cara arriba" + "Máis opcións" + "Feito" + "Ver todas" + "Escoller unha aplicación" + "Borrar consulta" + "Consulta de busca" + "Buscar" + "Enviar consulta" + "Busca de voz" + "Compartir con" + "Compartir con %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/dimens.xml deleted file mode 100644 index 09c43f0ef4a..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/dimens.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - 54dip - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/values-h720dp.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/values-h720dp.xml new file mode 100755 index 00000000000..7d80f6bc772 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-h720dp/values-h720dp.xml @@ -0,0 +1,6 @@ + + + + + 54dip + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/styles_base.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/styles_base.xml deleted file mode 100644 index 442ea292a00..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/styles_base.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/values-hdpi.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/values-hdpi.xml new file mode 100755 index 00000000000..d9db7afd29c --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-hdpi/values-hdpi.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hi/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hi/strings.xml deleted file mode 100644 index b236ebb5216..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-hi/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "पूर्ण" - "मुखपृष्ठ पर नेविगेट करें" - "ऊपर नेविगेट करें" - "अधिक विकल्प" - "खोजें" - "खोज क्वेरी" - "क्‍वेरी साफ़ करें" - "क्वेरी सबमिट करें" - "ध्वनि खोज" - "कोई एप्‍लिकेशन चुनें" - "सभी देखें" - "%s के साथ साझा करें" - "इसके द्वारा साझा करें" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hi/values-hi.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hi/values-hi.xml new file mode 100755 index 00000000000..5cee91f94a0 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-hi/values-hi.xml @@ -0,0 +1,18 @@ + + + + + "मुखपृष्ठ पर नेविगेट करें" + "ऊपर नेविगेट करें" + "अधिक विकल्प" + "पूर्ण" + "सभी देखें" + "कोई एप्‍लिकेशन चुनें" + "क्‍वेरी साफ़ करें" + "खोज क्वेरी" + "खोजें" + "क्वेरी सबमिट करें" + "ध्वनि खोज" + "इसके द्वारा साझा करें" + "%s के साथ साझा करें" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hr/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hr/strings.xml deleted file mode 100644 index 680e39f9753..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-hr/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Gotovo" - "Idi na početnu" - "Idi gore" - "Dodatne opcije" - "Pretraživanje" - "Upit za pretraživanje" - "Izbriši upit" - "Pošalji upit" - "Glasovno pretraživanje" - "Odabir aplikacije" - "Prikaži sve" - "Dijeljenje sa: %s" - "Dijeljenje sa" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hr/values-hr.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hr/values-hr.xml new file mode 100755 index 00000000000..cfbad3e8b33 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-hr/values-hr.xml @@ -0,0 +1,18 @@ + + + + + "Idi na početnu" + "Idi gore" + "Dodatne opcije" + "Gotovo" + "Prikaži sve" + "Odabir aplikacije" + "Izbriši upit" + "Upit za pretraživanje" + "Pretraživanje" + "Pošalji upit" + "Glasovno pretraživanje" + "Dijeljenje sa" + "Dijeljenje sa: %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hu/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hu/strings.xml deleted file mode 100644 index 52dafb0e9f3..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-hu/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Kész" - "Ugrás a főoldalra" - "Felfelé mozgatás" - "További lehetőségek" - "Keresés" - "Keresési lekérdezés" - "Lekérdezés törlése" - "Lekérdezés küldése" - "Hangalapú keresés" - "Válasszon ki egy alkalmazást" - "Összes megtekintése" - "Megosztás a következővel: %s" - "Megosztás a következővel:" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hu/values-hu.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hu/values-hu.xml new file mode 100755 index 00000000000..41362066e54 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-hu/values-hu.xml @@ -0,0 +1,18 @@ + + + + + "Ugrás a főoldalra" + "Felfelé mozgatás" + "További lehetőségek" + "Kész" + "Összes megtekintése" + "Válasszon ki egy alkalmazást" + "Lekérdezés törlése" + "Keresési lekérdezés" + "Keresés" + "Lekérdezés küldése" + "Hangalapú keresés" + "Megosztás a következővel:" + "Megosztás a következővel: %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/strings.xml deleted file mode 100644 index 6c0ee27cc5e..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Կատարված է" - "Ուղղվել տուն" - "Ուղղվել վերև" - "Այլ ընտրանքներ" - "Որոնել" - "Որոնման հարցում" - "Մաքրել հարցումը" - "Ուղարկել հարցումը" - "Ձայնային որոնում" - "Ընտրել ծրագիր" - "Տեսնել բոլորը" - "Տարածել ըստ %s" - "Տարածել" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/values-hy-rAM.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/values-hy-rAM.xml new file mode 100755 index 00000000000..d7b69d12c9b --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-hy-rAM/values-hy-rAM.xml @@ -0,0 +1,18 @@ + + + + + "Ուղղվել տուն" + "Ուղղվել վերև" + "Այլ ընտրանքներ" + "Կատարված է" + "Տեսնել բոլորը" + "Ընտրել ծրագիր" + "Մաքրել հարցումը" + "Որոնման հարցում" + "Որոնել" + "Ուղարկել հարցումը" + "Ձայնային որոնում" + "Տարածել" + "Տարածել ըստ %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-in/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-in/strings.xml deleted file mode 100644 index 9481e83635c..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-in/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Selesai" - "Navigasi ke beranda" - "Navigasi naik" - "Opsi lain" - "Telusuri" - "Kueri penelusuran" - "Hapus kueri" - "Kirim kueri" - "Penelusuran suara" - "Pilih aplikasi" - "Lihat semua" - "Bagikan dengan %s" - "Bagikan dengan" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-in/values-in.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-in/values-in.xml new file mode 100755 index 00000000000..a78f16e6956 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-in/values-in.xml @@ -0,0 +1,18 @@ + + + + + "Navigasi ke beranda" + "Navigasi naik" + "Opsi lain" + "Selesai" + "Lihat semua" + "Pilih aplikasi" + "Hapus kueri" + "Kueri penelusuran" + "Telusuri" + "Kirim kueri" + "Penelusuran suara" + "Bagikan dengan" + "Bagikan dengan %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/strings.xml deleted file mode 100644 index 8cac57028b3..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Lokið" - "Fara heim" - "Fara upp" - "Fleiri valkostir" - "Leita" - "Leitarfyrirspurn" - "Hreinsa fyrirspurn" - "Senda fyrirspurn" - "Raddleit" - "Veldu forrit" - "Sjá allt" - "Deila með %s" - "Deila með" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/values-is-rIS.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/values-is-rIS.xml new file mode 100755 index 00000000000..10416a84ca9 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-is-rIS/values-is-rIS.xml @@ -0,0 +1,18 @@ + + + + + "Fara heim" + "Fara upp" + "Fleiri valkostir" + "Lokið" + "Sjá allt" + "Veldu forrit" + "Hreinsa fyrirspurn" + "Leitarfyrirspurn" + "Leita" + "Senda fyrirspurn" + "Raddleit" + "Deila með" + "Deila með %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-it/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-it/strings.xml deleted file mode 100644 index a8b0f2c1625..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-it/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Fine" - "Vai alla home page" - "Vai in alto" - "Altre opzioni" - "Cerca" - "Query di ricerca" - "Cancella query" - "Invia query" - "Ricerca vocale" - "Scegli un\'applicazione" - "Visualizza tutte" - "Condividi con %s" - "Condividi con" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-it/values-it.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-it/values-it.xml new file mode 100755 index 00000000000..e12fdb0e6b6 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-it/values-it.xml @@ -0,0 +1,18 @@ + + + + + "Vai alla home page" + "Vai in alto" + "Altre opzioni" + "Fine" + "Visualizza tutte" + "Scegli un\'applicazione" + "Cancella query" + "Query di ricerca" + "Cerca" + "Invia query" + "Ricerca vocale" + "Condividi con" + "Condividi con %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-iw/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-iw/strings.xml deleted file mode 100644 index 1af07df88e6..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-iw/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "בוצע" - "נווט לדף הבית" - "נווט למעלה" - "עוד אפשרויות" - "חפש" - "שאילתת חיפוש" - "מחק שאילתה" - "שלח שאילתה" - "חיפוש קולי" - "בחר אפליקציה" - "ראה הכול" - "‏שתף עם %s" - "שתף עם" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-iw/values-iw.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-iw/values-iw.xml new file mode 100755 index 00000000000..db237295ae8 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-iw/values-iw.xml @@ -0,0 +1,18 @@ + + + + + "נווט לדף הבית" + "נווט למעלה" + "עוד אפשרויות" + "בוצע" + "ראה הכול" + "בחר אפליקציה" + "מחק שאילתה" + "שאילתת חיפוש" + "חפש" + "שלח שאילתה" + "חיפוש קולי" + "שתף עם" + "‏שתף עם %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ja/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ja/strings.xml deleted file mode 100644 index 659358a74c6..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-ja/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "完了" - "ホームへ移動" - "上へ移動" - "その他のオプション" - "検索" - "検索キーワード" - "検索キーワードを削除" - "検索キーワードを送信" - "音声検索" - "アプリの選択" - "すべて表示" - "%sと共有" - "共有" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ja/values-ja.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ja/values-ja.xml new file mode 100755 index 00000000000..167cc1245e1 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-ja/values-ja.xml @@ -0,0 +1,18 @@ + + + + + "ホームへ移動" + "上へ移動" + "その他のオプション" + "完了" + "すべて表示" + "アプリの選択" + "検索キーワードを削除" + "検索キーワード" + "検索" + "検索キーワードを送信" + "音声検索" + "共有" + "%sと共有" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/strings.xml deleted file mode 100644 index 0c430b1c440..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "დასრულდა" - "მთავარზე ნავიგაცია" - "ზემოთ ნავიგაცია" - "მეტი ვარიანტები" - "ძიება" - "ძიების მოთხოვნა" - "მოთხოვნის გასუფთავება" - "მოთხოვნის გადაგზავნა" - "ხმოვანი ძიება" - "აპის არჩევა" - "ყველას ნახვა" - "%s-თან გაზიარება" - "გაზიარება:" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/values-ka-rGE.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/values-ka-rGE.xml new file mode 100755 index 00000000000..5b2be7f1700 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-ka-rGE/values-ka-rGE.xml @@ -0,0 +1,18 @@ + + + + + "მთავარზე ნავიგაცია" + "ზემოთ ნავიგაცია" + "მეტი ვარიანტები" + "დასრულდა" + "ყველას ნახვა" + "აპის არჩევა" + "მოთხოვნის გასუფთავება" + "ძიების მოთხოვნა" + "ძიება" + "მოთხოვნის გადაგზავნა" + "ხმოვანი ძიება" + "გაზიარება:" + "%s-თან გაზიარება" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/strings.xml deleted file mode 100644 index d3ad9e8753c..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Орындалды" - "Негізгі бетте қозғалу" - "Жоғары қозғалу" - "Басқа опциялар" - "Іздеу" - "Сұрақты іздеу" - "Сұрақты жою" - "Сұрақты жіберу" - "Дауыс арқылы іздеу" - "Қолданбаны таңдау" - "Барлығын көру" - "%s бөлісу" - "Бөлісу" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/values-kk-rKZ.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/values-kk-rKZ.xml new file mode 100755 index 00000000000..ca71cffaef7 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-kk-rKZ/values-kk-rKZ.xml @@ -0,0 +1,18 @@ + + + + + "Негізгі бетте қозғалу" + "Жоғары қозғалу" + "Басқа опциялар" + "Орындалды" + "Барлығын көру" + "Қолданбаны таңдау" + "Сұрақты жою" + "Сұрақты іздеу" + "Іздеу" + "Сұрақты жіберу" + "Дауыс арқылы іздеу" + "Бөлісу" + "%s бөлісу" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/strings.xml deleted file mode 100644 index df783725896..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "រួចរាល់" - "រកមើល​ទៅ​ដើម" - "រកមើល​ឡើងលើ" - "ជម្រើស​ច្រើន​ទៀត" - "ស្វែងរក" - "ស្វែងរក​សំណួរ" - "សម្អាត​សំណួរ" - "ដាក់​​​ស្នើ​សំណួរ" - "ការស្វែងរក​សំឡេង" - "ជ្រើស​កម្មវិធី​​" - "មើល​ទាំងអស់" - "ចែករំលែក​ជាមួយ %s" - "ចែករំលែក​ជាមួយ" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/values-km-rKH.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/values-km-rKH.xml new file mode 100755 index 00000000000..88d4282802f --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-km-rKH/values-km-rKH.xml @@ -0,0 +1,18 @@ + + + + + "រកមើល​ទៅ​ដើម" + "រកមើល​ឡើងលើ" + "ជម្រើស​ច្រើន​ទៀត" + "រួចរាល់" + "មើល​ទាំងអស់" + "ជ្រើស​កម្មវិធី​​" + "សម្អាត​សំណួរ" + "ស្វែងរក​សំណួរ" + "ស្វែងរក" + "ដាក់​​​ស្នើ​សំណួរ" + "ការស្វែងរក​សំឡេង" + "ចែករំលែក​ជាមួយ" + "ចែករំលែក​ជាមួយ %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/strings.xml deleted file mode 100644 index 6af7a394cfe..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "ಮುಗಿದಿದೆ" - "ಮುಖಪುಟವನ್ನು ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ" - "ಮೇಲಕ್ಕೆ ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ" - "ಇನ್ನಷ್ಟು ಆಯ್ಕೆಗಳು" - "ಹುಡುಕು" - "ಪ್ರಶ್ನೆಯನ್ನು ಹುಡುಕಿ" - "ಪ್ರಶ್ನೆಯನ್ನು ತೆರವುಗೊಳಿಸು" - "ಪ್ರಶ್ನೆಯನ್ನು ಸಲ್ಲಿಸು" - "ಧ್ವನಿ ಹುಡುಕಾಟ" - "ಒಂದು ಅಪ್ಲಿಕೇಶನ್ ಆಯ್ಕೆಮಾಡಿ" - "ಎಲ್ಲವನ್ನೂ ನೋಡಿ" - "%s ಜೊತೆಗೆ ಹಂಚಿಕೊಳ್ಳಿ" - "ಇವರೊಂದಿಗೆ ಹಂಚಿಕೊಳ್ಳಿ" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/values-kn-rIN.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/values-kn-rIN.xml new file mode 100755 index 00000000000..cbb7321dea6 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-kn-rIN/values-kn-rIN.xml @@ -0,0 +1,18 @@ + + + + + "ಮುಖಪುಟವನ್ನು ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ" + "ಮೇಲಕ್ಕೆ ನ್ಯಾವಿಗೇಟ್ ಮಾಡಿ" + "ಇನ್ನಷ್ಟು ಆಯ್ಕೆಗಳು" + "ಮುಗಿದಿದೆ" + "ಎಲ್ಲವನ್ನೂ ನೋಡಿ" + "ಒಂದು ಅಪ್ಲಿಕೇಶನ್ ಆಯ್ಕೆಮಾಡಿ" + "ಪ್ರಶ್ನೆಯನ್ನು ತೆರವುಗೊಳಿಸು" + "ಪ್ರಶ್ನೆಯನ್ನು ಹುಡುಕಿ" + "ಹುಡುಕು" + "ಪ್ರಶ್ನೆಯನ್ನು ಸಲ್ಲಿಸು" + "ಧ್ವನಿ ಹುಡುಕಾಟ" + "ಇವರೊಂದಿಗೆ ಹಂಚಿಕೊಳ್ಳಿ" + "%s ಜೊತೆಗೆ ಹಂಚಿಕೊಳ್ಳಿ" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ko/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ko/strings.xml deleted file mode 100644 index d3319758474..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-ko/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "완료" - "홈 탐색" - "위로 탐색" - "옵션 더보기" - "검색" - "검색어" - "검색어 삭제" - "검색어 보내기" - "음성 검색" - "앱 선택" - "전체 보기" - "%s와(과) 공유" - "공유 대상" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ko/values-ko.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ko/values-ko.xml new file mode 100755 index 00000000000..01665e92043 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-ko/values-ko.xml @@ -0,0 +1,18 @@ + + + + + "홈 탐색" + "위로 탐색" + "옵션 더보기" + "완료" + "전체 보기" + "앱 선택" + "검색어 삭제" + "검색어" + "검색" + "검색어 보내기" + "음성 검색" + "공유 대상" + "%s와(과) 공유" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/strings.xml deleted file mode 100644 index 52abd9f0d80..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Даяр" - "Үйгө багыттоо" - "Жогору" - "Көбүрөөк мүмкүнчүлүктөр" - "Издөө" - "Издөө талаптары" - "Талаптарды тазалоо" - "Талап жөнөтүү" - "Үн аркылуу издөө" - "Колдонмо тандоо" - "Бардыгын көрүү" - "%s аркылуу бөлүшүү" - "Бөлүшүү" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/values-ky-rKG.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/values-ky-rKG.xml new file mode 100755 index 00000000000..c3e1291b2f8 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-ky-rKG/values-ky-rKG.xml @@ -0,0 +1,18 @@ + + + + + "Үйгө багыттоо" + "Жогору" + "Көбүрөөк мүмкүнчүлүктөр" + "Даяр" + "Бардыгын көрүү" + "Колдонмо тандоо" + "Талаптарды тазалоо" + "Издөө талаптары" + "Издөө" + "Талап жөнөтүү" + "Үн аркылуу издөө" + "Бөлүшүү" + "%s аркылуу бөлүшүү" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-land/bools.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-land/bools.xml deleted file mode 100644 index 7d1a1af3f3a..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-land/bools.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - true - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-land/config.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-land/config.xml deleted file mode 100644 index d0d990dc0b7..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-land/config.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - true - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens_material.xml deleted file mode 100644 index 08c4b0d7292..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-land/dimens_material.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - 48dp - - 0dp - - 14dp - - 12dp - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-land/values-land.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-land/values-land.xml new file mode 100755 index 00000000000..75a944b1607 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-land/values-land.xml @@ -0,0 +1,20 @@ + + + + + true + + + true + + + 48dp + 0dp + + + 32dp + + + 12dp + 14dp + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-large/bools.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-large/bools.xml deleted file mode 100644 index 7d1a1af3f3a..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-large/bools.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - true - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-large/config.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-large/config.xml deleted file mode 100644 index c4f04a300ea..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-large/config.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - true - - - 440dp - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-large/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-large/dimens.xml deleted file mode 100644 index de1cefcccee..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-large/dimens.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - 192dip - - 4 - - - 60% - - 90% - - 60% - - 90% - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-large/themes_base.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-large/themes_base.xml deleted file mode 100644 index aafef5fbd53..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-large/themes_base.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v11/styles_base_text.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v11/styles_base_text.xml deleted file mode 100644 index 4cf49666c3c..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-v11/styles_base_text.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v11/themes_base.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v11/values-v11.xml old mode 100644 new mode 100755 similarity index 67% rename from mobile/google-android-support-v7-appcompat_lib/res/values-v11/themes_base.xml rename to mobile/google-android-support-v7-appcompat_lib/res/values-v11/values-v11.xml index 2ca5b6fb901..734fb2f0f69 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-v11/themes_base.xml +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-v11/values-v11.xml @@ -1,31 +1,72 @@ - - - - + - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v12/values-v12.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v12/values-v12.xml new file mode 100755 index 00000000000..6e5ba69bde9 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-v12/values-v12.xml @@ -0,0 +1,13 @@ + + + + + + + - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v14/themes_base.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v14/themes_base.xml deleted file mode 100644 index 5fdc73c72b7..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-v14/themes_base.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v14/values-v14.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v14/values-v14.xml new file mode 100755 index 00000000000..8433f447449 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-v14/values-v14.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + - - - - - - - - - - - - - + - - - + + + + + + - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v18/values-v18.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v18/values-v18.xml new file mode 100755 index 00000000000..79ff579bebf --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-v18/values-v18.xml @@ -0,0 +1,6 @@ + + + + + 0px + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v21/styles_base.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v21/styles_base.xml deleted file mode 100644 index 1bf90ef2533..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-v21/styles_base.xml +++ /dev/null @@ -1,207 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-v21/values-v21.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-v21/values-v21.xml new file mode 100755 index 00000000000..438d393b4a6 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-v21/values-v21.xml @@ -0,0 +1,278 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-vi/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-vi/strings.xml deleted file mode 100644 index 0840f7391fd..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-vi/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Xong" - "Điều hướng về trang chủ" - "Điều hướng lên trên" - "Thêm tùy chọn" - "Tìm kiếm" - "Tìm kiếm truy vấn" - "Xóa truy vấn" - "Gửi truy vấn" - "Tìm kiếm bằng giọng nói" - "Chọn một ứng dụng" - "Xem tất cả" - "Chia sẻ với %s" - "Chia sẻ với" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-vi/values-vi.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-vi/values-vi.xml new file mode 100755 index 00000000000..8fd629bb077 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-vi/values-vi.xml @@ -0,0 +1,18 @@ + + + + + "Điều hướng về trang chủ" + "Điều hướng lên trên" + "Thêm tùy chọn" + "Xong" + "Xem tất cả" + "Chọn một ứng dụng" + "Xóa truy vấn" + "Tìm kiếm truy vấn" + "Tìm kiếm" + "Gửi truy vấn" + "Tìm kiếm bằng giọng nói" + "Chia sẻ với" + "Chia sẻ với %s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/dimens.xml deleted file mode 100644 index e5b2456c8e6..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/dimens.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - 3 - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/values-w360dp.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/values-w360dp.xml new file mode 100755 index 00000000000..2c8ee7dfc7c --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-w360dp/values-w360dp.xml @@ -0,0 +1,6 @@ + + + + + 3 + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/bools.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/bools.xml deleted file mode 100644 index 470f89be359..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/bools.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - true - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/config.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/config.xml deleted file mode 100644 index e95b6ff8c34..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/config.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - true - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/values-w480dp.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/values-w480dp.xml new file mode 100755 index 00000000000..060127bcbef --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-w480dp/values-w480dp.xml @@ -0,0 +1,9 @@ + + + + + true + + + true + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/dimens.xml deleted file mode 100644 index dd6458be71f..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/dimens.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - 4 - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/values-w500dp.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/values-w500dp.xml new file mode 100755 index 00000000000..67b3b2ffe1b --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-w500dp/values-w500dp.xml @@ -0,0 +1,6 @@ + + + + + 4 + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/dimens.xml deleted file mode 100644 index 252ba6a7b74..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/dimens.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - 5 - - - 192dip - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/values-w600dp.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/values-w600dp.xml new file mode 100755 index 00000000000..aef006da141 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-w600dp/values-w600dp.xml @@ -0,0 +1,7 @@ + + + + + 192dip + 5 + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/bools.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/bools.xml deleted file mode 100644 index 05c5aabffff..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/bools.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - false - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/values-w720dp.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/values-w720dp.xml new file mode 100755 index 00000000000..66422e8a5d4 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-w720dp/values-w720dp.xml @@ -0,0 +1,6 @@ + + + + + false + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/dimens.xml deleted file mode 100644 index dea6c74dadd..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/dimens.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - 256dip - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/values-xlarge-land.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/values-xlarge-land.xml new file mode 100755 index 00000000000..50415154f21 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge-land/values-xlarge-land.xml @@ -0,0 +1,6 @@ + + + + + 256dip + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/bools.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/bools.xml deleted file mode 100644 index 05c5aabffff..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/bools.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - false - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/dimens.xml deleted file mode 100644 index 3eb29620ba9..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/dimens.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - 5 - - - 192dip - - - 50% - - 70% - - 60% - - 90% - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/values-xlarge.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/values-xlarge.xml new file mode 100755 index 00000000000..35ed941e1d9 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-xlarge/values-xlarge.xml @@ -0,0 +1,14 @@ + + + + + false + + + 192dip + 60% + 90% + 50% + 70% + 5 + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/strings.xml deleted file mode 100644 index 87b36b0be99..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "完成" - "转到主屏幕" - "转到上一层级" - "更多选项" - "搜索" - "搜索查询" - "清除查询" - "提交查询" - "语音搜索" - "选择应用" - "查看全部" - "通过%s分享" - "分享方式" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/values-zh-rCN.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/values-zh-rCN.xml new file mode 100755 index 00000000000..495f124d06c --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rCN/values-zh-rCN.xml @@ -0,0 +1,18 @@ + + + + + "转到主屏幕" + "转到上一层级" + "更多选项" + "完成" + "查看全部" + "选择应用" + "清除查询" + "搜索查询" + "搜索" + "提交查询" + "语音搜索" + "分享方式" + "通过%s分享" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/strings.xml deleted file mode 100644 index f6a367d3a05..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "完成" - "瀏覽主頁" - "向上瀏覽" - "更多選項" - "搜尋" - "搜尋查詢" - "清除查詢" - "提交查詢" - "語音搜尋" - "選擇應用程式" - "顯示全部" - "與「%s」分享" - "分享對象" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/values-zh-rHK.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/values-zh-rHK.xml new file mode 100755 index 00000000000..23ab4bed681 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rHK/values-zh-rHK.xml @@ -0,0 +1,18 @@ + + + + + "瀏覽主頁" + "向上瀏覽" + "更多選項" + "完成" + "顯示全部" + "選擇應用程式" + "清除查詢" + "搜尋查詢" + "搜尋" + "提交查詢" + "語音搜尋" + "分享對象" + "與「%s」分享" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/strings.xml deleted file mode 100644 index c804ccf3a97..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "完成" - "瀏覽首頁" - "向上瀏覽" - "更多選項" - "搜尋" - "搜尋查詢" - "清除查詢" - "提交查詢" - "語音搜尋" - "選擇應用程式" - "查看全部" - "與「%s」分享" - "選擇分享對象" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/values-zh-rTW.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/values-zh-rTW.xml new file mode 100755 index 00000000000..8e253eb6d9c --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-zh-rTW/values-zh-rTW.xml @@ -0,0 +1,18 @@ + + + + + "瀏覽首頁" + "向上瀏覽" + "更多選項" + "完成" + "查看全部" + "選擇應用程式" + "清除查詢" + "搜尋查詢" + "搜尋" + "提交查詢" + "語音搜尋" + "選擇分享對象" + "與「%s」分享" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zu/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zu/strings.xml deleted file mode 100644 index 92eac7ecaf9..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values-zu/strings.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - "Kwenziwe" - "Zulazulela ekhaya" - "Zulazulela phezulu" - "Izinketho eziningi" - "Sesha" - "Umbuzo wosesho" - "Sula inkinga" - "Hambisa umbuzo" - "Ukusesha ngezwi" - "Khetha uhlelo lokusebenza" - "Buka konke" - "Yabelana no-%s" - "Yabelana no-" - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values-zu/values-zu.xml b/mobile/google-android-support-v7-appcompat_lib/res/values-zu/values-zu.xml new file mode 100755 index 00000000000..3c8273b5c5c --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values-zu/values-zu.xml @@ -0,0 +1,18 @@ + + + + + "Zulazulela ekhaya" + "Zulazulela phezulu" + "Izinketho eziningi" + "Kwenziwe" + "Buka konke" + "Khetha uhlelo lokusebenza" + "Sula inkinga" + "Umbuzo wosesho" + "Sesha" + "Hambisa umbuzo" + "Ukusesha ngezwi" + "Yabelana no-" + "Yabelana no-%s" + \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/attrs.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/attrs.xml deleted file mode 100644 index 693da4c4ced..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/attrs.xml +++ /dev/null @@ -1,927 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/bools.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/bools.xml deleted file mode 100644 index 79a5035ad9a..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/bools.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - true - false - true - - false - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/colors.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/colors.xml deleted file mode 100644 index 32142019f09..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/colors.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - #7fa87f - @android:color/black - @android:color/black - - @android:color/black - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/colors_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/colors_material.xml deleted file mode 100644 index 6b3cca5d484..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/colors_material.xml +++ /dev/null @@ -1,99 +0,0 @@ - - - - - - #ff303030 - #ffeeeeee - #ff424242 - #ffeeeeee - - #ff212121 - #ffefefef - #ff000000 - #ff757575 - - #4dffffff - #1f000000 - - @color/material_deep_teal_500 - @color/material_deep_teal_200 - - #ff5a595b - #ffd6d7d7 - - #ffbdbdbd - #fff1f1f1 - #ff616161 - #ffbdbdbd - - @android:color/white - @android:color/black - - #80ffffff - - #80000000 - @color/bright_foreground_material_light - @color/bright_foreground_material_dark - - #ffbebebe - #ff323232 - #80bebebe - #80323232 - - @color/bright_foreground_disabled_material_dark - @color/bright_foreground_disabled_material_light - - - #6680cbc4 - - #66009688 - - @color/material_deep_teal_200 - @color/material_deep_teal_500 - - - - - #de000000 - #8a000000 - - #ffffffff - #b3ffffff - - 0.26 - 0.30 - - - #39000000 - #24000000 - - - #4Dffffff - #36ffffff - - - - - #ff80cbc4 - #ff009688 - - #ff37474f - #ff263238 - #ff21272b - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/config.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/config.xml deleted file mode 100644 index be6a7a1a120..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/config.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - false - - - 320dp - - - true - - - 150 - 220 - - true - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/dimens.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/dimens.xml deleted file mode 100644 index b8fa76c8e5c..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/dimens.xml +++ /dev/null @@ -1,100 +0,0 @@ - - - - - - - 2 - - - 180dp - - - 48dp - - 40dp - - 296dp - - - 160dip - - 320dip - - - 8dip - 8dip - - 32dip - - - - 320dp - - 320dp - - 80% - - 100% - - 6dp - @dimen/abc_control_inset_material - - @dimen/abc_control_padding_material - 8dp - - - 4dp - - 4dp - - 2dp - - 4dp - 10dp - 7dp - - - 3dp - - 24dp - 18dp - - - 48dp - - - 8dp - - - 65% - - - 95% - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/dimens_material.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/dimens_material.xml deleted file mode 100644 index 2d0f738133e..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/dimens_material.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - 56dp - - 4dp - - 16dp - - 16dp - - -3dp - - 5dp - - - @dimen/abc_action_bar_content_inset_material - - - 0dp - - 6dp - - 10dp - - 36dp - 48dp - 48dp - - 112sp - 56sp - 45sp - 34sp - 24sp - 20sp - 16sp - 20dp - 16dp - 16sp - 14sp - 14sp - 12sp - 14sp - - 22sp - 18sp - 14sp - - 16dp - - 0.26 - 0.30 - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/ids.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/ids.xml deleted file mode 100644 index 2e6ef2497eb..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/ids.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/strings.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/strings.xml deleted file mode 100644 index 87cfd29cf96..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/strings.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - Done - - Navigate home - - Navigate up - - More options - - - Collapse - - - %1$s, %2$s - - %1$s, %2$s, %3$s - - - Search - - Search… - - Search query - - Clear query - - Submit query - - Voice search - - - - Choose an app - - See all - - Share with %s - - Share with - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/styles.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/styles.xml deleted file mode 100644 index 1f30314c0ff..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/styles.xml +++ /dev/null @@ -1,324 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/styles_rtl.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/styles_rtl.xml deleted file mode 100644 index 53208008a52..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/styles_rtl.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/themes.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/themes.xml deleted file mode 100644 index f8962df6b47..00000000000 --- a/mobile/google-android-support-v7-appcompat_lib/res/values/themes.xml +++ /dev/null @@ -1,95 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/google-android-support-v7-appcompat_lib/res/values/values.xml b/mobile/google-android-support-v7-appcompat_lib/res/values/values.xml new file mode 100755 index 00000000000..54cf69519d2 --- /dev/null +++ b/mobile/google-android-support-v7-appcompat_lib/res/values/values.xml @@ -0,0 +1,1517 @@ + + + + + + + + + + + true + false + true + + + true + false + true + + + false + + + @android:color/black + #7fa87f + @android:color/black + @android:color/black + + + @color/material_deep_teal_200 + @color/material_deep_teal_500 + #ff424242 + #ffeeeeee + #ff303030 + #ffeeeeee + #80ffffff + #80000000 + @color/bright_foreground_material_light + @color/bright_foreground_material_dark + @android:color/white + @android:color/black + #ff5a595b + #ffd6d7d7 + #80bebebe + #80323232 + #ffbebebe + #ff323232 + #6680cbc4 + #66009688 + @color/bright_foreground_disabled_material_dark + @color/bright_foreground_disabled_material_light + @color/material_deep_teal_200 + @color/material_deep_teal_500 + #ff37474f + #ff263238 + #ff21272b + #ff80cbc4 + #ff009688 + #ff000000 + #ff757575 + #ff212121 + #ffefefef + #ffffffff + #de000000 + #4Dffffff + #39000000 + #4dffffff + #1f000000 + #b3ffffff + #8a000000 + #36ffffff + #24000000 + #ff616161 + #ffbdbdbd + #ffbdbdbd + #fff1f1f1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + 16dp + 56dp + 4dp + 16dp + 0dp + 10dp + 6dp + + + 40dp + 48dp + 180dp + + + 5dp + -3dp + 48dp + 48dp + 36dp + + + 48dp + @dimen/abc_control_inset_material + 6dp + 8dp + @dimen/abc_control_padding_material + + + 320dp + + + 2dp + 4dp + 4dp + 8dp + 65% + 95% + 24dp + 18dp + + + 0.30 + 0.26 + + + 32dip + 8dip + 8dip + 7dp + 4dp + 10dp + + + 16dp + @dimen/abc_action_bar_content_inset_material + + + 296dp + 320dip + 160dip + 3dp + + + 14sp + 14sp + 14sp + 12sp + 34sp + 45sp + 56sp + 112sp + 24sp + 22sp + 18sp + 16sp + 14sp + 16sp + 16dp + 20sp + 20dp + + + 80% + 100% + 320dp + 320dp + + + 0.30 + 0.26 + + + 64dp + 64dp + 12dp + + + #3333B5E5 + + + + + + + + + + + + + + 220 + 150 + + + 2 + + + 127 + 999 + + + Navigate home + %1$s, %2$s + %1$s, %2$s, %3$s + Navigate up + More options + Done + See all + Choose an app + Search… + Clear query + Search query + Search + Submit query + Voice search + Share with + Share with %s + Collapse + 999+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +