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