1 /*
2  * Copyright (C) 2015 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 com.android.messaging.ui;
18 
19 import android.graphics.drawable.ColorDrawable;
20 import android.os.Bundle;
21 import androidx.appcompat.app.ActionBar;
22 import androidx.appcompat.app.AppCompatActivity;
23 import android.view.ActionMode;
24 import android.view.Menu;
25 import android.view.MenuInflater;
26 import android.view.MenuItem;
27 import android.view.View;
28 
29 import com.android.messaging.R;
30 import com.android.messaging.util.BugleActivityUtil;
31 import com.android.messaging.util.ImeUtil;
32 import com.android.messaging.util.LogUtil;
33 import com.android.messaging.util.UiUtils;
34 
35 import java.util.HashSet;
36 import java.util.Set;
37 
38 /**
39  * Base class for app activities that use an action bar. Responsible for logging/telemetry/other
40  * needs that will be common for all activities.  We can break out the common code if/when we need
41  * a version that doesn't use an actionbar.
42  */
43 public class BugleActionBarActivity extends AppCompatActivity implements ImeUtil.ImeStateHost {
44     // Tracks the list of observers opting in for IME state change.
45     private final Set<ImeUtil.ImeStateObserver> mImeStateObservers = new HashSet<>();
46 
47     // Tracks the soft keyboard display state
48     private boolean mImeOpen;
49 
50     // The ActionMode that represents the modal contextual action bar, using our own implementation
51     // rather than the built in contextual action bar to reduce jank
52     private CustomActionMode mActionMode;
53 
54     // The menu for the action bar
55     private Menu mActionBarMenu;
56 
57     // Used to determine if a onDisplayHeightChanged was due to the IME opening or rotation of the
58     // device
59     private int mLastScreenHeight;
60 
61     @Override
onCreate(final Bundle savedInstanceState)62     protected void onCreate(final Bundle savedInstanceState) {
63         super.onCreate(savedInstanceState);
64         if (UiUtils.redirectToPermissionCheckIfNeeded(this)) {
65             return;
66         }
67 
68         mLastScreenHeight = getResources().getDisplayMetrics().heightPixels;
69         if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) {
70             LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onCreate");
71         }
72     }
73 
74     @Override
onStart()75     protected void onStart() {
76         super.onStart();
77         if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) {
78             LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onStart");
79         }
80     }
81 
82     @Override
onRestart()83     protected void onRestart() {
84         super.onStop();
85         if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) {
86             LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onRestart");
87         }
88     }
89 
90     @Override
onResume()91     protected void onResume() {
92         super.onResume();
93         if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) {
94             LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onResume");
95         }
96         BugleActivityUtil.onActivityResume(this, BugleActionBarActivity.this);
97     }
98 
99     @Override
onPause()100     protected void onPause() {
101         super.onPause();
102         if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) {
103             LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onPause");
104         }
105     }
106 
107     @Override
onStop()108     protected void onStop() {
109         super.onStop();
110         if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) {
111             LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onStop");
112         }
113     }
114 
115     private boolean mDestroyed;
116 
117     @Override
onDestroy()118     protected void onDestroy() {
119         super.onDestroy();
120         mDestroyed = true;
121     }
122 
getIsDestroyed()123     public boolean getIsDestroyed() {
124         return mDestroyed;
125     }
126 
127     @Override
onDisplayHeightChanged(final int heightSpecification)128     public void onDisplayHeightChanged(final int heightSpecification) {
129         int screenHeight = getResources().getDisplayMetrics().heightPixels;
130 
131         if (screenHeight != mLastScreenHeight) {
132             // Appears to be an orientation change, don't fire ime updates
133             mLastScreenHeight = screenHeight;
134             LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onDisplayHeightChanged " +
135                     " screenHeight: " + screenHeight + " lastScreenHeight: " + mLastScreenHeight +
136                     " Skipped, appears to be orientation change.");
137             return;
138         }
139         final ActionBar actionBar = getSupportActionBar();
140         if (actionBar != null && actionBar.isShowing()) {
141             screenHeight -= actionBar.getHeight();
142         }
143         final int height = View.MeasureSpec.getSize(heightSpecification);
144 
145         final boolean imeWasOpen = mImeOpen;
146         mImeOpen = screenHeight - height > 100;
147 
148         if (LogUtil.isLoggable(LogUtil.BUGLE_TAG, LogUtil.VERBOSE)) {
149             LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onDisplayHeightChanged " +
150                     "imeWasOpen: " + imeWasOpen + " mImeOpen: " + mImeOpen + " screenHeight: " +
151                     screenHeight + " height: " + height);
152         }
153 
154         if (imeWasOpen != mImeOpen) {
155             for (final ImeUtil.ImeStateObserver observer : mImeStateObservers) {
156                 observer.onImeStateChanged(mImeOpen);
157             }
158         }
159     }
160 
161     @Override
registerImeStateObserver(final ImeUtil.ImeStateObserver observer)162     public void registerImeStateObserver(final ImeUtil.ImeStateObserver observer) {
163         mImeStateObservers.add(observer);
164     }
165 
166     @Override
unregisterImeStateObserver(final ImeUtil.ImeStateObserver observer)167     public void unregisterImeStateObserver(final ImeUtil.ImeStateObserver observer) {
168         mImeStateObservers.remove(observer);
169     }
170 
171     @Override
isImeOpen()172     public boolean isImeOpen() {
173         return mImeOpen;
174     }
175 
176     @Override
onCreateOptionsMenu(final Menu menu)177     public boolean onCreateOptionsMenu(final Menu menu) {
178         mActionBarMenu = menu;
179         if (mActionMode != null &&
180                 mActionMode.getCallback().onCreateActionMode(mActionMode, menu)) {
181             return true;
182         }
183         return false;
184     }
185 
186     @Override
onPrepareOptionsMenu(final Menu menu)187     public boolean onPrepareOptionsMenu(final Menu menu) {
188         mActionBarMenu = menu;
189         if (mActionMode != null &&
190                 mActionMode.getCallback().onPrepareActionMode(mActionMode, menu)) {
191             return true;
192         }
193         return super.onPrepareOptionsMenu(menu);
194     }
195 
196     @Override
onOptionsItemSelected(final MenuItem menuItem)197     public boolean onOptionsItemSelected(final MenuItem menuItem) {
198         if (mActionMode != null &&
199                 mActionMode.getCallback().onActionItemClicked(mActionMode, menuItem)) {
200             return true;
201         }
202 
203         switch (menuItem.getItemId()) {
204             case android.R.id.home:
205                 if (mActionMode != null) {
206                     dismissActionMode();
207                     return true;
208                 }
209         }
210         return super.onOptionsItemSelected(menuItem);
211     }
212 
213     @Override
startActionMode(final ActionMode.Callback callback)214     public ActionMode startActionMode(final ActionMode.Callback callback) {
215         mActionMode = new CustomActionMode(callback);
216         supportInvalidateOptionsMenu();
217         invalidateActionBar();
218         return mActionMode;
219     }
220 
dismissActionMode()221     public void dismissActionMode() {
222         if (mActionMode != null) {
223             mActionMode.finish();
224             mActionMode = null;
225             invalidateActionBar();
226         }
227     }
228 
getActionMode()229     public ActionMode getActionMode() {
230         return mActionMode;
231     }
232 
getActionModeCallback()233     protected ActionMode.Callback getActionModeCallback() {
234         if (mActionMode == null) {
235             return null;
236         }
237 
238         return mActionMode.getCallback();
239     }
240 
241     /**
242      * Receives and handles action bar invalidation request from sub-components of this activity.
243      *
244      * <p>Normally actions have sole control over the action bar, but in order to support seamless
245      * transitions for components such as the full screen media picker, we have to let it take over
246      * the action bar and then restore its state afterwards</p>
247      *
248      * <p>If a fragment does anything that may change the action bar, it should call this method
249      * and then it is this method's responsibility to figure out which component "controls" the
250      * action bar and delegate the updating of the action bar to that component</p>
251      */
invalidateActionBar()252     public final void invalidateActionBar() {
253         if (mActionMode != null) {
254             mActionMode.updateActionBar(getSupportActionBar());
255         } else {
256             updateActionBar(getSupportActionBar());
257         }
258     }
259 
updateActionBar(final ActionBar actionBar)260     protected void updateActionBar(final ActionBar actionBar) {
261         actionBar.setHomeAsUpIndicator(null);
262     }
263 
264     /**
265      * Custom ActionMode implementation which allows us to just replace the contents of the main
266      * action bar rather than overlay over it
267      */
268     private class CustomActionMode extends ActionMode {
269         private CharSequence mTitle;
270         private CharSequence mSubtitle;
271         private View mCustomView;
272         private final Callback mCallback;
273 
CustomActionMode(final Callback callback)274         public CustomActionMode(final Callback callback) {
275             mCallback = callback;
276         }
277 
278         @Override
setTitle(final CharSequence title)279         public void setTitle(final CharSequence title) {
280             mTitle = title;
281         }
282 
283         @Override
setTitle(final int resId)284         public void setTitle(final int resId) {
285             mTitle = getResources().getString(resId);
286         }
287 
288         @Override
setSubtitle(final CharSequence subtitle)289         public void setSubtitle(final CharSequence subtitle) {
290             mSubtitle = subtitle;
291         }
292 
293         @Override
setSubtitle(final int resId)294         public void setSubtitle(final int resId) {
295             mSubtitle = getResources().getString(resId);
296         }
297 
298         @Override
setCustomView(final View view)299         public void setCustomView(final View view) {
300             mCustomView = view;
301         }
302 
303         @Override
invalidate()304         public void invalidate() {
305             invalidateActionBar();
306         }
307 
308         @Override
finish()309         public void finish() {
310             mActionMode = null;
311             mCallback.onDestroyActionMode(this);
312             supportInvalidateOptionsMenu();
313             invalidateActionBar();
314         }
315 
316         @Override
getMenu()317         public Menu getMenu() {
318             return mActionBarMenu;
319         }
320 
321         @Override
getTitle()322         public CharSequence getTitle() {
323             return mTitle;
324         }
325 
326         @Override
getSubtitle()327         public CharSequence getSubtitle() {
328             return mSubtitle;
329         }
330 
331         @Override
getCustomView()332         public View getCustomView() {
333             return mCustomView;
334         }
335 
336         @Override
getMenuInflater()337         public MenuInflater getMenuInflater() {
338             return BugleActionBarActivity.this.getMenuInflater();
339         }
340 
getCallback()341         public Callback getCallback() {
342             return mCallback;
343         }
344 
updateActionBar(final ActionBar actionBar)345         public void updateActionBar(final ActionBar actionBar) {
346             actionBar.setDisplayOptions(ActionBar.DISPLAY_HOME_AS_UP);
347             actionBar.setDisplayShowTitleEnabled(false);
348             actionBar.setDisplayShowCustomEnabled(false);
349             mActionMode.getCallback().onPrepareActionMode(mActionMode, mActionBarMenu);
350             actionBar.setBackgroundDrawable(new ColorDrawable(
351                     getResources().getColor(R.color.contextual_action_bar_background_color)));
352             actionBar.setHomeAsUpIndicator(R.drawable.ic_cancel_small_dark);
353             actionBar.show();
354         }
355     }
356 }
357