/* * Copyright 2014 Yaroslav Mytkalyk * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES 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.docd.purefm.commandline; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.support.annotation.NonNull; import android.util.Log; import android.util.Pair; import com.docd.purefm.settings.Settings; import com.stericson.RootTools.execution.Shell; import android.support.annotation.Nullable; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.Collection; import java.util.HashSet; import java.util.LinkedList; import java.util.Set; /** * ShellHolder holds shared Shell instance */ public final class ShellHolder { public interface OnShellChangedListener { void onShellChanged(boolean hasShell, boolean isRootShell); } private static final int REASK_FOR_ROOT_SHELL_THRESHOLD = 6; private static final Object sInstanceLock = new Object(); private static ShellHolder sInstance; private static int sCommandId; public static int getNextCommandId() { return sCommandId++; } @NonNull public static ShellHolder getInstance() { if (sInstance == null) { synchronized (sInstanceLock) { if (sInstance == null) { sInstance = new ShellHolder(); } } } return sInstance; } private final Collection<WeakReference<OnShellChangedListener>> sListeners = new LinkedList<>(); private final Object mShellLock = new Object(); private final Handler mHandler; private boolean mIsRootShell; private Shell mShell; private int mSkipReaskForRootShellCount; private ShellHolder() { mHandler = new ShellHolderHandler(this); } public void addOnShellChangedListener(@NonNull final OnShellChangedListener listener) { final Set<WeakReference<OnShellChangedListener>> toRemove = new HashSet<>(); try { for (final WeakReference<OnShellChangedListener> ref : sListeners) { final OnShellChangedListener l = ref.get(); if (l == null) { toRemove.add(ref); } else if (l == listener) { //already contains return; } } sListeners.add(new WeakReference<>(listener)); } finally { sListeners.removeAll(toRemove); } } public void removeOnShellChangedListener(@NonNull final OnShellChangedListener listener) { final Set<WeakReference<OnShellChangedListener>> toRemove = new HashSet<>(); try { for (final WeakReference<OnShellChangedListener> ref : sListeners) { final OnShellChangedListener l = ref.get(); if (l == null || l == listener) { toRemove.add(ref); } } } finally { sListeners.removeAll(toRemove); } } public boolean isCurrentShellRoot() { return mIsRootShell; } public void releaseShell(final boolean notifyListeners) { synchronized (mShellLock) { releaseShellAsync(notifyListeners); } } private void releaseShellAsync(final boolean notifyListeners) { if (mShell != null) { try { mShell.close(); } catch (IOException e) { e.printStackTrace(); } mShell = null; mIsRootShell = false; if (notifyListeners) { mHandler.removeMessages(ShellHolderHandler.MESSAGE_NOTIFY_LISTENERS); mHandler.sendEmptyMessage(ShellHolderHandler.MESSAGE_NOTIFY_LISTENERS); } } } public boolean execute(@NonNull final Command command) { final Shell shell = getShell(); if (shell == null) { Log.w("ShellHolder", "No shell. Execution aborted."); return false; } try { shell.add(command); return true; } catch (IOException e) { e.printStackTrace(); return false; } } public boolean hasShell() { return getShell() != null; } /** * The shell is set by BrowserPagerActivity and is released when BrowserPagerActivity * is destroyed. * Returns current global shell. If current shell is non-root, but root shell is requested, * it will ask for root shell every #REASK_FOR_ROOT_SHELL_THRESHOLD calls. * * @return shell shared Shell instance */ @Nullable private Shell getShell() { synchronized (mShellLock) { final boolean suEnabled = Settings.getInstance().isSuEnabled(); if (suEnabled && mShell != null && !mIsRootShell) { if (mSkipReaskForRootShellCount >= REASK_FOR_ROOT_SHELL_THRESHOLD) { mSkipReaskForRootShellCount = 0; final Pair<Boolean, Shell> result = ShellFactory.getRootShell(); applyResult(result); } else { mSkipReaskForRootShellCount++; } } if (!suEnabled && mShell != null && mIsRootShell) { openNewShell(); mSkipReaskForRootShellCount = REASK_FOR_ROOT_SHELL_THRESHOLD; } if (mShell == null || !Shell.isAnyShellOpen()) { openNewShell(); mSkipReaskForRootShellCount = REASK_FOR_ROOT_SHELL_THRESHOLD; } return mShell; } } /** * Opens new shell. This method is called from {@link #getShell()} and is already synchronized * with {@link #mShellLock} */ private void openNewShell() { try { final Pair<Boolean, Shell> result = ShellFactory.getShell(); applyResult(result); } catch (IOException e) { Log.w("getShell() error:", e); } } /** * Applies shell result. This method is called from {@link #getShell()} and is already * synchronized with {@link #mShellLock} * * @param result {@link ShellFactory#getShell} result */ private void applyResult(@Nullable final Pair<Boolean, Shell> result) { if (result != null) { releaseShellAsync(false); mIsRootShell = result.first; mShell = result.second; mSkipReaskForRootShellCount = 0; mHandler.removeMessages(ShellHolderHandler.MESSAGE_NOTIFY_LISTENERS); mHandler.sendEmptyMessage(ShellHolderHandler.MESSAGE_NOTIFY_LISTENERS); } } void notifyListeners() { synchronized (mShellLock) { for (final WeakReference<OnShellChangedListener> ref : sListeners) { final OnShellChangedListener l = ref.get(); if (l != null) { l.onShellChanged(mShell != null, mIsRootShell); } } } } private static final class ShellHolderHandler extends Handler { static final int MESSAGE_NOTIFY_LISTENERS = 13; private final WeakReference<ShellHolder> mShellHolderReference; ShellHolderHandler(@NonNull final ShellHolder shellHolder) { super(Looper.getMainLooper()); this.mShellHolderReference = new WeakReference<>(shellHolder); } @Override public void handleMessage(final Message msg) { if (msg.what == MESSAGE_NOTIFY_LISTENERS) { final ShellHolder holder = mShellHolderReference.get(); if (holder != null) { holder.notifyListeners(); } } else { super.handleMessage(msg); } } } }