1 /*
2  * Copyright 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.pdf.viewer;
18 
19 import android.annotation.SuppressLint;
20 import android.os.Bundle;
21 import android.view.LayoutInflater;
22 import android.view.View;
23 import android.view.ViewGroup;
24 
25 import androidx.annotation.CallSuper;
26 import androidx.annotation.RestrictTo;
27 import androidx.fragment.app.Fragment;
28 import androidx.pdf.data.DisplayData;
29 import androidx.pdf.util.ObservableValue;
30 import androidx.pdf.util.Observables;
31 import androidx.pdf.util.Observables.ExposedValue;
32 
33 import org.jspecify.annotations.NonNull;
34 import org.jspecify.annotations.Nullable;
35 
36 /**
37  * A widget that displays the contents of a file in a given PDF format.
38  *
39  * <p>This class introduces 2 new life-cycle callbacks:
40  *
41  * <ul>
42  *   <li>{@link #onEnter} will be called either when the user slides the film strip so that this
43  *       Viewer comes on-screen or during {@link #onStart()}) if it starts visible.
44  *   <li>{@link #onExit} is the reverse of {@link #onEnter} and will be called when the user slides
45  *       the film strip so that this Viewer goes off-screen or during {@link #onStop()}.
46  * </ul>
47  *
48  * <p>A Viewer also reports precisely on the status of its view hierarchy, since typically it takes
49  * time to load the relevant data and make it ready to be displayed: {@link #mViewState} reports 3
50  * states: {@link ViewState#NO_VIEW}, {@link ViewState#VIEW_CREATED} (the view skeleton has been
51  * created, and is empty) and {@link ViewState#VIEW_READY} (the data has been loaded into the view).
52  *
53  * <p>This class doesn't take care of any loading or saving of data - subclasses must handle this
54  * themselves - see {@link LoadingViewer} which handles some of this.
55  */
56 @RestrictTo(RestrictTo.Scope.LIBRARY)
57 @SuppressWarnings("deprecation")
58 public abstract class Viewer extends Fragment {
59 
60     protected static final String KEY_DATA = "data";
61 
62     /**
63      * The state of the view hierarchy for this {@link Fragment}, as exposed by {@link #mViewState}.
64      */
65     public enum ViewState {
66 
67         /**
68          * The view hierarchy does not exist yet or anymore (e.g. before {@link #onCreateView}).
69          */
70         NO_VIEW,
71 
72         /**
73          * The view hierarchy exists but may be showing no or partial contents.
74          * <p>
75          * The state as reported by {@link #mViewState} is guaranteed to change to this value after
76          * {@link #onCreateView} completes, and when {@link #getView} returns a non-null View.
77          */
78         VIEW_CREATED,
79 
80         /**
81          * The view hierarchy is ready for prime time: all Views are populated and responding.
82          * This is to be reported by subclasses, when that condition happens (but no sooner than
83          * {@link #onStart}).
84          * <p>
85          * This is unrelated to {@link #onStart}, {@link #onResume} or {@link #onEnter}, as the view
86          * could be ready but not currently showing because of other factors.
87          */
88         VIEW_READY,
89 
90         /**
91          * There is no view because this Viewer failed to start up (e.g. broken file).
92          */
93         ERROR
94     }
95 
96     /** True when this Fragment's life-cycle is between {@link #onStart} and {@link #onStop}. */
97     private boolean mStarted;
98 
99     /**
100      * True when this Viewer is on-screen (but independent on whether it is actually started, so it
101      * could be invisible, because obscured by another app).
102      * This value is controlled by {@link #postEnter} and {@link #exit}.
103      */
104     private boolean mOnScreen;
105 
106     /** Marks that {@link #onEnter} must be run after {@link #onCreateView}. */
107     private boolean mDelayedEnter;
108 
109     protected boolean mIsPasswordProtected;
110 
111     /** The container where this viewer is attached. */
112     protected @NonNull ViewGroup mContainer;
113 
114     protected @NonNull ExposedValue<ViewState> mViewState =
115             Observables.newExposedValueWithInitialValue(ViewState.NO_VIEW);
116 
117     {
118         // We can call getArguments() from setters and know that it will not be null.
setArguments(new Bundle())119         setArguments(new Bundle());
120     }
121 
122     /** Reports the {@link ViewState} of this Fragment. */
viewState()123     public @NonNull ObservableValue<ViewState> viewState() {
124         return mViewState;
125     }
126 
127     /**
128      * Configures whether this viewer has to share scroll gestures in any direction with its
129      * container or any neighbouring view.
130      * <p>
131      * This call is only permitted when the viewer has a view, i.e. {@link #mViewState} reports at
132      * least {@link ViewState#VIEW_CREATED}.
133      *
134      * @param left   If true, will pass on scroll gestures that extend beyond the left bound.
135      * @param right  If true, will pass on scroll gestures that extend beyond the right bound.
136      * @param top    If true, will pass on scroll gestures that extend beyond the top bound.
137      * @param bottom If true, will pass on scroll gestures that extend beyond the bottom bound.
138      */
configureShareScroll(boolean left, boolean right, boolean top, boolean bottom)139     public void configureShareScroll(boolean left, boolean right, boolean top, boolean bottom) {
140         // Nothing by default.
141     }
142 
143     @Override
onCreate(@ullable Bundle savedInstanceState)144     public void onCreate(@Nullable Bundle savedInstanceState) {
145         super.onCreate(savedInstanceState);
146 
147         // editFabTarget = new BaseViewerEditFabTargetImpl(requireActivity(), this);
148     }
149 
150     @Override
onCreateView(@onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedState)151     public @Nullable View onCreateView(@NonNull LayoutInflater inflater,
152             @Nullable ViewGroup container, @Nullable Bundle savedState) {
153         if (container == null) {
154             // Don't throw an exception here, as this may happen during restoreInstanceState for
155             // Viewers that we don't need anymore.
156             return null;
157         }
158         this.mContainer = container;
159         return null;
160     }
161 
162     @Override
onActivityCreated(@ullable Bundle savedInstanceState)163     public void onActivityCreated(@Nullable Bundle savedInstanceState) {
164         super.onActivityCreated(savedInstanceState);
165         if (mViewState.get() == ViewState.NO_VIEW || mViewState.get() == ViewState.ERROR) {
166             mViewState.set(ViewState.VIEW_CREATED);
167         }
168     }
169 
170     @Override
onStart()171     public void onStart() {
172         super.onStart();
173         mStarted = true;
174         if (mDelayedEnter || mOnScreen) {
175             onEnter();
176             mDelayedEnter = false;
177         }
178     }
179 
180     /**
181      * Notifies this Viewer goes on-screen. Guarantees that {@link #onEnter} will be called now or
182      * when the Viewer is started.
183      */
postEnter()184     public void postEnter() {
185 
186         mOnScreen = true;
187         if (mStarted) {
188             onEnter();
189         } else {
190             mDelayedEnter = true;
191         }
192     }
193 
194     /** Called after this viewer enters the screen and becomes visible. */
195     @CallSuper
onEnter()196     protected void onEnter() {
197         // TODO: Track file opened event, content length and view progress.
198         participateInAccessibility(true);
199     }
200 
201     /** Called after this viewer exits the screen and becomes invisible to the user. */
202     @CallSuper
onExit()203     protected void onExit() {
204         // TODO: Track file closed event, content length and view progress.
205         participateInAccessibility(false);
206     }
207 
208     @Override
onStop()209     public void onStop() {
210         if (mOnScreen) {
211             onExit();
212         }
213         mStarted = false;
214         super.onStop();
215     }
216 
217     @Override
onDestroyView()218     public void onDestroyView() {
219         destroyView();
220         mContainer = null;
221         super.onDestroyView();
222     }
223 
224     /**
225      * Called when this viewer no longer needs (or has) a view. Resets {@link #mViewState} to
226      * {@link ViewState#NO_VIEW}. If the viewer is to be reused, it will restart its whole
227      * life-cycle including {@link #onCreateView}.
228      * When overridden by subclasses, it must be idempotent, and this method must be called. It is
229      * possible (and likely) it will be called more than once.
230      * <p>
231      * We could include this in {@link #onDestroyView}, if only it was guaranteed to be called.
232      */
destroyView()233     protected void destroyView() {
234         if (mViewState.get() != ViewState.NO_VIEW) {
235             mViewState.set(ViewState.NO_VIEW);
236         }
237         if (mContainer != null && getView() != null && mContainer == getView().getParent()) {
238             // Some viewers add extra views to their container, e.g. toasts. Remove them all.
239             // Do not remove what's under it though.
240             int count = mContainer.getChildCount();
241             View child;
242             for (int i = count - 1; i > 0; i--) {
243                 child = mContainer.getChildAt(i);
244                 mContainer.removeView(child);
245                 if (child == getView()) {
246                     break;
247                 }
248             }
249         }
250     }
251 
252     @Override
onDestroy()253     public void onDestroy() {
254         super.onDestroy();
255     }
256 
257     /**
258      * Returns true when this Viewer is on-screen (= entered but not exited) and active (i.e. the
259      * Activity is resumed).
260      */
isShowing()261     protected boolean isShowing() {
262         return isResumed() && mOnScreen;
263     }
264 
isStarted()265     protected boolean isStarted() {
266         return mStarted;
267     }
268 
269     /** Makes the views of this Viewer visible to TalkBack (in the swipe gesture circus) or not. */
270     @SuppressLint(
271             "NewApi")
272     // Call requires API 16 and we're on API 19 but our Manifest config thinks its 14
participateInAccessibility(boolean participate)273     protected void participateInAccessibility(boolean participate) {
274         if (!participate) {
275             disableAccessibilityPostKitKat();
276         } else {
277             requireView()
278                     .setImportantForAccessibility(
279                             participate
280                                     ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
281                                     : View.IMPORTANT_FOR_ACCESSIBILITY_NO);
282         }
283     }
284 
285     @SuppressLint(
286             "NewApi")
287     // Call requires API 16 and we're on API 19 but our Manifest config thinks its 14
disableAccessibilityPostKitKat()288     private void disableAccessibilityPostKitKat() {
289         getView().setImportantForAccessibility(
290                 View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
291     }
292 
293     /** Save the {@link DisplayData}'s content reference (not the contents itself) to arguments. */
saveToArguments(@onNull DisplayData data)294     protected void saveToArguments(@NonNull DisplayData data) {
295         getArguments().putBundle(KEY_DATA, data.asBundle());
296     }
297 
298     @Override
finalize()299     protected void finalize() throws Throwable {
300         super.finalize();
301     }
302 }
303